@openrewrite/recipes-nodejs 0.37.0-20260106-083133 → 0.37.0-20260106-170728
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/dist/security/dependency-vulnerability-check.d.ts +8 -54
- package/dist/security/dependency-vulnerability-check.d.ts.map +1 -1
- package/dist/security/dependency-vulnerability-check.js +176 -287
- package/dist/security/dependency-vulnerability-check.js.map +1 -1
- package/dist/security/index.d.ts +3 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +3 -0
- package/dist/security/index.js.map +1 -1
- package/dist/security/npm-utils.d.ts +8 -2
- package/dist/security/npm-utils.d.ts.map +1 -1
- package/dist/security/npm-utils.js +114 -14
- package/dist/security/npm-utils.js.map +1 -1
- package/dist/security/override-utils.d.ts +23 -0
- package/dist/security/override-utils.d.ts.map +1 -0
- package/dist/security/override-utils.js +169 -0
- package/dist/security/override-utils.js.map +1 -0
- package/dist/security/remove-redundant-overrides.d.ts +1 -10
- package/dist/security/remove-redundant-overrides.d.ts.map +1 -1
- package/dist/security/remove-redundant-overrides.js +4 -152
- package/dist/security/remove-redundant-overrides.js.map +1 -1
- package/dist/security/types.d.ts +42 -0
- package/dist/security/types.d.ts.map +1 -0
- package/dist/security/types.js +7 -0
- package/dist/security/types.js.map +1 -0
- package/dist/security/version-utils.d.ts +13 -0
- package/dist/security/version-utils.d.ts.map +1 -0
- package/dist/security/version-utils.js +173 -0
- package/dist/security/version-utils.js.map +1 -0
- package/package.json +1 -1
- package/src/security/dependency-vulnerability-check.ts +300 -525
- package/src/security/index.ts +3 -0
- package/src/security/npm-utils.ts +172 -37
- package/src/security/override-utils.ts +253 -0
- package/src/security/remove-redundant-overrides.ts +9 -211
- package/src/security/types.ts +115 -0
- package/src/security/version-utils.ts +198 -0
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
DependencyScope,
|
|
26
26
|
findNodeResolutionResult,
|
|
27
27
|
getUpdatedLockFileContent,
|
|
28
|
+
NodeResolutionResult,
|
|
28
29
|
NpmrcScope,
|
|
29
30
|
PackageManager,
|
|
30
31
|
ResolvedDependency,
|
|
@@ -38,40 +39,32 @@ import * as semver from "semver";
|
|
|
38
39
|
import * as path from "path";
|
|
39
40
|
import {parseSeverity, Severity, severityOrdinal, Vulnerability, VulnerabilityDatabase} from "./vulnerability";
|
|
40
41
|
import {
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
findDirectUpgradeWithSafeTransitiveInIsolation,
|
|
43
|
+
verifyAllUpgradesFixTransitive,
|
|
44
|
+
extractAllVersionsFromLockFile,
|
|
43
45
|
DependencyScope as NpmUtilsDependencyScope
|
|
44
46
|
} from "./npm-utils";
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
* applying direct fixes, then adds overrides only for those.
|
|
67
|
-
* - 'prefer-direct-upgrade': First applies minimum direct dependency upgrades,
|
|
68
|
-
* then for any remaining transitive vulnerabilities,
|
|
69
|
-
* tries to find a higher direct dependency version
|
|
70
|
-
* that fixes them. Falls back to overrides if no
|
|
71
|
-
* suitable direct upgrade exists. Queries npm registry.
|
|
72
|
-
* Most thorough, slowest.
|
|
73
|
-
*/
|
|
74
|
-
export type TransitiveFixStrategy = 'report' | 'override' | 'prefer-direct-upgrade';
|
|
47
|
+
import {
|
|
48
|
+
UpgradeDelta,
|
|
49
|
+
extractMinimumVersion,
|
|
50
|
+
isVersionWithinDelta,
|
|
51
|
+
isVersionAffected,
|
|
52
|
+
isUpgradeableWithinDelta,
|
|
53
|
+
getUpgradeVersion,
|
|
54
|
+
applyVersionPrefix
|
|
55
|
+
} from "./version-utils";
|
|
56
|
+
import {
|
|
57
|
+
ALL_DEPENDENCY_SCOPES,
|
|
58
|
+
TransitiveFixStrategy,
|
|
59
|
+
PathSegment,
|
|
60
|
+
VulnerableDependency,
|
|
61
|
+
VulnerabilityFix,
|
|
62
|
+
ProjectUpdateInfo
|
|
63
|
+
} from "./types";
|
|
64
|
+
import {
|
|
65
|
+
findDirectDependencyScope,
|
|
66
|
+
getOverridesFromPackageJson
|
|
67
|
+
} from "./override-utils";
|
|
75
68
|
|
|
76
69
|
/**
|
|
77
70
|
* Row for the vulnerability report data table.
|
|
@@ -186,89 +179,6 @@ class VulnerabilityReportRow {
|
|
|
186
179
|
}
|
|
187
180
|
}
|
|
188
181
|
|
|
189
|
-
/**
|
|
190
|
-
* Represents a segment in the dependency path.
|
|
191
|
-
*/
|
|
192
|
-
interface PathSegment {
|
|
193
|
-
name: string;
|
|
194
|
-
version: string;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Represents a vulnerable dependency found during scanning.
|
|
199
|
-
*/
|
|
200
|
-
interface VulnerableDependency {
|
|
201
|
-
/** The resolved dependency that is vulnerable */
|
|
202
|
-
resolved: ResolvedDependency;
|
|
203
|
-
/** The vulnerability affecting it */
|
|
204
|
-
vulnerability: Vulnerability;
|
|
205
|
-
/** Depth in dependency tree (0 = direct) */
|
|
206
|
-
depth: number;
|
|
207
|
-
/** Whether this is a direct dependency */
|
|
208
|
-
isDirect: boolean;
|
|
209
|
-
/** The scope where this dependency was found (for direct deps) */
|
|
210
|
-
scope?: DependencyScope;
|
|
211
|
-
/** Path from root to this dependency */
|
|
212
|
-
path: PathSegment[];
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Represents a fix to apply for a vulnerability.
|
|
217
|
-
*/
|
|
218
|
-
interface VulnerabilityFix {
|
|
219
|
-
/** Package name to upgrade (the vulnerable package) */
|
|
220
|
-
packageName: string;
|
|
221
|
-
/** Version to upgrade to */
|
|
222
|
-
newVersion: string;
|
|
223
|
-
/** Whether the vulnerable package is a transitive dependency */
|
|
224
|
-
isTransitive: boolean;
|
|
225
|
-
/** CVEs this fix resolves */
|
|
226
|
-
cves: string[];
|
|
227
|
-
/** CVE summaries for generating comments (CVE ID -> summary) */
|
|
228
|
-
cveSummaries: Map<string, string>;
|
|
229
|
-
/** The scope where this dependency was found (for direct deps) */
|
|
230
|
-
scope?: DependencyScope;
|
|
231
|
-
/** The original major version (for version-specific overrides) */
|
|
232
|
-
originalMajorVersion?: number;
|
|
233
|
-
/**
|
|
234
|
-
* For transitive vulnerabilities, info about the direct dependency that brings
|
|
235
|
-
* this transitive in. Used by prefer-direct-upgrade to find higher versions
|
|
236
|
-
* of the direct dependency that might fix the transitive.
|
|
237
|
-
*/
|
|
238
|
-
directDepInfo?: {
|
|
239
|
-
name: string;
|
|
240
|
-
version: string;
|
|
241
|
-
scope: DependencyScope;
|
|
242
|
-
};
|
|
243
|
-
/**
|
|
244
|
-
* If set, fix this transitive vulnerability by upgrading a direct dependency
|
|
245
|
-
* instead of using overrides. This is preferred when a newer version of a
|
|
246
|
-
* direct dependency includes a fixed version of the vulnerable transitive.
|
|
247
|
-
*/
|
|
248
|
-
fixViaDirectUpgrade?: {
|
|
249
|
-
/** Name of the direct dependency to upgrade */
|
|
250
|
-
directDepName: string;
|
|
251
|
-
/** Version to upgrade the direct dependency to */
|
|
252
|
-
directDepVersion: string;
|
|
253
|
-
/** Scope of the direct dependency */
|
|
254
|
-
directDepScope: DependencyScope;
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Project info for lock file updates.
|
|
260
|
-
*/
|
|
261
|
-
interface ProjectUpdateInfo {
|
|
262
|
-
/** Relative path to package.json (from source root) */
|
|
263
|
-
packageJsonPath: string;
|
|
264
|
-
/** Original package.json content */
|
|
265
|
-
originalPackageJson: string;
|
|
266
|
-
/** The package manager used by this project */
|
|
267
|
-
packageManager: PackageManager;
|
|
268
|
-
/** Config file contents extracted from the project (e.g., .npmrc) */
|
|
269
|
-
configFiles?: Record<string, string>;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
182
|
/**
|
|
273
183
|
* Accumulator for the scanning recipe.
|
|
274
184
|
*/
|
|
@@ -324,7 +234,7 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
324
234
|
@Option({
|
|
325
235
|
displayName: "Scope",
|
|
326
236
|
description: "Match dependencies with the specified scope. Default includes all scopes. " +
|
|
327
|
-
"Use
|
|
237
|
+
"Use `dependencies` for production dependencies, `devDependencies` for development only, etc.",
|
|
328
238
|
required: false,
|
|
329
239
|
example: "dependencies",
|
|
330
240
|
valid: ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]
|
|
@@ -334,20 +244,31 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
334
244
|
@Option({
|
|
335
245
|
displayName: "Transitive fix strategy",
|
|
336
246
|
description: "Strategy for handling transitive dependency vulnerabilities. " +
|
|
337
|
-
"
|
|
338
|
-
"
|
|
339
|
-
"
|
|
340
|
-
"Default is
|
|
247
|
+
"`report` only reports them without fixing. " +
|
|
248
|
+
"`override` adds overrides/resolutions for transitive vulnerabilities. " +
|
|
249
|
+
"`lock-file` updates the lock file to resolve safe versions without modifying `package.json` (similar to Dependabot). " +
|
|
250
|
+
"Default is `report`.",
|
|
341
251
|
required: false,
|
|
342
252
|
example: "override",
|
|
343
|
-
valid: ["report", "override", "
|
|
253
|
+
valid: ["report", "override", "lock-file"]
|
|
344
254
|
})
|
|
345
255
|
transitiveFixStrategy?: TransitiveFixStrategy;
|
|
346
256
|
|
|
257
|
+
@Option({
|
|
258
|
+
displayName: "Prefer direct upgrade",
|
|
259
|
+
description: "When fixing transitive vulnerabilities, first try to find higher versions of " +
|
|
260
|
+
"direct dependencies that include safe transitive versions. Falls back to the " +
|
|
261
|
+
"`transitiveFixStrategy` if no suitable direct upgrade exists. Queries npm registry. " +
|
|
262
|
+
"Default is `true`.",
|
|
263
|
+
required: false,
|
|
264
|
+
example: "false"
|
|
265
|
+
})
|
|
266
|
+
preferDirectUpgrade?: boolean;
|
|
267
|
+
|
|
347
268
|
@Option({
|
|
348
269
|
displayName: "Maximum upgrade delta",
|
|
349
270
|
description: "The maximum difference to allow when upgrading a dependency version. " +
|
|
350
|
-
"Use
|
|
271
|
+
"Use `none` to only report vulnerabilities without making any changes. " +
|
|
351
272
|
"Patch version upgrades are the default and safest option. " +
|
|
352
273
|
"Minor version upgrades can introduce new features but typically no breaking changes. " +
|
|
353
274
|
"Major version upgrades may require code changes.",
|
|
@@ -360,8 +281,8 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
360
281
|
@Option({
|
|
361
282
|
displayName: "Minimum severity",
|
|
362
283
|
description: "Only fix vulnerabilities with a severity level equal to or higher than the specified minimum. " +
|
|
363
|
-
"Vulnerabilities are classified as LOW
|
|
364
|
-
"Default is LOW
|
|
284
|
+
"Vulnerabilities are classified as `LOW`, `MODERATE`, `HIGH`, or `CRITICAL` based on their potential impact. " +
|
|
285
|
+
"Default is `LOW`, which includes all severity levels.",
|
|
365
286
|
required: false,
|
|
366
287
|
example: "MODERATE",
|
|
367
288
|
valid: ["LOW", "MODERATE", "HIGH", "CRITICAL"]
|
|
@@ -381,11 +302,11 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
381
302
|
|
|
382
303
|
@Option({
|
|
383
304
|
displayName: "Fix declared versions",
|
|
384
|
-
description: "When enabled, also upgrades version specifiers declared in package.json that specify vulnerable versions, " +
|
|
305
|
+
description: "When enabled, also upgrades version specifiers declared in `package.json` that specify vulnerable versions, " +
|
|
385
306
|
"even if the lock file already resolves to a safe version. This is a preventive measure to ensure that " +
|
|
386
307
|
"future installs (e.g., on a different machine or after lock file changes) won't install vulnerable versions. " +
|
|
387
308
|
"These preventive upgrades are NOT reported in the vulnerability data table since there's no actual vulnerability. " +
|
|
388
|
-
"Default is false
|
|
309
|
+
"Default is `false`.",
|
|
389
310
|
required: false,
|
|
390
311
|
example: "true"
|
|
391
312
|
})
|
|
@@ -393,9 +314,9 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
393
314
|
|
|
394
315
|
@Option({
|
|
395
316
|
displayName: "Add override comments",
|
|
396
|
-
description: "When enabled, adds a comment field (e.g.,
|
|
317
|
+
description: "When enabled, adds a comment field (e.g., `//overrides`) alongside overrides to document which CVEs " +
|
|
397
318
|
"each override is fixing. This helps with auditing and knowing when overrides can be removed. " +
|
|
398
|
-
"Default is true
|
|
319
|
+
"Default is `true`.",
|
|
399
320
|
required: false,
|
|
400
321
|
example: "true"
|
|
401
322
|
})
|
|
@@ -407,6 +328,7 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
407
328
|
constructor(options?: {
|
|
408
329
|
scope?: DependencyScope;
|
|
409
330
|
transitiveFixStrategy?: TransitiveFixStrategy;
|
|
331
|
+
preferDirectUpgrade?: boolean;
|
|
410
332
|
maximumUpgradeDelta?: UpgradeDelta;
|
|
411
333
|
minimumSeverity?: string;
|
|
412
334
|
cvePattern?: string;
|
|
@@ -415,6 +337,7 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
415
337
|
}) {
|
|
416
338
|
super(options);
|
|
417
339
|
this.transitiveFixStrategy ??= 'report';
|
|
340
|
+
this.preferDirectUpgrade ??= true;
|
|
418
341
|
this.maximumUpgradeDelta ??= 'patch';
|
|
419
342
|
this.minimumSeverity ??= Severity.LOW;
|
|
420
343
|
this.fixDeclaredVersions ??= false;
|
|
@@ -434,25 +357,22 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
434
357
|
|
|
435
358
|
/**
|
|
436
359
|
* Returns true if transitive vulnerabilities should be scanned.
|
|
360
|
+
* Always true - we want to report transitive vulnerabilities even if not fixing them.
|
|
437
361
|
*/
|
|
438
362
|
private shouldScanTransitives(): boolean {
|
|
439
|
-
return
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* Returns true if transitive vulnerabilities should be fixed with overrides.
|
|
444
|
-
*/
|
|
445
|
-
private shouldFixTransitives(): boolean {
|
|
446
|
-
return this.transitiveFixStrategy !== 'report';
|
|
363
|
+
return true;
|
|
447
364
|
}
|
|
448
365
|
|
|
449
366
|
/**
|
|
450
367
|
* Returns true if we should verify that transitive fixes are still needed
|
|
451
|
-
* after applying direct fixes
|
|
368
|
+
* after applying direct fixes. Only needed when preferDirectUpgrade is true,
|
|
369
|
+
* because it tries to fix transitives via direct dependency upgrades first.
|
|
370
|
+
*
|
|
371
|
+
* For 'override' and 'lock-file' strategies without preferDirectUpgrade,
|
|
372
|
+
* we apply the strategy directly without checking if direct upgrades helped.
|
|
452
373
|
*/
|
|
453
374
|
private shouldVerifyTransitiveFixes(): boolean {
|
|
454
|
-
return this.
|
|
455
|
-
this.transitiveFixStrategy === 'prefer-direct-upgrade';
|
|
375
|
+
return this.preferDirectUpgrade === true;
|
|
456
376
|
}
|
|
457
377
|
|
|
458
378
|
/**
|
|
@@ -474,37 +394,41 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
474
394
|
|
|
475
395
|
for (const fix of fixes) {
|
|
476
396
|
// Always keep direct fixes and fixes via direct upgrade
|
|
477
|
-
if (!fix.isTransitive || fix.
|
|
397
|
+
if (!fix.isTransitive || (fix.fixViaDirectUpgrades && fix.fixViaDirectUpgrades.length > 0)) {
|
|
478
398
|
result.push(fix);
|
|
479
399
|
continue;
|
|
480
400
|
}
|
|
481
401
|
|
|
482
|
-
// For transitive fixes, check if the package is still vulnerable
|
|
483
|
-
|
|
402
|
+
// For transitive fixes, check if ANY version of the package is still vulnerable.
|
|
403
|
+
// A package can appear at multiple locations in the tree at different versions
|
|
404
|
+
// (e.g., hoisted at 2.1.1 and nested at 1.0.2), so we need to check ALL versions.
|
|
405
|
+
const resolvedVersions = extractAllVersionsFromLockFile(
|
|
484
406
|
lockFileContent,
|
|
485
407
|
fix.packageName,
|
|
486
408
|
packageManager
|
|
487
409
|
);
|
|
488
410
|
|
|
489
|
-
if (
|
|
411
|
+
if (resolvedVersions.length === 0) {
|
|
490
412
|
// Package not found in lock file - might have been removed by direct upgrade
|
|
491
413
|
// Skip this fix (transitive is no longer in the tree)
|
|
492
414
|
continue;
|
|
493
415
|
}
|
|
494
416
|
|
|
495
|
-
// Check if
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
417
|
+
// Check if ANY resolved version is still vulnerable
|
|
418
|
+
const anyVersionVulnerable = resolvedVersions.some(version =>
|
|
419
|
+
this.isVersionStillVulnerable(
|
|
420
|
+
fix.packageName,
|
|
421
|
+
version,
|
|
422
|
+
fix.cves,
|
|
423
|
+
db
|
|
424
|
+
)
|
|
501
425
|
);
|
|
502
426
|
|
|
503
|
-
if (
|
|
504
|
-
//
|
|
427
|
+
if (anyVersionVulnerable) {
|
|
428
|
+
// At least one version of the transitive is still vulnerable, keep the fix
|
|
505
429
|
result.push(fix);
|
|
506
430
|
}
|
|
507
|
-
// Otherwise,
|
|
431
|
+
// Otherwise, all versions are safe - skip the override
|
|
508
432
|
}
|
|
509
433
|
|
|
510
434
|
return result;
|
|
@@ -521,7 +445,7 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
521
445
|
): boolean {
|
|
522
446
|
const vulns = db.getVulnerabilities(packageName);
|
|
523
447
|
for (const vuln of vulns) {
|
|
524
|
-
if (cves.includes(vuln.cve) &&
|
|
448
|
+
if (cves.includes(vuln.cve) && isVersionAffected(version, vuln)) {
|
|
525
449
|
return true;
|
|
526
450
|
}
|
|
527
451
|
}
|
|
@@ -559,147 +483,73 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
559
483
|
}
|
|
560
484
|
|
|
561
485
|
/**
|
|
562
|
-
*
|
|
486
|
+
* Renders a dependency path as a string.
|
|
487
|
+
* Example: "dependencies > lodash@4.17.20 > vulnerable-pkg@1.0.0"
|
|
563
488
|
*/
|
|
564
|
-
private
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
// Check introduced version
|
|
570
|
-
if (vulnerability.introducedVersion && vulnerability.introducedVersion !== '0') {
|
|
571
|
-
const introduced = semver.parse(vulnerability.introducedVersion);
|
|
572
|
-
if (introduced && semver.lt(v, introduced)) {
|
|
573
|
-
return false; // Version is before the vulnerability was introduced
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Check if fixed
|
|
578
|
-
if (vulnerability.fixedVersion) {
|
|
579
|
-
const fixed = semver.parse(vulnerability.fixedVersion);
|
|
580
|
-
if (fixed && semver.gte(v, fixed)) {
|
|
581
|
-
return false; // Version is at or after the fix
|
|
582
|
-
}
|
|
583
|
-
return true; // Version is in the vulnerable range
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// Check last affected version
|
|
587
|
-
if (vulnerability.lastAffectedVersion) {
|
|
588
|
-
const lastAffected = semver.parse(vulnerability.lastAffectedVersion);
|
|
589
|
-
if (lastAffected && semver.gt(v, lastAffected)) {
|
|
590
|
-
return false; // Version is after the last affected
|
|
591
|
-
}
|
|
592
|
-
return true;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
return true; // No fix information, assume vulnerable
|
|
596
|
-
} catch {
|
|
597
|
-
return false; // Invalid version, skip
|
|
489
|
+
private renderPath(scope: DependencyScope | undefined, path: PathSegment[]): string {
|
|
490
|
+
const parts: string[] = [];
|
|
491
|
+
if (scope) {
|
|
492
|
+
parts.push(scope);
|
|
598
493
|
}
|
|
494
|
+
for (const seg of path) {
|
|
495
|
+
parts.push(`${seg.name}@${seg.version}`);
|
|
496
|
+
}
|
|
497
|
+
return parts.join(' > ');
|
|
599
498
|
}
|
|
600
499
|
|
|
601
500
|
/**
|
|
602
|
-
*
|
|
501
|
+
* Finds all direct dependencies that have a given transitive package in their dependency tree.
|
|
502
|
+
* This is needed because the visited set in findVulnerabilities prevents recording multiple
|
|
503
|
+
* paths to the same vulnerable package.
|
|
603
504
|
*/
|
|
604
|
-
private
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const current = semver.parse(currentVersion);
|
|
611
|
-
if (!current) return false;
|
|
612
|
-
|
|
613
|
-
const delta = this.maximumUpgradeDelta!;
|
|
614
|
-
|
|
615
|
-
// If we have a fixed version, check if it's reachable within delta
|
|
616
|
-
if (vulnerability.fixedVersion) {
|
|
617
|
-
const fixed = semver.parse(vulnerability.fixedVersion);
|
|
618
|
-
if (!fixed) return false;
|
|
619
|
-
|
|
620
|
-
switch (delta) {
|
|
621
|
-
case 'patch':
|
|
622
|
-
return current.major === fixed.major && current.minor === fixed.minor;
|
|
623
|
-
case 'minor':
|
|
624
|
-
return current.major === fixed.major;
|
|
625
|
-
case 'major':
|
|
626
|
-
return true;
|
|
627
|
-
case 'none':
|
|
628
|
-
return false;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
505
|
+
private findAllDirectDepsForTransitive(
|
|
506
|
+
marker: NodeResolutionResult,
|
|
507
|
+
transitivePackageName: string,
|
|
508
|
+
scopesToCheck: DependencyScope[]
|
|
509
|
+
): { name: string; version: string; scope: DependencyScope }[] {
|
|
510
|
+
const results: { name: string; version: string; scope: DependencyScope }[] = [];
|
|
631
511
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
current.minor === lastAffected.minor;
|
|
642
|
-
case 'minor':
|
|
643
|
-
return current.major === lastAffected.major;
|
|
644
|
-
case 'major':
|
|
645
|
-
return true;
|
|
646
|
-
case 'none':
|
|
647
|
-
return false;
|
|
512
|
+
for (const scope of scopesToCheck) {
|
|
513
|
+
const deps = marker[scope] || [];
|
|
514
|
+
for (const dep of deps) {
|
|
515
|
+
if (dep.resolved && this.hasTransitiveInTree(dep.resolved, transitivePackageName, new Set())) {
|
|
516
|
+
results.push({
|
|
517
|
+
name: dep.resolved.name,
|
|
518
|
+
version: dep.resolved.version,
|
|
519
|
+
scope
|
|
520
|
+
});
|
|
648
521
|
}
|
|
649
522
|
}
|
|
650
|
-
|
|
651
|
-
return false;
|
|
652
|
-
} catch {
|
|
653
|
-
return false;
|
|
654
523
|
}
|
|
655
|
-
}
|
|
656
524
|
|
|
657
|
-
|
|
658
|
-
* Gets the version to upgrade to for a vulnerability.
|
|
659
|
-
*/
|
|
660
|
-
private getUpgradeVersion(vulnerability: Vulnerability): string | undefined {
|
|
661
|
-
if (vulnerability.fixedVersion) {
|
|
662
|
-
return vulnerability.fixedVersion;
|
|
663
|
-
}
|
|
664
|
-
// For lastAffectedVersion, we need the next version after it
|
|
665
|
-
// We can't determine this exactly, so return undefined and let the
|
|
666
|
-
// upgrade recipe handle version selection
|
|
667
|
-
return undefined;
|
|
525
|
+
return results;
|
|
668
526
|
}
|
|
669
527
|
|
|
670
528
|
/**
|
|
671
|
-
*
|
|
672
|
-
* This allows future patches that might fix additional vulnerabilities.
|
|
673
|
-
*
|
|
674
|
-
* - 'patch' → '~' (allows patch updates, e.g., ~1.2.3 matches 1.2.x)
|
|
675
|
-
* - 'minor' → '^' (allows minor and patch updates, e.g., ^1.2.3 matches 1.x.x)
|
|
676
|
-
* - 'major' → '^' (same as minor; ^ is reasonable even for major upgrades)
|
|
529
|
+
* Checks if a resolved dependency has a specific transitive in its dependency tree.
|
|
677
530
|
*/
|
|
678
|
-
private
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
return '';
|
|
687
|
-
}
|
|
688
|
-
}
|
|
531
|
+
private hasTransitiveInTree(
|
|
532
|
+
resolved: ResolvedDependency,
|
|
533
|
+
targetPackageName: string,
|
|
534
|
+
visited: Set<string>
|
|
535
|
+
): boolean {
|
|
536
|
+
const key = `${resolved.name}@${resolved.version}`;
|
|
537
|
+
if (visited.has(key)) return false;
|
|
538
|
+
visited.add(key);
|
|
689
539
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
parts.push(`${seg.name}@${seg.version}`);
|
|
540
|
+
// Check direct children
|
|
541
|
+
if (resolved.dependencies) {
|
|
542
|
+
for (const child of resolved.dependencies) {
|
|
543
|
+
if (child.name === targetPackageName) {
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
if (child.resolved && this.hasTransitiveInTree(child.resolved, targetPackageName, visited)) {
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
701
550
|
}
|
|
702
|
-
|
|
551
|
+
|
|
552
|
+
return false;
|
|
703
553
|
}
|
|
704
554
|
|
|
705
555
|
/**
|
|
@@ -736,7 +586,7 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
736
586
|
}
|
|
737
587
|
|
|
738
588
|
// Check if this version is affected
|
|
739
|
-
if (
|
|
589
|
+
if (isVersionAffected(resolved.version, vuln)) {
|
|
740
590
|
results.push({
|
|
741
591
|
resolved,
|
|
742
592
|
vulnerability: vuln,
|
|
@@ -793,7 +643,7 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
793
643
|
if (!dep.resolved) continue;
|
|
794
644
|
|
|
795
645
|
// Extract the minimum version from the version constraint
|
|
796
|
-
const declaredMinVersion =
|
|
646
|
+
const declaredMinVersion = extractMinimumVersion(dep.versionConstraint);
|
|
797
647
|
if (!declaredMinVersion) continue;
|
|
798
648
|
|
|
799
649
|
// Skip if declared version equals resolved version (no discrepancy)
|
|
@@ -817,8 +667,8 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
817
667
|
}
|
|
818
668
|
|
|
819
669
|
// Check if declared version is affected but resolved is not
|
|
820
|
-
if (
|
|
821
|
-
!
|
|
670
|
+
if (isVersionAffected(declaredMinVersion, vuln) &&
|
|
671
|
+
!isVersionAffected(dep.resolved.version, vuln)) {
|
|
822
672
|
affectedCves.push(vuln.cve);
|
|
823
673
|
affectedCveSummaries.set(vuln.cve, vuln.summary);
|
|
824
674
|
|
|
@@ -863,30 +713,7 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
863
713
|
if (this.isReportOnly()) {
|
|
864
714
|
return false;
|
|
865
715
|
}
|
|
866
|
-
return
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
/**
|
|
870
|
-
* Extracts the minimum version from a version constraint.
|
|
871
|
-
* For example: "^3.0.2" -> "3.0.2", "~1.2.3" -> "1.2.3", ">=2.0.0" -> "2.0.0"
|
|
872
|
-
*/
|
|
873
|
-
private extractMinimumVersion(constraint: string): string | undefined {
|
|
874
|
-
if (!constraint) return undefined;
|
|
875
|
-
|
|
876
|
-
// Handle exact versions
|
|
877
|
-
if (semver.valid(constraint)) {
|
|
878
|
-
return constraint;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// Handle ranges with prefix: ^, ~, >=, >, etc.
|
|
882
|
-
const match = constraint.match(/^[~^>=<]*\s*(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?)/);
|
|
883
|
-
if (match && semver.valid(match[1])) {
|
|
884
|
-
return match[1];
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
// Try to coerce other formats
|
|
888
|
-
const coerced = semver.coerce(constraint);
|
|
889
|
-
return coerced?.version;
|
|
716
|
+
return isVersionWithinDelta(fromVersion, toVersion, this.maximumUpgradeDelta!);
|
|
890
717
|
}
|
|
891
718
|
|
|
892
719
|
/**
|
|
@@ -915,7 +742,7 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
915
742
|
|
|
916
743
|
// Check if the fix version itself has vulnerabilities
|
|
917
744
|
const vulnsInFixVersion = db.getVulnerabilities(packageName)
|
|
918
|
-
.filter(v =>
|
|
745
|
+
.filter(v => isVersionAffected(initialFixVersion, v));
|
|
919
746
|
|
|
920
747
|
if (vulnsInFixVersion.length === 0) {
|
|
921
748
|
// No vulnerabilities in this version - it's safe
|
|
@@ -925,10 +752,10 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
925
752
|
// Find fix versions for vulnerabilities in the current fix version
|
|
926
753
|
let highestFixVersion = initialFixVersion;
|
|
927
754
|
for (const vuln of vulnsInFixVersion) {
|
|
928
|
-
const fixVersion =
|
|
755
|
+
const fixVersion = getUpgradeVersion(vuln);
|
|
929
756
|
if (fixVersion && semver.valid(fixVersion)) {
|
|
930
757
|
// Check if this fix version is within delta from the ORIGINAL version
|
|
931
|
-
if (
|
|
758
|
+
if (isVersionWithinDelta(originalVersion, fixVersion, this.maximumUpgradeDelta!)) {
|
|
932
759
|
if (semver.gt(fixVersion, highestFixVersion)) {
|
|
933
760
|
highestFixVersion = fixVersion;
|
|
934
761
|
}
|
|
@@ -951,32 +778,6 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
951
778
|
return initialFixVersion;
|
|
952
779
|
}
|
|
953
780
|
|
|
954
|
-
/**
|
|
955
|
-
* Checks if a target version is within the allowed upgrade delta from the original version.
|
|
956
|
-
*/
|
|
957
|
-
private isVersionWithinDelta(originalVersion: string, targetVersion: string): boolean {
|
|
958
|
-
try {
|
|
959
|
-
const original = semver.parse(originalVersion);
|
|
960
|
-
const target = semver.parse(targetVersion);
|
|
961
|
-
if (!original || !target) return false;
|
|
962
|
-
|
|
963
|
-
switch (this.maximumUpgradeDelta) {
|
|
964
|
-
case 'patch':
|
|
965
|
-
return original.major === target.major && original.minor === target.minor;
|
|
966
|
-
case 'minor':
|
|
967
|
-
return original.major === target.major;
|
|
968
|
-
case 'major':
|
|
969
|
-
return true;
|
|
970
|
-
case 'none':
|
|
971
|
-
return false;
|
|
972
|
-
default:
|
|
973
|
-
return false;
|
|
974
|
-
}
|
|
975
|
-
} catch {
|
|
976
|
-
return false;
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
|
|
980
781
|
/**
|
|
981
782
|
* Computes the fixes to apply for the vulnerabilities found.
|
|
982
783
|
* Groups vulnerabilities by package AND major version to handle packages with multiple
|
|
@@ -998,6 +799,8 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
998
799
|
packageManager: PackageManager;
|
|
999
800
|
originalPackageJson: string;
|
|
1000
801
|
configFiles?: Record<string, string>;
|
|
802
|
+
marker?: NodeResolutionResult;
|
|
803
|
+
scopesToCheck?: DependencyScope[];
|
|
1001
804
|
}
|
|
1002
805
|
): Promise<VulnerabilityFix[]> {
|
|
1003
806
|
if (this.isReportOnly()) {
|
|
@@ -1041,8 +844,8 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1041
844
|
let isTransitive = true;
|
|
1042
845
|
let scope: DependencyScope | undefined;
|
|
1043
846
|
let originalVersion: string | undefined;
|
|
1044
|
-
// Track
|
|
1045
|
-
|
|
847
|
+
// Track ALL direct dependencies that bring in this transitive
|
|
848
|
+
const directDepInfosMap = new Map<string, { name: string; version: string; scope: DependencyScope }>();
|
|
1046
849
|
|
|
1047
850
|
for (const vuln of vulns) {
|
|
1048
851
|
// Track the original version for delta checking
|
|
@@ -1050,22 +853,28 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1050
853
|
originalVersion = vuln.resolved.version;
|
|
1051
854
|
}
|
|
1052
855
|
|
|
1053
|
-
//
|
|
1054
|
-
if (!vuln.isDirect && vuln.path.length > 0
|
|
856
|
+
// Collect all unique direct dependencies that bring in this transitive
|
|
857
|
+
if (!vuln.isDirect && vuln.path.length > 0) {
|
|
1055
858
|
const directDep = vuln.path[0];
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
859
|
+
const depScope = vuln.scope || 'dependencies';
|
|
860
|
+
// Use name@scope as key to deduplicate (same dep could appear in different scopes)
|
|
861
|
+
const key = `${directDep.name}@${depScope}`;
|
|
862
|
+
if (!directDepInfosMap.has(key)) {
|
|
863
|
+
directDepInfosMap.set(key, {
|
|
864
|
+
name: directDep.name,
|
|
865
|
+
version: directDep.version,
|
|
866
|
+
scope: depScope
|
|
867
|
+
});
|
|
868
|
+
}
|
|
1061
869
|
}
|
|
1062
870
|
|
|
1063
871
|
// Check if upgradeable within delta
|
|
1064
|
-
|
|
872
|
+
const upgradeCheck = isUpgradeableWithinDelta(vuln.resolved.version, vuln.vulnerability, this.maximumUpgradeDelta!);
|
|
873
|
+
if (this.isReportOnly() || !upgradeCheck) {
|
|
1065
874
|
continue;
|
|
1066
875
|
}
|
|
1067
876
|
|
|
1068
|
-
const fixVersion =
|
|
877
|
+
const fixVersion = getUpgradeVersion(vuln.vulnerability);
|
|
1069
878
|
if (fixVersion) {
|
|
1070
879
|
// When there are multiple major versions of the same package in the tree
|
|
1071
880
|
// (e.g., semver@6.x and semver@7.x), only consider fix versions that match
|
|
@@ -1092,6 +901,23 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1092
901
|
}
|
|
1093
902
|
}
|
|
1094
903
|
|
|
904
|
+
// If this is a transitive vulnerability and we have a marker, find ALL direct deps
|
|
905
|
+
// that have this transitive in their tree. The visited set in findVulnerabilities
|
|
906
|
+
// prevents recording multiple paths to the same package, so we need this extra step.
|
|
907
|
+
if (isTransitive && projectContext?.marker && projectContext?.scopesToCheck) {
|
|
908
|
+
const allDirectDeps = this.findAllDirectDepsForTransitive(
|
|
909
|
+
projectContext.marker,
|
|
910
|
+
packageName,
|
|
911
|
+
projectContext.scopesToCheck
|
|
912
|
+
);
|
|
913
|
+
for (const dep of allDirectDeps) {
|
|
914
|
+
const key = `${dep.name}@${dep.scope}`;
|
|
915
|
+
if (!directDepInfosMap.has(key)) {
|
|
916
|
+
directDepInfosMap.set(key, dep);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
1095
921
|
if (highestFixVersion && cves.length > 0 && originalVersion) {
|
|
1096
922
|
// Recursively find the highest safe version (checking if fix versions have vulns)
|
|
1097
923
|
const safeVersion = this.findHighestSafeVersion(
|
|
@@ -1101,6 +927,9 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1101
927
|
db
|
|
1102
928
|
);
|
|
1103
929
|
|
|
930
|
+
// Convert directDepInfosMap to array for storage
|
|
931
|
+
const directDepInfosArray = Array.from(directDepInfosMap.values());
|
|
932
|
+
|
|
1104
933
|
const fix: VulnerabilityFix = {
|
|
1105
934
|
packageName,
|
|
1106
935
|
newVersion: safeVersion || highestFixVersion,
|
|
@@ -1109,12 +938,8 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1109
938
|
cveSummaries,
|
|
1110
939
|
scope,
|
|
1111
940
|
originalMajorVersion: originalMajor,
|
|
1112
|
-
// Store direct
|
|
1113
|
-
|
|
1114
|
-
name: directDepInfo.name,
|
|
1115
|
-
version: directDepInfo.version,
|
|
1116
|
-
scope: directDepInfo.scope
|
|
1117
|
-
} : undefined
|
|
941
|
+
// Store ALL direct dependencies for potential later use by preferDirectUpgrade
|
|
942
|
+
directDepInfos: isTransitive && directDepInfosArray.length > 0 ? directDepInfosArray : undefined
|
|
1118
943
|
};
|
|
1119
944
|
|
|
1120
945
|
fixes.push(fix);
|
|
@@ -1125,103 +950,108 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1125
950
|
}
|
|
1126
951
|
|
|
1127
952
|
/**
|
|
1128
|
-
*
|
|
1129
|
-
*
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
transitivePackageName: string,
|
|
1133
|
-
requiredFixVersion: string,
|
|
1134
|
-
directDepInfo: { name: string; version: string; scope: DependencyScope },
|
|
1135
|
-
projectContext: {
|
|
1136
|
-
packageManager: PackageManager;
|
|
1137
|
-
originalPackageJson: string;
|
|
1138
|
-
configFiles?: Record<string, string>;
|
|
1139
|
-
},
|
|
1140
|
-
db: VulnerabilityDatabase
|
|
1141
|
-
): Promise<string | undefined> {
|
|
1142
|
-
// Create a vulnerability check function for the transitive
|
|
1143
|
-
const isVulnerable = (version: string): boolean => {
|
|
1144
|
-
try {
|
|
1145
|
-
// Version is vulnerable if it's less than the required fix version
|
|
1146
|
-
return semver.lt(version, requiredFixVersion);
|
|
1147
|
-
} catch {
|
|
1148
|
-
return true; // Assume vulnerable if we can't parse
|
|
1149
|
-
}
|
|
1150
|
-
};
|
|
1151
|
-
|
|
1152
|
-
// Create a delta check function based on recipe settings
|
|
1153
|
-
const isWithinDelta = (from: string, to: string): boolean => {
|
|
1154
|
-
return this.isVersionWithinDelta(from, to);
|
|
1155
|
-
};
|
|
1156
|
-
|
|
1157
|
-
try {
|
|
1158
|
-
const upgradeVersion = await findDirectUpgradeThatFixesTransitive(
|
|
1159
|
-
projectContext.packageManager,
|
|
1160
|
-
directDepInfo.name,
|
|
1161
|
-
directDepInfo.version,
|
|
1162
|
-
transitivePackageName,
|
|
1163
|
-
isVulnerable,
|
|
1164
|
-
isWithinDelta,
|
|
1165
|
-
projectContext.originalPackageJson,
|
|
1166
|
-
directDepInfo.scope as NpmUtilsDependencyScope,
|
|
1167
|
-
projectContext.configFiles
|
|
1168
|
-
);
|
|
1169
|
-
|
|
1170
|
-
return upgradeVersion;
|
|
1171
|
-
} catch {
|
|
1172
|
-
return undefined;
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
/**
|
|
1177
|
-
* For each remaining transitive fix, tries to find a higher direct dependency version
|
|
1178
|
-
* that fixes the transitive vulnerability. Modifies fixes in place by setting
|
|
1179
|
-
* `fixViaDirectUpgrade` when a suitable upgrade is found.
|
|
953
|
+
* For each remaining transitive fix, tries to find higher versions of ALL direct
|
|
954
|
+
* dependencies that bring in the transitive. Uses isolation-based testing to find
|
|
955
|
+
* candidate upgrades for each direct dep independently, then verifies all upgrades
|
|
956
|
+
* together fix the vulnerability.
|
|
1180
957
|
*
|
|
1181
958
|
* @param fixes The transitive fixes that are still needed after applying direct fixes
|
|
1182
959
|
* @param updateInfo Project update info containing package manager and config
|
|
1183
|
-
* @
|
|
1184
|
-
* @returns The fixes array with `fixViaDirectUpgrade` set where applicable
|
|
960
|
+
* @returns The fixes array with `fixViaDirectUpgrades` set where applicable
|
|
1185
961
|
*/
|
|
1186
962
|
private async tryDirectUpgradesForTransitives(
|
|
1187
963
|
fixes: VulnerabilityFix[],
|
|
1188
|
-
updateInfo: ProjectUpdateInfo
|
|
1189
|
-
db: VulnerabilityDatabase
|
|
964
|
+
updateInfo: ProjectUpdateInfo
|
|
1190
965
|
): Promise<VulnerabilityFix[]> {
|
|
1191
966
|
const result: VulnerabilityFix[] = [];
|
|
1192
967
|
|
|
1193
968
|
for (const fix of fixes) {
|
|
1194
969
|
// Skip if no direct dependency info available
|
|
1195
|
-
if (!fix.
|
|
970
|
+
if (!fix.directDepInfos || fix.directDepInfos.length === 0) {
|
|
1196
971
|
result.push(fix);
|
|
1197
972
|
continue;
|
|
1198
973
|
}
|
|
1199
974
|
|
|
1200
|
-
//
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
975
|
+
// Phase 1: Find candidate upgrades for ALL direct dependencies in isolation
|
|
976
|
+
// Each direct dep is tested independently to find a version that brings in a safe transitive
|
|
977
|
+
const candidateUpgrades: {
|
|
978
|
+
directDepName: string;
|
|
979
|
+
directDepVersion: string;
|
|
980
|
+
directDepScope: DependencyScope;
|
|
981
|
+
}[] = [];
|
|
982
|
+
|
|
983
|
+
// Create a vulnerability check function for the transitive
|
|
984
|
+
const isVulnerable = (version: string): boolean => {
|
|
985
|
+
try {
|
|
986
|
+
return semver.lt(version, fix.newVersion);
|
|
987
|
+
} catch {
|
|
988
|
+
return true;
|
|
989
|
+
}
|
|
990
|
+
};
|
|
1212
991
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
992
|
+
// Create a delta check function based on recipe settings
|
|
993
|
+
const isWithinDelta = (from: string, to: string): boolean => {
|
|
994
|
+
return isVersionWithinDelta(from, to, this.maximumUpgradeDelta!);
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
for (const directDepInfo of fix.directDepInfos) {
|
|
998
|
+
const directUpgradeVersion = await findDirectUpgradeWithSafeTransitiveInIsolation(
|
|
999
|
+
updateInfo.packageManager,
|
|
1000
|
+
directDepInfo.name,
|
|
1001
|
+
directDepInfo.version,
|
|
1002
|
+
fix.packageName,
|
|
1003
|
+
isVulnerable,
|
|
1004
|
+
isWithinDelta,
|
|
1005
|
+
directDepInfo.scope as NpmUtilsDependencyScope,
|
|
1006
|
+
updateInfo.configFiles
|
|
1007
|
+
);
|
|
1008
|
+
|
|
1009
|
+
if (directUpgradeVersion) {
|
|
1010
|
+
candidateUpgrades.push({
|
|
1011
|
+
directDepName: directDepInfo.name,
|
|
1219
1012
|
directDepVersion: directUpgradeVersion,
|
|
1220
|
-
directDepScope:
|
|
1013
|
+
directDepScope: directDepInfo.scope
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Phase 2: Verify all candidate upgrades together fix the transitive
|
|
1019
|
+
if (candidateUpgrades.length > 0) {
|
|
1020
|
+
// Check if we found upgrades for ALL direct deps
|
|
1021
|
+
const foundAllUpgrades = candidateUpgrades.length === fix.directDepInfos.length;
|
|
1022
|
+
|
|
1023
|
+
if (foundAllUpgrades) {
|
|
1024
|
+
// Verify all upgrades together fix the vulnerability
|
|
1025
|
+
const allFixed = await verifyAllUpgradesFixTransitive(
|
|
1026
|
+
updateInfo.packageManager,
|
|
1027
|
+
candidateUpgrades.map(u => ({
|
|
1028
|
+
name: u.directDepName,
|
|
1029
|
+
version: u.directDepVersion,
|
|
1030
|
+
scope: u.directDepScope as NpmUtilsDependencyScope
|
|
1031
|
+
})),
|
|
1032
|
+
fix.packageName,
|
|
1033
|
+
isVulnerable,
|
|
1034
|
+
updateInfo.originalPackageJson,
|
|
1035
|
+
updateInfo.configFiles
|
|
1036
|
+
);
|
|
1037
|
+
|
|
1038
|
+
if (allFixed) {
|
|
1039
|
+
result.push({
|
|
1040
|
+
...fix,
|
|
1041
|
+
fixViaDirectUpgrades: candidateUpgrades
|
|
1042
|
+
});
|
|
1043
|
+
continue;
|
|
1221
1044
|
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Partial upgrades found or verification failed - apply what we have
|
|
1048
|
+
// The two-phase mechanism will add an override if still needed
|
|
1049
|
+
result.push({
|
|
1050
|
+
...fix,
|
|
1051
|
+
fixViaDirectUpgrades: candidateUpgrades
|
|
1222
1052
|
});
|
|
1223
1053
|
} else {
|
|
1224
|
-
// No direct
|
|
1054
|
+
// No direct upgrades found, will use override
|
|
1225
1055
|
result.push(fix);
|
|
1226
1056
|
}
|
|
1227
1057
|
}
|
|
@@ -1340,7 +1170,9 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1340
1170
|
fixes = await recipe.computeFixes(vulnerabilities, acc.db, {
|
|
1341
1171
|
packageManager: pm,
|
|
1342
1172
|
originalPackageJson,
|
|
1343
|
-
configFiles: Object.keys(configFiles).length > 0 ? configFiles : undefined
|
|
1173
|
+
configFiles: Object.keys(configFiles).length > 0 ? configFiles : undefined,
|
|
1174
|
+
marker,
|
|
1175
|
+
scopesToCheck
|
|
1344
1176
|
});
|
|
1345
1177
|
}
|
|
1346
1178
|
|
|
@@ -1514,9 +1346,10 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1514
1346
|
// Insert rows into the data table for each vulnerability (always, even in report-only mode)
|
|
1515
1347
|
if (vulnerabilities && vulnerabilities.length > 0) {
|
|
1516
1348
|
for (const vuln of vulnerabilities) {
|
|
1517
|
-
const upgradeable = recipe.isUpgradeableWithinDelta(
|
|
1349
|
+
const upgradeable = !recipe.isReportOnly() && isUpgradeableWithinDelta(
|
|
1518
1350
|
vuln.resolved.version,
|
|
1519
|
-
vuln.vulnerability
|
|
1351
|
+
vuln.vulnerability,
|
|
1352
|
+
recipe.maximumUpgradeDelta!
|
|
1520
1353
|
);
|
|
1521
1354
|
|
|
1522
1355
|
recipe.vulnerabilityReport.insertRow(ctx, new VulnerabilityReportRow(
|
|
@@ -1793,10 +1626,10 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1793
1626
|
* Writes a modified package.json with all new versions, then runs install.
|
|
1794
1627
|
* All file contents are provided from in-memory sources (SourceFiles), not read from disk.
|
|
1795
1628
|
*
|
|
1796
|
-
*
|
|
1629
|
+
* When preferDirectUpgrade is enabled, this uses a two-phase approach:
|
|
1797
1630
|
* 1. First run with only direct fixes (+ fixes via direct upgrade)
|
|
1798
1631
|
* 2. Check the resulting lock file for remaining transitive vulnerabilities
|
|
1799
|
-
* 3. If any remain,
|
|
1632
|
+
* 3. If any remain, apply the fallback strategy (override or lock-file)
|
|
1800
1633
|
*/
|
|
1801
1634
|
private async runPackageManagerInstall(
|
|
1802
1635
|
acc: Accumulator,
|
|
@@ -1804,10 +1637,11 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1804
1637
|
fixes: VulnerabilityFix[]
|
|
1805
1638
|
): Promise<void> {
|
|
1806
1639
|
// Separate direct fixes from transitive fixes
|
|
1807
|
-
|
|
1808
|
-
const
|
|
1640
|
+
// Fixes with fixViaDirectUpgrades are treated as direct fixes (they upgrade direct deps)
|
|
1641
|
+
const directFixes = fixes.filter(f => !f.isTransitive || (f.fixViaDirectUpgrades && f.fixViaDirectUpgrades.length > 0));
|
|
1642
|
+
const transitiveFixes = fixes.filter(f => f.isTransitive && (!f.fixViaDirectUpgrades || f.fixViaDirectUpgrades.length === 0));
|
|
1809
1643
|
|
|
1810
|
-
//
|
|
1644
|
+
// When preferDirectUpgrade is enabled, use two-phase approach
|
|
1811
1645
|
if (this.shouldVerifyTransitiveFixes() && transitiveFixes.length > 0) {
|
|
1812
1646
|
// Phase 1: Apply only direct fixes and check what transitives are still vulnerable
|
|
1813
1647
|
const phase1PackageJson = this.createModifiedPackageJson(
|
|
@@ -1858,13 +1692,12 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1858
1692
|
return;
|
|
1859
1693
|
}
|
|
1860
1694
|
|
|
1861
|
-
//
|
|
1695
|
+
// When preferDirectUpgrade is enabled, try to find higher direct dependency versions
|
|
1862
1696
|
// that fix the remaining transitive vulnerabilities
|
|
1863
|
-
if (this.
|
|
1697
|
+
if (this.preferDirectUpgrade) {
|
|
1864
1698
|
remainingTransitiveFixes = await this.tryDirectUpgradesForTransitives(
|
|
1865
1699
|
remainingTransitiveFixes,
|
|
1866
|
-
updateInfo
|
|
1867
|
-
acc.db
|
|
1700
|
+
updateInfo
|
|
1868
1701
|
);
|
|
1869
1702
|
}
|
|
1870
1703
|
|
|
@@ -1917,10 +1750,10 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1917
1750
|
* Runs the package manager in a temporary directory for a workspace.
|
|
1918
1751
|
* Handles direct dependency fixes in workspace members and transitive fixes in root.
|
|
1919
1752
|
*
|
|
1920
|
-
*
|
|
1753
|
+
* When preferDirectUpgrade is enabled, this uses a two-phase approach:
|
|
1921
1754
|
* 1. First run with only direct fixes (+ fixes via direct upgrade)
|
|
1922
1755
|
* 2. Check the resulting lock file for remaining transitive vulnerabilities
|
|
1923
|
-
* 3. If any remain,
|
|
1756
|
+
* 3. If any remain, apply the fallback strategy (override or lock-file)
|
|
1924
1757
|
*/
|
|
1925
1758
|
private async runWorkspacePackageManagerInstall(
|
|
1926
1759
|
acc: Accumulator,
|
|
@@ -1931,26 +1764,23 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1931
1764
|
const pm = rootUpdateInfo.packageManager;
|
|
1932
1765
|
|
|
1933
1766
|
// Collect all fixes from root and members, separating direct from transitive
|
|
1934
|
-
const allDirectFixes: VulnerabilityFix[] = [];
|
|
1935
1767
|
const allTransitiveFixes: VulnerabilityFix[] = [];
|
|
1936
1768
|
|
|
1937
1769
|
// Process root fixes
|
|
1938
1770
|
const rootFixes = acc.fixesByProject.get(rootPath) || [];
|
|
1939
|
-
const rootDirectFixes = rootFixes.filter(f => !f.isTransitive || f.
|
|
1940
|
-
const rootTransitiveFixes = rootFixes.filter(f => f.isTransitive && !f.
|
|
1941
|
-
allDirectFixes.push(...rootDirectFixes);
|
|
1771
|
+
const rootDirectFixes = rootFixes.filter(f => !f.isTransitive || (f.fixViaDirectUpgrades && f.fixViaDirectUpgrades.length > 0));
|
|
1772
|
+
const rootTransitiveFixes = rootFixes.filter(f => f.isTransitive && (!f.fixViaDirectUpgrades || f.fixViaDirectUpgrades.length === 0));
|
|
1942
1773
|
allTransitiveFixes.push(...rootTransitiveFixes);
|
|
1943
1774
|
|
|
1944
1775
|
// Process member fixes and collect transitive fixes
|
|
1945
1776
|
const memberDirectFixes = new Map<string, VulnerabilityFix[]>();
|
|
1946
1777
|
for (const memberPath of memberPaths) {
|
|
1947
1778
|
const memberFixes = acc.fixesByProject.get(memberPath) || [];
|
|
1948
|
-
const directFixes = memberFixes.filter(f => !f.isTransitive || f.
|
|
1949
|
-
const transitiveFixes = memberFixes.filter(f => f.isTransitive && !f.
|
|
1779
|
+
const directFixes = memberFixes.filter(f => !f.isTransitive || (f.fixViaDirectUpgrades && f.fixViaDirectUpgrades.length > 0));
|
|
1780
|
+
const transitiveFixes = memberFixes.filter(f => f.isTransitive && (!f.fixViaDirectUpgrades || f.fixViaDirectUpgrades.length === 0));
|
|
1950
1781
|
|
|
1951
1782
|
if (directFixes.length > 0) {
|
|
1952
1783
|
memberDirectFixes.set(memberPath, directFixes);
|
|
1953
|
-
allDirectFixes.push(...directFixes);
|
|
1954
1784
|
}
|
|
1955
1785
|
allTransitiveFixes.push(...transitiveFixes);
|
|
1956
1786
|
}
|
|
@@ -1980,7 +1810,7 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1980
1810
|
|
|
1981
1811
|
const rootOriginalContent = acc.allPackageJsonContents.get(rootPath) || rootUpdateInfo.originalPackageJson;
|
|
1982
1812
|
|
|
1983
|
-
//
|
|
1813
|
+
// When preferDirectUpgrade is enabled, use two-phase approach
|
|
1984
1814
|
if (this.shouldVerifyTransitiveFixes() && allTransitiveFixes.length > 0) {
|
|
1985
1815
|
// Phase 1: Apply only direct fixes
|
|
1986
1816
|
const phase1RootPackageJson = this.createModifiedPackageJson(
|
|
@@ -2033,13 +1863,12 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
2033
1863
|
return;
|
|
2034
1864
|
}
|
|
2035
1865
|
|
|
2036
|
-
//
|
|
1866
|
+
// When preferDirectUpgrade is enabled, try to find higher direct dependency versions
|
|
2037
1867
|
// that fix the remaining transitive vulnerabilities
|
|
2038
|
-
if (this.
|
|
1868
|
+
if (this.preferDirectUpgrade) {
|
|
2039
1869
|
remainingTransitiveFixes = await this.tryDirectUpgradesForTransitives(
|
|
2040
1870
|
remainingTransitiveFixes,
|
|
2041
|
-
rootUpdateInfo
|
|
2042
|
-
acc.db
|
|
1871
|
+
rootUpdateInfo
|
|
2043
1872
|
);
|
|
2044
1873
|
}
|
|
2045
1874
|
|
|
@@ -2142,18 +1971,27 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
2142
1971
|
packageJson[fix.scope][fix.packageName] = applyVersionPrefix(originalVersion, fix.newVersion);
|
|
2143
1972
|
}
|
|
2144
1973
|
} else if (fix.isTransitive) {
|
|
2145
|
-
//
|
|
2146
|
-
//
|
|
1974
|
+
// For 'lock-file' strategy, we don't add overrides for transitive deps.
|
|
1975
|
+
// The lock file update alone is expected to resolve safe versions.
|
|
1976
|
+
if (this.transitiveFixStrategy === 'lock-file') {
|
|
1977
|
+
continue;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
// If we found direct dependency upgrades that fix this transitive vulnerability,
|
|
1981
|
+
// upgrade those direct dependencies instead of using overrides. This is preferred because:
|
|
2147
1982
|
// 1. It's cleaner - no override/resolution clutter in package.json
|
|
2148
1983
|
// 2. It's more compatible - the direct dep author tested with their transitive deps
|
|
2149
1984
|
// 3. Future installs work naturally without forced versions
|
|
2150
|
-
if (fix.
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
const
|
|
2154
|
-
packageJson[directDepScope][directDepName]
|
|
2155
|
-
|
|
1985
|
+
if (fix.fixViaDirectUpgrades && fix.fixViaDirectUpgrades.length > 0) {
|
|
1986
|
+
// Apply ALL direct dependency upgrades that were found
|
|
1987
|
+
for (const upgrade of fix.fixViaDirectUpgrades) {
|
|
1988
|
+
const {directDepName, directDepVersion, directDepScope} = upgrade;
|
|
1989
|
+
if (packageJson[directDepScope]?.[directDepName]) {
|
|
1990
|
+
const originalVersion = packageJson[directDepScope][directDepName];
|
|
1991
|
+
packageJson[directDepScope][directDepName] = applyVersionPrefix(originalVersion, directDepVersion);
|
|
1992
|
+
}
|
|
2156
1993
|
}
|
|
1994
|
+
continue; // Skip the override logic below
|
|
2157
1995
|
}
|
|
2158
1996
|
|
|
2159
1997
|
// Check if this package is also a direct dependency in any scope.
|
|
@@ -2341,66 +2179,3 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
2341
2179
|
return packageJson;
|
|
2342
2180
|
}
|
|
2343
2181
|
}
|
|
2344
|
-
|
|
2345
|
-
/**
|
|
2346
|
-
* Extracts the version prefix (e.g., ^, ~, >=) from a version string.
|
|
2347
|
-
* Returns the prefix and the bare version separately.
|
|
2348
|
-
*/
|
|
2349
|
-
export function extractVersionPrefix(versionString: string): { prefix: string; version: string } {
|
|
2350
|
-
// Match common semver range prefixes: ^, ~, >=, <=, >, <, =
|
|
2351
|
-
const match = versionString.match(/^([~^]|>=?|<=?|=)?(.*)$/);
|
|
2352
|
-
if (match) {
|
|
2353
|
-
return {
|
|
2354
|
-
prefix: match[1] || '',
|
|
2355
|
-
version: match[2]
|
|
2356
|
-
};
|
|
2357
|
-
}
|
|
2358
|
-
return {prefix: '', version: versionString};
|
|
2359
|
-
}
|
|
2360
|
-
|
|
2361
|
-
/**
|
|
2362
|
-
* Applies the original version prefix to a new version.
|
|
2363
|
-
*/
|
|
2364
|
-
export function applyVersionPrefix(originalVersion: string, newVersion: string): string {
|
|
2365
|
-
const {prefix} = extractVersionPrefix(originalVersion);
|
|
2366
|
-
return prefix + newVersion;
|
|
2367
|
-
}
|
|
2368
|
-
|
|
2369
|
-
/**
|
|
2370
|
-
* Finds the dependency scope where a package is declared as a direct dependency.
|
|
2371
|
-
* Returns the scope name (dependencies, devDependencies, etc.) or undefined if not found.
|
|
2372
|
-
*/
|
|
2373
|
-
function findDirectDependencyScope(
|
|
2374
|
-
packageJson: Record<string, any>,
|
|
2375
|
-
packageName: string
|
|
2376
|
-
): DependencyScope | undefined {
|
|
2377
|
-
for (const scope of ALL_DEPENDENCY_SCOPES) {
|
|
2378
|
-
if (packageJson[scope]?.[packageName]) {
|
|
2379
|
-
return scope;
|
|
2380
|
-
}
|
|
2381
|
-
}
|
|
2382
|
-
return undefined;
|
|
2383
|
-
}
|
|
2384
|
-
|
|
2385
|
-
/**
|
|
2386
|
-
* Extracts the overrides section from a package.json based on the package manager.
|
|
2387
|
-
* Returns the overrides object or undefined if not present.
|
|
2388
|
-
*/
|
|
2389
|
-
function getOverridesFromPackageJson(
|
|
2390
|
-
packageJson: Record<string, any>,
|
|
2391
|
-
packageManager: PackageManager
|
|
2392
|
-
): Record<string, string> | undefined {
|
|
2393
|
-
switch (packageManager) {
|
|
2394
|
-
case PackageManager.Npm:
|
|
2395
|
-
case PackageManager.Bun:
|
|
2396
|
-
return packageJson.overrides;
|
|
2397
|
-
case PackageManager.Pnpm:
|
|
2398
|
-
return packageJson.pnpm?.overrides;
|
|
2399
|
-
case PackageManager.YarnClassic:
|
|
2400
|
-
case PackageManager.YarnBerry:
|
|
2401
|
-
return packageJson.resolutions;
|
|
2402
|
-
default:
|
|
2403
|
-
return undefined;
|
|
2404
|
-
}
|
|
2405
|
-
}
|
|
2406
|
-
|