@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
package/src/security/index.ts
CHANGED
|
@@ -0,0 +1,414 @@
|
|
|
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 {PackageManager, runInstallInTempDir} from "@openrewrite/rewrite/javascript";
|
|
8
|
+
import * as semver from "semver";
|
|
9
|
+
import {spawnSync, SpawnSyncReturns} from "child_process";
|
|
10
|
+
import * as fs from "fs";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
import * as os from "os";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Runs an npm command with optional .npmrc configuration.
|
|
16
|
+
* Writes a temp .npmrc file and uses --userconfig flag when config is provided.
|
|
17
|
+
*
|
|
18
|
+
* Note: Auth tokens cannot be passed via npm_config_* environment variables due to
|
|
19
|
+
* npm's underscore-to-dash conversion bug. See: https://github.com/npm/config/issues/64
|
|
20
|
+
*/
|
|
21
|
+
function runNpmWithConfig(
|
|
22
|
+
args: string[],
|
|
23
|
+
configFiles?: Record<string, string>,
|
|
24
|
+
options?: { timeout?: number }
|
|
25
|
+
): SpawnSyncReturns<string> {
|
|
26
|
+
const timeout = options?.timeout ?? 30000;
|
|
27
|
+
let tempConfigPath: string | undefined;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const npmrcContent = configFiles?.['.npmrc'];
|
|
31
|
+
if (npmrcContent) {
|
|
32
|
+
tempConfigPath = path.join(os.tmpdir(), `npmrc-${process.pid}-${Date.now()}`);
|
|
33
|
+
fs.writeFileSync(tempConfigPath, npmrcContent);
|
|
34
|
+
args = [...args, `--userconfig=${tempConfigPath}`];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return spawnSync('npm', args, {
|
|
38
|
+
encoding: 'utf-8',
|
|
39
|
+
timeout,
|
|
40
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
41
|
+
});
|
|
42
|
+
} finally {
|
|
43
|
+
if (tempConfigPath) {
|
|
44
|
+
try {
|
|
45
|
+
fs.unlinkSync(tempConfigPath);
|
|
46
|
+
} catch {
|
|
47
|
+
// Ignore cleanup errors
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Response from npm registry for a package.
|
|
55
|
+
*/
|
|
56
|
+
export interface NpmPackageInfo {
|
|
57
|
+
name: string;
|
|
58
|
+
versions: Record<string, {
|
|
59
|
+
version: string;
|
|
60
|
+
dependencies?: Record<string, string>;
|
|
61
|
+
}>;
|
|
62
|
+
'dist-tags': Record<string, string>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Fetches package information using `npm view` command.
|
|
67
|
+
* This leverages npm's local cache and respects .npmrc settings.
|
|
68
|
+
*
|
|
69
|
+
* @param packageName The package name to fetch info for
|
|
70
|
+
* @param configFiles Optional config files (e.g., {'.npmrc': '...'}) for registry/auth settings
|
|
71
|
+
*/
|
|
72
|
+
export async function fetchNpmPackageInfo(
|
|
73
|
+
packageName: string,
|
|
74
|
+
configFiles?: Record<string, string>
|
|
75
|
+
): Promise<NpmPackageInfo | undefined> {
|
|
76
|
+
try {
|
|
77
|
+
const result = runNpmWithConfig(
|
|
78
|
+
['view', packageName, '--json', '--prefer-offline'],
|
|
79
|
+
configFiles
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (result.error || result.status !== 0) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const data = JSON.parse(result.stdout);
|
|
87
|
+
|
|
88
|
+
// npm view returns different structures depending on the package
|
|
89
|
+
// For a single version it returns the version object directly
|
|
90
|
+
// For multiple versions (when using package name without version), it returns full info
|
|
91
|
+
if (data.versions) {
|
|
92
|
+
return data as NpmPackageInfo;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// If we got a single version object, we need to fetch all versions
|
|
96
|
+
const versionsResult = runNpmWithConfig(
|
|
97
|
+
['view', packageName, 'versions', '--json', '--prefer-offline'],
|
|
98
|
+
configFiles
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (versionsResult.error || versionsResult.status !== 0) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const versions = JSON.parse(versionsResult.stdout);
|
|
106
|
+
const versionMap: Record<string, { version: string }> = {};
|
|
107
|
+
|
|
108
|
+
if (Array.isArray(versions)) {
|
|
109
|
+
for (const v of versions) {
|
|
110
|
+
versionMap[v] = { version: v };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
name: packageName,
|
|
116
|
+
versions: versionMap,
|
|
117
|
+
'dist-tags': data['dist-tags'] || {}
|
|
118
|
+
};
|
|
119
|
+
} catch {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Gets available versions of a package using npm's cache, sorted by semver descending.
|
|
126
|
+
*
|
|
127
|
+
* @param packageName The package name to get versions for
|
|
128
|
+
* @param configFiles Optional config files (e.g., {'.npmrc': '...'}) for registry/auth settings
|
|
129
|
+
*/
|
|
130
|
+
export async function getAvailableVersions(
|
|
131
|
+
packageName: string,
|
|
132
|
+
configFiles?: Record<string, string>
|
|
133
|
+
): Promise<string[]> {
|
|
134
|
+
try {
|
|
135
|
+
// Use npm view versions which leverages npm's cache
|
|
136
|
+
const result = runNpmWithConfig(
|
|
137
|
+
['view', packageName, 'versions', '--json', '--prefer-offline'],
|
|
138
|
+
configFiles
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (result.error || result.status !== 0) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const versions = JSON.parse(result.stdout);
|
|
146
|
+
|
|
147
|
+
if (!Array.isArray(versions)) {
|
|
148
|
+
// Single version returned as string
|
|
149
|
+
return typeof versions === 'string' ? [versions] : [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Sort by semver descending (newest first), excluding prereleases
|
|
153
|
+
return versions
|
|
154
|
+
.filter((v: string) => !v.includes('-'))
|
|
155
|
+
.sort((a: string, b: string) => semver.rcompare(a, b));
|
|
156
|
+
} catch {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Extracts the resolved version of a package from a lock file content.
|
|
163
|
+
*/
|
|
164
|
+
export function extractVersionFromLockFile(
|
|
165
|
+
lockFileContent: string,
|
|
166
|
+
packageName: string,
|
|
167
|
+
pm: PackageManager
|
|
168
|
+
): string | undefined {
|
|
169
|
+
try {
|
|
170
|
+
switch (pm) {
|
|
171
|
+
case PackageManager.Npm: {
|
|
172
|
+
const lockJson = JSON.parse(lockFileContent);
|
|
173
|
+
// npm v2+ format
|
|
174
|
+
if (lockJson.packages) {
|
|
175
|
+
for (const [key, value] of Object.entries(lockJson.packages)) {
|
|
176
|
+
if (key.endsWith(`node_modules/${packageName}`) || key === `node_modules/${packageName}`) {
|
|
177
|
+
return (value as any).version;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// npm v1 format
|
|
182
|
+
if (lockJson.dependencies?.[packageName]) {
|
|
183
|
+
return lockJson.dependencies[packageName].version;
|
|
184
|
+
}
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
case PackageManager.Pnpm: {
|
|
189
|
+
// Simple regex extraction for pnpm-lock.yaml
|
|
190
|
+
const regex = new RegExp(`['"]?${escapeRegExp(packageName)}@([\\d.]+)['"]?:`, 'g');
|
|
191
|
+
const matches: string[] = [];
|
|
192
|
+
let match;
|
|
193
|
+
while ((match = regex.exec(lockFileContent)) !== null) {
|
|
194
|
+
matches.push(match[1]);
|
|
195
|
+
}
|
|
196
|
+
// Return highest version found
|
|
197
|
+
return matches.sort((a, b) => semver.rcompare(a, b))[0];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
case PackageManager.YarnClassic:
|
|
201
|
+
case PackageManager.YarnBerry: {
|
|
202
|
+
const regex = new RegExp(`"?${escapeRegExp(packageName)}@[^":\\n]+[":]*\\s*\\n\\s*version:?\\s*"?([^"\\n]+)"?`, 'g');
|
|
203
|
+
const match = regex.exec(lockFileContent);
|
|
204
|
+
return match?.[1]?.trim();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
case PackageManager.Bun: {
|
|
208
|
+
const lockJson = JSON.parse(lockFileContent);
|
|
209
|
+
if (lockJson.packages) {
|
|
210
|
+
for (const [key, value] of Object.entries(lockJson.packages)) {
|
|
211
|
+
if (key === packageName || key.endsWith(`/${packageName}`)) {
|
|
212
|
+
if (Array.isArray(value) && value[0]) {
|
|
213
|
+
return String(value[0]);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
default:
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Escapes special regex characters in a string.
|
|
231
|
+
*/
|
|
232
|
+
function escapeRegExp(str: string): string {
|
|
233
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Result of checking if a direct dependency upgrade fixes a transitive vulnerability.
|
|
238
|
+
*/
|
|
239
|
+
export interface DirectUpgradeCheckResult {
|
|
240
|
+
/** Whether upgrading the direct dependency fixes the transitive vulnerability */
|
|
241
|
+
fixed: boolean;
|
|
242
|
+
/** The version of the direct dependency that was tested */
|
|
243
|
+
directDepVersion: string;
|
|
244
|
+
/** The resolved version of the transitive after upgrade (if found) */
|
|
245
|
+
transitiveVersion?: string;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Dependency scope type.
|
|
250
|
+
*/
|
|
251
|
+
export type DependencyScope = 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies';
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Checks if upgrading a direct dependency to a specific version fixes a transitive vulnerability.
|
|
255
|
+
* Does this by running npm install in a temp directory and checking the resolved transitive version.
|
|
256
|
+
*
|
|
257
|
+
* @param pm The package manager to use
|
|
258
|
+
* @param directDepName The direct dependency to upgrade
|
|
259
|
+
* @param directDepVersion The version to upgrade to
|
|
260
|
+
* @param transitiveDepName The transitive dependency that has a vulnerability
|
|
261
|
+
* @param isVulnerable Function that checks if a version is vulnerable
|
|
262
|
+
* @param originalPackageJson The original package.json content
|
|
263
|
+
* @param scope The dependency scope of the direct dependency
|
|
264
|
+
* @param configFiles Optional config files (e.g., .npmrc)
|
|
265
|
+
* @returns Result indicating if the upgrade fixes the vulnerability
|
|
266
|
+
*/
|
|
267
|
+
export async function checkDirectUpgradeFixesTransitive(
|
|
268
|
+
pm: PackageManager,
|
|
269
|
+
directDepName: string,
|
|
270
|
+
directDepVersion: string,
|
|
271
|
+
transitiveDepName: string,
|
|
272
|
+
isVulnerable: (version: string) => boolean,
|
|
273
|
+
originalPackageJson: string,
|
|
274
|
+
scope: DependencyScope,
|
|
275
|
+
configFiles?: Record<string, string>
|
|
276
|
+
): Promise<DirectUpgradeCheckResult> {
|
|
277
|
+
try {
|
|
278
|
+
// Parse and modify package.json with upgraded direct dependency
|
|
279
|
+
const pkgJson = JSON.parse(originalPackageJson);
|
|
280
|
+
if (!pkgJson[scope]?.[directDepName]) {
|
|
281
|
+
return {fixed: false, directDepVersion};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Upgrade the direct dependency
|
|
285
|
+
pkgJson[scope][directDepName] = directDepVersion;
|
|
286
|
+
const modifiedPackageJson = JSON.stringify(pkgJson, null, 2);
|
|
287
|
+
|
|
288
|
+
// Run install in temp directory
|
|
289
|
+
const result = await runInstallInTempDir(pm, modifiedPackageJson, {
|
|
290
|
+
configFiles,
|
|
291
|
+
lockOnly: true
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
if (!result.success || !result.lockFileContent) {
|
|
295
|
+
return {fixed: false, directDepVersion};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Parse lock file to find transitive version
|
|
299
|
+
const transitiveVersion = extractVersionFromLockFile(
|
|
300
|
+
result.lockFileContent,
|
|
301
|
+
transitiveDepName,
|
|
302
|
+
pm
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
if (!transitiveVersion) {
|
|
306
|
+
// Transitive not found - might mean it's no longer a dependency
|
|
307
|
+
return {fixed: true, directDepVersion, transitiveVersion: undefined};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Check if the resolved version is still vulnerable
|
|
311
|
+
const stillVulnerable = isVulnerable(transitiveVersion);
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
fixed: !stillVulnerable,
|
|
315
|
+
directDepVersion,
|
|
316
|
+
transitiveVersion
|
|
317
|
+
};
|
|
318
|
+
} catch {
|
|
319
|
+
return {fixed: false, directDepVersion};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Finds a direct dependency upgrade that fixes a transitive vulnerability.
|
|
325
|
+
* Tries versions from newest to oldest within the allowed delta.
|
|
326
|
+
*
|
|
327
|
+
* @param pm The package manager to use
|
|
328
|
+
* @param directDepName The direct dependency to upgrade
|
|
329
|
+
* @param currentDirectVersion The current version of the direct dependency
|
|
330
|
+
* @param transitiveDepName The transitive dependency that has a vulnerability
|
|
331
|
+
* @param isVulnerable Function that checks if a version is vulnerable
|
|
332
|
+
* @param isWithinDelta Function that checks if a version is within the allowed upgrade delta
|
|
333
|
+
* @param originalPackageJson The original package.json content
|
|
334
|
+
* @param scope The dependency scope of the direct dependency
|
|
335
|
+
* @param configFiles Optional config files (e.g., .npmrc)
|
|
336
|
+
* @returns The direct dependency version that fixes the transitive, or undefined if none found
|
|
337
|
+
*/
|
|
338
|
+
export async function findDirectUpgradeThatFixesTransitive(
|
|
339
|
+
pm: PackageManager,
|
|
340
|
+
directDepName: string,
|
|
341
|
+
currentDirectVersion: string,
|
|
342
|
+
transitiveDepName: string,
|
|
343
|
+
isVulnerable: (version: string) => boolean,
|
|
344
|
+
isWithinDelta: (fromVersion: string, toVersion: string) => boolean,
|
|
345
|
+
originalPackageJson: string,
|
|
346
|
+
scope: DependencyScope,
|
|
347
|
+
configFiles?: Record<string, string>
|
|
348
|
+
): Promise<string | undefined> {
|
|
349
|
+
// Get available versions of the direct dependency
|
|
350
|
+
const availableVersions = await getAvailableVersions(directDepName, configFiles);
|
|
351
|
+
if (availableVersions.length === 0) {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const currentMajor = semver.major(currentDirectVersion);
|
|
356
|
+
|
|
357
|
+
// Filter to versions newer than current and within delta
|
|
358
|
+
// Note: getAvailableVersions already filters out prereleases
|
|
359
|
+
const candidateVersions = availableVersions.filter(v => {
|
|
360
|
+
try {
|
|
361
|
+
return semver.gt(v, currentDirectVersion) && isWithinDelta(currentDirectVersion, v);
|
|
362
|
+
} catch {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
if (candidateVersions.length === 0) {
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Sort candidates: same major version first (descending), then other majors (descending)
|
|
372
|
+
// This prioritizes peer-compatible versions while still allowing major upgrades if needed
|
|
373
|
+
candidateVersions.sort((a, b) => {
|
|
374
|
+
const aMajor = semver.major(a);
|
|
375
|
+
const bMajor = semver.major(b);
|
|
376
|
+
const aIsSameMajor = aMajor === currentMajor;
|
|
377
|
+
const bIsSameMajor = bMajor === currentMajor;
|
|
378
|
+
|
|
379
|
+
// Same major versions come first
|
|
380
|
+
if (aIsSameMajor && !bIsSameMajor) return -1;
|
|
381
|
+
if (!aIsSameMajor && bIsSameMajor) return 1;
|
|
382
|
+
|
|
383
|
+
// Within the same priority group, sort by version descending
|
|
384
|
+
return semver.rcompare(a, b);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Try candidates in order (same major first, then others)
|
|
388
|
+
for (const candidate of candidateVersions) {
|
|
389
|
+
const result = await checkDirectUpgradeFixesTransitive(
|
|
390
|
+
pm,
|
|
391
|
+
directDepName,
|
|
392
|
+
candidate,
|
|
393
|
+
transitiveDepName,
|
|
394
|
+
isVulnerable,
|
|
395
|
+
originalPackageJson,
|
|
396
|
+
scope,
|
|
397
|
+
configFiles
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
if (result.fixed) {
|
|
401
|
+
return candidate;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Only try a few candidates to avoid excessive npm installs
|
|
405
|
+
// After trying the newest same-major and newest other-major, give up
|
|
406
|
+
const candidateMajor = semver.major(candidate);
|
|
407
|
+
if (candidateMajor !== currentMajor) {
|
|
408
|
+
// We've tried a different major version and it didn't work, give up
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|