@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.
Files changed (36) hide show
  1. package/dist/security/dependency-vulnerability-check.d.ts +8 -54
  2. package/dist/security/dependency-vulnerability-check.d.ts.map +1 -1
  3. package/dist/security/dependency-vulnerability-check.js +176 -287
  4. package/dist/security/dependency-vulnerability-check.js.map +1 -1
  5. package/dist/security/index.d.ts +3 -0
  6. package/dist/security/index.d.ts.map +1 -1
  7. package/dist/security/index.js +3 -0
  8. package/dist/security/index.js.map +1 -1
  9. package/dist/security/npm-utils.d.ts +8 -2
  10. package/dist/security/npm-utils.d.ts.map +1 -1
  11. package/dist/security/npm-utils.js +114 -14
  12. package/dist/security/npm-utils.js.map +1 -1
  13. package/dist/security/override-utils.d.ts +23 -0
  14. package/dist/security/override-utils.d.ts.map +1 -0
  15. package/dist/security/override-utils.js +169 -0
  16. package/dist/security/override-utils.js.map +1 -0
  17. package/dist/security/remove-redundant-overrides.d.ts +1 -10
  18. package/dist/security/remove-redundant-overrides.d.ts.map +1 -1
  19. package/dist/security/remove-redundant-overrides.js +4 -152
  20. package/dist/security/remove-redundant-overrides.js.map +1 -1
  21. package/dist/security/types.d.ts +42 -0
  22. package/dist/security/types.d.ts.map +1 -0
  23. package/dist/security/types.js +7 -0
  24. package/dist/security/types.js.map +1 -0
  25. package/dist/security/version-utils.d.ts +13 -0
  26. package/dist/security/version-utils.d.ts.map +1 -0
  27. package/dist/security/version-utils.js +173 -0
  28. package/dist/security/version-utils.js.map +1 -0
  29. package/package.json +1 -1
  30. package/src/security/dependency-vulnerability-check.ts +300 -525
  31. package/src/security/index.ts +3 -0
  32. package/src/security/npm-utils.ts +172 -37
  33. package/src/security/override-utils.ts +253 -0
  34. package/src/security/remove-redundant-overrides.ts +9 -211
  35. package/src/security/types.ts +115 -0
  36. package/src/security/version-utils.ts +198 -0
@@ -21,22 +21,12 @@ import {
21
21
  } from "@openrewrite/rewrite/javascript";
22
22
  import * as semver from "semver";
23
23
  import {extractVersionFromLockFile} from "./npm-utils";
24
-
25
- /**
26
- * Information about an override that might be redundant.
27
- */
28
- interface OverrideInfo {
29
- /** The package name (or version-specific key like "package@^1") */
30
- key: string;
31
- /** The base package name (without version specifier) */
32
- packageName: string;
33
- /** The version the override pins to */
34
- version: string;
35
- /** Whether this is a version-specific override (e.g., "package@^1") */
36
- isVersionSpecific: boolean;
37
- /** The version range specifier if version-specific (e.g., "^1") */
38
- versionRange?: string;
39
- }
24
+ import {
25
+ OverrideInfo,
26
+ extractOverrides,
27
+ removeOverrideFromObject,
28
+ removeOverridesFromContent
29
+ } from "./override-utils";
40
30
 
41
31
  /**
42
32
  * Project information for override analysis.
@@ -138,7 +128,7 @@ export class RemoveRedundantOverrides extends ScanningRecipe<Accumulator> {
138
128
  }
139
129
 
140
130
  // Extract overrides based on package manager
141
- const overrides = recipe.extractOverrides(packageJson, pm);
131
+ const overrides = extractOverrides(packageJson, pm);
142
132
  if (overrides.length === 0) {
143
133
  return doc;
144
134
  }
@@ -205,7 +195,7 @@ export class RemoveRedundantOverrides extends ScanningRecipe<Accumulator> {
205
195
  }
206
196
 
207
197
  // Remove redundant overrides
208
- const modifiedContent = recipe.removeOverrides(
198
+ const modifiedContent = removeOverridesFromContent(
209
199
  project.originalPackageJson,
210
200
  project.packageManager,
211
201
  redundant
@@ -226,82 +216,6 @@ export class RemoveRedundantOverrides extends ScanningRecipe<Accumulator> {
226
216
  };
227
217
  }
228
218
 
229
- /**
230
- * Extracts override information from package.json.
231
- */
232
- private extractOverrides(
233
- packageJson: Record<string, any>,
234
- pm: PackageManager
235
- ): OverrideInfo[] {
236
- const overrides: OverrideInfo[] = [];
237
-
238
- let overrideObj: Record<string, any> | undefined;
239
-
240
- switch (pm) {
241
- case PackageManager.Npm:
242
- case PackageManager.Bun:
243
- overrideObj = packageJson.overrides;
244
- break;
245
- case PackageManager.Pnpm:
246
- overrideObj = packageJson.pnpm?.overrides;
247
- break;
248
- case PackageManager.YarnClassic:
249
- case PackageManager.YarnBerry:
250
- overrideObj = packageJson.resolutions;
251
- break;
252
- }
253
-
254
- if (!overrideObj) {
255
- return overrides;
256
- }
257
-
258
- for (const [key, value] of Object.entries(overrideObj)) {
259
- // Skip nested overrides (npm supports objects as values)
260
- if (typeof value !== 'string') {
261
- continue;
262
- }
263
-
264
- // Parse the key to extract package name and version range
265
- const atIndex = key.lastIndexOf('@');
266
- let packageName: string;
267
- let versionRange: string | undefined;
268
- let isVersionSpecific = false;
269
-
270
- // Check if this is a version-specific override like "package@^1"
271
- // But be careful with scoped packages like "@scope/package"
272
- if (atIndex > 0 && !key.startsWith('@')) {
273
- // Unscoped package with version specifier
274
- packageName = key.substring(0, atIndex);
275
- versionRange = key.substring(atIndex + 1);
276
- isVersionSpecific = true;
277
- } else if (atIndex > 0 && key.startsWith('@')) {
278
- // Scoped package - check if there's another @ after the scope
279
- const secondAtIndex = key.indexOf('@', 1);
280
- if (secondAtIndex > 0 && secondAtIndex !== atIndex) {
281
- // Has version specifier: @scope/package@^1
282
- packageName = key.substring(0, secondAtIndex);
283
- versionRange = key.substring(secondAtIndex + 1);
284
- isVersionSpecific = true;
285
- } else {
286
- // Just @scope/package
287
- packageName = key;
288
- }
289
- } else {
290
- packageName = key;
291
- }
292
-
293
- overrides.push({
294
- key,
295
- packageName,
296
- version: value,
297
- isVersionSpecific,
298
- versionRange
299
- });
300
- }
301
-
302
- return overrides;
303
- }
304
-
305
219
  /**
306
220
  * Tests each override to see if it's redundant.
307
221
  *
@@ -320,7 +234,7 @@ export class RemoveRedundantOverrides extends ScanningRecipe<Accumulator> {
320
234
  // Create package.json with ALL overrides removed
321
235
  const packageJson = JSON.parse(project.originalPackageJson);
322
236
  for (const override of project.overrides) {
323
- this.removeOverrideFromObject(packageJson, project.packageManager, override.key);
237
+ removeOverrideFromObject(packageJson, project.packageManager, override.key);
324
238
  }
325
239
  const modifiedPackageJson = JSON.stringify(packageJson, null, 2);
326
240
 
@@ -396,120 +310,4 @@ export class RemoveRedundantOverrides extends ScanningRecipe<Accumulator> {
396
310
 
397
311
  return false;
398
312
  }
399
-
400
- /**
401
- * Removes a single override from the package.json object.
402
- */
403
- private removeOverrideFromObject(
404
- packageJson: Record<string, any>,
405
- pm: PackageManager,
406
- key: string
407
- ): void {
408
- switch (pm) {
409
- case PackageManager.Npm:
410
- case PackageManager.Bun:
411
- if (packageJson.overrides) {
412
- delete packageJson.overrides[key];
413
- if (Object.keys(packageJson.overrides).length === 0) {
414
- delete packageJson.overrides;
415
- }
416
- }
417
- break;
418
- case PackageManager.Pnpm:
419
- if (packageJson.pnpm?.overrides) {
420
- delete packageJson.pnpm.overrides[key];
421
- if (Object.keys(packageJson.pnpm.overrides).length === 0) {
422
- delete packageJson.pnpm.overrides;
423
- }
424
- if (Object.keys(packageJson.pnpm).length === 0) {
425
- delete packageJson.pnpm;
426
- }
427
- }
428
- break;
429
- case PackageManager.YarnClassic:
430
- case PackageManager.YarnBerry:
431
- if (packageJson.resolutions) {
432
- delete packageJson.resolutions[key];
433
- if (Object.keys(packageJson.resolutions).length === 0) {
434
- delete packageJson.resolutions;
435
- }
436
- }
437
- break;
438
- }
439
- }
440
-
441
- /**
442
- * Removes the specified overrides from package.json content.
443
- * Also removes associated comments.
444
- */
445
- private removeOverrides(
446
- originalContent: string,
447
- pm: PackageManager,
448
- keysToRemove: Set<string>
449
- ): string {
450
- const packageJson = JSON.parse(originalContent);
451
-
452
- // Determine field names
453
- let overrideField: string;
454
- let commentField: string;
455
-
456
- switch (pm) {
457
- case PackageManager.Npm:
458
- case PackageManager.Bun:
459
- overrideField = 'overrides';
460
- commentField = '//overrides';
461
- break;
462
- case PackageManager.Pnpm:
463
- overrideField = 'pnpm';
464
- commentField = '//pnpm.overrides';
465
- break;
466
- case PackageManager.YarnClassic:
467
- case PackageManager.YarnBerry:
468
- overrideField = 'resolutions';
469
- commentField = '//resolutions';
470
- break;
471
- default:
472
- return originalContent;
473
- }
474
-
475
- // Remove overrides
476
- if (pm === PackageManager.Pnpm) {
477
- if (packageJson.pnpm?.overrides) {
478
- for (const key of keysToRemove) {
479
- delete packageJson.pnpm.overrides[key];
480
- }
481
- if (Object.keys(packageJson.pnpm.overrides).length === 0) {
482
- delete packageJson.pnpm.overrides;
483
- }
484
- if (Object.keys(packageJson.pnpm).length === 0) {
485
- delete packageJson.pnpm;
486
- }
487
- }
488
- } else {
489
- if (packageJson[overrideField]) {
490
- for (const key of keysToRemove) {
491
- delete packageJson[overrideField][key];
492
- }
493
- if (Object.keys(packageJson[overrideField]).length === 0) {
494
- delete packageJson[overrideField];
495
- }
496
- }
497
- }
498
-
499
- // Remove associated comments
500
- if (packageJson[commentField]) {
501
- for (const key of keysToRemove) {
502
- delete packageJson[commentField][key];
503
- }
504
- if (Object.keys(packageJson[commentField]).length === 0) {
505
- delete packageJson[commentField];
506
- }
507
- }
508
-
509
- // Preserve original indentation
510
- const indentMatch = originalContent.match(/^(\s+)"/m);
511
- const indent = indentMatch ? indentMatch[1].length : 2;
512
-
513
- return JSON.stringify(packageJson, null, indent);
514
- }
515
313
  }
@@ -0,0 +1,115 @@
1
+ /*
2
+ * Copyright 2025 the original author or authors.
3
+ *
4
+ * Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
5
+ */
6
+
7
+ import {DependencyScope, PackageManager, ResolvedDependency} from "@openrewrite/rewrite/javascript";
8
+ import {Vulnerability} from "./vulnerability";
9
+
10
+ /**
11
+ * All dependency scopes that can contain dependencies in package.json.
12
+ */
13
+ export const ALL_DEPENDENCY_SCOPES: DependencyScope[] = [
14
+ 'dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'
15
+ ];
16
+
17
+ /**
18
+ * Strategy for handling transitive dependency vulnerabilities.
19
+ *
20
+ * - 'report': Report transitive vulnerabilities but don't fix them. (Default)
21
+ * - 'override': Add overrides/resolutions for transitive vulnerabilities.
22
+ * Ensures fixes are persistent by explicitly specifying versions.
23
+ * - 'lock-file': Update the lock file to resolve safe versions of transitive
24
+ * dependencies without modifying package.json. Similar to
25
+ * Dependabot's approach. Less intrusive than overrides but may
26
+ * be less persistent if lock file is regenerated.
27
+ *
28
+ * Note: Use the separate `preferDirectUpgrade` option to first try upgrading
29
+ * direct dependencies before falling back to the chosen strategy.
30
+ */
31
+ export type TransitiveFixStrategy = 'report' | 'override' | 'lock-file';
32
+
33
+ /**
34
+ * Represents a segment in the dependency path.
35
+ */
36
+ export interface PathSegment {
37
+ name: string;
38
+ version: string;
39
+ }
40
+
41
+ /**
42
+ * Represents a vulnerable dependency found during scanning.
43
+ */
44
+ export interface VulnerableDependency {
45
+ /** The resolved dependency that is vulnerable */
46
+ resolved: ResolvedDependency;
47
+ /** The vulnerability affecting it */
48
+ vulnerability: Vulnerability;
49
+ /** Depth in dependency tree (0 = direct) */
50
+ depth: number;
51
+ /** Whether this is a direct dependency */
52
+ isDirect: boolean;
53
+ /** The scope where this dependency was found (for direct deps) */
54
+ scope?: DependencyScope;
55
+ /** Path from root to this dependency */
56
+ path: PathSegment[];
57
+ }
58
+
59
+ /**
60
+ * Represents a fix to apply for a vulnerability.
61
+ */
62
+ export interface VulnerabilityFix {
63
+ /** Package name to upgrade (the vulnerable package) */
64
+ packageName: string;
65
+ /** Version to upgrade to */
66
+ newVersion: string;
67
+ /** Whether the vulnerable package is a transitive dependency */
68
+ isTransitive: boolean;
69
+ /** CVEs this fix resolves */
70
+ cves: string[];
71
+ /** CVE summaries for generating comments (CVE ID -> summary) */
72
+ cveSummaries: Map<string, string>;
73
+ /** The scope where this dependency was found (for direct deps) */
74
+ scope?: DependencyScope;
75
+ /** The original major version (for version-specific overrides) */
76
+ originalMajorVersion?: number;
77
+ /**
78
+ * For transitive vulnerabilities, info about ALL direct dependencies that bring
79
+ * this transitive in. Used by preferDirectUpgrade to find higher versions
80
+ * of the direct dependencies that might fix the transitive.
81
+ */
82
+ directDepInfos?: {
83
+ name: string;
84
+ version: string;
85
+ scope: DependencyScope;
86
+ }[];
87
+ /**
88
+ * If set, fix this transitive vulnerability by upgrading direct dependencies
89
+ * instead of using overrides. This is preferred when a newer version of a
90
+ * direct dependency includes a fixed version of the vulnerable transitive.
91
+ * Multiple entries when the transitive is brought in by multiple direct deps.
92
+ */
93
+ fixViaDirectUpgrades?: {
94
+ /** Name of the direct dependency to upgrade */
95
+ directDepName: string;
96
+ /** Version to upgrade the direct dependency to */
97
+ directDepVersion: string;
98
+ /** Scope of the direct dependency */
99
+ directDepScope: DependencyScope;
100
+ }[];
101
+ }
102
+
103
+ /**
104
+ * Project info for lock file updates.
105
+ */
106
+ export interface ProjectUpdateInfo {
107
+ /** Relative path to package.json (from source root) */
108
+ packageJsonPath: string;
109
+ /** Original package.json content */
110
+ originalPackageJson: string;
111
+ /** The package manager used by this project */
112
+ packageManager: PackageManager;
113
+ /** Config file contents extracted from the project (e.g., .npmrc) */
114
+ configFiles?: Record<string, string>;
115
+ }
@@ -0,0 +1,198 @@
1
+ /*
2
+ * Copyright 2025 the original author or authors.
3
+ *
4
+ * Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
5
+ */
6
+
7
+ import * as semver from "semver";
8
+ import {Vulnerability} from "./vulnerability";
9
+
10
+ /**
11
+ * Upgrade delta options for version upgrades.
12
+ */
13
+ export type UpgradeDelta = 'none' | 'patch' | 'minor' | 'major';
14
+
15
+ /**
16
+ * Extracts the version prefix (e.g., ^, ~, >=) from a version string.
17
+ * Returns the prefix and the bare version separately.
18
+ */
19
+ export function extractVersionPrefix(versionString: string): { prefix: string; version: string } {
20
+ const match = versionString.match(/^([~^]|>=?|<=?|=)?(.*)$/);
21
+ if (match) {
22
+ return {
23
+ prefix: match[1] || '',
24
+ version: match[2]
25
+ };
26
+ }
27
+ return {prefix: '', version: versionString};
28
+ }
29
+
30
+ /**
31
+ * Applies the original version prefix to a new version.
32
+ */
33
+ export function applyVersionPrefix(originalVersion: string, newVersion: string): string {
34
+ const {prefix} = extractVersionPrefix(originalVersion);
35
+ return prefix + newVersion;
36
+ }
37
+
38
+ /**
39
+ * Extracts the minimum version from a version constraint.
40
+ * For example: "^3.0.2" -> "3.0.2", "~1.2.3" -> "1.2.3", ">=2.0.0" -> "2.0.0"
41
+ */
42
+ export function extractMinimumVersion(constraint: string): string | undefined {
43
+ if (!constraint) return undefined;
44
+
45
+ // Handle exact versions
46
+ if (semver.valid(constraint)) {
47
+ return constraint;
48
+ }
49
+
50
+ // Handle ranges with prefix: ^, ~, >=, >, etc.
51
+ const match = constraint.match(/^[~^>=<]*\s*(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?)/);
52
+ if (match && semver.valid(match[1])) {
53
+ return match[1];
54
+ }
55
+
56
+ // Try to coerce other formats
57
+ const coerced = semver.coerce(constraint);
58
+ return coerced?.version;
59
+ }
60
+
61
+ /**
62
+ * Checks if a target version is within the allowed upgrade delta from the original version.
63
+ */
64
+ export function isVersionWithinDelta(
65
+ originalVersion: string,
66
+ targetVersion: string,
67
+ delta: UpgradeDelta
68
+ ): boolean {
69
+ if (delta === 'none') {
70
+ return false;
71
+ }
72
+
73
+ try {
74
+ const original = semver.parse(originalVersion);
75
+ const target = semver.parse(targetVersion);
76
+ if (!original || !target) return false;
77
+
78
+ switch (delta) {
79
+ case 'patch':
80
+ return original.major === target.major && original.minor === target.minor;
81
+ case 'minor':
82
+ return original.major === target.major;
83
+ case 'major':
84
+ return true;
85
+ default:
86
+ return false;
87
+ }
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Checks if a specific version is affected by a vulnerability.
95
+ */
96
+ export function isVersionAffected(version: string, vulnerability: Vulnerability): boolean {
97
+ try {
98
+ const v = semver.parse(version);
99
+ if (!v) return false;
100
+
101
+ // Check introduced version
102
+ if (vulnerability.introducedVersion && vulnerability.introducedVersion !== '0') {
103
+ const introduced = semver.parse(vulnerability.introducedVersion);
104
+ if (introduced && semver.lt(v, introduced)) {
105
+ return false; // Version is before the vulnerability was introduced
106
+ }
107
+ }
108
+
109
+ // Check if fixed
110
+ if (vulnerability.fixedVersion) {
111
+ const fixed = semver.parse(vulnerability.fixedVersion);
112
+ if (fixed && semver.gte(v, fixed)) {
113
+ return false; // Version is at or after the fix
114
+ }
115
+ return true; // Version is in the vulnerable range
116
+ }
117
+
118
+ // Check last affected version
119
+ if (vulnerability.lastAffectedVersion) {
120
+ const lastAffected = semver.parse(vulnerability.lastAffectedVersion);
121
+ if (lastAffected && semver.gt(v, lastAffected)) {
122
+ return false; // Version is after the last affected
123
+ }
124
+ return true;
125
+ }
126
+
127
+ return true; // No fix information, assume vulnerable
128
+ } catch {
129
+ return false; // Invalid version, skip
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Checks if a vulnerability can be fixed within the maximum upgrade delta.
135
+ */
136
+ export function isUpgradeableWithinDelta(
137
+ currentVersion: string,
138
+ vulnerability: Vulnerability,
139
+ delta: UpgradeDelta
140
+ ): boolean {
141
+ if (delta === 'none') {
142
+ return false;
143
+ }
144
+
145
+ try {
146
+ const current = semver.parse(currentVersion);
147
+ if (!current) return false;
148
+
149
+ // If we have a fixed version, check if it's reachable within delta
150
+ if (vulnerability.fixedVersion) {
151
+ const fixed = semver.parse(vulnerability.fixedVersion);
152
+ if (!fixed) return false;
153
+
154
+ switch (delta) {
155
+ case 'patch':
156
+ return current.major === fixed.major && current.minor === fixed.minor;
157
+ case 'minor':
158
+ return current.major === fixed.major;
159
+ case 'major':
160
+ return true;
161
+ }
162
+ }
163
+
164
+ // If we only have lastAffectedVersion, check if upgrading within delta
165
+ // could get us past it (i.e., to a non-vulnerable version)
166
+ if (vulnerability.lastAffectedVersion) {
167
+ const lastAffected = semver.parse(vulnerability.lastAffectedVersion);
168
+ if (!lastAffected) return false;
169
+
170
+ switch (delta) {
171
+ case 'patch':
172
+ return current.major === lastAffected.major &&
173
+ current.minor === lastAffected.minor;
174
+ case 'minor':
175
+ return current.major === lastAffected.major;
176
+ case 'major':
177
+ return true;
178
+ }
179
+ }
180
+
181
+ return false;
182
+ } catch {
183
+ return false;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Gets the version to upgrade to for a vulnerability.
189
+ */
190
+ export function getUpgradeVersion(vulnerability: Vulnerability): string | undefined {
191
+ if (vulnerability.fixedVersion) {
192
+ return vulnerability.fixedVersion;
193
+ }
194
+ // For lastAffectedVersion, we need the next version after it
195
+ // We can't determine this exactly, so return undefined and let the
196
+ // upgrade recipe handle version selection
197
+ return undefined;
198
+ }