@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
|
@@ -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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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,116 @@
|
|
|
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 transitives that aren't fixed
|
|
22
|
+
* by direct dependency upgrades. Runs an extra npm install
|
|
23
|
+
* to check which transitives are still vulnerable after
|
|
24
|
+
* applying direct fixes, then adds overrides only for those.
|
|
25
|
+
* - 'prefer-direct-upgrade': First applies minimum direct dependency upgrades,
|
|
26
|
+
* then for any remaining transitive vulnerabilities,
|
|
27
|
+
* tries to find a higher direct dependency version
|
|
28
|
+
* that fixes them. Falls back to overrides if no
|
|
29
|
+
* suitable direct upgrade exists. Queries npm registry.
|
|
30
|
+
* Most thorough, slowest.
|
|
31
|
+
*/
|
|
32
|
+
export type TransitiveFixStrategy = 'report' | 'override' | 'prefer-direct-upgrade';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Represents a segment in the dependency path.
|
|
36
|
+
*/
|
|
37
|
+
export interface PathSegment {
|
|
38
|
+
name: string;
|
|
39
|
+
version: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Represents a vulnerable dependency found during scanning.
|
|
44
|
+
*/
|
|
45
|
+
export interface VulnerableDependency {
|
|
46
|
+
/** The resolved dependency that is vulnerable */
|
|
47
|
+
resolved: ResolvedDependency;
|
|
48
|
+
/** The vulnerability affecting it */
|
|
49
|
+
vulnerability: Vulnerability;
|
|
50
|
+
/** Depth in dependency tree (0 = direct) */
|
|
51
|
+
depth: number;
|
|
52
|
+
/** Whether this is a direct dependency */
|
|
53
|
+
isDirect: boolean;
|
|
54
|
+
/** The scope where this dependency was found (for direct deps) */
|
|
55
|
+
scope?: DependencyScope;
|
|
56
|
+
/** Path from root to this dependency */
|
|
57
|
+
path: PathSegment[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Represents a fix to apply for a vulnerability.
|
|
62
|
+
*/
|
|
63
|
+
export interface VulnerabilityFix {
|
|
64
|
+
/** Package name to upgrade (the vulnerable package) */
|
|
65
|
+
packageName: string;
|
|
66
|
+
/** Version to upgrade to */
|
|
67
|
+
newVersion: string;
|
|
68
|
+
/** Whether the vulnerable package is a transitive dependency */
|
|
69
|
+
isTransitive: boolean;
|
|
70
|
+
/** CVEs this fix resolves */
|
|
71
|
+
cves: string[];
|
|
72
|
+
/** CVE summaries for generating comments (CVE ID -> summary) */
|
|
73
|
+
cveSummaries: Map<string, string>;
|
|
74
|
+
/** The scope where this dependency was found (for direct deps) */
|
|
75
|
+
scope?: DependencyScope;
|
|
76
|
+
/** The original major version (for version-specific overrides) */
|
|
77
|
+
originalMajorVersion?: number;
|
|
78
|
+
/**
|
|
79
|
+
* For transitive vulnerabilities, info about ALL direct dependencies that bring
|
|
80
|
+
* this transitive in. Used by prefer-direct-upgrade to find higher versions
|
|
81
|
+
* of the direct dependencies that might fix the transitive.
|
|
82
|
+
*/
|
|
83
|
+
directDepInfos?: {
|
|
84
|
+
name: string;
|
|
85
|
+
version: string;
|
|
86
|
+
scope: DependencyScope;
|
|
87
|
+
}[];
|
|
88
|
+
/**
|
|
89
|
+
* If set, fix this transitive vulnerability by upgrading direct dependencies
|
|
90
|
+
* instead of using overrides. This is preferred when a newer version of a
|
|
91
|
+
* direct dependency includes a fixed version of the vulnerable transitive.
|
|
92
|
+
* Multiple entries when the transitive is brought in by multiple direct deps.
|
|
93
|
+
*/
|
|
94
|
+
fixViaDirectUpgrades?: {
|
|
95
|
+
/** Name of the direct dependency to upgrade */
|
|
96
|
+
directDepName: string;
|
|
97
|
+
/** Version to upgrade the direct dependency to */
|
|
98
|
+
directDepVersion: string;
|
|
99
|
+
/** Scope of the direct dependency */
|
|
100
|
+
directDepScope: DependencyScope;
|
|
101
|
+
}[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Project info for lock file updates.
|
|
106
|
+
*/
|
|
107
|
+
export interface ProjectUpdateInfo {
|
|
108
|
+
/** Relative path to package.json (from source root) */
|
|
109
|
+
packageJsonPath: string;
|
|
110
|
+
/** Original package.json content */
|
|
111
|
+
originalPackageJson: string;
|
|
112
|
+
/** The package manager used by this project */
|
|
113
|
+
packageManager: PackageManager;
|
|
114
|
+
/** Config file contents extracted from the project (e.g., .npmrc) */
|
|
115
|
+
configFiles?: Record<string, string>;
|
|
116
|
+
}
|
|
@@ -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
|
+
}
|