dependencyiq 2.1.0 ā 2.2.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/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -72,7 +72,7 @@ async function analyzeRepository(repoPath, projectId, options = {}) {
|
|
|
72
72
|
|
|
73
73
|
console.log('\nš Analyzing blast radius with GitLab Orbit...');
|
|
74
74
|
for (const vuln of vulnerabilities) {
|
|
75
|
-
const exposure = await analyzeExposure(projectId, vuln.package,
|
|
75
|
+
const exposure = await analyzeExposure(projectId, vuln.package, vuln.ecosystem, pathHints);
|
|
76
76
|
|
|
77
77
|
vuln.riskScore = calculateRiskScore(vuln, {
|
|
78
78
|
affectedFilesCount: exposure.affectedFilesCount,
|
|
@@ -86,7 +86,7 @@ async function analyzeRepository(repoPath, projectId, options = {}) {
|
|
|
86
86
|
vuln.affectedFiles = exposure.affectedFiles;
|
|
87
87
|
vuln.exposure = exposure;
|
|
88
88
|
vuln.recommendation = vuln.riskScore.recommendation;
|
|
89
|
-
vuln.dependencyChain = resolveDependencyChain(repoPath, vuln.ecosystem, vuln.package);
|
|
89
|
+
vuln.dependencyChain = resolveDependencyChain(repoPath, vuln.ecosystem, vuln.package, vuln.currentVersion);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
console.log('\nšÆ Ranking by actual risk...');
|
package/src/blastRadius.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const orbitClient = require('./orbitClient');
|
|
10
|
+
const { hasAnyToken } = require('./gitlabAuth');
|
|
10
11
|
|
|
11
12
|
// Built-in heuristic defaults, used when AGENTS.md doesn't specify
|
|
12
13
|
// public_api_paths / test_paths. A project with a non-standard layout
|
|
@@ -39,7 +40,12 @@ let orbitReachable = null; // cached per-process: null = unknown, true/false onc
|
|
|
39
40
|
|
|
40
41
|
async function isOrbitReachable() {
|
|
41
42
|
if (orbitReachable !== null) return orbitReachable;
|
|
42
|
-
|
|
43
|
+
// CI_JOB_TOKEN (auto-present in every CI job) is sufficient for this
|
|
44
|
+
// same-project read call ā see gitlabAuth.js's getAuthHeaders, which
|
|
45
|
+
// already falls back to it. Requiring GITLAB_TOKEN specifically here
|
|
46
|
+
// reported Orbit as unavailable in every normal CI/Flow run that never
|
|
47
|
+
// set that CI/CD variable, even when Orbit was actually healthy.
|
|
48
|
+
if (!hasAnyToken()) {
|
|
43
49
|
orbitReachable = false;
|
|
44
50
|
return false;
|
|
45
51
|
}
|
|
@@ -99,7 +105,7 @@ const ECOSYSTEM_LANGUAGE = {
|
|
|
99
105
|
Packagist: 'php',
|
|
100
106
|
};
|
|
101
107
|
|
|
102
|
-
async function analyzeExposure(projectId, packageName, hints = {}
|
|
108
|
+
async function analyzeExposure(projectId, packageName, ecosystem, hints = {}) {
|
|
103
109
|
if (projectId && await isOrbitReachable()) {
|
|
104
110
|
try {
|
|
105
111
|
const files = await orbitClient.findPackageImporters(projectId, packageName);
|
|
@@ -160,15 +160,45 @@ function buildPoetryGraph(repoPath) {
|
|
|
160
160
|
return { directDeps, lookup };
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/**
|
|
164
|
+
* The hoisted top-level lockfile entry only ever records ONE resolved
|
|
165
|
+
* version of `packageName`. When OSV flags a version that doesn't match
|
|
166
|
+
* that top-level entry, the vulnerable copy is a separate nested
|
|
167
|
+
* duplicate the hoisted lookup never sees (see module docstring) ā find
|
|
168
|
+
* it by scanning every `node_modules/.../node_modules/<packageName>`
|
|
169
|
+
* path for one whose `version` matches, and return its immediate parent
|
|
170
|
+
* package name so the caller can scope an `overrides` entry to that
|
|
171
|
+
* subtree instead of the (already-safe) top-level package.
|
|
172
|
+
* @returns {string|null} immediate parent package name, or null if no
|
|
173
|
+
* nested duplicate at that exact version was found
|
|
174
|
+
*/
|
|
175
|
+
function findNestedDuplicateParent(lockfile, packageName, vulnerableVersion) {
|
|
176
|
+
if (!lockfile?.packages || !vulnerableVersion) return null;
|
|
177
|
+
const suffix = `/node_modules/${packageName}`;
|
|
178
|
+
for (const [pkgPath, entry] of Object.entries(lockfile.packages)) {
|
|
179
|
+
const isNestedDuplicate = pkgPath !== `node_modules/${packageName}` && pkgPath.endsWith(suffix);
|
|
180
|
+
if (isNestedDuplicate && entry.version === vulnerableVersion) {
|
|
181
|
+
const parentPath = pkgPath.slice(0, -suffix.length);
|
|
182
|
+
const lastSegment = parentPath.split('node_modules/').pop();
|
|
183
|
+
if (lastSegment) return lastSegment;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
163
189
|
/**
|
|
164
190
|
* @param {string} repoPath
|
|
165
191
|
* @param {string} ecosystem
|
|
166
192
|
* @param {string} packageName
|
|
193
|
+
* @param {string} [vulnerableVersion] - the specific version OSV flagged;
|
|
194
|
+
* when it disagrees with the top-level hoisted resolution, this looks
|
|
195
|
+
* for a nested duplicate at that version instead of trusting the
|
|
196
|
+
* "direct dependency" name match (see findNestedDuplicateParent)
|
|
167
197
|
* @returns {Object} { available, isDirect, chain } ā chain is null when
|
|
168
198
|
* not determinable (unsupported ecosystem, no lockfile, or not found
|
|
169
199
|
* within the BFS depth limit)
|
|
170
200
|
*/
|
|
171
|
-
function resolveDependencyChain(repoPath, ecosystem, packageName) {
|
|
201
|
+
function resolveDependencyChain(repoPath, ecosystem, packageName, vulnerableVersion) {
|
|
172
202
|
if (ecosystem === 'npm') {
|
|
173
203
|
const lockfile = loadNpmLockfile(repoPath);
|
|
174
204
|
if (!lockfile) {
|
|
@@ -179,6 +209,13 @@ function resolveDependencyChain(repoPath, ecosystem, packageName) {
|
|
|
179
209
|
return { available: false, isDirect: null, chain: null, reason: 'Unsupported or malformed lockfile format' };
|
|
180
210
|
}
|
|
181
211
|
if (graph.directDeps.includes(packageName)) {
|
|
212
|
+
const topLevelEntry = lockfile.packages[`node_modules/${packageName}`];
|
|
213
|
+
if (vulnerableVersion && topLevelEntry && topLevelEntry.version !== vulnerableVersion) {
|
|
214
|
+
const parent = findNestedDuplicateParent(lockfile, packageName, vulnerableVersion);
|
|
215
|
+
if (parent) {
|
|
216
|
+
return { available: true, isDirect: false, chain: [parent, packageName], nestedDuplicateOfDirectDep: true };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
182
219
|
return { available: true, isDirect: true, chain: [packageName] };
|
|
183
220
|
}
|
|
184
221
|
return { available: true, isDirect: false, chain: findShortestChain(graph, packageName) };
|
|
@@ -220,6 +257,7 @@ module.exports = {
|
|
|
220
257
|
resolveDependencyChain,
|
|
221
258
|
buildNpmGraph,
|
|
222
259
|
findShortestChain,
|
|
260
|
+
findNestedDuplicateParent,
|
|
223
261
|
getLockfileEntry,
|
|
224
262
|
normalizePyPiName,
|
|
225
263
|
loadPoetryDirectDeps,
|
|
@@ -55,10 +55,41 @@ function transformNpmContent(content, packageName, fixedVersion) {
|
|
|
55
55
|
* still pick a parent-compatible version, just never one below the
|
|
56
56
|
* patched floor.
|
|
57
57
|
*/
|
|
58
|
-
function addNpmOverrideContent(content, packageName, floorVersion) {
|
|
58
|
+
function addNpmOverrideContent(content, packageName, floorVersion, parentPackage) {
|
|
59
59
|
const pkg = JSON.parse(content);
|
|
60
60
|
pkg.overrides = pkg.overrides || {};
|
|
61
61
|
const constraint = `>=${floorVersion}`;
|
|
62
|
+
const isAlsoDirectDep = ['dependencies', 'devDependencies', 'optionalDependencies']
|
|
63
|
+
.some((section) => pkg[section]?.[packageName]);
|
|
64
|
+
|
|
65
|
+
// A flat top-level override (`overrides: { js-yaml: ">=x" }`) collides
|
|
66
|
+
// with npm's own resolution of a same-named DIRECT dependency ā npm
|
|
67
|
+
// rejects that combination at install time with EOVERRIDE ("conflicts
|
|
68
|
+
// with direct dependency"), even when the constraint is compatible.
|
|
69
|
+
// When the flagged vulnerable copy is nested under a known parent
|
|
70
|
+
// (the common case: a transitive dev-tool dependency shadows a
|
|
71
|
+
// package you also depend on directly at a safe version), scope the
|
|
72
|
+
// override to that parent instead ā it only affects that subtree and
|
|
73
|
+
// never touches the direct dependency's own resolution.
|
|
74
|
+
if (isAlsoDirectDep && !parentPackage) {
|
|
75
|
+
return { touched: false, warning: `${packageName} is already a direct dependency, and the vulnerable copy's parent package is unknown ā a flat "overrides" entry would conflict with npm's EOVERRIDE check; resolve the dependency chain first or add a scoped override (overrides: { "<parent>": { "${packageName}": "${constraint}" } }) manually` };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (parentPackage) {
|
|
79
|
+
pkg.overrides[parentPackage] = pkg.overrides[parentPackage] || {};
|
|
80
|
+
if (pkg.overrides[parentPackage][packageName] === constraint) {
|
|
81
|
+
return { touched: false, warning: `package.json already overrides ${packageName} to ${constraint} under ${parentPackage}` };
|
|
82
|
+
}
|
|
83
|
+
pkg.overrides[parentPackage][packageName] = constraint;
|
|
84
|
+
return {
|
|
85
|
+
touched: true,
|
|
86
|
+
action: 'override',
|
|
87
|
+
content: JSON.stringify(pkg, null, 2) + '\n',
|
|
88
|
+
followUp: 'npm install (regenerates package-lock.json with the override applied)',
|
|
89
|
+
warning: `Forced ${packageName} to ${constraint} within ${parentPackage}'s dependency subtree via package.json "overrides" ā verify the resolved tree with "npm ls ${packageName}" after install`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
62
93
|
if (pkg.overrides[packageName] === constraint) {
|
|
63
94
|
return { touched: false, warning: `package.json already overrides ${packageName} to ${constraint}` };
|
|
64
95
|
}
|
|
@@ -251,10 +282,10 @@ function bumpNpm(repoPath, manifestRelPath, packageName, fixedVersion) {
|
|
|
251
282
|
return applyContentResultToFile(repoPath, manifestRelPath, 'package.json', transformNpmContent(content, packageName, fixedVersion));
|
|
252
283
|
}
|
|
253
284
|
|
|
254
|
-
function overrideNpm(repoPath, manifestRelPath, packageName, floorVersion) {
|
|
285
|
+
function overrideNpm(repoPath, manifestRelPath, packageName, floorVersion, parentPackage) {
|
|
255
286
|
const content = readManifest(repoPath, manifestRelPath, 'package.json');
|
|
256
287
|
if (content === null) return { applied: false, warning: 'package.json not found' };
|
|
257
|
-
return applyContentResultToFile(repoPath, manifestRelPath, 'package.json', addNpmOverrideContent(content, packageName, floorVersion));
|
|
288
|
+
return applyContentResultToFile(repoPath, manifestRelPath, 'package.json', addNpmOverrideContent(content, packageName, floorVersion, parentPackage));
|
|
258
289
|
}
|
|
259
290
|
|
|
260
291
|
function bumpPython(repoPath, manifestRelPath, packageName, fixedVersion) {
|
|
@@ -330,7 +361,8 @@ function applyFix(repoPath, vulnerability) {
|
|
|
330
361
|
const chain = vulnerability.dependencyChain;
|
|
331
362
|
if (vulnerability.ecosystem === 'npm' && chain?.available && chain.isDirect === false) {
|
|
332
363
|
const floor = vulnerability.osvFloorVersion || vulnerability.fixedVersion;
|
|
333
|
-
|
|
364
|
+
const parent = chain.chain?.length >= 2 ? chain.chain[chain.chain.length - 2] : undefined;
|
|
365
|
+
return overrideNpm(repoPath, vulnerability.manifestFile, vulnerability.package, floor, parent);
|
|
334
366
|
}
|
|
335
367
|
|
|
336
368
|
const fixers = { npm: bumpNpm, PyPI: bumpPython, Go: bumpGo, Maven: bumpMaven };
|
package/src/strategyGenerator.js
CHANGED
|
@@ -16,6 +16,11 @@ function generateRefactoringAnalysis(vulnerability, codeContext = {}) {
|
|
|
16
16
|
usageExamples: [],
|
|
17
17
|
...codeContext
|
|
18
18
|
};
|
|
19
|
+
const orbitChecked = vulnerability.riskScore?.exposureDataSource === 'orbit'
|
|
20
|
+
|| vulnerability.exposure?.source === 'orbit';
|
|
21
|
+
const noFilesNote = orbitChecked
|
|
22
|
+
? ' - GitLab Orbit confirmed no importers of this package in this project'
|
|
23
|
+
: ' - Orbit exposure data unavailable for this project ā file list unknown';
|
|
19
24
|
|
|
20
25
|
return `
|
|
21
26
|
# Refactoring Analysis: ${vulnerability.package}
|
|
@@ -28,14 +33,14 @@ function generateRefactoringAnalysis(vulnerability, codeContext = {}) {
|
|
|
28
33
|
- **Issue**: ${vulnerability.vulnerability}
|
|
29
34
|
|
|
30
35
|
## Code Impact
|
|
31
|
-
- **Files affected**: ${context.affectedFiles?.length ||
|
|
32
|
-
${context.affectedFiles?.map(f => ` - ${f.path || f}`).join('\n')
|
|
33
|
-
|
|
36
|
+
- **Files affected**: ${context.affectedFiles?.length || 0}
|
|
37
|
+
${context.affectedFiles?.length ? context.affectedFiles.map(f => ` - ${f.path || f}`).join('\n') : noFilesNote}
|
|
38
|
+
${context.usageExamples?.[0] ? `
|
|
34
39
|
## Usage Example
|
|
35
40
|
\`\`\`javascript
|
|
36
|
-
${context.usageExamples
|
|
41
|
+
${context.usageExamples[0]}
|
|
37
42
|
\`\`\`
|
|
38
|
-
|
|
43
|
+
` : ''}
|
|
39
44
|
## Task for GitLab Duo Chat
|
|
40
45
|
|
|
41
46
|
Generate **3 upgrade strategies** ranked by safety vs speed:
|
|
@@ -155,8 +160,8 @@ ${vulnerabilities.slice(0, 3).map((v, i) => `
|
|
|
155
160
|
- **Risk Score**: ${v.riskScore?.score}/100 (${v.riskScore?.priority})
|
|
156
161
|
- **Issue**: ${v.vulnerability}
|
|
157
162
|
- **Severity**: ${v.severity} (CVSS ${v.cvss})
|
|
158
|
-
- **Files Affected**: ${v.affectedFiles?.length ||
|
|
159
|
-
${v.affectedFiles?.slice(0, 2).map(f => ` - ${f.path || f}`).join('\n')
|
|
163
|
+
- **Files Affected**: ${v.affectedFiles?.length || 0}
|
|
164
|
+
${v.affectedFiles?.length ? v.affectedFiles.slice(0, 2).map(f => ` - ${f.path || f}`).join('\n') : (v.riskScore?.exposureDataSource === 'orbit' ? ' - GitLab Orbit confirmed no importers' : ' - Orbit exposure data unavailable')}
|
|
160
165
|
`).join('\n')}
|
|
161
166
|
|
|
162
167
|
## Recommendation
|
|
@@ -199,7 +204,7 @@ ${icon} **${index}. ${vuln.package} ā ${vuln.fixedVersion}**
|
|
|
199
204
|
| **Issue** | ${vuln.vulnerability} |
|
|
200
205
|
| **Severity** | ${vuln.severity} (CVSS ${vuln.cvss}) |
|
|
201
206
|
| **Your Risk Score** | ${vuln.riskScore?.score || '?'}/100 (${vuln.riskScore?.priority}) |
|
|
202
|
-
| **Files Affected** | ${vuln.affectedFiles?.length ||
|
|
207
|
+
| **Files Affected** | ${vuln.affectedFiles?.length || 0} |
|
|
203
208
|
| **Exposed to API?** | ${vuln.riskScore?.isInPublicAPI ? 'ā
Yes' : 'ā No'} |
|
|
204
209
|
| **Effort to Fix** | ${vuln.riskScore?.effortMinutes ? Math.ceil(vuln.riskScore.effortMinutes / 60) + 'h' : '?'} |
|
|
205
210
|
`;
|