@openrewrite/recipes-nodejs 0.37.0-20260104-170507 → 0.37.0-20260106-082310
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/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/resources/advisories-npm.csv +19 -4
- package/dist/security/dependency-vulnerability-check.d.ts +25 -2
- package/dist/security/dependency-vulnerability-check.d.ts.map +1 -1
- package/dist/security/dependency-vulnerability-check.js +338 -96
- package/dist/security/dependency-vulnerability-check.js.map +1 -1
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +1 -0
- package/dist/security/index.js.map +1 -1
- package/dist/security/npm-utils.d.ts +21 -0
- package/dist/security/npm-utils.d.ts.map +1 -0
- package/dist/security/npm-utils.js +268 -0
- package/dist/security/npm-utils.js.map +1 -0
- package/dist/security/remove-redundant-overrides.d.ts +40 -0
- package/dist/security/remove-redundant-overrides.d.ts.map +1 -0
- package/dist/security/remove-redundant-overrides.js +379 -0
- package/dist/security/remove-redundant-overrides.js.map +1 -0
- package/package.json +7 -3
- package/src/index.ts +2 -1
- package/src/security/dependency-vulnerability-check.ts +622 -66
- package/src/security/index.ts +1 -0
- package/src/security/npm-utils.ts +414 -0
- package/src/security/remove-redundant-overrides.ts +515 -0
|
@@ -37,6 +37,11 @@ import {
|
|
|
37
37
|
import * as semver from "semver";
|
|
38
38
|
import * as path from "path";
|
|
39
39
|
import {parseSeverity, Severity, severityOrdinal, Vulnerability, VulnerabilityDatabase} from "./vulnerability";
|
|
40
|
+
import {
|
|
41
|
+
findDirectUpgradeThatFixesTransitive,
|
|
42
|
+
extractVersionFromLockFile,
|
|
43
|
+
DependencyScope as NpmUtilsDependencyScope
|
|
44
|
+
} from "./npm-utils";
|
|
40
45
|
|
|
41
46
|
/**
|
|
42
47
|
* All dependency scopes that can contain dependencies in package.json.
|
|
@@ -51,6 +56,23 @@ const ALL_DEPENDENCY_SCOPES: DependencyScope[] = [
|
|
|
51
56
|
*/
|
|
52
57
|
export type UpgradeDelta = 'none' | 'patch' | 'minor' | 'major';
|
|
53
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Strategy for handling transitive dependency vulnerabilities.
|
|
61
|
+
*
|
|
62
|
+
* - 'report': Report transitive vulnerabilities but don't fix them. (Default)
|
|
63
|
+
* - 'override': Add overrides/resolutions for transitives that aren't fixed
|
|
64
|
+
* by direct dependency upgrades. Runs an extra npm install
|
|
65
|
+
* to check which transitives are still vulnerable after
|
|
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';
|
|
75
|
+
|
|
54
76
|
/**
|
|
55
77
|
* Row for the vulnerability report data table.
|
|
56
78
|
*/
|
|
@@ -194,18 +216,43 @@ interface VulnerableDependency {
|
|
|
194
216
|
* Represents a fix to apply for a vulnerability.
|
|
195
217
|
*/
|
|
196
218
|
interface VulnerabilityFix {
|
|
197
|
-
/** Package name to upgrade */
|
|
219
|
+
/** Package name to upgrade (the vulnerable package) */
|
|
198
220
|
packageName: string;
|
|
199
221
|
/** Version to upgrade to */
|
|
200
222
|
newVersion: string;
|
|
201
|
-
/** Whether
|
|
223
|
+
/** Whether the vulnerable package is a transitive dependency */
|
|
202
224
|
isTransitive: boolean;
|
|
203
225
|
/** CVEs this fix resolves */
|
|
204
226
|
cves: string[];
|
|
227
|
+
/** CVE summaries for generating comments (CVE ID -> summary) */
|
|
228
|
+
cveSummaries: Map<string, string>;
|
|
205
229
|
/** The scope where this dependency was found (for direct deps) */
|
|
206
230
|
scope?: DependencyScope;
|
|
207
231
|
/** The original major version (for version-specific overrides) */
|
|
208
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
|
+
};
|
|
209
256
|
}
|
|
210
257
|
|
|
211
258
|
/**
|
|
@@ -285,14 +332,17 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
285
332
|
scope?: DependencyScope;
|
|
286
333
|
|
|
287
334
|
@Option({
|
|
288
|
-
displayName: "
|
|
289
|
-
description: "
|
|
290
|
-
"
|
|
291
|
-
"
|
|
335
|
+
displayName: "Transitive fix strategy",
|
|
336
|
+
description: "Strategy for handling transitive dependency vulnerabilities. " +
|
|
337
|
+
"'report' only reports them without fixing. " +
|
|
338
|
+
"'override' adds overrides only for transitives not fixed by direct upgrades (runs extra npm install to verify). " +
|
|
339
|
+
"'prefer-direct-upgrade' tries to find higher direct dependency versions that fix transitives, falls back to overrides (queries npm registry). " +
|
|
340
|
+
"Default is 'report'.",
|
|
292
341
|
required: false,
|
|
293
|
-
example: "
|
|
342
|
+
example: "override",
|
|
343
|
+
valid: ["report", "override", "prefer-direct-upgrade"]
|
|
294
344
|
})
|
|
295
|
-
|
|
345
|
+
transitiveFixStrategy?: TransitiveFixStrategy;
|
|
296
346
|
|
|
297
347
|
@Option({
|
|
298
348
|
displayName: "Maximum upgrade delta",
|
|
@@ -341,21 +391,34 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
341
391
|
})
|
|
342
392
|
fixDeclaredVersions?: boolean;
|
|
343
393
|
|
|
394
|
+
@Option({
|
|
395
|
+
displayName: "Add override comments",
|
|
396
|
+
description: "When enabled, adds a comment field (e.g., '//overrides') alongside overrides to document which CVEs " +
|
|
397
|
+
"each override is fixing. This helps with auditing and knowing when overrides can be removed. " +
|
|
398
|
+
"Default is true.",
|
|
399
|
+
required: false,
|
|
400
|
+
example: "true"
|
|
401
|
+
})
|
|
402
|
+
addOverrideComments?: boolean;
|
|
403
|
+
|
|
344
404
|
/** Cached compiled regex for CVE pattern matching */
|
|
345
405
|
private cvePatternRegex?: RegExp;
|
|
346
406
|
|
|
347
407
|
constructor(options?: {
|
|
348
408
|
scope?: DependencyScope;
|
|
349
|
-
|
|
409
|
+
transitiveFixStrategy?: TransitiveFixStrategy;
|
|
350
410
|
maximumUpgradeDelta?: UpgradeDelta;
|
|
351
411
|
minimumSeverity?: string;
|
|
352
412
|
cvePattern?: string;
|
|
353
413
|
fixDeclaredVersions?: boolean;
|
|
414
|
+
addOverrideComments?: boolean;
|
|
354
415
|
}) {
|
|
355
416
|
super(options);
|
|
417
|
+
this.transitiveFixStrategy ??= 'report';
|
|
356
418
|
this.maximumUpgradeDelta ??= 'patch';
|
|
357
419
|
this.minimumSeverity ??= Severity.LOW;
|
|
358
420
|
this.fixDeclaredVersions ??= false;
|
|
421
|
+
this.addOverrideComments ??= true;
|
|
359
422
|
if (options?.minimumSeverity) {
|
|
360
423
|
this.minimumSeverity = parseSeverity(options.minimumSeverity);
|
|
361
424
|
}
|
|
@@ -369,6 +432,102 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
369
432
|
}
|
|
370
433
|
}
|
|
371
434
|
|
|
435
|
+
/**
|
|
436
|
+
* Returns true if transitive vulnerabilities should be scanned.
|
|
437
|
+
*/
|
|
438
|
+
private shouldScanTransitives(): boolean {
|
|
439
|
+
return this.transitiveFixStrategy !== 'report';
|
|
440
|
+
}
|
|
441
|
+
|
|
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
|
+
/**
|
|
450
|
+
* Returns true if we should verify that transitive fixes are still needed
|
|
451
|
+
* after applying direct fixes (for override and prefer-direct-upgrade strategies).
|
|
452
|
+
*/
|
|
453
|
+
private shouldVerifyTransitiveFixes(): boolean {
|
|
454
|
+
return this.transitiveFixStrategy === 'override' ||
|
|
455
|
+
this.transitiveFixStrategy === 'prefer-direct-upgrade';
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Filters out transitive fixes that are no longer needed based on the lock file
|
|
460
|
+
* after applying direct fixes. Returns only the fixes that are still required.
|
|
461
|
+
*
|
|
462
|
+
* @param fixes All computed fixes
|
|
463
|
+
* @param lockFileContent The lock file content after applying direct fixes
|
|
464
|
+
* @param packageManager The package manager used
|
|
465
|
+
* @param db The vulnerability database
|
|
466
|
+
*/
|
|
467
|
+
private filterRemainingTransitiveFixes(
|
|
468
|
+
fixes: VulnerabilityFix[],
|
|
469
|
+
lockFileContent: string,
|
|
470
|
+
packageManager: PackageManager,
|
|
471
|
+
db: VulnerabilityDatabase
|
|
472
|
+
): VulnerabilityFix[] {
|
|
473
|
+
const result: VulnerabilityFix[] = [];
|
|
474
|
+
|
|
475
|
+
for (const fix of fixes) {
|
|
476
|
+
// Always keep direct fixes and fixes via direct upgrade
|
|
477
|
+
if (!fix.isTransitive || fix.fixViaDirectUpgrade) {
|
|
478
|
+
result.push(fix);
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// For transitive fixes, check if the package is still vulnerable in the lock file
|
|
483
|
+
const resolvedVersion = extractVersionFromLockFile(
|
|
484
|
+
lockFileContent,
|
|
485
|
+
fix.packageName,
|
|
486
|
+
packageManager
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
if (!resolvedVersion) {
|
|
490
|
+
// Package not found in lock file - might have been removed by direct upgrade
|
|
491
|
+
// Skip this fix (transitive is no longer in the tree)
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Check if the resolved version is still vulnerable
|
|
496
|
+
const isStillVulnerable = this.isVersionStillVulnerable(
|
|
497
|
+
fix.packageName,
|
|
498
|
+
resolvedVersion,
|
|
499
|
+
fix.cves,
|
|
500
|
+
db
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
if (isStillVulnerable) {
|
|
504
|
+
// Transitive is still vulnerable, keep the fix
|
|
505
|
+
result.push(fix);
|
|
506
|
+
}
|
|
507
|
+
// Otherwise, the direct upgrade already fixed it - skip the override
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return result;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Checks if a package version is still vulnerable to any of the specified CVEs.
|
|
515
|
+
*/
|
|
516
|
+
private isVersionStillVulnerable(
|
|
517
|
+
packageName: string,
|
|
518
|
+
version: string,
|
|
519
|
+
cves: string[],
|
|
520
|
+
db: VulnerabilityDatabase
|
|
521
|
+
): boolean {
|
|
522
|
+
const vulns = db.getVulnerabilities(packageName);
|
|
523
|
+
for (const vuln of vulns) {
|
|
524
|
+
if (cves.includes(vuln.cve) && this.isVersionAffected(version, vuln)) {
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
|
|
372
531
|
override initialValue(_ctx: ExecutionContext): Accumulator {
|
|
373
532
|
return {
|
|
374
533
|
// DependencyRecipeAccumulator fields
|
|
@@ -589,8 +748,8 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
589
748
|
}
|
|
590
749
|
}
|
|
591
750
|
|
|
592
|
-
// Recurse into transitive dependencies if
|
|
593
|
-
if (this.
|
|
751
|
+
// Recurse into transitive dependencies if we need to scan them
|
|
752
|
+
if (this.shouldScanTransitives()) {
|
|
594
753
|
const transitives = [
|
|
595
754
|
...(resolved.dependencies || []),
|
|
596
755
|
...(resolved.devDependencies || []),
|
|
@@ -643,6 +802,7 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
643
802
|
// Check if the declared minimum version is vulnerable
|
|
644
803
|
const vulns = db.getVulnerabilities(dep.name);
|
|
645
804
|
const affectedCves: string[] = [];
|
|
805
|
+
const affectedCveSummaries = new Map<string, string>();
|
|
646
806
|
let highestFixVersion: string | undefined;
|
|
647
807
|
|
|
648
808
|
for (const vuln of vulns) {
|
|
@@ -660,6 +820,7 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
660
820
|
if (this.isVersionAffected(declaredMinVersion, vuln) &&
|
|
661
821
|
!this.isVersionAffected(dep.resolved.version, vuln)) {
|
|
662
822
|
affectedCves.push(vuln.cve);
|
|
823
|
+
affectedCveSummaries.set(vuln.cve, vuln.summary);
|
|
663
824
|
|
|
664
825
|
// Find fix version for this vulnerability.
|
|
665
826
|
// Only use fixedVersion - don't guess based on lastAffectedVersion
|
|
@@ -683,6 +844,7 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
683
844
|
scope,
|
|
684
845
|
isTransitive: false,
|
|
685
846
|
cves: affectedCves,
|
|
847
|
+
cveSummaries: affectedCveSummaries,
|
|
686
848
|
originalMajorVersion: majorVersion
|
|
687
849
|
});
|
|
688
850
|
}
|
|
@@ -821,13 +983,23 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
821
983
|
* major versions in the dependency tree (e.g., semver@6.x and semver@7.x).
|
|
822
984
|
* Recursively checks if fix versions have their own vulnerabilities.
|
|
823
985
|
*
|
|
986
|
+
* For transitive vulnerabilities, first tries to find a direct dependency upgrade
|
|
987
|
+
* that includes a fixed version of the transitive. Falls back to overrides if
|
|
988
|
+
* no suitable direct dependency upgrade is found.
|
|
989
|
+
*
|
|
824
990
|
* @param vulnerabilities The vulnerabilities found during scanning
|
|
825
991
|
* @param db The vulnerability database
|
|
992
|
+
* @param projectContext Context needed to check direct dependency upgrades
|
|
826
993
|
*/
|
|
827
|
-
private computeFixes(
|
|
994
|
+
private async computeFixes(
|
|
828
995
|
vulnerabilities: VulnerableDependency[],
|
|
829
|
-
db: VulnerabilityDatabase
|
|
830
|
-
|
|
996
|
+
db: VulnerabilityDatabase,
|
|
997
|
+
projectContext?: {
|
|
998
|
+
packageManager: PackageManager;
|
|
999
|
+
originalPackageJson: string;
|
|
1000
|
+
configFiles?: Record<string, string>;
|
|
1001
|
+
}
|
|
1002
|
+
): Promise<VulnerabilityFix[]> {
|
|
831
1003
|
if (this.isReportOnly()) {
|
|
832
1004
|
return [];
|
|
833
1005
|
}
|
|
@@ -865,9 +1037,12 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
865
1037
|
// Find the highest fix version needed across all vulnerabilities for this package@major
|
|
866
1038
|
let highestFixVersion: string | undefined;
|
|
867
1039
|
const cves: string[] = [];
|
|
1040
|
+
const cveSummaries = new Map<string, string>();
|
|
868
1041
|
let isTransitive = true;
|
|
869
1042
|
let scope: DependencyScope | undefined;
|
|
870
1043
|
let originalVersion: string | undefined;
|
|
1044
|
+
// Track the direct dependency that brings in this transitive (from the first vuln's path)
|
|
1045
|
+
let directDepInfo: { name: string; version: string; scope: DependencyScope } | undefined;
|
|
871
1046
|
|
|
872
1047
|
for (const vuln of vulns) {
|
|
873
1048
|
// Track the original version for delta checking
|
|
@@ -875,6 +1050,16 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
875
1050
|
originalVersion = vuln.resolved.version;
|
|
876
1051
|
}
|
|
877
1052
|
|
|
1053
|
+
// Track direct dependency info for transitive vulnerabilities
|
|
1054
|
+
if (!vuln.isDirect && vuln.path.length > 0 && !directDepInfo) {
|
|
1055
|
+
const directDep = vuln.path[0];
|
|
1056
|
+
directDepInfo = {
|
|
1057
|
+
name: directDep.name,
|
|
1058
|
+
version: directDep.version,
|
|
1059
|
+
scope: vuln.scope || 'dependencies'
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
|
|
878
1063
|
// Check if upgradeable within delta
|
|
879
1064
|
if (!this.isUpgradeableWithinDelta(vuln.resolved.version, vuln.vulnerability)) {
|
|
880
1065
|
continue;
|
|
@@ -899,6 +1084,7 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
899
1084
|
}
|
|
900
1085
|
|
|
901
1086
|
cves.push(vuln.vulnerability.cve);
|
|
1087
|
+
cveSummaries.set(vuln.vulnerability.cve, vuln.vulnerability.summary);
|
|
902
1088
|
|
|
903
1089
|
if (vuln.isDirect) {
|
|
904
1090
|
isTransitive = false;
|
|
@@ -915,20 +1101,134 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
915
1101
|
db
|
|
916
1102
|
);
|
|
917
1103
|
|
|
918
|
-
|
|
1104
|
+
const fix: VulnerabilityFix = {
|
|
919
1105
|
packageName,
|
|
920
1106
|
newVersion: safeVersion || highestFixVersion,
|
|
921
1107
|
isTransitive,
|
|
922
1108
|
cves,
|
|
1109
|
+
cveSummaries,
|
|
923
1110
|
scope,
|
|
924
|
-
originalMajorVersion: originalMajor
|
|
925
|
-
|
|
1111
|
+
originalMajorVersion: originalMajor,
|
|
1112
|
+
// Store direct dependency info for potential later use by prefer-direct-upgrade
|
|
1113
|
+
directDepInfo: isTransitive && directDepInfo ? {
|
|
1114
|
+
name: directDepInfo.name,
|
|
1115
|
+
version: directDepInfo.version,
|
|
1116
|
+
scope: directDepInfo.scope
|
|
1117
|
+
} : undefined
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
fixes.push(fix);
|
|
926
1121
|
}
|
|
927
1122
|
}
|
|
928
1123
|
|
|
929
1124
|
return fixes;
|
|
930
1125
|
}
|
|
931
1126
|
|
|
1127
|
+
/**
|
|
1128
|
+
* Tries to find a direct dependency upgrade that fixes a transitive vulnerability.
|
|
1129
|
+
* Returns the version to upgrade to, or undefined if no suitable upgrade found.
|
|
1130
|
+
*/
|
|
1131
|
+
private async tryFindDirectDepUpgrade(
|
|
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.
|
|
1180
|
+
*
|
|
1181
|
+
* @param fixes The transitive fixes that are still needed after applying direct fixes
|
|
1182
|
+
* @param updateInfo Project update info containing package manager and config
|
|
1183
|
+
* @param db The vulnerability database
|
|
1184
|
+
* @returns The fixes array with `fixViaDirectUpgrade` set where applicable
|
|
1185
|
+
*/
|
|
1186
|
+
private async tryDirectUpgradesForTransitives(
|
|
1187
|
+
fixes: VulnerabilityFix[],
|
|
1188
|
+
updateInfo: ProjectUpdateInfo,
|
|
1189
|
+
db: VulnerabilityDatabase
|
|
1190
|
+
): Promise<VulnerabilityFix[]> {
|
|
1191
|
+
const result: VulnerabilityFix[] = [];
|
|
1192
|
+
|
|
1193
|
+
for (const fix of fixes) {
|
|
1194
|
+
// Skip if no direct dependency info available
|
|
1195
|
+
if (!fix.directDepInfo) {
|
|
1196
|
+
result.push(fix);
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Try to find a direct dependency upgrade that fixes this transitive
|
|
1201
|
+
const directUpgradeVersion = await this.tryFindDirectDepUpgrade(
|
|
1202
|
+
fix.packageName,
|
|
1203
|
+
fix.newVersion,
|
|
1204
|
+
fix.directDepInfo,
|
|
1205
|
+
{
|
|
1206
|
+
packageManager: updateInfo.packageManager,
|
|
1207
|
+
originalPackageJson: updateInfo.originalPackageJson,
|
|
1208
|
+
configFiles: updateInfo.configFiles
|
|
1209
|
+
},
|
|
1210
|
+
db
|
|
1211
|
+
);
|
|
1212
|
+
|
|
1213
|
+
if (directUpgradeVersion) {
|
|
1214
|
+
// Found a direct upgrade that fixes the transitive
|
|
1215
|
+
result.push({
|
|
1216
|
+
...fix,
|
|
1217
|
+
fixViaDirectUpgrade: {
|
|
1218
|
+
directDepName: fix.directDepInfo.name,
|
|
1219
|
+
directDepVersion: directUpgradeVersion,
|
|
1220
|
+
directDepScope: fix.directDepInfo.scope
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
} else {
|
|
1224
|
+
// No direct upgrade found, will use override
|
|
1225
|
+
result.push(fix);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
return result;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
932
1232
|
override async scanner(acc: Accumulator): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
933
1233
|
const recipe = this;
|
|
934
1234
|
|
|
@@ -1036,7 +1336,12 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1036
1336
|
let fixes: VulnerabilityFix[] = [];
|
|
1037
1337
|
if (vulnerabilities.length > 0) {
|
|
1038
1338
|
acc.vulnerableByProject.set(doc.sourcePath, vulnerabilities);
|
|
1039
|
-
|
|
1339
|
+
const originalPackageJson = storedContent || await TreePrinters.print(doc);
|
|
1340
|
+
fixes = await recipe.computeFixes(vulnerabilities, acc.db, {
|
|
1341
|
+
packageManager: pm,
|
|
1342
|
+
originalPackageJson,
|
|
1343
|
+
configFiles: Object.keys(configFiles).length > 0 ? configFiles : undefined
|
|
1344
|
+
});
|
|
1040
1345
|
}
|
|
1041
1346
|
|
|
1042
1347
|
// Fix declared versions for preventive upgrades (when enabled)
|
|
@@ -1487,13 +1792,103 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1487
1792
|
* Runs the package manager in a temporary directory to update the lock file.
|
|
1488
1793
|
* Writes a modified package.json with all new versions, then runs install.
|
|
1489
1794
|
* All file contents are provided from in-memory sources (SourceFiles), not read from disk.
|
|
1795
|
+
*
|
|
1796
|
+
* For 'override' and 'prefer-direct-upgrade' strategies, this uses a two-phase approach:
|
|
1797
|
+
* 1. First run with only direct fixes (+ fixes via direct upgrade)
|
|
1798
|
+
* 2. Check the resulting lock file for remaining transitive vulnerabilities
|
|
1799
|
+
* 3. If any remain, add overrides and run again
|
|
1490
1800
|
*/
|
|
1491
1801
|
private async runPackageManagerInstall(
|
|
1492
1802
|
acc: Accumulator,
|
|
1493
1803
|
updateInfo: ProjectUpdateInfo,
|
|
1494
1804
|
fixes: VulnerabilityFix[]
|
|
1495
1805
|
): Promise<void> {
|
|
1496
|
-
//
|
|
1806
|
+
// Separate direct fixes from transitive fixes
|
|
1807
|
+
const directFixes = fixes.filter(f => !f.isTransitive || f.fixViaDirectUpgrade);
|
|
1808
|
+
const transitiveFixes = fixes.filter(f => f.isTransitive && !f.fixViaDirectUpgrade);
|
|
1809
|
+
|
|
1810
|
+
// For 'override' and 'prefer-direct-upgrade', use two-phase approach
|
|
1811
|
+
if (this.shouldVerifyTransitiveFixes() && transitiveFixes.length > 0) {
|
|
1812
|
+
// Phase 1: Apply only direct fixes and check what transitives are still vulnerable
|
|
1813
|
+
const phase1PackageJson = this.createModifiedPackageJson(
|
|
1814
|
+
updateInfo.originalPackageJson,
|
|
1815
|
+
directFixes,
|
|
1816
|
+
updateInfo.packageManager
|
|
1817
|
+
);
|
|
1818
|
+
|
|
1819
|
+
const phase1Result = await runInstallInTempDir(
|
|
1820
|
+
updateInfo.packageManager,
|
|
1821
|
+
phase1PackageJson,
|
|
1822
|
+
{
|
|
1823
|
+
configFiles: updateInfo.configFiles
|
|
1824
|
+
}
|
|
1825
|
+
);
|
|
1826
|
+
|
|
1827
|
+
if (!phase1Result.success || !phase1Result.lockFileContent) {
|
|
1828
|
+
// Phase 1 failed, fall back to applying all fixes
|
|
1829
|
+
const modifiedPackageJson = this.createModifiedPackageJson(
|
|
1830
|
+
updateInfo.originalPackageJson,
|
|
1831
|
+
fixes,
|
|
1832
|
+
updateInfo.packageManager
|
|
1833
|
+
);
|
|
1834
|
+
|
|
1835
|
+
const result = await runInstallInTempDir(
|
|
1836
|
+
updateInfo.packageManager,
|
|
1837
|
+
modifiedPackageJson,
|
|
1838
|
+
{
|
|
1839
|
+
configFiles: updateInfo.configFiles
|
|
1840
|
+
}
|
|
1841
|
+
);
|
|
1842
|
+
|
|
1843
|
+
storeInstallResult(result, acc, updateInfo, modifiedPackageJson);
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// Filter out transitive fixes that are no longer needed
|
|
1848
|
+
let remainingTransitiveFixes = this.filterRemainingTransitiveFixes(
|
|
1849
|
+
transitiveFixes,
|
|
1850
|
+
phase1Result.lockFileContent,
|
|
1851
|
+
updateInfo.packageManager,
|
|
1852
|
+
acc.db
|
|
1853
|
+
);
|
|
1854
|
+
|
|
1855
|
+
if (remainingTransitiveFixes.length === 0) {
|
|
1856
|
+
// All transitives were fixed by direct upgrades - use phase 1 result
|
|
1857
|
+
storeInstallResult(phase1Result, acc, updateInfo, phase1PackageJson);
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// For 'prefer-direct-upgrade', try to find higher direct dependency versions
|
|
1862
|
+
// that fix the remaining transitive vulnerabilities
|
|
1863
|
+
if (this.transitiveFixStrategy === 'prefer-direct-upgrade') {
|
|
1864
|
+
remainingTransitiveFixes = await this.tryDirectUpgradesForTransitives(
|
|
1865
|
+
remainingTransitiveFixes,
|
|
1866
|
+
updateInfo,
|
|
1867
|
+
acc.db
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// Phase 2: Apply remaining transitive fixes (either as direct upgrades or overrides)
|
|
1872
|
+
const finalFixes = [...directFixes, ...remainingTransitiveFixes];
|
|
1873
|
+
const finalPackageJson = this.createModifiedPackageJson(
|
|
1874
|
+
updateInfo.originalPackageJson,
|
|
1875
|
+
finalFixes,
|
|
1876
|
+
updateInfo.packageManager
|
|
1877
|
+
);
|
|
1878
|
+
|
|
1879
|
+
const finalResult = await runInstallInTempDir(
|
|
1880
|
+
updateInfo.packageManager,
|
|
1881
|
+
finalPackageJson,
|
|
1882
|
+
{
|
|
1883
|
+
configFiles: updateInfo.configFiles
|
|
1884
|
+
}
|
|
1885
|
+
);
|
|
1886
|
+
|
|
1887
|
+
storeInstallResult(finalResult, acc, updateInfo, finalPackageJson);
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
// When there are no transitive fixes, apply all fixes directly
|
|
1497
1892
|
const modifiedPackageJson = this.createModifiedPackageJson(
|
|
1498
1893
|
updateInfo.originalPackageJson,
|
|
1499
1894
|
fixes,
|
|
@@ -1521,6 +1916,11 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1521
1916
|
/**
|
|
1522
1917
|
* Runs the package manager in a temporary directory for a workspace.
|
|
1523
1918
|
* Handles direct dependency fixes in workspace members and transitive fixes in root.
|
|
1919
|
+
*
|
|
1920
|
+
* For 'override' and 'prefer-direct-upgrade' strategies, this uses a two-phase approach:
|
|
1921
|
+
* 1. First run with only direct fixes (+ fixes via direct upgrade)
|
|
1922
|
+
* 2. Check the resulting lock file for remaining transitive vulnerabilities
|
|
1923
|
+
* 3. If any remain, add overrides and run again
|
|
1524
1924
|
*/
|
|
1525
1925
|
private async runWorkspacePackageManagerInstall(
|
|
1526
1926
|
acc: Accumulator,
|
|
@@ -1530,39 +1930,32 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1530
1930
|
const memberPaths = acc.workspaceRoots.get(rootPath) || [];
|
|
1531
1931
|
const pm = rootUpdateInfo.packageManager;
|
|
1532
1932
|
|
|
1533
|
-
// Collect all
|
|
1933
|
+
// Collect all fixes from root and members, separating direct from transitive
|
|
1934
|
+
const allDirectFixes: VulnerabilityFix[] = [];
|
|
1534
1935
|
const allTransitiveFixes: VulnerabilityFix[] = [];
|
|
1535
1936
|
|
|
1536
1937
|
// Process root fixes
|
|
1537
1938
|
const rootFixes = acc.fixesByProject.get(rootPath) || [];
|
|
1538
|
-
const rootDirectFixes = rootFixes.filter(f => !f.isTransitive);
|
|
1539
|
-
const rootTransitiveFixes = rootFixes.filter(f => f.isTransitive);
|
|
1939
|
+
const rootDirectFixes = rootFixes.filter(f => !f.isTransitive || f.fixViaDirectUpgrade);
|
|
1940
|
+
const rootTransitiveFixes = rootFixes.filter(f => f.isTransitive && !f.fixViaDirectUpgrade);
|
|
1941
|
+
allDirectFixes.push(...rootDirectFixes);
|
|
1540
1942
|
allTransitiveFixes.push(...rootTransitiveFixes);
|
|
1541
1943
|
|
|
1542
1944
|
// Process member fixes and collect transitive fixes
|
|
1543
1945
|
const memberDirectFixes = new Map<string, VulnerabilityFix[]>();
|
|
1544
1946
|
for (const memberPath of memberPaths) {
|
|
1545
1947
|
const memberFixes = acc.fixesByProject.get(memberPath) || [];
|
|
1546
|
-
const directFixes = memberFixes.filter(f => !f.isTransitive);
|
|
1547
|
-
const transitiveFixes = memberFixes.filter(f => f.isTransitive);
|
|
1948
|
+
const directFixes = memberFixes.filter(f => !f.isTransitive || f.fixViaDirectUpgrade);
|
|
1949
|
+
const transitiveFixes = memberFixes.filter(f => f.isTransitive && !f.fixViaDirectUpgrade);
|
|
1548
1950
|
|
|
1549
1951
|
if (directFixes.length > 0) {
|
|
1550
1952
|
memberDirectFixes.set(memberPath, directFixes);
|
|
1953
|
+
allDirectFixes.push(...directFixes);
|
|
1551
1954
|
}
|
|
1552
1955
|
allTransitiveFixes.push(...transitiveFixes);
|
|
1553
1956
|
}
|
|
1554
1957
|
|
|
1555
|
-
// Create modified root package.json (with direct fixes + all transitive overrides)
|
|
1556
|
-
const rootOriginalContent = acc.allPackageJsonContents.get(rootPath) || rootUpdateInfo.originalPackageJson;
|
|
1557
|
-
const modifiedRootPackageJson = this.createModifiedPackageJson(
|
|
1558
|
-
rootOriginalContent,
|
|
1559
|
-
[...rootDirectFixes, ...allTransitiveFixes],
|
|
1560
|
-
pm
|
|
1561
|
-
);
|
|
1562
|
-
|
|
1563
1958
|
// Create modified workspace member package.json files (with direct fixes only)
|
|
1564
|
-
// Keep the full package.json content including workspace: dependencies so Yarn
|
|
1565
|
-
// can properly resolve them in the temp directory
|
|
1566
1959
|
const workspacePackages: Record<string, string> = {};
|
|
1567
1960
|
for (const memberPath of memberPaths) {
|
|
1568
1961
|
const originalContent = acc.allPackageJsonContents.get(memberPath);
|
|
@@ -1573,28 +1966,110 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1573
1966
|
const directFixes = memberDirectFixes.get(memberPath);
|
|
1574
1967
|
let modifiedContent: string;
|
|
1575
1968
|
if (directFixes && directFixes.length > 0) {
|
|
1576
|
-
// Apply direct dependency fixes to this member
|
|
1577
1969
|
modifiedContent = this.createModifiedPackageJsonDirectOnly(
|
|
1578
1970
|
originalContent,
|
|
1579
1971
|
directFixes
|
|
1580
1972
|
);
|
|
1581
|
-
// Store the modified content for the actual output
|
|
1582
1973
|
acc.modifiedWorkspaceMemberContents.set(memberPath, modifiedContent);
|
|
1583
1974
|
} else {
|
|
1584
1975
|
modifiedContent = originalContent;
|
|
1585
1976
|
}
|
|
1586
1977
|
|
|
1587
|
-
// Keep the full package.json including workspace: dependencies
|
|
1588
1978
|
workspacePackages[memberPath] = modifiedContent;
|
|
1589
1979
|
}
|
|
1590
1980
|
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
//
|
|
1594
|
-
|
|
1981
|
+
const rootOriginalContent = acc.allPackageJsonContents.get(rootPath) || rootUpdateInfo.originalPackageJson;
|
|
1982
|
+
|
|
1983
|
+
// For 'override' and 'prefer-direct-upgrade', use two-phase approach
|
|
1984
|
+
if (this.shouldVerifyTransitiveFixes() && allTransitiveFixes.length > 0) {
|
|
1985
|
+
// Phase 1: Apply only direct fixes
|
|
1986
|
+
const phase1RootPackageJson = this.createModifiedPackageJson(
|
|
1987
|
+
rootOriginalContent,
|
|
1988
|
+
rootDirectFixes,
|
|
1989
|
+
pm
|
|
1990
|
+
);
|
|
1991
|
+
|
|
1992
|
+
const phase1Result = await runWorkspaceInstallInTempDir(
|
|
1993
|
+
pm,
|
|
1994
|
+
phase1RootPackageJson,
|
|
1995
|
+
{
|
|
1996
|
+
configFiles: rootUpdateInfo.configFiles,
|
|
1997
|
+
workspacePackages
|
|
1998
|
+
}
|
|
1999
|
+
);
|
|
2000
|
+
|
|
2001
|
+
if (!phase1Result.success || !phase1Result.lockFileContent) {
|
|
2002
|
+
// Phase 1 failed, fall back to applying all fixes
|
|
2003
|
+
const modifiedRootPackageJson = this.createModifiedPackageJson(
|
|
2004
|
+
rootOriginalContent,
|
|
2005
|
+
[...rootDirectFixes, ...allTransitiveFixes],
|
|
2006
|
+
pm
|
|
2007
|
+
);
|
|
2008
|
+
|
|
2009
|
+
const result = await runWorkspaceInstallInTempDir(
|
|
2010
|
+
pm,
|
|
2011
|
+
modifiedRootPackageJson,
|
|
2012
|
+
{
|
|
2013
|
+
configFiles: rootUpdateInfo.configFiles,
|
|
2014
|
+
workspacePackages
|
|
2015
|
+
}
|
|
2016
|
+
);
|
|
2017
|
+
|
|
2018
|
+
storeInstallResult(result, acc, rootUpdateInfo, modifiedRootPackageJson);
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
// Filter out transitive fixes that are no longer needed
|
|
2023
|
+
let remainingTransitiveFixes = this.filterRemainingTransitiveFixes(
|
|
2024
|
+
allTransitiveFixes,
|
|
2025
|
+
phase1Result.lockFileContent,
|
|
2026
|
+
pm,
|
|
2027
|
+
acc.db
|
|
2028
|
+
);
|
|
2029
|
+
|
|
2030
|
+
if (remainingTransitiveFixes.length === 0) {
|
|
2031
|
+
// All transitives were fixed by direct upgrades - use phase 1 result
|
|
2032
|
+
storeInstallResult(phase1Result, acc, rootUpdateInfo, phase1RootPackageJson);
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
// For 'prefer-direct-upgrade', try to find higher direct dependency versions
|
|
2037
|
+
// that fix the remaining transitive vulnerabilities
|
|
2038
|
+
if (this.transitiveFixStrategy === 'prefer-direct-upgrade') {
|
|
2039
|
+
remainingTransitiveFixes = await this.tryDirectUpgradesForTransitives(
|
|
2040
|
+
remainingTransitiveFixes,
|
|
2041
|
+
rootUpdateInfo,
|
|
2042
|
+
acc.db
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
// Phase 2: Apply remaining transitive fixes (either as direct upgrades or overrides)
|
|
2047
|
+
const finalRootPackageJson = this.createModifiedPackageJson(
|
|
2048
|
+
rootOriginalContent,
|
|
2049
|
+
[...rootDirectFixes, ...remainingTransitiveFixes],
|
|
2050
|
+
pm
|
|
2051
|
+
);
|
|
2052
|
+
|
|
2053
|
+
const finalResult = await runWorkspaceInstallInTempDir(
|
|
2054
|
+
pm,
|
|
2055
|
+
finalRootPackageJson,
|
|
2056
|
+
{
|
|
2057
|
+
configFiles: rootUpdateInfo.configFiles,
|
|
2058
|
+
workspacePackages
|
|
2059
|
+
}
|
|
2060
|
+
);
|
|
2061
|
+
|
|
2062
|
+
storeInstallResult(finalResult, acc, rootUpdateInfo, finalRootPackageJson);
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
// When there are no transitive fixes, apply all fixes directly
|
|
2067
|
+
const modifiedRootPackageJson = this.createModifiedPackageJson(
|
|
2068
|
+
rootOriginalContent,
|
|
2069
|
+
[...rootDirectFixes, ...allTransitiveFixes],
|
|
2070
|
+
pm
|
|
2071
|
+
);
|
|
1595
2072
|
|
|
1596
|
-
// Run workspace install with full workspace structure
|
|
1597
|
-
// We keep workspace: dependencies and the workspaces field so Yarn can resolve them
|
|
1598
2073
|
const result = await runWorkspaceInstallInTempDir(
|
|
1599
2074
|
pm,
|
|
1600
2075
|
modifiedRootPackageJson,
|
|
@@ -1655,6 +2130,10 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1655
2130
|
fixesByPackage.set(fix.packageName, existing);
|
|
1656
2131
|
}
|
|
1657
2132
|
|
|
2133
|
+
// Track overrides added and their CVE comments for the comment field
|
|
2134
|
+
// Maps override key (e.g., "lodash" or "semver@^6") to CVE comment string
|
|
2135
|
+
const overrideComments = new Map<string, string>();
|
|
2136
|
+
|
|
1658
2137
|
for (const fix of fixes) {
|
|
1659
2138
|
if (!fix.isTransitive && fix.scope) {
|
|
1660
2139
|
// Direct dependency: update the version in the scope
|
|
@@ -1663,6 +2142,20 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1663
2142
|
packageJson[fix.scope][fix.packageName] = applyVersionPrefix(originalVersion, fix.newVersion);
|
|
1664
2143
|
}
|
|
1665
2144
|
} else if (fix.isTransitive) {
|
|
2145
|
+
// If we found a direct dependency upgrade that fixes this transitive vulnerability,
|
|
2146
|
+
// upgrade that direct dependency instead of using overrides. This is preferred because:
|
|
2147
|
+
// 1. It's cleaner - no override/resolution clutter in package.json
|
|
2148
|
+
// 2. It's more compatible - the direct dep author tested with their transitive deps
|
|
2149
|
+
// 3. Future installs work naturally without forced versions
|
|
2150
|
+
if (fix.fixViaDirectUpgrade) {
|
|
2151
|
+
const {directDepName, directDepVersion, directDepScope} = fix.fixViaDirectUpgrade;
|
|
2152
|
+
if (packageJson[directDepScope]?.[directDepName]) {
|
|
2153
|
+
const originalVersion = packageJson[directDepScope][directDepName];
|
|
2154
|
+
packageJson[directDepScope][directDepName] = applyVersionPrefix(originalVersion, directDepVersion);
|
|
2155
|
+
continue; // Skip the override logic below
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
|
|
1666
2159
|
// Check if this package is also a direct dependency in any scope.
|
|
1667
2160
|
// Package managers handle overrides for direct dependencies differently:
|
|
1668
2161
|
// - npm: EOVERRIDE error - doesn't allow overrides that conflict with direct deps
|
|
@@ -1697,6 +2190,8 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1697
2190
|
versionSpecificKey,
|
|
1698
2191
|
fix.newVersion
|
|
1699
2192
|
);
|
|
2193
|
+
// Track comment for this override
|
|
2194
|
+
overrideComments.set(versionSpecificKey, this.generateOverrideComment(fix));
|
|
1700
2195
|
} else {
|
|
1701
2196
|
// Yarn doesn't support version-specific resolutions
|
|
1702
2197
|
// Skip this fix - the direct dependency at a higher version is already safe
|
|
@@ -1739,7 +2234,7 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1739
2234
|
if (useVersionSpecificOverride) {
|
|
1740
2235
|
// Use version-specific override for packages with multiple major versions
|
|
1741
2236
|
// e.g., "semver@^6": "6.3.1", "semver@^7": "7.5.2"
|
|
1742
|
-
// Note: This
|
|
2237
|
+
// Note: This works for npm, pnpm, and bun - not Yarn
|
|
1743
2238
|
const versionSpecificKey = `${fix.packageName}@^${fix.originalMajorVersion}`;
|
|
1744
2239
|
packageJson = applyOverrideToPackageJson(
|
|
1745
2240
|
packageJson,
|
|
@@ -1747,6 +2242,8 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1747
2242
|
versionSpecificKey,
|
|
1748
2243
|
fix.newVersion
|
|
1749
2244
|
);
|
|
2245
|
+
// Track comment for this override
|
|
2246
|
+
overrideComments.set(versionSpecificKey, this.generateOverrideComment(fix));
|
|
1750
2247
|
} else {
|
|
1751
2248
|
// Single major version and no existing version-specific overrides
|
|
1752
2249
|
// Use simple package override
|
|
@@ -1756,34 +2253,93 @@ export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
|
1756
2253
|
fix.packageName,
|
|
1757
2254
|
fix.newVersion
|
|
1758
2255
|
);
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
// For pnpm, also add as devDependency. This is needed because pnpm's
|
|
1762
|
-
// overrides don't work reliably in all cases:
|
|
1763
|
-
// 1. Auto-installed peer dependencies when autoInstallPeers is enabled
|
|
1764
|
-
// 2. Packages with multiple major versions in the dependency tree
|
|
1765
|
-
// 3. Optional dependencies resolved before overrides take effect
|
|
1766
|
-
// Adding as devDependency ensures pnpm installs the specified version.
|
|
1767
|
-
if (packageManager === PackageManager.Pnpm) {
|
|
1768
|
-
if (!packageJson.devDependencies) {
|
|
1769
|
-
packageJson.devDependencies = {};
|
|
1770
|
-
}
|
|
1771
|
-
// Only add if not already a direct dependency in any scope
|
|
1772
|
-
if (!packageJson.dependencies?.[fix.packageName] &&
|
|
1773
|
-
!packageJson.devDependencies[fix.packageName] &&
|
|
1774
|
-
!packageJson.peerDependencies?.[fix.packageName] &&
|
|
1775
|
-
!packageJson.optionalDependencies?.[fix.packageName]) {
|
|
1776
|
-
// Use version prefix based on maximumUpgradeDelta to allow
|
|
1777
|
-
// future patches that might fix additional vulnerabilities
|
|
1778
|
-
const prefix = this.getVersionPrefixForDelta();
|
|
1779
|
-
packageJson.devDependencies[fix.packageName] = prefix + fix.newVersion;
|
|
1780
|
-
}
|
|
2256
|
+
// Track comment for this override
|
|
2257
|
+
overrideComments.set(fix.packageName, this.generateOverrideComment(fix));
|
|
1781
2258
|
}
|
|
1782
2259
|
}
|
|
1783
2260
|
}
|
|
1784
2261
|
|
|
2262
|
+
// Add override comments if enabled and there are comments to add
|
|
2263
|
+
if (this.addOverrideComments && overrideComments.size > 0) {
|
|
2264
|
+
packageJson = this.addOverrideCommentsToPackageJson(
|
|
2265
|
+
packageJson,
|
|
2266
|
+
packageManager,
|
|
2267
|
+
overrideComments
|
|
2268
|
+
);
|
|
2269
|
+
}
|
|
2270
|
+
|
|
1785
2271
|
return JSON.stringify(packageJson, null, 2);
|
|
1786
2272
|
}
|
|
2273
|
+
|
|
2274
|
+
/**
|
|
2275
|
+
* Generates a comment string for an override from its CVE summaries.
|
|
2276
|
+
* Format: "CVE-XXXX-YYYY: summary; CVE-ZZZZ-WWWW: summary"
|
|
2277
|
+
*/
|
|
2278
|
+
private generateOverrideComment(fix: VulnerabilityFix): string {
|
|
2279
|
+
if (!fix.cveSummaries || fix.cveSummaries.size === 0) {
|
|
2280
|
+
// Fallback: just list CVE IDs if no summaries available
|
|
2281
|
+
return fix.cves.join(', ');
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
const parts: string[] = [];
|
|
2285
|
+
for (const cve of fix.cves) {
|
|
2286
|
+
const summary = fix.cveSummaries.get(cve);
|
|
2287
|
+
if (summary) {
|
|
2288
|
+
// Truncate summary if too long (keep first 80 chars)
|
|
2289
|
+
const truncatedSummary = summary.length > 80
|
|
2290
|
+
? summary.substring(0, 77) + '...'
|
|
2291
|
+
: summary;
|
|
2292
|
+
parts.push(`${cve}: ${truncatedSummary}`);
|
|
2293
|
+
} else {
|
|
2294
|
+
parts.push(cve);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
return parts.join('; ');
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
/**
|
|
2301
|
+
* Adds a comment field alongside the overrides/resolutions to document CVEs.
|
|
2302
|
+
* Merges with existing comments, updating entries for packages being modified.
|
|
2303
|
+
* Uses different field names based on package manager:
|
|
2304
|
+
* - npm/bun: "//overrides"
|
|
2305
|
+
* - pnpm: "//pnpm.overrides"
|
|
2306
|
+
* - yarn: "//resolutions"
|
|
2307
|
+
*/
|
|
2308
|
+
private addOverrideCommentsToPackageJson(
|
|
2309
|
+
packageJson: Record<string, any>,
|
|
2310
|
+
packageManager: PackageManager,
|
|
2311
|
+
comments: Map<string, string>
|
|
2312
|
+
): Record<string, any> {
|
|
2313
|
+
// Determine the comment field name based on package manager
|
|
2314
|
+
let commentFieldName: string;
|
|
2315
|
+
switch (packageManager) {
|
|
2316
|
+
case PackageManager.Npm:
|
|
2317
|
+
case PackageManager.Bun:
|
|
2318
|
+
commentFieldName = '//overrides';
|
|
2319
|
+
break;
|
|
2320
|
+
case PackageManager.Pnpm:
|
|
2321
|
+
commentFieldName = '//pnpm.overrides';
|
|
2322
|
+
break;
|
|
2323
|
+
case PackageManager.YarnClassic:
|
|
2324
|
+
case PackageManager.YarnBerry:
|
|
2325
|
+
commentFieldName = '//resolutions';
|
|
2326
|
+
break;
|
|
2327
|
+
default:
|
|
2328
|
+
return packageJson;
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
// Get existing comments (if any) and merge with new ones
|
|
2332
|
+
const existingComments: Record<string, string> = packageJson[commentFieldName] || {};
|
|
2333
|
+
const mergedComments: Record<string, string> = {...existingComments};
|
|
2334
|
+
|
|
2335
|
+
// Add/update comments for new overrides
|
|
2336
|
+
for (const [key, comment] of comments) {
|
|
2337
|
+
mergedComments[key] = comment;
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
packageJson[commentFieldName] = mergedComments;
|
|
2341
|
+
return packageJson;
|
|
2342
|
+
}
|
|
1787
2343
|
}
|
|
1788
2344
|
|
|
1789
2345
|
/**
|