@openrewrite/recipes-nodejs 0.37.0-20260106-082310 → 0.37.0-20260106-104324
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 +6 -54
- package/dist/security/dependency-vulnerability-check.d.ts.map +1 -1
- package/dist/security/dependency-vulnerability-check.js +133 -259
- 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 +232 -485
- 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 +116 -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
|
+
findDirectUpgradeWithSafeTransitiveInIsolation,
|
|
43
|
+
verifyAllUpgradesFixTransitive,
|
|
42
44
|
extractVersionFromLockFile,
|
|
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
|
*/
|
|
@@ -439,13 +349,6 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
439
349
|
return this.transitiveFixStrategy !== 'report';
|
|
440
350
|
}
|
|
441
351
|
|
|
442
|
-
/**
|
|
443
|
-
* Returns true if transitive vulnerabilities should be fixed with overrides.
|
|
444
|
-
*/
|
|
445
|
-
private shouldFixTransitives(): boolean {
|
|
446
|
-
return this.transitiveFixStrategy !== 'report';
|
|
447
|
-
}
|
|
448
|
-
|
|
449
352
|
/**
|
|
450
353
|
* Returns true if we should verify that transitive fixes are still needed
|
|
451
354
|
* after applying direct fixes (for override and prefer-direct-upgrade strategies).
|
|
@@ -474,7 +377,7 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
474
377
|
|
|
475
378
|
for (const fix of fixes) {
|
|
476
379
|
// Always keep direct fixes and fixes via direct upgrade
|
|
477
|
-
if (!fix.isTransitive || fix.
|
|
380
|
+
if (!fix.isTransitive || (fix.fixViaDirectUpgrades && fix.fixViaDirectUpgrades.length > 0)) {
|
|
478
381
|
result.push(fix);
|
|
479
382
|
continue;
|
|
480
383
|
}
|
|
@@ -521,7 +424,7 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
521
424
|
): boolean {
|
|
522
425
|
const vulns = db.getVulnerabilities(packageName);
|
|
523
426
|
for (const vuln of vulns) {
|
|
524
|
-
if (cves.includes(vuln.cve) &&
|
|
427
|
+
if (cves.includes(vuln.cve) && isVersionAffected(version, vuln)) {
|
|
525
428
|
return true;
|
|
526
429
|
}
|
|
527
430
|
}
|
|
@@ -559,147 +462,73 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
559
462
|
}
|
|
560
463
|
|
|
561
464
|
/**
|
|
562
|
-
*
|
|
465
|
+
* Renders a dependency path as a string.
|
|
466
|
+
* Example: "dependencies > lodash@4.17.20 > vulnerable-pkg@1.0.0"
|
|
563
467
|
*/
|
|
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
|
|
468
|
+
private renderPath(scope: DependencyScope | undefined, path: PathSegment[]): string {
|
|
469
|
+
const parts: string[] = [];
|
|
470
|
+
if (scope) {
|
|
471
|
+
parts.push(scope);
|
|
598
472
|
}
|
|
473
|
+
for (const seg of path) {
|
|
474
|
+
parts.push(`${seg.name}@${seg.version}`);
|
|
475
|
+
}
|
|
476
|
+
return parts.join(' > ');
|
|
599
477
|
}
|
|
600
478
|
|
|
601
479
|
/**
|
|
602
|
-
*
|
|
480
|
+
* Finds all direct dependencies that have a given transitive package in their dependency tree.
|
|
481
|
+
* This is needed because the visited set in findVulnerabilities prevents recording multiple
|
|
482
|
+
* paths to the same vulnerable package.
|
|
603
483
|
*/
|
|
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
|
-
}
|
|
484
|
+
private findAllDirectDepsForTransitive(
|
|
485
|
+
marker: NodeResolutionResult,
|
|
486
|
+
transitivePackageName: string,
|
|
487
|
+
scopesToCheck: DependencyScope[]
|
|
488
|
+
): { name: string; version: string; scope: DependencyScope }[] {
|
|
489
|
+
const results: { name: string; version: string; scope: DependencyScope }[] = [];
|
|
631
490
|
|
|
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;
|
|
491
|
+
for (const scope of scopesToCheck) {
|
|
492
|
+
const deps = marker[scope] || [];
|
|
493
|
+
for (const dep of deps) {
|
|
494
|
+
if (dep.resolved && this.hasTransitiveInTree(dep.resolved, transitivePackageName, new Set())) {
|
|
495
|
+
results.push({
|
|
496
|
+
name: dep.resolved.name,
|
|
497
|
+
version: dep.resolved.version,
|
|
498
|
+
scope
|
|
499
|
+
});
|
|
648
500
|
}
|
|
649
501
|
}
|
|
650
|
-
|
|
651
|
-
return false;
|
|
652
|
-
} catch {
|
|
653
|
-
return false;
|
|
654
502
|
}
|
|
655
|
-
}
|
|
656
503
|
|
|
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;
|
|
504
|
+
return results;
|
|
668
505
|
}
|
|
669
506
|
|
|
670
507
|
/**
|
|
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)
|
|
508
|
+
* Checks if a resolved dependency has a specific transitive in its dependency tree.
|
|
677
509
|
*/
|
|
678
|
-
private
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
return '';
|
|
687
|
-
}
|
|
688
|
-
}
|
|
510
|
+
private hasTransitiveInTree(
|
|
511
|
+
resolved: ResolvedDependency,
|
|
512
|
+
targetPackageName: string,
|
|
513
|
+
visited: Set<string>
|
|
514
|
+
): boolean {
|
|
515
|
+
const key = `${resolved.name}@${resolved.version}`;
|
|
516
|
+
if (visited.has(key)) return false;
|
|
517
|
+
visited.add(key);
|
|
689
518
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
parts.push(`${seg.name}@${seg.version}`);
|
|
519
|
+
// Check direct children
|
|
520
|
+
if (resolved.dependencies) {
|
|
521
|
+
for (const child of resolved.dependencies) {
|
|
522
|
+
if (child.name === targetPackageName) {
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
if (child.resolved && this.hasTransitiveInTree(child.resolved, targetPackageName, visited)) {
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
701
529
|
}
|
|
702
|
-
|
|
530
|
+
|
|
531
|
+
return false;
|
|
703
532
|
}
|
|
704
533
|
|
|
705
534
|
/**
|
|
@@ -736,7 +565,7 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
736
565
|
}
|
|
737
566
|
|
|
738
567
|
// Check if this version is affected
|
|
739
|
-
if (
|
|
568
|
+
if (isVersionAffected(resolved.version, vuln)) {
|
|
740
569
|
results.push({
|
|
741
570
|
resolved,
|
|
742
571
|
vulnerability: vuln,
|
|
@@ -793,7 +622,7 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
793
622
|
if (!dep.resolved) continue;
|
|
794
623
|
|
|
795
624
|
// Extract the minimum version from the version constraint
|
|
796
|
-
const declaredMinVersion =
|
|
625
|
+
const declaredMinVersion = extractMinimumVersion(dep.versionConstraint);
|
|
797
626
|
if (!declaredMinVersion) continue;
|
|
798
627
|
|
|
799
628
|
// Skip if declared version equals resolved version (no discrepancy)
|
|
@@ -817,8 +646,8 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
817
646
|
}
|
|
818
647
|
|
|
819
648
|
// Check if declared version is affected but resolved is not
|
|
820
|
-
if (
|
|
821
|
-
!
|
|
649
|
+
if (isVersionAffected(declaredMinVersion, vuln) &&
|
|
650
|
+
!isVersionAffected(dep.resolved.version, vuln)) {
|
|
822
651
|
affectedCves.push(vuln.cve);
|
|
823
652
|
affectedCveSummaries.set(vuln.cve, vuln.summary);
|
|
824
653
|
|
|
@@ -863,30 +692,7 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
863
692
|
if (this.isReportOnly()) {
|
|
864
693
|
return false;
|
|
865
694
|
}
|
|
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;
|
|
695
|
+
return isVersionWithinDelta(fromVersion, toVersion, this.maximumUpgradeDelta!);
|
|
890
696
|
}
|
|
891
697
|
|
|
892
698
|
/**
|
|
@@ -915,7 +721,7 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
915
721
|
|
|
916
722
|
// Check if the fix version itself has vulnerabilities
|
|
917
723
|
const vulnsInFixVersion = db.getVulnerabilities(packageName)
|
|
918
|
-
.filter(v =>
|
|
724
|
+
.filter(v => isVersionAffected(initialFixVersion, v));
|
|
919
725
|
|
|
920
726
|
if (vulnsInFixVersion.length === 0) {
|
|
921
727
|
// No vulnerabilities in this version - it's safe
|
|
@@ -925,10 +731,10 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
925
731
|
// Find fix versions for vulnerabilities in the current fix version
|
|
926
732
|
let highestFixVersion = initialFixVersion;
|
|
927
733
|
for (const vuln of vulnsInFixVersion) {
|
|
928
|
-
const fixVersion =
|
|
734
|
+
const fixVersion = getUpgradeVersion(vuln);
|
|
929
735
|
if (fixVersion && semver.valid(fixVersion)) {
|
|
930
736
|
// Check if this fix version is within delta from the ORIGINAL version
|
|
931
|
-
if (
|
|
737
|
+
if (isVersionWithinDelta(originalVersion, fixVersion, this.maximumUpgradeDelta!)) {
|
|
932
738
|
if (semver.gt(fixVersion, highestFixVersion)) {
|
|
933
739
|
highestFixVersion = fixVersion;
|
|
934
740
|
}
|
|
@@ -951,32 +757,6 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
951
757
|
return initialFixVersion;
|
|
952
758
|
}
|
|
953
759
|
|
|
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
760
|
/**
|
|
981
761
|
* Computes the fixes to apply for the vulnerabilities found.
|
|
982
762
|
* Groups vulnerabilities by package AND major version to handle packages with multiple
|
|
@@ -998,6 +778,8 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
998
778
|
packageManager: PackageManager;
|
|
999
779
|
originalPackageJson: string;
|
|
1000
780
|
configFiles?: Record<string, string>;
|
|
781
|
+
marker?: NodeResolutionResult;
|
|
782
|
+
scopesToCheck?: DependencyScope[];
|
|
1001
783
|
}
|
|
1002
784
|
): Promise<VulnerabilityFix[]> {
|
|
1003
785
|
if (this.isReportOnly()) {
|
|
@@ -1041,8 +823,8 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1041
823
|
let isTransitive = true;
|
|
1042
824
|
let scope: DependencyScope | undefined;
|
|
1043
825
|
let originalVersion: string | undefined;
|
|
1044
|
-
// Track
|
|
1045
|
-
|
|
826
|
+
// Track ALL direct dependencies that bring in this transitive
|
|
827
|
+
const directDepInfosMap = new Map<string, { name: string; version: string; scope: DependencyScope }>();
|
|
1046
828
|
|
|
1047
829
|
for (const vuln of vulns) {
|
|
1048
830
|
// Track the original version for delta checking
|
|
@@ -1050,22 +832,27 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1050
832
|
originalVersion = vuln.resolved.version;
|
|
1051
833
|
}
|
|
1052
834
|
|
|
1053
|
-
//
|
|
1054
|
-
if (!vuln.isDirect && vuln.path.length > 0
|
|
835
|
+
// Collect all unique direct dependencies that bring in this transitive
|
|
836
|
+
if (!vuln.isDirect && vuln.path.length > 0) {
|
|
1055
837
|
const directDep = vuln.path[0];
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
838
|
+
const depScope = vuln.scope || 'dependencies';
|
|
839
|
+
// Use name@scope as key to deduplicate (same dep could appear in different scopes)
|
|
840
|
+
const key = `${directDep.name}@${depScope}`;
|
|
841
|
+
if (!directDepInfosMap.has(key)) {
|
|
842
|
+
directDepInfosMap.set(key, {
|
|
843
|
+
name: directDep.name,
|
|
844
|
+
version: directDep.version,
|
|
845
|
+
scope: depScope
|
|
846
|
+
});
|
|
847
|
+
}
|
|
1061
848
|
}
|
|
1062
849
|
|
|
1063
850
|
// Check if upgradeable within delta
|
|
1064
|
-
if (
|
|
851
|
+
if (this.isReportOnly() || !isUpgradeableWithinDelta(vuln.resolved.version, vuln.vulnerability, this.maximumUpgradeDelta!)) {
|
|
1065
852
|
continue;
|
|
1066
853
|
}
|
|
1067
854
|
|
|
1068
|
-
const fixVersion =
|
|
855
|
+
const fixVersion = getUpgradeVersion(vuln.vulnerability);
|
|
1069
856
|
if (fixVersion) {
|
|
1070
857
|
// When there are multiple major versions of the same package in the tree
|
|
1071
858
|
// (e.g., semver@6.x and semver@7.x), only consider fix versions that match
|
|
@@ -1092,6 +879,23 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1092
879
|
}
|
|
1093
880
|
}
|
|
1094
881
|
|
|
882
|
+
// If this is a transitive vulnerability and we have a marker, find ALL direct deps
|
|
883
|
+
// that have this transitive in their tree. The visited set in findVulnerabilities
|
|
884
|
+
// prevents recording multiple paths to the same package, so we need this extra step.
|
|
885
|
+
if (isTransitive && projectContext?.marker && projectContext?.scopesToCheck) {
|
|
886
|
+
const allDirectDeps = this.findAllDirectDepsForTransitive(
|
|
887
|
+
projectContext.marker,
|
|
888
|
+
packageName,
|
|
889
|
+
projectContext.scopesToCheck
|
|
890
|
+
);
|
|
891
|
+
for (const dep of allDirectDeps) {
|
|
892
|
+
const key = `${dep.name}@${dep.scope}`;
|
|
893
|
+
if (!directDepInfosMap.has(key)) {
|
|
894
|
+
directDepInfosMap.set(key, dep);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
1095
899
|
if (highestFixVersion && cves.length > 0 && originalVersion) {
|
|
1096
900
|
// Recursively find the highest safe version (checking if fix versions have vulns)
|
|
1097
901
|
const safeVersion = this.findHighestSafeVersion(
|
|
@@ -1101,6 +905,9 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1101
905
|
db
|
|
1102
906
|
);
|
|
1103
907
|
|
|
908
|
+
// Convert directDepInfosMap to array for storage
|
|
909
|
+
const directDepInfosArray = Array.from(directDepInfosMap.values());
|
|
910
|
+
|
|
1104
911
|
const fix: VulnerabilityFix = {
|
|
1105
912
|
packageName,
|
|
1106
913
|
newVersion: safeVersion || highestFixVersion,
|
|
@@ -1109,12 +916,8 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1109
916
|
cveSummaries,
|
|
1110
917
|
scope,
|
|
1111
918
|
originalMajorVersion: originalMajor,
|
|
1112
|
-
// Store direct
|
|
1113
|
-
|
|
1114
|
-
name: directDepInfo.name,
|
|
1115
|
-
version: directDepInfo.version,
|
|
1116
|
-
scope: directDepInfo.scope
|
|
1117
|
-
} : undefined
|
|
919
|
+
// Store ALL direct dependencies for potential later use by prefer-direct-upgrade
|
|
920
|
+
directDepInfos: isTransitive && directDepInfosArray.length > 0 ? directDepInfosArray : undefined
|
|
1118
921
|
};
|
|
1119
922
|
|
|
1120
923
|
fixes.push(fix);
|
|
@@ -1125,103 +928,108 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1125
928
|
}
|
|
1126
929
|
|
|
1127
930
|
/**
|
|
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.
|
|
931
|
+
* For each remaining transitive fix, tries to find higher versions of ALL direct
|
|
932
|
+
* dependencies that bring in the transitive. Uses isolation-based testing to find
|
|
933
|
+
* candidate upgrades for each direct dep independently, then verifies all upgrades
|
|
934
|
+
* together fix the vulnerability.
|
|
1180
935
|
*
|
|
1181
936
|
* @param fixes The transitive fixes that are still needed after applying direct fixes
|
|
1182
937
|
* @param updateInfo Project update info containing package manager and config
|
|
1183
|
-
* @
|
|
1184
|
-
* @returns The fixes array with `fixViaDirectUpgrade` set where applicable
|
|
938
|
+
* @returns The fixes array with `fixViaDirectUpgrades` set where applicable
|
|
1185
939
|
*/
|
|
1186
940
|
private async tryDirectUpgradesForTransitives(
|
|
1187
941
|
fixes: VulnerabilityFix[],
|
|
1188
|
-
updateInfo: ProjectUpdateInfo
|
|
1189
|
-
db: VulnerabilityDatabase
|
|
942
|
+
updateInfo: ProjectUpdateInfo
|
|
1190
943
|
): Promise<VulnerabilityFix[]> {
|
|
1191
944
|
const result: VulnerabilityFix[] = [];
|
|
1192
945
|
|
|
1193
946
|
for (const fix of fixes) {
|
|
1194
947
|
// Skip if no direct dependency info available
|
|
1195
|
-
if (!fix.
|
|
948
|
+
if (!fix.directDepInfos || fix.directDepInfos.length === 0) {
|
|
1196
949
|
result.push(fix);
|
|
1197
950
|
continue;
|
|
1198
951
|
}
|
|
1199
952
|
|
|
1200
|
-
//
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
953
|
+
// Phase 1: Find candidate upgrades for ALL direct dependencies in isolation
|
|
954
|
+
// Each direct dep is tested independently to find a version that brings in a safe transitive
|
|
955
|
+
const candidateUpgrades: {
|
|
956
|
+
directDepName: string;
|
|
957
|
+
directDepVersion: string;
|
|
958
|
+
directDepScope: DependencyScope;
|
|
959
|
+
}[] = [];
|
|
960
|
+
|
|
961
|
+
// Create a vulnerability check function for the transitive
|
|
962
|
+
const isVulnerable = (version: string): boolean => {
|
|
963
|
+
try {
|
|
964
|
+
return semver.lt(version, fix.newVersion);
|
|
965
|
+
} catch {
|
|
966
|
+
return true;
|
|
967
|
+
}
|
|
968
|
+
};
|
|
1212
969
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
970
|
+
// Create a delta check function based on recipe settings
|
|
971
|
+
const isWithinDelta = (from: string, to: string): boolean => {
|
|
972
|
+
return isVersionWithinDelta(from, to, this.maximumUpgradeDelta!);
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
for (const directDepInfo of fix.directDepInfos) {
|
|
976
|
+
const directUpgradeVersion = await findDirectUpgradeWithSafeTransitiveInIsolation(
|
|
977
|
+
updateInfo.packageManager,
|
|
978
|
+
directDepInfo.name,
|
|
979
|
+
directDepInfo.version,
|
|
980
|
+
fix.packageName,
|
|
981
|
+
isVulnerable,
|
|
982
|
+
isWithinDelta,
|
|
983
|
+
directDepInfo.scope as NpmUtilsDependencyScope,
|
|
984
|
+
updateInfo.configFiles
|
|
985
|
+
);
|
|
986
|
+
|
|
987
|
+
if (directUpgradeVersion) {
|
|
988
|
+
candidateUpgrades.push({
|
|
989
|
+
directDepName: directDepInfo.name,
|
|
1219
990
|
directDepVersion: directUpgradeVersion,
|
|
1220
|
-
directDepScope:
|
|
991
|
+
directDepScope: directDepInfo.scope
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Phase 2: Verify all candidate upgrades together fix the transitive
|
|
997
|
+
if (candidateUpgrades.length > 0) {
|
|
998
|
+
// Check if we found upgrades for ALL direct deps
|
|
999
|
+
const foundAllUpgrades = candidateUpgrades.length === fix.directDepInfos.length;
|
|
1000
|
+
|
|
1001
|
+
if (foundAllUpgrades) {
|
|
1002
|
+
// Verify all upgrades together fix the vulnerability
|
|
1003
|
+
const allFixed = await verifyAllUpgradesFixTransitive(
|
|
1004
|
+
updateInfo.packageManager,
|
|
1005
|
+
candidateUpgrades.map(u => ({
|
|
1006
|
+
name: u.directDepName,
|
|
1007
|
+
version: u.directDepVersion,
|
|
1008
|
+
scope: u.directDepScope as NpmUtilsDependencyScope
|
|
1009
|
+
})),
|
|
1010
|
+
fix.packageName,
|
|
1011
|
+
isVulnerable,
|
|
1012
|
+
updateInfo.originalPackageJson,
|
|
1013
|
+
updateInfo.configFiles
|
|
1014
|
+
);
|
|
1015
|
+
|
|
1016
|
+
if (allFixed) {
|
|
1017
|
+
result.push({
|
|
1018
|
+
...fix,
|
|
1019
|
+
fixViaDirectUpgrades: candidateUpgrades
|
|
1020
|
+
});
|
|
1021
|
+
continue;
|
|
1221
1022
|
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// Partial upgrades found or verification failed - apply what we have
|
|
1026
|
+
// The two-phase mechanism will add an override if still needed
|
|
1027
|
+
result.push({
|
|
1028
|
+
...fix,
|
|
1029
|
+
fixViaDirectUpgrades: candidateUpgrades
|
|
1222
1030
|
});
|
|
1223
1031
|
} else {
|
|
1224
|
-
// No direct
|
|
1032
|
+
// No direct upgrades found, will use override
|
|
1225
1033
|
result.push(fix);
|
|
1226
1034
|
}
|
|
1227
1035
|
}
|
|
@@ -1340,7 +1148,9 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1340
1148
|
fixes = await recipe.computeFixes(vulnerabilities, acc.db, {
|
|
1341
1149
|
packageManager: pm,
|
|
1342
1150
|
originalPackageJson,
|
|
1343
|
-
configFiles: Object.keys(configFiles).length > 0 ? configFiles : undefined
|
|
1151
|
+
configFiles: Object.keys(configFiles).length > 0 ? configFiles : undefined,
|
|
1152
|
+
marker,
|
|
1153
|
+
scopesToCheck
|
|
1344
1154
|
});
|
|
1345
1155
|
}
|
|
1346
1156
|
|
|
@@ -1514,9 +1324,10 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1514
1324
|
// Insert rows into the data table for each vulnerability (always, even in report-only mode)
|
|
1515
1325
|
if (vulnerabilities && vulnerabilities.length > 0) {
|
|
1516
1326
|
for (const vuln of vulnerabilities) {
|
|
1517
|
-
const upgradeable = recipe.isUpgradeableWithinDelta(
|
|
1327
|
+
const upgradeable = !recipe.isReportOnly() && isUpgradeableWithinDelta(
|
|
1518
1328
|
vuln.resolved.version,
|
|
1519
|
-
vuln.vulnerability
|
|
1329
|
+
vuln.vulnerability,
|
|
1330
|
+
recipe.maximumUpgradeDelta!
|
|
1520
1331
|
);
|
|
1521
1332
|
|
|
1522
1333
|
recipe.vulnerabilityReport.insertRow(ctx, new VulnerabilityReportRow(
|
|
@@ -1804,8 +1615,9 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1804
1615
|
fixes: VulnerabilityFix[]
|
|
1805
1616
|
): Promise<void> {
|
|
1806
1617
|
// Separate direct fixes from transitive fixes
|
|
1807
|
-
|
|
1808
|
-
const
|
|
1618
|
+
// Fixes with fixViaDirectUpgrades are treated as direct fixes (they upgrade direct deps)
|
|
1619
|
+
const directFixes = fixes.filter(f => !f.isTransitive || (f.fixViaDirectUpgrades && f.fixViaDirectUpgrades.length > 0));
|
|
1620
|
+
const transitiveFixes = fixes.filter(f => f.isTransitive && (!f.fixViaDirectUpgrades || f.fixViaDirectUpgrades.length === 0));
|
|
1809
1621
|
|
|
1810
1622
|
// For 'override' and 'prefer-direct-upgrade', use two-phase approach
|
|
1811
1623
|
if (this.shouldVerifyTransitiveFixes() && transitiveFixes.length > 0) {
|
|
@@ -1863,8 +1675,7 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1863
1675
|
if (this.transitiveFixStrategy === 'prefer-direct-upgrade') {
|
|
1864
1676
|
remainingTransitiveFixes = await this.tryDirectUpgradesForTransitives(
|
|
1865
1677
|
remainingTransitiveFixes,
|
|
1866
|
-
updateInfo
|
|
1867
|
-
acc.db
|
|
1678
|
+
updateInfo
|
|
1868
1679
|
);
|
|
1869
1680
|
}
|
|
1870
1681
|
|
|
@@ -1931,26 +1742,23 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1931
1742
|
const pm = rootUpdateInfo.packageManager;
|
|
1932
1743
|
|
|
1933
1744
|
// Collect all fixes from root and members, separating direct from transitive
|
|
1934
|
-
const allDirectFixes: VulnerabilityFix[] = [];
|
|
1935
1745
|
const allTransitiveFixes: VulnerabilityFix[] = [];
|
|
1936
1746
|
|
|
1937
1747
|
// Process root fixes
|
|
1938
1748
|
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);
|
|
1749
|
+
const rootDirectFixes = rootFixes.filter(f => !f.isTransitive || (f.fixViaDirectUpgrades && f.fixViaDirectUpgrades.length > 0));
|
|
1750
|
+
const rootTransitiveFixes = rootFixes.filter(f => f.isTransitive && (!f.fixViaDirectUpgrades || f.fixViaDirectUpgrades.length === 0));
|
|
1942
1751
|
allTransitiveFixes.push(...rootTransitiveFixes);
|
|
1943
1752
|
|
|
1944
1753
|
// Process member fixes and collect transitive fixes
|
|
1945
1754
|
const memberDirectFixes = new Map<string, VulnerabilityFix[]>();
|
|
1946
1755
|
for (const memberPath of memberPaths) {
|
|
1947
1756
|
const memberFixes = acc.fixesByProject.get(memberPath) || [];
|
|
1948
|
-
const directFixes = memberFixes.filter(f => !f.isTransitive || f.
|
|
1949
|
-
const transitiveFixes = memberFixes.filter(f => f.isTransitive && !f.
|
|
1757
|
+
const directFixes = memberFixes.filter(f => !f.isTransitive || (f.fixViaDirectUpgrades && f.fixViaDirectUpgrades.length > 0));
|
|
1758
|
+
const transitiveFixes = memberFixes.filter(f => f.isTransitive && (!f.fixViaDirectUpgrades || f.fixViaDirectUpgrades.length === 0));
|
|
1950
1759
|
|
|
1951
1760
|
if (directFixes.length > 0) {
|
|
1952
1761
|
memberDirectFixes.set(memberPath, directFixes);
|
|
1953
|
-
allDirectFixes.push(...directFixes);
|
|
1954
1762
|
}
|
|
1955
1763
|
allTransitiveFixes.push(...transitiveFixes);
|
|
1956
1764
|
}
|
|
@@ -2038,8 +1846,7 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
2038
1846
|
if (this.transitiveFixStrategy === 'prefer-direct-upgrade') {
|
|
2039
1847
|
remainingTransitiveFixes = await this.tryDirectUpgradesForTransitives(
|
|
2040
1848
|
remainingTransitiveFixes,
|
|
2041
|
-
rootUpdateInfo
|
|
2042
|
-
acc.db
|
|
1849
|
+
rootUpdateInfo
|
|
2043
1850
|
);
|
|
2044
1851
|
}
|
|
2045
1852
|
|
|
@@ -2142,18 +1949,21 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
2142
1949
|
packageJson[fix.scope][fix.packageName] = applyVersionPrefix(originalVersion, fix.newVersion);
|
|
2143
1950
|
}
|
|
2144
1951
|
} else if (fix.isTransitive) {
|
|
2145
|
-
// If we found
|
|
2146
|
-
// upgrade
|
|
1952
|
+
// If we found direct dependency upgrades that fix this transitive vulnerability,
|
|
1953
|
+
// upgrade those direct dependencies instead of using overrides. This is preferred because:
|
|
2147
1954
|
// 1. It's cleaner - no override/resolution clutter in package.json
|
|
2148
1955
|
// 2. It's more compatible - the direct dep author tested with their transitive deps
|
|
2149
1956
|
// 3. Future installs work naturally without forced versions
|
|
2150
|
-
if (fix.
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
const
|
|
2154
|
-
packageJson[directDepScope][directDepName]
|
|
2155
|
-
|
|
1957
|
+
if (fix.fixViaDirectUpgrades && fix.fixViaDirectUpgrades.length > 0) {
|
|
1958
|
+
// Apply ALL direct dependency upgrades that were found
|
|
1959
|
+
for (const upgrade of fix.fixViaDirectUpgrades) {
|
|
1960
|
+
const {directDepName, directDepVersion, directDepScope} = upgrade;
|
|
1961
|
+
if (packageJson[directDepScope]?.[directDepName]) {
|
|
1962
|
+
const originalVersion = packageJson[directDepScope][directDepName];
|
|
1963
|
+
packageJson[directDepScope][directDepName] = applyVersionPrefix(originalVersion, directDepVersion);
|
|
1964
|
+
}
|
|
2156
1965
|
}
|
|
1966
|
+
continue; // Skip the override logic below
|
|
2157
1967
|
}
|
|
2158
1968
|
|
|
2159
1969
|
// Check if this package is also a direct dependency in any scope.
|
|
@@ -2341,66 +2151,3 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
2341
2151
|
return packageJson;
|
|
2342
2152
|
}
|
|
2343
2153
|
}
|
|
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
|
-
|