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,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strategy Generator (GitLab-native)
|
|
3
|
+
* Generates analysis summaries and instructions for GitLab Duo Agentic Chat
|
|
4
|
+
* Uses GitLab's built-in AI models via the Agent Platform
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate refactoring strategy analysis for GitLab Chat
|
|
9
|
+
* @param {Object} vulnerability - Vulnerability object
|
|
10
|
+
* @param {Object} codeContext - Code usage information
|
|
11
|
+
* @returns {string} Analysis prompt for GitLab Chat
|
|
12
|
+
*/
|
|
13
|
+
function generateRefactoringAnalysis(vulnerability, codeContext = {}) {
|
|
14
|
+
const context = {
|
|
15
|
+
affectedFiles: [],
|
|
16
|
+
usageExamples: [],
|
|
17
|
+
...codeContext
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return `
|
|
21
|
+
# Refactoring Analysis: ${vulnerability.package}
|
|
22
|
+
|
|
23
|
+
## Vulnerability Details
|
|
24
|
+
- **Package**: ${vulnerability.package}
|
|
25
|
+
- **Current**: ${vulnerability.currentVersion}
|
|
26
|
+
- **Target**: ${vulnerability.fixedVersion}
|
|
27
|
+
- **Severity**: ${vulnerability.severity} (CVSS ${vulnerability.cvss})
|
|
28
|
+
- **Issue**: ${vulnerability.vulnerability}
|
|
29
|
+
|
|
30
|
+
## Code Impact
|
|
31
|
+
- **Files affected**: ${context.affectedFiles?.length || 1}
|
|
32
|
+
${context.affectedFiles?.map(f => ` - ${f.path || f}`).join('\n') || ' - no Orbit exposure data available'}
|
|
33
|
+
|
|
34
|
+
## Usage Example
|
|
35
|
+
\`\`\`javascript
|
|
36
|
+
${context.usageExamples?.[0] || 'const merged = _.merge({}, defaults, userInput);'}
|
|
37
|
+
\`\`\`
|
|
38
|
+
|
|
39
|
+
## Task for GitLab Duo Chat
|
|
40
|
+
|
|
41
|
+
Generate **3 upgrade strategies** ranked by safety vs speed:
|
|
42
|
+
|
|
43
|
+
### Strategy 1: Safest Approach
|
|
44
|
+
- What code changes are needed?
|
|
45
|
+
- Why is this the safest?
|
|
46
|
+
- What testing is required?
|
|
47
|
+
- Estimated time to implement?
|
|
48
|
+
- Best for: Large teams, critical services?
|
|
49
|
+
|
|
50
|
+
### Strategy 2: Recommended Approach (Balanced)
|
|
51
|
+
- What changes would you recommend?
|
|
52
|
+
- Why balance safety and speed?
|
|
53
|
+
- What's the testing strategy?
|
|
54
|
+
- Estimated time?
|
|
55
|
+
- Best for: Most development teams?
|
|
56
|
+
|
|
57
|
+
### Strategy 3: Fastest Approach
|
|
58
|
+
- What's the minimal viable change?
|
|
59
|
+
- What risks exist with this approach?
|
|
60
|
+
- How to mitigate those risks?
|
|
61
|
+
- Estimated time?
|
|
62
|
+
- Best for: Non-critical services, internal tools?
|
|
63
|
+
|
|
64
|
+
For each strategy, provide:
|
|
65
|
+
- Complete code examples
|
|
66
|
+
- Rationale for the approach
|
|
67
|
+
- Success criteria
|
|
68
|
+
- Potential gotchas
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Generate migration timeline analysis for GitLab Chat
|
|
74
|
+
* @param {Array} vulnerabilities - Vulnerabilities with risk scores
|
|
75
|
+
* @returns {string} Timeline planning prompt
|
|
76
|
+
*/
|
|
77
|
+
function generateMigrationTimeline(vulnerabilities = []) {
|
|
78
|
+
const vulnList = vulnerabilities
|
|
79
|
+
.slice(0, 5)
|
|
80
|
+
.map((v, i) => `${i + 1}. **${v.package}** (Risk ${v.riskScore?.score || '?'}/100, ${v.severity})\n - Issue: ${v.vulnerability}`)
|
|
81
|
+
.join('\n');
|
|
82
|
+
|
|
83
|
+
return `
|
|
84
|
+
# Vulnerability Migration Timeline
|
|
85
|
+
|
|
86
|
+
## Vulnerabilities to Address (Priority Order)
|
|
87
|
+
${vulnList}
|
|
88
|
+
|
|
89
|
+
## Task: Create Phased Migration Plan
|
|
90
|
+
|
|
91
|
+
### Phase 1: This Week (Most Urgent)
|
|
92
|
+
- Which vulnerabilities should we fix first?
|
|
93
|
+
- Why prioritize these?
|
|
94
|
+
- Estimated team effort?
|
|
95
|
+
- Testing and QA strategy?
|
|
96
|
+
- Deployment approach (direct, canary, feature flag)?
|
|
97
|
+
- What's the rollback procedure?
|
|
98
|
+
|
|
99
|
+
### Phase 2: Next Week (High Priority)
|
|
100
|
+
- Next batch of vulnerabilities?
|
|
101
|
+
- Expected effort hours?
|
|
102
|
+
- Testing approach?
|
|
103
|
+
- Deployment strategy?
|
|
104
|
+
- Any dependencies between Phase 1 & 2?
|
|
105
|
+
|
|
106
|
+
### Phase 3: Following Week+ (Medium/Low Priority)
|
|
107
|
+
- Remaining vulnerabilities to fix?
|
|
108
|
+
- Nice-to-have improvements?
|
|
109
|
+
- Documentation updates needed?
|
|
110
|
+
- Estimated total effort?
|
|
111
|
+
|
|
112
|
+
## For All Phases
|
|
113
|
+
|
|
114
|
+
- Why prioritize in this specific order?
|
|
115
|
+
- What risks if we delay any phase?
|
|
116
|
+
- How do we measure success?
|
|
117
|
+
- Team communication plan?
|
|
118
|
+
- How to track completion?
|
|
119
|
+
|
|
120
|
+
Provide realistic estimates based on:
|
|
121
|
+
- Team size and velocity
|
|
122
|
+
- Complexity of each change
|
|
123
|
+
- Testing requirements
|
|
124
|
+
- Deployment procedures
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Generate executive summary of vulnerability analysis
|
|
130
|
+
* @param {Array} vulnerabilities - Analyzed vulnerabilities
|
|
131
|
+
* @returns {string} Summary for quick review
|
|
132
|
+
*/
|
|
133
|
+
function generateAnalysisSummary(vulnerabilities = []) {
|
|
134
|
+
if (!vulnerabilities || vulnerabilities.length === 0) {
|
|
135
|
+
return '✅ No vulnerabilities found in this project.';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const urgent = vulnerabilities.filter(v => v.riskScore?.priority === 'URGENT');
|
|
139
|
+
const high = vulnerabilities.filter(v => v.riskScore?.priority === 'HIGH');
|
|
140
|
+
const medium = vulnerabilities.filter(v => v.riskScore?.priority === 'MEDIUM');
|
|
141
|
+
|
|
142
|
+
let summary = `
|
|
143
|
+
# Vulnerability Analysis Summary
|
|
144
|
+
|
|
145
|
+
## Overview
|
|
146
|
+
- **Total Found**: ${vulnerabilities.length}
|
|
147
|
+
- 🔴 **URGENT** (Risk 80-100): ${urgent.length}
|
|
148
|
+
- 🟠 **HIGH** (Risk 50-79): ${high.length}
|
|
149
|
+
- 🟡 **MEDIUM** (Risk 20-49): ${medium.length}
|
|
150
|
+
|
|
151
|
+
## Top 3 Priorities
|
|
152
|
+
|
|
153
|
+
${vulnerabilities.slice(0, 3).map((v, i) => `
|
|
154
|
+
### ${i + 1}. ${v.package}
|
|
155
|
+
- **Risk Score**: ${v.riskScore?.score}/100 (${v.riskScore?.priority})
|
|
156
|
+
- **Issue**: ${v.vulnerability}
|
|
157
|
+
- **Severity**: ${v.severity} (CVSS ${v.cvss})
|
|
158
|
+
- **Files Affected**: ${v.affectedFiles?.length || 1}
|
|
159
|
+
${v.affectedFiles?.slice(0, 2).map(f => ` - ${f.path || f}`).join('\n') || ' - internal usage'}
|
|
160
|
+
`).join('\n')}
|
|
161
|
+
|
|
162
|
+
## Recommendation
|
|
163
|
+
|
|
164
|
+
✅ **Focus on URGENT vulnerabilities first** (${urgent.length} found)
|
|
165
|
+
- These have actual code exposure in your project
|
|
166
|
+
- Not just generic CVSS scores—real risk to your systems
|
|
167
|
+
|
|
168
|
+
🎯 **Expect** to fix all urgents + highs in about 1-2 weeks
|
|
169
|
+
- Phase 1 (this week): URGENT items
|
|
170
|
+
- Phase 2 (next week): HIGH items
|
|
171
|
+
- Phase 3 (ongoing): MEDIUM + LOW
|
|
172
|
+
|
|
173
|
+
💡 **Next Step**: Ask GitLab Duo Chat for specific upgrade strategies
|
|
174
|
+
- It will analyze your code and suggest safe refactoring approaches
|
|
175
|
+
- It can generate migration plans and timelines
|
|
176
|
+
- Ask it to create a draft MR for Phase 1
|
|
177
|
+
`;
|
|
178
|
+
|
|
179
|
+
return summary;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Format a vulnerability for human-readable display
|
|
184
|
+
* @param {Object} vuln - Vulnerability object
|
|
185
|
+
* @param {number} index - Position in list
|
|
186
|
+
* @returns {string} Formatted display string
|
|
187
|
+
*/
|
|
188
|
+
function formatVulnerabilityCard(vuln, index = 1) {
|
|
189
|
+
const icon = vuln.riskScore?.priority === 'URGENT' ? '🔴'
|
|
190
|
+
: vuln.riskScore?.priority === 'HIGH' ? '🟠'
|
|
191
|
+
: vuln.riskScore?.priority === 'MEDIUM' ? '🟡' : '🟢';
|
|
192
|
+
|
|
193
|
+
return `
|
|
194
|
+
${icon} **${index}. ${vuln.package} → ${vuln.fixedVersion}**
|
|
195
|
+
|
|
196
|
+
| Field | Value |
|
|
197
|
+
|-------|-------|
|
|
198
|
+
| **Current** | ${vuln.currentVersion} |
|
|
199
|
+
| **Issue** | ${vuln.vulnerability} |
|
|
200
|
+
| **Severity** | ${vuln.severity} (CVSS ${vuln.cvss}) |
|
|
201
|
+
| **Your Risk Score** | ${vuln.riskScore?.score || '?'}/100 (${vuln.riskScore?.priority}) |
|
|
202
|
+
| **Files Affected** | ${vuln.affectedFiles?.length || 1} |
|
|
203
|
+
| **Exposed to API?** | ${vuln.riskScore?.isInPublicAPI ? '✅ Yes' : '❌ No'} |
|
|
204
|
+
| **Effort to Fix** | ${vuln.riskScore?.effortMinutes ? Math.ceil(vuln.riskScore.effortMinutes / 60) + 'h' : '?'} |
|
|
205
|
+
`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Generate a merge request description (deterministic template — no AI
|
|
210
|
+
* call). Deeper strategy reasoning is available interactively by asking
|
|
211
|
+
* the DependencyIQ GitLab Duo agent in Chat, which uses GitLab's managed
|
|
212
|
+
* model rather than an external API key.
|
|
213
|
+
* @param {Object} vulnerability - vulnerability being fixed
|
|
214
|
+
* @param {Object} fixResult - result from ecosystemFixers.applyFix
|
|
215
|
+
* @returns {string} PR description markdown
|
|
216
|
+
*/
|
|
217
|
+
function generatePRDescription(vulnerability, fixResult = {}) {
|
|
218
|
+
if (fixResult.action === 'remove') {
|
|
219
|
+
return `# Cleanup: Remove unused dependency ${vulnerability.package}
|
|
220
|
+
|
|
221
|
+
## Summary
|
|
222
|
+
\`${vulnerability.package}\` (${vulnerability.ecosystem}) has a known vulnerability — **${vulnerability.vulnerability}** (${vulnerability.id}) — but **GitLab Orbit confirms zero files in this project import it**. Patching a CVE in code nothing uses doesn't reduce real risk, so this removes the dependency entirely instead of bumping its version.
|
|
223
|
+
|
|
224
|
+
- **Vulnerability that prompted this**: ${vulnerability.id} (${vulnerability.severity}, CVSS ${vulnerability.cvss})
|
|
225
|
+
- **Exposure data**: GitLab Orbit blast-radius query — 0 importers found
|
|
226
|
+
- **Why removal instead of upgrade**: nothing to break by removing it; upgrading would just carry a version of a package you don't use
|
|
227
|
+
|
|
228
|
+
## Changes
|
|
229
|
+
- Removed \`${vulnerability.package}\` from \`${fixResult.filePath || 'manifest'}\`.
|
|
230
|
+
${fixResult.followUp ? `- **Follow-up required**: run \`${fixResult.followUp}\` to refresh the lockfile before merging.` : ''}
|
|
231
|
+
${fixResult.warning ? `- ⚠️ ${fixResult.warning}` : ''}
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
Generated by DependencyIQ using a GitLab Orbit blast-radius query — not a guess based on CVSS alone.
|
|
235
|
+
`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const sel = vulnerability.versionSelection;
|
|
239
|
+
const floor = vulnerability.osvFloorVersion;
|
|
240
|
+
const isOverride = fixResult.action === 'override';
|
|
241
|
+
|
|
242
|
+
// "Which version?" section — only when a real decision was made (a smart
|
|
243
|
+
// target above the OSV floor, or a major-crossing flag). Shows the
|
|
244
|
+
// reasoning instead of silently taking OSV's minimum.
|
|
245
|
+
let versionSection = '';
|
|
246
|
+
if (sel?.available && (sel.recommendedVersion !== floor || sel.crossesMajor)) {
|
|
247
|
+
versionSection = `
|
|
248
|
+
|
|
249
|
+
## Version selection
|
|
250
|
+
- **OSV security floor**: ${floor} (minimum version that patches the advisory)
|
|
251
|
+
- **Chosen target**: ${vulnerability.fixedVersion}${sel.settled ? ` — settled (${sel.ageDays}d since release)` : ' — note: recently published'}
|
|
252
|
+
- **Reasoning**: ${sel.rationale}${sel.crossesMajor ? '\n- ⚠️ **Crosses a major version** — review for breaking changes before merging.' : ''}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const changesSection = isOverride
|
|
256
|
+
? `## Changes
|
|
257
|
+
- ${vulnerability.package} is a **transitive** dependency (pulled in via ${vulnerability.dependencyChain?.chain ? vulnerability.dependencyChain.chain.join(' → ') : 'another dependency'}), so there's no direct version line to bump.
|
|
258
|
+
- Added an \`overrides\` entry in \`${fixResult.filePath || 'package.json'}\` forcing ${vulnerability.package} to \`>=${floor || vulnerability.fixedVersion}\` (the patched floor) regardless of what the parent declares — the standard fix for the Log4j/Axios class of transitive incident.
|
|
259
|
+
${fixResult.followUp ? `- **Follow-up required**: run \`${fixResult.followUp}\`.` : ''}
|
|
260
|
+
${fixResult.warning ? `- ⚠️ ${fixResult.warning}` : ''}`
|
|
261
|
+
: `## Changes
|
|
262
|
+
- Updated \`${fixResult.filePath || 'manifest'}\` to pin ${vulnerability.package} to ${vulnerability.fixedVersion}.
|
|
263
|
+
${fixResult.followUp ? `- **Follow-up required**: run \`${fixResult.followUp}\` to refresh the lockfile before merging.` : ''}
|
|
264
|
+
${fixResult.warning ? `- ⚠️ ${fixResult.warning}` : ''}`;
|
|
265
|
+
|
|
266
|
+
return `# Security Update: ${vulnerability.package}
|
|
267
|
+
|
|
268
|
+
## Summary
|
|
269
|
+
Fixes **${vulnerability.vulnerability}** (${vulnerability.id}) in ${vulnerability.package} (${vulnerability.ecosystem})${isOverride ? ' — a transitive dependency, fixed via an npm override' : ''}
|
|
270
|
+
|
|
271
|
+
- **Current**: ${vulnerability.currentVersion}
|
|
272
|
+
- **Target**: ${isOverride ? `>=${floor || vulnerability.fixedVersion} (transitive override)` : vulnerability.fixedVersion}
|
|
273
|
+
- **Severity**: ${vulnerability.severity} (CVSS ${vulnerability.cvss})
|
|
274
|
+
- **Risk Score**: ${vulnerability.riskScore?.score ?? 'N/A'}/100 (${vulnerability.riskScore?.priority ?? 'unscored'})
|
|
275
|
+
- **Exposure data**: ${vulnerability.riskScore?.exposureDataSource === 'orbit' ? 'GitLab Orbit blast-radius query' : 'unavailable — Orbit not enabled/reachable for this project'}${versionSection}
|
|
276
|
+
|
|
277
|
+
${changesSection}
|
|
278
|
+
|
|
279
|
+
## References
|
|
280
|
+
${(vulnerability.references || []).slice(0, 3).map(r => `- ${r}`).join('\n') || '- ' + vulnerability.cveLink}
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
Generated by DependencyIQ. Ask the DependencyIQ agent in GitLab Duo Chat for upgrade strategies or a phased migration plan for the remaining findings.
|
|
284
|
+
`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Generate a structured, ordered migration plan for one upgrade, using
|
|
289
|
+
* the impact report's real affected-file count and effort estimate
|
|
290
|
+
* rather than a generic template. Deterministic — no AI call.
|
|
291
|
+
* @param {Object} impactReport - result from impactReport.buildImpactReport
|
|
292
|
+
* @returns {Object} { steps: [{order, title, detail}], estimatedHours }
|
|
293
|
+
*/
|
|
294
|
+
function generateStructuredMigrationPlan(impactReport) {
|
|
295
|
+
const steps = [];
|
|
296
|
+
let order = 0;
|
|
297
|
+
const nextOrder = () => { order += 1; return order; };
|
|
298
|
+
const { simulation } = impactReport;
|
|
299
|
+
|
|
300
|
+
if (simulation) {
|
|
301
|
+
steps.push({
|
|
302
|
+
order: nextOrder(),
|
|
303
|
+
title: 'Review upgrade impact simulation',
|
|
304
|
+
detail: `${simulation.difficulty} difficulty; ${simulation.relationship} dependency; ${simulation.affectedServicesCount} affected service area(s). Automation mode: ${simulation.automationMode}.`,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (impactReport.affectedFilesCount > 0) {
|
|
309
|
+
steps.push({
|
|
310
|
+
order: nextOrder(),
|
|
311
|
+
title: 'Review affected call sites',
|
|
312
|
+
detail: `${impactReport.affectedFilesCount} file(s) import ${impactReport.package} — read each one to confirm how it's used before changing the version.`,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (impactReport.likelyBreaking) {
|
|
317
|
+
steps.push({
|
|
318
|
+
order: nextOrder(),
|
|
319
|
+
title: 'Check for breaking changes',
|
|
320
|
+
detail: `${impactReport.breakingChangeReasoning} Read the package's changelog/release notes for ${impactReport.from} → ${impactReport.to} before touching code.`,
|
|
321
|
+
});
|
|
322
|
+
steps.push({
|
|
323
|
+
order: nextOrder(),
|
|
324
|
+
title: 'Update call sites for the new major version',
|
|
325
|
+
detail: 'Adjust any usages flagged in the previous step to match the new API surface, guided by the changelog, not a guess.',
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
steps.push({
|
|
330
|
+
order: nextOrder(),
|
|
331
|
+
title: `Bump ${impactReport.package} to ${impactReport.to}`,
|
|
332
|
+
detail: 'Update the manifest (already done automatically if this came from `--fix`) and refresh the lockfile.',
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
steps.push({
|
|
336
|
+
order: nextOrder(),
|
|
337
|
+
title: 'Run the test suite',
|
|
338
|
+
detail: simulation?.safetyChecks?.length
|
|
339
|
+
? `Confirm nothing regressed before opening or merging the MR. Required checks: ${simulation.safetyChecks.join('; ')}.`
|
|
340
|
+
: 'Confirm nothing regressed before opening or merging the MR.',
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
if (impactReport.affectedFilesCount === 0) {
|
|
344
|
+
steps.unshift({
|
|
345
|
+
order: 0,
|
|
346
|
+
title: 'Confirm this dependency is actually unused',
|
|
347
|
+
detail: 'Orbit found zero importers — consider removing the dependency entirely instead of running this plan (see riskScore.recommendation).',
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
package: impactReport.package,
|
|
353
|
+
from: impactReport.from,
|
|
354
|
+
to: impactReport.to,
|
|
355
|
+
steps,
|
|
356
|
+
estimatedHours: impactReport.estimatedEffort.hours,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Render a structured migration plan as markdown.
|
|
362
|
+
*/
|
|
363
|
+
function formatMigrationPlan(plan) {
|
|
364
|
+
const stepLines = plan.steps
|
|
365
|
+
.map(s => `${s.order}. **${s.title}**\n ${s.detail}`)
|
|
366
|
+
.join('\n\n');
|
|
367
|
+
|
|
368
|
+
return `## Migration Plan: ${plan.package} ${plan.from} → ${plan.to}
|
|
369
|
+
|
|
370
|
+
${stepLines}
|
|
371
|
+
|
|
372
|
+
**Estimated effort**: ~${plan.estimatedHours}h
|
|
373
|
+
`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
module.exports = {
|
|
377
|
+
generateRefactoringAnalysis,
|
|
378
|
+
generateMigrationTimeline,
|
|
379
|
+
generateAnalysisSummary,
|
|
380
|
+
formatVulnerabilityCard,
|
|
381
|
+
generatePRDescription,
|
|
382
|
+
generateStructuredMigrationPlan,
|
|
383
|
+
formatMigrationPlan
|
|
384
|
+
};
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upgrade Impact Simulator.
|
|
3
|
+
*
|
|
4
|
+
* Turns the raw impact report into the engineering question leaders ask:
|
|
5
|
+
* "How painful will this remediation be, and what work should happen
|
|
6
|
+
* before the MR is merged?"
|
|
7
|
+
*
|
|
8
|
+
* This is evidence-based, not an API-diff oracle. It uses Orbit-derived
|
|
9
|
+
* affected files, dependency-chain data, exposure categories, and semver
|
|
10
|
+
* distance to identify likely change areas, blockers, checks, and a
|
|
11
|
+
* phased migration plan.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const SERVICE_ROOTS = ['services', 'apps', 'packages', 'workspaces'];
|
|
15
|
+
|
|
16
|
+
function filePathOf(file) {
|
|
17
|
+
return String(file?.path || file || '');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function inferServiceName(filePath) {
|
|
21
|
+
const normalized = filePath.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
22
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
23
|
+
if (parts.length >= 2 && SERVICE_ROOTS.includes(parts[0])) {
|
|
24
|
+
return `${parts[0]}/${parts[1]}`;
|
|
25
|
+
}
|
|
26
|
+
return 'current repository';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function unique(values) {
|
|
30
|
+
return [...new Set(values.filter(Boolean))];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hasPathMatch(paths, patterns) {
|
|
34
|
+
return paths.some(p => patterns.some(pattern => p.toLowerCase().includes(pattern)));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function dependencyRelationship(vulnerability) {
|
|
38
|
+
if (vulnerability.recommendation === 'remove') return 'unused';
|
|
39
|
+
const chain = vulnerability.dependencyChain;
|
|
40
|
+
if (!chain?.available) return 'unknown';
|
|
41
|
+
return chain.isDirect ? 'direct' : 'transitive';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function difficultyFor({
|
|
45
|
+
affectedFilesCount,
|
|
46
|
+
affectedServicesCount,
|
|
47
|
+
likelyBreaking,
|
|
48
|
+
relationship,
|
|
49
|
+
touchesPublicApi,
|
|
50
|
+
touchesAuth,
|
|
51
|
+
touchesConfig,
|
|
52
|
+
}) {
|
|
53
|
+
if (relationship === 'unused') return 'low';
|
|
54
|
+
if (likelyBreaking && (affectedFilesCount >= 30 || affectedServicesCount >= 5)) return 'critical';
|
|
55
|
+
if (touchesPublicApi && (affectedServicesCount >= 2 || likelyBreaking)) return 'high';
|
|
56
|
+
if (touchesAuth && touchesConfig && affectedServicesCount >= 2) return 'high';
|
|
57
|
+
if (likelyBreaking || affectedFilesCount >= 15 || affectedServicesCount >= 3) return 'high';
|
|
58
|
+
if (affectedFilesCount >= 5 || affectedServicesCount >= 2 || relationship === 'transitive') return 'medium';
|
|
59
|
+
return 'low';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function automationMode({ relationship, likelyBreaking, exposureDataSource }) {
|
|
63
|
+
if (relationship === 'unused') return 'remove';
|
|
64
|
+
if (exposureDataSource !== 'orbit') return 'needs-orbit-validation';
|
|
65
|
+
if (relationship === 'transitive') return 'override-and-review';
|
|
66
|
+
if (likelyBreaking) return 'migration-mr-with-review';
|
|
67
|
+
return 'auto-mr-ready';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildRequiredChanges(vulnerability, impactReport, context) {
|
|
71
|
+
const changes = [];
|
|
72
|
+
|
|
73
|
+
if (context.relationship === 'unused') {
|
|
74
|
+
return [{
|
|
75
|
+
title: 'Remove the dependency instead of upgrading it',
|
|
76
|
+
detail: 'Orbit confirmed zero importing files, so the safest remediation is deleting the dependency and refreshing the lockfile.',
|
|
77
|
+
confidence: 'high',
|
|
78
|
+
}];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (context.relationship === 'transitive') {
|
|
82
|
+
changes.push({
|
|
83
|
+
title: 'Patch the transitive dependency path',
|
|
84
|
+
detail: vulnerability.dependencyChain?.chain
|
|
85
|
+
? `Resolved chain: ${vulnerability.dependencyChain.chain.join(' -> ')}. Use an ecosystem override or upgrade the parent dependency that pulls it in.`
|
|
86
|
+
: 'The package is transitive, so there is no direct manifest line to bump. Force a secure resolver version or upgrade the parent dependency.',
|
|
87
|
+
confidence: 'high',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (impactReport.affectedFilesCount > 0) {
|
|
92
|
+
changes.push({
|
|
93
|
+
title: 'Review affected import sites',
|
|
94
|
+
detail: `${impactReport.affectedFilesCount} file(s) import this package across ${context.affectedServicesCount} service area(s). These are the files most likely to need code changes.`,
|
|
95
|
+
confidence: impactReport.exposureDataSource === 'orbit' ? 'high' : 'low',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (impactReport.likelyBreaking) {
|
|
100
|
+
changes.push({
|
|
101
|
+
title: 'Check release notes for removed or renamed APIs',
|
|
102
|
+
detail: impactReport.breakingChangeReasoning,
|
|
103
|
+
confidence: 'medium',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (context.touchesPublicApi) {
|
|
108
|
+
changes.push({
|
|
109
|
+
title: 'Validate public API compatibility',
|
|
110
|
+
detail: 'At least one importing file is classified as public API, so contract tests or consumer checks should run before merge.',
|
|
111
|
+
confidence: 'high',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (context.touchesConfig) {
|
|
116
|
+
changes.push({
|
|
117
|
+
title: 'Review configuration and schema usage',
|
|
118
|
+
detail: 'Affected paths include config/schema/env-style files. Verify default values, renamed options, and deployment configuration.',
|
|
119
|
+
confidence: 'medium',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (context.touchesAuth) {
|
|
124
|
+
changes.push({
|
|
125
|
+
title: 'Review authentication or authorization adapters',
|
|
126
|
+
detail: 'Affected paths include auth/security/session-style code. Prioritize regression tests around login, token handling, and permissions.',
|
|
127
|
+
confidence: 'medium',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (changes.length === 0) {
|
|
132
|
+
changes.push({
|
|
133
|
+
title: 'Apply the version bump and run verification',
|
|
134
|
+
detail: 'No affected import sites were available, so treat this as a manifest-only remediation until Orbit data is present.',
|
|
135
|
+
confidence: 'low',
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return changes;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildSafetyChecks(context) {
|
|
143
|
+
const checks = ['Run the project test suite after the manifest and lockfile update'];
|
|
144
|
+
if (context.touchesPublicApi) checks.push('Run API/contract tests for public entry points');
|
|
145
|
+
if (context.touchesAuth) checks.push('Run auth and permission regression tests');
|
|
146
|
+
if (context.touchesConfig) checks.push('Validate deployment config and environment defaults');
|
|
147
|
+
if (context.relationship === 'transitive') checks.push('Run dependency-tree verification to confirm the patched version resolves');
|
|
148
|
+
return checks;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildMigrationPhases(context) {
|
|
152
|
+
if (context.relationship === 'unused') {
|
|
153
|
+
return [
|
|
154
|
+
{ phase: 1, title: 'Remove', detail: 'Delete the unused dependency from the manifest.' },
|
|
155
|
+
{ phase: 2, title: 'Verify', detail: 'Refresh the lockfile and run tests to prove removal is safe.' },
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const phases = [
|
|
160
|
+
{ phase: 1, title: 'Map exposure', detail: 'Use Orbit evidence to review each affected file and service area.' },
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
if (context.likelyBreaking) {
|
|
164
|
+
phases.push({ phase: 2, title: 'Adapt code', detail: 'Apply any source changes required by the major-version migration notes.' });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
phases.push(
|
|
168
|
+
{ phase: phases.length + 1, title: 'Upgrade', detail: 'Apply the dependency fix and refresh the lockfile with the ecosystem package manager.' },
|
|
169
|
+
{ phase: phases.length + 2, title: 'Verify and ship', detail: 'Run safety checks, open the MR, and let a human review before merge.' }
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
return phases;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function buildUpgradeImpactSimulation(vulnerability, impactReport) {
|
|
176
|
+
const paths = (vulnerability.affectedFiles || []).map(filePathOf);
|
|
177
|
+
const affectedServices = unique(paths.map(inferServiceName));
|
|
178
|
+
const relationship = dependencyRelationship(vulnerability);
|
|
179
|
+
const context = {
|
|
180
|
+
relationship,
|
|
181
|
+
affectedServices,
|
|
182
|
+
affectedServicesCount: affectedServices.length,
|
|
183
|
+
affectedFilesCount: impactReport.affectedFilesCount,
|
|
184
|
+
likelyBreaking: impactReport.likelyBreaking,
|
|
185
|
+
exposureDataSource: impactReport.exposureDataSource,
|
|
186
|
+
touchesPublicApi: (vulnerability.affectedFiles || []).some(f => f.category === 'public-api'),
|
|
187
|
+
touchesConfig: hasPathMatch(paths, ['config', 'schema', '.env', '.yaml', '.yml', '.json']),
|
|
188
|
+
touchesAuth: hasPathMatch(paths, ['auth', 'oauth', 'jwt', 'session', 'permission']),
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
package: vulnerability.package,
|
|
193
|
+
from: vulnerability.currentVersion,
|
|
194
|
+
to: vulnerability.fixedVersion,
|
|
195
|
+
relationship,
|
|
196
|
+
difficulty: difficultyFor(context),
|
|
197
|
+
automationMode: automationMode(context),
|
|
198
|
+
affectedServicesCount: context.affectedServicesCount,
|
|
199
|
+
affectedServices,
|
|
200
|
+
affectedFilesCount: context.affectedFilesCount,
|
|
201
|
+
requiredChanges: buildRequiredChanges(vulnerability, impactReport, context),
|
|
202
|
+
safetyChecks: buildSafetyChecks(context),
|
|
203
|
+
migrationPhases: buildMigrationPhases(context),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function formatUpgradeImpactSimulation(simulation) {
|
|
208
|
+
const services = simulation.affectedServices.length
|
|
209
|
+
? simulation.affectedServices.join(', ')
|
|
210
|
+
: 'none confirmed';
|
|
211
|
+
const changes = simulation.requiredChanges
|
|
212
|
+
.map(change => ` - ${change.title} (${change.confidence} confidence): ${change.detail}`)
|
|
213
|
+
.join('\n');
|
|
214
|
+
const checks = simulation.safetyChecks.map(check => ` - ${check}`).join('\n');
|
|
215
|
+
const phases = simulation.migrationPhases
|
|
216
|
+
.map(phase => ` ${phase.phase}. ${phase.title}: ${phase.detail}`)
|
|
217
|
+
.join('\n');
|
|
218
|
+
|
|
219
|
+
return `## Upgrade Impact Simulator
|
|
220
|
+
|
|
221
|
+
- **Dependency relationship**: ${simulation.relationship}
|
|
222
|
+
- **Upgrade difficulty**: ${simulation.difficulty}
|
|
223
|
+
- **Automation mode**: ${simulation.automationMode}
|
|
224
|
+
- **Affected services**: ${simulation.affectedServicesCount} (${services})
|
|
225
|
+
- **Affected files**: ${simulation.affectedFilesCount}
|
|
226
|
+
|
|
227
|
+
### Required changes
|
|
228
|
+
${changes}
|
|
229
|
+
|
|
230
|
+
### Safety checks
|
|
231
|
+
${checks}
|
|
232
|
+
|
|
233
|
+
### Migration phases
|
|
234
|
+
${phases}
|
|
235
|
+
`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = {
|
|
239
|
+
buildUpgradeImpactSimulation,
|
|
240
|
+
formatUpgradeImpactSimulation,
|
|
241
|
+
};
|