dependencyiq 2.0.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.
- package/LICENSE +21 -0
- package/package.json +50 -0
- package/src/activityFetcher.js +66 -0
- package/src/agent.js +506 -0
- package/src/blastRadius.js +134 -0
- package/src/configLoader.js +61 -0
- package/src/crossProjectFanOut.js +180 -0
- package/src/dashboardGenerator.js +642 -0
- package/src/executiveSummary.js +76 -0
- package/src/fleetAggregator.js +155 -0
- package/src/fleetDashboardGenerator.js +199 -0
- package/src/fleetSnapshot.js +103 -0
- package/src/freshnessChecker.js +306 -0
- package/src/freshnessPolicy.js +73 -0
- package/src/gitlabAuth.js +38 -0
- package/src/httpRetry.js +48 -0
- package/src/impactReport.js +92 -0
- package/src/mrReviewer.js +245 -0
- package/src/orbitClient.js +214 -0
- package/src/prGenerator.js +228 -0
- package/src/remoteFixer.js +129 -0
- package/src/riskCalculator.js +143 -0
- package/src/scanners/cvss.js +78 -0
- package/src/scanners/dependencyTreeBuilder.js +227 -0
- package/src/scanners/ecosystemFixers.js +371 -0
- package/src/scanners/manifestParser.js +99 -0
- package/src/scanners/osvScanner.js +228 -0
- package/src/scanners/supplyChainTrustSignals.js +472 -0
- package/src/strategyGenerator.js +384 -0
- package/src/upgradeImpactSimulator.js +241 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-ecosystem dependency version bumpers/removers.
|
|
3
|
+
*
|
|
4
|
+
* Each ecosystem has a pure `transform*Content(content, packageName, ...)`
|
|
5
|
+
* function that takes a manifest's raw text and returns the patched text
|
|
6
|
+
* (or `touched: false` if the package wasn't found as a direct
|
|
7
|
+
* dependency). These are reused by two callers:
|
|
8
|
+
* - the local fs-based functions below (used by `agent.js analyze --fix`
|
|
9
|
+
* against a checked-out repo)
|
|
10
|
+
* - `remoteFixer.js` (used by the cross-project emergency response in
|
|
11
|
+
* `crossProjectFanOut.js`, which patches files via the GitLab API
|
|
12
|
+
* without a local checkout)
|
|
13
|
+
*
|
|
14
|
+
* Local fixers intentionally do NOT regenerate lockfiles (npm install, go
|
|
15
|
+
* mod tidy, mvn versions:set, bundle lock) — that requires running the
|
|
16
|
+
* ecosystem's own toolchain, which the CI job does as a follow-up step
|
|
17
|
+
* (see .gitlab-ci.yml) before the commit is made.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
function escapeRegex(str) {
|
|
24
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --- Pure content transforms (no fs access) ---
|
|
28
|
+
|
|
29
|
+
function transformNpmContent(content, packageName, fixedVersion) {
|
|
30
|
+
const pkg = JSON.parse(content);
|
|
31
|
+
let touched = false;
|
|
32
|
+
|
|
33
|
+
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
34
|
+
if (pkg[section]?.[packageName]) {
|
|
35
|
+
const prefix = /^[~^]/.test(pkg[section][packageName]) ? pkg[section][packageName][0] : '';
|
|
36
|
+
pkg[section][packageName] = `${prefix}${fixedVersion}`;
|
|
37
|
+
touched = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!touched) {
|
|
42
|
+
return { touched: false, warning: `${packageName} not a direct dependency in package.json (likely transitive — run "npm audit fix" instead)` };
|
|
43
|
+
}
|
|
44
|
+
return { touched: true, content: JSON.stringify(pkg, null, 2) + '\n', followUp: 'npm install' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Force a SECURE version of a TRANSITIVE npm package via the `overrides`
|
|
49
|
+
* field — the real-world fix for the Axios/follow-redirects class of
|
|
50
|
+
* incident, where a vulnerable package is pulled in by a dependency you
|
|
51
|
+
* don't control. You can't bump a direct line that doesn't exist; npm's
|
|
52
|
+
* `overrides` (Yarn `resolutions`, pnpm `pnpm.overrides`) force the
|
|
53
|
+
* resolver to use a version satisfying the constraint regardless of what
|
|
54
|
+
* the parent declares. The constraint is `>=floor` so the resolver can
|
|
55
|
+
* still pick a parent-compatible version, just never one below the
|
|
56
|
+
* patched floor.
|
|
57
|
+
*/
|
|
58
|
+
function addNpmOverrideContent(content, packageName, floorVersion) {
|
|
59
|
+
const pkg = JSON.parse(content);
|
|
60
|
+
pkg.overrides = pkg.overrides || {};
|
|
61
|
+
const constraint = `>=${floorVersion}`;
|
|
62
|
+
if (pkg.overrides[packageName] === constraint) {
|
|
63
|
+
return { touched: false, warning: `package.json already overrides ${packageName} to ${constraint}` };
|
|
64
|
+
}
|
|
65
|
+
pkg.overrides[packageName] = constraint;
|
|
66
|
+
return {
|
|
67
|
+
touched: true,
|
|
68
|
+
action: 'override',
|
|
69
|
+
content: JSON.stringify(pkg, null, 2) + '\n',
|
|
70
|
+
followUp: 'npm install (regenerates package-lock.json with the override applied)',
|
|
71
|
+
warning: `Forced transitive dependency ${packageName} to ${constraint} via package.json "overrides" — verify the resolved tree with "npm ls ${packageName}" after install`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function transformPythonContent(content, packageName, fixedVersion) {
|
|
76
|
+
const lineRegex = new RegExp(`^(${escapeRegex(packageName)})\\s*(==|>=|~=)\\s*([\\w.]+)`, 'im');
|
|
77
|
+
if (!lineRegex.test(content)) {
|
|
78
|
+
return { touched: false, warning: `${packageName} not found as a pinned line in requirements.txt — review manually` };
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
touched: true,
|
|
82
|
+
content: content.replace(lineRegex, `$1==${fixedVersion}`),
|
|
83
|
+
followUp: 'pip install -r requirements.txt (or poetry lock / pip-compile)',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function transformGoContent(content, packageName, fixedVersion) {
|
|
88
|
+
const lineRegex = new RegExp(`(^|\\s)(${escapeRegex(packageName)}\\s+)v?[\\w.+-]+`, 'm');
|
|
89
|
+
if (!lineRegex.test(content)) {
|
|
90
|
+
return { touched: false, warning: `${packageName} not found as a direct require in go.mod — likely transitive; run "go get ${packageName}@${fixedVersion}" then "go mod tidy"` };
|
|
91
|
+
}
|
|
92
|
+
const normalizedVersion = fixedVersion.startsWith('v') ? fixedVersion : `v${fixedVersion}`;
|
|
93
|
+
return {
|
|
94
|
+
touched: true,
|
|
95
|
+
content: content.replace(lineRegex, `$1$2${normalizedVersion}`),
|
|
96
|
+
followUp: `go get ${packageName}@${normalizedVersion} && go mod tidy`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function transformMavenContent(content, packageName, fixedVersion) {
|
|
101
|
+
const artifactId = packageName.includes(':') ? packageName.split(':')[1] : packageName;
|
|
102
|
+
const blockRegex = new RegExp(
|
|
103
|
+
`(<artifactId>${escapeRegex(artifactId)}</artifactId>[\\s\\S]{0,200}?<version>)([^<]+)(</version>)`,
|
|
104
|
+
'm'
|
|
105
|
+
);
|
|
106
|
+
if (!blockRegex.test(content)) {
|
|
107
|
+
return { touched: false, warning: `Could not locate a <version> tag near <artifactId>${artifactId}</artifactId> — likely managed via a parent POM or BOM; update there instead` };
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
touched: true,
|
|
111
|
+
content: content.replace(blockRegex, `$1${fixedVersion}$3`),
|
|
112
|
+
followUp: 'mvn verify',
|
|
113
|
+
warning: 'Maven version bump is a best-effort text patch — verify the diff before merging',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- Pure content transforms: removal (used when Orbit confirms zero
|
|
118
|
+
// importers anywhere in the project — see blastRadius.js).
|
|
119
|
+
|
|
120
|
+
function removeNpmContent(content, packageName) {
|
|
121
|
+
const pkg = JSON.parse(content);
|
|
122
|
+
let touched = false;
|
|
123
|
+
|
|
124
|
+
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
125
|
+
if (pkg[section]?.[packageName]) {
|
|
126
|
+
delete pkg[section][packageName];
|
|
127
|
+
touched = true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!touched) {
|
|
132
|
+
return { touched: false, warning: `${packageName} not a direct dependency in package.json — likely transitive, nothing to remove here` };
|
|
133
|
+
}
|
|
134
|
+
return { touched: true, action: 'remove', content: JSON.stringify(pkg, null, 2) + '\n', followUp: 'npm install' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function removePythonContent(content, packageName) {
|
|
138
|
+
const lines = content.split('\n');
|
|
139
|
+
const lineRegex = new RegExp(`^${escapeRegex(packageName)}\\s*(==|>=|~=)`, 'i');
|
|
140
|
+
const filtered = lines.filter(line => !lineRegex.test(line.trim()));
|
|
141
|
+
|
|
142
|
+
if (filtered.length === lines.length) {
|
|
143
|
+
return { touched: false, warning: `${packageName} not found as a pinned line in requirements.txt — review manually` };
|
|
144
|
+
}
|
|
145
|
+
return { touched: true, action: 'remove', content: filtered.join('\n'), followUp: 'pip install -r requirements.txt (or poetry lock / pip-compile)' };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function removeGoContent(content, packageName) {
|
|
149
|
+
const lines = content.split('\n');
|
|
150
|
+
const lineRegex = new RegExp(`^\\s*${escapeRegex(packageName)}\\s+v?[\\w.+-]+`);
|
|
151
|
+
const filtered = lines.filter(line => !lineRegex.test(line));
|
|
152
|
+
|
|
153
|
+
if (filtered.length === lines.length) {
|
|
154
|
+
return { touched: false, warning: `${packageName} not found as a direct require in go.mod — likely transitive, nothing to remove here` };
|
|
155
|
+
}
|
|
156
|
+
return { touched: true, action: 'remove', content: filtered.join('\n'), followUp: 'go mod tidy' };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function removeMavenContent(content, packageName) {
|
|
160
|
+
const artifactId = packageName.includes(':') ? packageName.split(':')[1] : packageName;
|
|
161
|
+
const blockRegex = new RegExp(
|
|
162
|
+
`\\s*<dependency>(?:(?!</dependency>)[\\s\\S])*?<artifactId>${escapeRegex(artifactId)}</artifactId>[\\s\\S]*?</dependency>`,
|
|
163
|
+
'm'
|
|
164
|
+
);
|
|
165
|
+
if (!blockRegex.test(content)) {
|
|
166
|
+
return { touched: false, warning: `Could not locate a <dependency> block for <artifactId>${artifactId}</artifactId> — likely managed via a parent POM or BOM; remove it there instead` };
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
touched: true,
|
|
170
|
+
action: 'remove',
|
|
171
|
+
content: content.replace(blockRegex, ''),
|
|
172
|
+
followUp: 'mvn verify',
|
|
173
|
+
warning: 'Maven dependency removal is a best-effort text patch — verify the diff before merging',
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const CONTENT_TRANSFORMS = {
|
|
178
|
+
npm: transformNpmContent,
|
|
179
|
+
PyPI: transformPythonContent,
|
|
180
|
+
Go: transformGoContent,
|
|
181
|
+
Maven: transformMavenContent,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const CONTENT_REMOVERS = {
|
|
185
|
+
npm: removeNpmContent,
|
|
186
|
+
PyPI: removePythonContent,
|
|
187
|
+
Go: removeGoContent,
|
|
188
|
+
Maven: removeMavenContent,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const DEFAULT_MANIFEST = {
|
|
192
|
+
npm: 'package.json',
|
|
193
|
+
PyPI: 'requirements.txt',
|
|
194
|
+
Go: 'go.mod',
|
|
195
|
+
Maven: 'pom.xml',
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Apply a content transform/removal for an ecosystem to an in-memory
|
|
200
|
+
* string — no fs access. Used directly by remoteFixer.js for cross-project
|
|
201
|
+
* fixes via the GitLab API.
|
|
202
|
+
* @returns {Object} { touched, content?, warning?, followUp?, action? }
|
|
203
|
+
*/
|
|
204
|
+
function applyFixToContent(ecosystem, content, packageName, fixedVersion, recommendation = 'upgrade') {
|
|
205
|
+
if (recommendation === 'remove') {
|
|
206
|
+
const remover = CONTENT_REMOVERS[ecosystem];
|
|
207
|
+
if (!remover) return { touched: false, warning: `No automated remover for ecosystem "${ecosystem}" yet` };
|
|
208
|
+
const result = remover(content, packageName);
|
|
209
|
+
return result.touched ? { ...result, action: 'remove' } : result;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!fixedVersion || fixedVersion === 'unknown') return { touched: false, warning: 'No fixed version reported by OSV for this advisory' };
|
|
213
|
+
|
|
214
|
+
// 'override' forces a secure version of a transitive npm dependency via
|
|
215
|
+
// the `overrides` field (the Axios/follow-redirects case) — only npm
|
|
216
|
+
// supports this mechanism here.
|
|
217
|
+
if (recommendation === 'override') {
|
|
218
|
+
if (ecosystem !== 'npm') return { touched: false, warning: `Transitive override fix is only supported for npm, not "${ecosystem}"` };
|
|
219
|
+
return addNpmOverrideContent(content, packageName, fixedVersion);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const transform = CONTENT_TRANSFORMS[ecosystem];
|
|
223
|
+
if (!transform) return { touched: false, warning: `No automated fixer for ecosystem "${ecosystem}" yet` };
|
|
224
|
+
return transform(content, packageName, fixedVersion);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// --- Local fs-based wrappers (used by `agent.js analyze --fix`) ---
|
|
228
|
+
|
|
229
|
+
function applyContentResultToFile(repoPath, manifestRelPath, defaultName, result) {
|
|
230
|
+
if (!result.touched) return { applied: false, warning: result.warning };
|
|
231
|
+
const filePath = path.join(repoPath, manifestRelPath || defaultName);
|
|
232
|
+
fs.writeFileSync(filePath, result.content);
|
|
233
|
+
return {
|
|
234
|
+
applied: true,
|
|
235
|
+
action: result.action,
|
|
236
|
+
filePath: path.relative(repoPath, filePath),
|
|
237
|
+
followUp: result.followUp,
|
|
238
|
+
warning: result.warning || null,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function readManifest(repoPath, manifestRelPath, defaultName) {
|
|
243
|
+
const filePath = path.join(repoPath, manifestRelPath || defaultName);
|
|
244
|
+
if (!fs.existsSync(filePath)) return null;
|
|
245
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function bumpNpm(repoPath, manifestRelPath, packageName, fixedVersion) {
|
|
249
|
+
const content = readManifest(repoPath, manifestRelPath, 'package.json');
|
|
250
|
+
if (content === null) return { applied: false, warning: 'package.json not found' };
|
|
251
|
+
return applyContentResultToFile(repoPath, manifestRelPath, 'package.json', transformNpmContent(content, packageName, fixedVersion));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function overrideNpm(repoPath, manifestRelPath, packageName, floorVersion) {
|
|
255
|
+
const content = readManifest(repoPath, manifestRelPath, 'package.json');
|
|
256
|
+
if (content === null) return { applied: false, warning: 'package.json not found' };
|
|
257
|
+
return applyContentResultToFile(repoPath, manifestRelPath, 'package.json', addNpmOverrideContent(content, packageName, floorVersion));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function bumpPython(repoPath, manifestRelPath, packageName, fixedVersion) {
|
|
261
|
+
const content = readManifest(repoPath, manifestRelPath, 'requirements.txt');
|
|
262
|
+
if (content === null) return { applied: false, warning: 'requirements.txt not found' };
|
|
263
|
+
return applyContentResultToFile(repoPath, manifestRelPath, 'requirements.txt', transformPythonContent(content, packageName, fixedVersion));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function bumpGo(repoPath, manifestRelPath, packageName, fixedVersion) {
|
|
267
|
+
const content = readManifest(repoPath, manifestRelPath, 'go.mod');
|
|
268
|
+
if (content === null) return { applied: false, warning: 'go.mod not found' };
|
|
269
|
+
return applyContentResultToFile(repoPath, manifestRelPath, 'go.mod', transformGoContent(content, packageName, fixedVersion));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function bumpMaven(repoPath, manifestRelPath, packageName, fixedVersion) {
|
|
273
|
+
const content = readManifest(repoPath, manifestRelPath, 'pom.xml');
|
|
274
|
+
if (content === null) return { applied: false, warning: 'pom.xml not found' };
|
|
275
|
+
return applyContentResultToFile(repoPath, manifestRelPath, 'pom.xml', transformMavenContent(content, packageName, fixedVersion));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function removeNpm(repoPath, manifestRelPath, packageName) {
|
|
279
|
+
const content = readManifest(repoPath, manifestRelPath, 'package.json');
|
|
280
|
+
if (content === null) return { applied: false, warning: 'package.json not found' };
|
|
281
|
+
return applyContentResultToFile(repoPath, manifestRelPath, 'package.json', removeNpmContent(content, packageName));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function removePython(repoPath, manifestRelPath, packageName) {
|
|
285
|
+
const content = readManifest(repoPath, manifestRelPath, 'requirements.txt');
|
|
286
|
+
if (content === null) return { applied: false, warning: 'requirements.txt not found' };
|
|
287
|
+
return applyContentResultToFile(repoPath, manifestRelPath, 'requirements.txt', removePythonContent(content, packageName));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function removeGo(repoPath, manifestRelPath, packageName) {
|
|
291
|
+
const content = readManifest(repoPath, manifestRelPath, 'go.mod');
|
|
292
|
+
if (content === null) return { applied: false, warning: 'go.mod not found' };
|
|
293
|
+
return applyContentResultToFile(repoPath, manifestRelPath, 'go.mod', removeGoContent(content, packageName));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function removeMaven(repoPath, manifestRelPath, packageName) {
|
|
297
|
+
const content = readManifest(repoPath, manifestRelPath, 'pom.xml');
|
|
298
|
+
if (content === null) return { applied: false, warning: 'pom.xml not found' };
|
|
299
|
+
return applyContentResultToFile(repoPath, manifestRelPath, 'pom.xml', removeMavenContent(content, packageName));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Apply the appropriate fixer for a vulnerability's ecosystem against a
|
|
304
|
+
* local checkout. If Orbit flagged the package as unused
|
|
305
|
+
* (vulnerability.recommendation === 'remove'), deletes the dependency
|
|
306
|
+
* instead of bumping its version.
|
|
307
|
+
* @param {string} repoPath - local checkout path
|
|
308
|
+
* @param {Object} vulnerability - normalized vulnerability (from osvScanner),
|
|
309
|
+
* optionally carrying a `recommendation` field from riskCalculator
|
|
310
|
+
* @returns {Object} fix result
|
|
311
|
+
*/
|
|
312
|
+
function applyFix(repoPath, vulnerability) {
|
|
313
|
+
if (vulnerability.recommendation === 'remove') {
|
|
314
|
+
const removers = { npm: removeNpm, PyPI: removePython, Go: removeGo, Maven: removeMaven };
|
|
315
|
+
const remover = removers[vulnerability.ecosystem];
|
|
316
|
+
if (!remover) {
|
|
317
|
+
return { applied: false, warning: `No automated remover for ecosystem "${vulnerability.ecosystem}" yet — remove the dependency manually` };
|
|
318
|
+
}
|
|
319
|
+
return remover(repoPath, vulnerability.manifestFile, vulnerability.package);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!vulnerability.fixedVersion || vulnerability.fixedVersion === 'unknown') {
|
|
323
|
+
return { applied: false, warning: 'No fixed version reported by OSV for this advisory' };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Transitive npm dependency: there's no direct line to bump, so force a
|
|
327
|
+
// secure version with an `overrides` entry instead of giving up. The
|
|
328
|
+
// floor is the original OSV fixed version (osvFloorVersion), preserved
|
|
329
|
+
// even when a smarter direct target was chosen for direct deps.
|
|
330
|
+
const chain = vulnerability.dependencyChain;
|
|
331
|
+
if (vulnerability.ecosystem === 'npm' && chain?.available && chain.isDirect === false) {
|
|
332
|
+
const floor = vulnerability.osvFloorVersion || vulnerability.fixedVersion;
|
|
333
|
+
return overrideNpm(repoPath, vulnerability.manifestFile, vulnerability.package, floor);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const fixers = { npm: bumpNpm, PyPI: bumpPython, Go: bumpGo, Maven: bumpMaven };
|
|
337
|
+
const fixer = fixers[vulnerability.ecosystem];
|
|
338
|
+
if (!fixer) {
|
|
339
|
+
return { applied: false, warning: `No automated fixer for ecosystem "${vulnerability.ecosystem}" yet — open the MR manually` };
|
|
340
|
+
}
|
|
341
|
+
const result = fixer(repoPath, vulnerability.manifestFile, vulnerability.package, vulnerability.fixedVersion);
|
|
342
|
+
|
|
343
|
+
// npm transitive fallback: if the direct bump found no line to change,
|
|
344
|
+
// the package is transitive. Rather than give up with no committable
|
|
345
|
+
// change (and therefore no MR), force a secure version through an
|
|
346
|
+
// `overrides` entry. This catches the case the dependency-chain path
|
|
347
|
+
// above missed because chain resolution was unavailable/ambiguous — so a
|
|
348
|
+
// transitive npm vuln still produces a real, automated remediation.
|
|
349
|
+
if (vulnerability.ecosystem === 'npm' && !result.applied) {
|
|
350
|
+
const floor = vulnerability.osvFloorVersion || vulnerability.fixedVersion;
|
|
351
|
+
const override = overrideNpm(repoPath, vulnerability.manifestFile, vulnerability.package, floor);
|
|
352
|
+
if (override.applied) return override;
|
|
353
|
+
}
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
module.exports = {
|
|
358
|
+
applyFix,
|
|
359
|
+
applyFixToContent,
|
|
360
|
+
bumpNpm,
|
|
361
|
+
bumpPython,
|
|
362
|
+
bumpGo,
|
|
363
|
+
bumpMaven,
|
|
364
|
+
overrideNpm,
|
|
365
|
+
addNpmOverrideContent,
|
|
366
|
+
removeNpm,
|
|
367
|
+
removePython,
|
|
368
|
+
removeGo,
|
|
369
|
+
removeMaven,
|
|
370
|
+
DEFAULT_MANIFEST,
|
|
371
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lists every direct dependency declared in a repo's manifests, regardless
|
|
3
|
+
* of whether OSV has flagged it as vulnerable. The vulnerability scanner
|
|
4
|
+
* (osvScanner.js) only reports packages with a known CVE; freshness
|
|
5
|
+
* policy enforcement ("never more than 2 minor versions behind") needs
|
|
6
|
+
* the full dependency list, vulnerable or not — that's the tech-debt
|
|
7
|
+
* prevention angle, not the security angle.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
function parseNpmManifest(repoPath, manifestFile) {
|
|
14
|
+
const filePath = path.join(repoPath, manifestFile);
|
|
15
|
+
if (!fs.existsSync(filePath)) return [];
|
|
16
|
+
const pkg = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
17
|
+
const deps = [];
|
|
18
|
+
for (const section of ['dependencies', 'devDependencies']) {
|
|
19
|
+
for (const [name, versionRange] of Object.entries(pkg[section] || {})) {
|
|
20
|
+
const currentVersion = versionRange.replace(/^[~^]/, '');
|
|
21
|
+
deps.push({ ecosystem: 'npm', package: name, currentVersion, manifestFile });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return deps;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parsePythonManifest(repoPath, manifestFile) {
|
|
28
|
+
const filePath = path.join(repoPath, manifestFile);
|
|
29
|
+
if (!fs.existsSync(filePath)) return [];
|
|
30
|
+
const lines = fs.readFileSync(filePath, 'utf-8').split('\n');
|
|
31
|
+
const deps = [];
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const match = line.trim().match(/^([A-Za-z0-9_.-]+)\s*==\s*([\w.]+)/);
|
|
34
|
+
if (match) deps.push({ ecosystem: 'PyPI', package: match[1], currentVersion: match[2], manifestFile });
|
|
35
|
+
}
|
|
36
|
+
return deps;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseGoManifest(repoPath, manifestFile) {
|
|
40
|
+
const filePath = path.join(repoPath, manifestFile);
|
|
41
|
+
if (!fs.existsSync(filePath)) return [];
|
|
42
|
+
const lines = fs.readFileSync(filePath, 'utf-8').split('\n');
|
|
43
|
+
const deps = [];
|
|
44
|
+
for (const rawLine of lines) {
|
|
45
|
+
const trimmed = rawLine.trim();
|
|
46
|
+
// Strip a leading "require " (single-line form); lines inside a
|
|
47
|
+
// "require ( ... )" block are already bare "module vX.Y.Z".
|
|
48
|
+
const candidate = trimmed.replace(/^require\s+/, '');
|
|
49
|
+
const isOtherDirective = /^(module|go|require\s*\(|require\s*\)|^\)|replace|exclude|retract)\b/.test(candidate) || candidate === ')';
|
|
50
|
+
const match = candidate.match(/^([\w.\-/]+)\s+v?([\w.+-]+)/);
|
|
51
|
+
if (match && !isOtherDirective) {
|
|
52
|
+
deps.push({ ecosystem: 'Go', package: match[1], currentVersion: match[2], manifestFile });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return deps;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseMavenManifest(repoPath, manifestFile) {
|
|
59
|
+
const filePath = path.join(repoPath, manifestFile);
|
|
60
|
+
if (!fs.existsSync(filePath)) return [];
|
|
61
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
62
|
+
const blockRegex = /<dependency>([\s\S]*?)<\/dependency>/g;
|
|
63
|
+
const blocks = content.match(blockRegex) || [];
|
|
64
|
+
const deps = blocks
|
|
65
|
+
.map(block => ({
|
|
66
|
+
groupId: block.match(/<groupId>([^<]+)<\/groupId>/)?.[1],
|
|
67
|
+
artifactId: block.match(/<artifactId>([^<]+)<\/artifactId>/)?.[1],
|
|
68
|
+
version: block.match(/<version>([^<]+)<\/version>/)?.[1],
|
|
69
|
+
}))
|
|
70
|
+
.filter(({ groupId, artifactId, version }) => groupId && artifactId && version)
|
|
71
|
+
.map(({ groupId, artifactId, version }) => ({
|
|
72
|
+
ecosystem: 'Maven',
|
|
73
|
+
package: `${groupId}:${artifactId}`,
|
|
74
|
+
currentVersion: version,
|
|
75
|
+
manifestFile,
|
|
76
|
+
}));
|
|
77
|
+
return deps;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const PARSERS = [
|
|
81
|
+
{ file: 'package.json', parser: parseNpmManifest },
|
|
82
|
+
{ file: 'requirements.txt', parser: parsePythonManifest },
|
|
83
|
+
{ file: 'go.mod', parser: parseGoManifest },
|
|
84
|
+
{ file: 'pom.xml', parser: parseMavenManifest },
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {string} repoPath
|
|
89
|
+
* @returns {Array} every direct dependency found, across all supported ecosystems
|
|
90
|
+
*/
|
|
91
|
+
function listDirectDependencies(repoPath) {
|
|
92
|
+
const deps = [];
|
|
93
|
+
for (const { file, parser } of PARSERS) {
|
|
94
|
+
deps.push(...parser(repoPath, file));
|
|
95
|
+
}
|
|
96
|
+
return deps;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { listDirectDependencies, parseNpmManifest, parsePythonManifest, parseGoManifest, parseMavenManifest };
|