@openrewrite/recipes-nodejs 0.36.0-20251211-172625 → 0.36.0-20251212-170419
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 +2 -0
- package/dist/index.js.map +1 -1
- package/dist/resources/advisories-npm.csv +5357 -0
- package/dist/security/dependency-vulnerability-check.d.ts +63 -0
- package/dist/security/dependency-vulnerability-check.d.ts.map +1 -0
- package/dist/security/dependency-vulnerability-check.js +639 -0
- package/dist/security/dependency-vulnerability-check.js.map +1 -0
- package/dist/security/index.d.ts +3 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +19 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/vulnerability.d.ts +33 -0
- package/dist/security/vulnerability.d.ts.map +1 -0
- package/dist/security/vulnerability.js +182 -0
- package/dist/security/vulnerability.js.map +1 -0
- package/package.json +6 -3
- package/src/index.ts +2 -0
- package/src/security/dependency-vulnerability-check.ts +934 -0
- package/src/security/index.ts +8 -0
- package/src/security/vulnerability.ts +265 -0
|
@@ -0,0 +1,934 @@
|
|
|
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 {Column, DataTable, ExecutionContext, Option, Recipe, ScanningRecipe, Tree, TreeVisitor, markupWarn} from "@openrewrite/rewrite";
|
|
8
|
+
import {getMemberKeyName, isLiteral, Json, JsonParser, JsonVisitor, isJson} from "@openrewrite/rewrite/json";
|
|
9
|
+
import {TreePrinters} from "@openrewrite/rewrite";
|
|
10
|
+
import {PlainText, PlainTextParser, isPlainText} from "@openrewrite/rewrite/text";
|
|
11
|
+
import {isDocuments, isYaml, Yaml, YamlParser} from "@openrewrite/rewrite/yaml";
|
|
12
|
+
import {
|
|
13
|
+
DependencyScope,
|
|
14
|
+
findNodeResolutionResult,
|
|
15
|
+
ResolvedDependency,
|
|
16
|
+
PackageManager,
|
|
17
|
+
createDependencyRecipeAccumulator,
|
|
18
|
+
DependencyRecipeAccumulator,
|
|
19
|
+
getUpdatedLockFileContent,
|
|
20
|
+
runInstallIfNeeded,
|
|
21
|
+
runInstallInTempDir,
|
|
22
|
+
storeInstallResult,
|
|
23
|
+
updateNodeResolutionMarker
|
|
24
|
+
} from "@openrewrite/rewrite/javascript";
|
|
25
|
+
import * as semver from "semver";
|
|
26
|
+
import * as path from "path";
|
|
27
|
+
import {parseSeverity, Severity, severityOrdinal, Vulnerability, VulnerabilityDatabase} from "./vulnerability";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Maximum upgrade delta for version upgrades.
|
|
31
|
+
* Use 'none' to only report vulnerabilities without making any changes.
|
|
32
|
+
*/
|
|
33
|
+
export type UpgradeDelta = 'none' | 'patch' | 'minor' | 'major';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Row for the vulnerability report data table.
|
|
37
|
+
*/
|
|
38
|
+
class VulnerabilityReportRow {
|
|
39
|
+
@Column({
|
|
40
|
+
displayName: "Source Path",
|
|
41
|
+
description: "Path to the package.json file where the vulnerability was found"
|
|
42
|
+
})
|
|
43
|
+
sourcePath!: string;
|
|
44
|
+
|
|
45
|
+
@Column({
|
|
46
|
+
displayName: "CVE",
|
|
47
|
+
description: "The CVE identifier of the vulnerability"
|
|
48
|
+
})
|
|
49
|
+
cve!: string;
|
|
50
|
+
|
|
51
|
+
@Column({
|
|
52
|
+
displayName: "Package",
|
|
53
|
+
description: "The name of the vulnerable package"
|
|
54
|
+
})
|
|
55
|
+
packageName!: string;
|
|
56
|
+
|
|
57
|
+
@Column({
|
|
58
|
+
displayName: "Version",
|
|
59
|
+
description: "The resolved version of the package"
|
|
60
|
+
})
|
|
61
|
+
version!: string;
|
|
62
|
+
|
|
63
|
+
@Column({
|
|
64
|
+
displayName: "Fixed Version",
|
|
65
|
+
description: "The version that fixes the vulnerability"
|
|
66
|
+
})
|
|
67
|
+
fixedVersion!: string;
|
|
68
|
+
|
|
69
|
+
@Column({
|
|
70
|
+
displayName: "Last Affected",
|
|
71
|
+
description: "The last version affected by the vulnerability"
|
|
72
|
+
})
|
|
73
|
+
lastAffectedVersion!: string;
|
|
74
|
+
|
|
75
|
+
@Column({
|
|
76
|
+
displayName: "Upgradeable",
|
|
77
|
+
description: "Whether the vulnerability can be fixed with a version upgrade within the maximum delta"
|
|
78
|
+
})
|
|
79
|
+
upgradeable!: boolean;
|
|
80
|
+
|
|
81
|
+
@Column({
|
|
82
|
+
displayName: "Summary",
|
|
83
|
+
description: "Brief description of the vulnerability"
|
|
84
|
+
})
|
|
85
|
+
summary!: string;
|
|
86
|
+
|
|
87
|
+
@Column({
|
|
88
|
+
displayName: "Severity",
|
|
89
|
+
description: "Severity level of the vulnerability"
|
|
90
|
+
})
|
|
91
|
+
severity!: string;
|
|
92
|
+
|
|
93
|
+
@Column({
|
|
94
|
+
displayName: "Depth",
|
|
95
|
+
description: "Depth in the dependency tree (0 = direct, 1+ = transitive)"
|
|
96
|
+
})
|
|
97
|
+
depth!: number;
|
|
98
|
+
|
|
99
|
+
@Column({
|
|
100
|
+
displayName: "CWEs",
|
|
101
|
+
description: "CWE identifiers associated with this vulnerability"
|
|
102
|
+
})
|
|
103
|
+
cwes!: string;
|
|
104
|
+
|
|
105
|
+
@Column({
|
|
106
|
+
displayName: "Is Direct",
|
|
107
|
+
description: "Whether this is a direct dependency"
|
|
108
|
+
})
|
|
109
|
+
isDirect!: boolean;
|
|
110
|
+
|
|
111
|
+
@Column({
|
|
112
|
+
displayName: "Dependency Path",
|
|
113
|
+
description: "Path showing how the vulnerable dependency is brought in (e.g., 'dependencies > lodash@4.0.0 > vulnerable-pkg@1.0.0')"
|
|
114
|
+
})
|
|
115
|
+
dependencyPath!: string;
|
|
116
|
+
|
|
117
|
+
constructor(
|
|
118
|
+
sourcePath: string,
|
|
119
|
+
cve: string,
|
|
120
|
+
packageName: string,
|
|
121
|
+
version: string,
|
|
122
|
+
fixedVersion: string,
|
|
123
|
+
lastAffectedVersion: string,
|
|
124
|
+
upgradeable: boolean,
|
|
125
|
+
summary: string,
|
|
126
|
+
severity: string,
|
|
127
|
+
depth: number,
|
|
128
|
+
cwes: string,
|
|
129
|
+
isDirect: boolean,
|
|
130
|
+
dependencyPath: string
|
|
131
|
+
) {
|
|
132
|
+
this.sourcePath = sourcePath;
|
|
133
|
+
this.cve = cve;
|
|
134
|
+
this.packageName = packageName;
|
|
135
|
+
this.version = version;
|
|
136
|
+
this.fixedVersion = fixedVersion;
|
|
137
|
+
this.lastAffectedVersion = lastAffectedVersion;
|
|
138
|
+
this.upgradeable = upgradeable;
|
|
139
|
+
this.summary = summary;
|
|
140
|
+
this.severity = severity;
|
|
141
|
+
this.depth = depth;
|
|
142
|
+
this.cwes = cwes;
|
|
143
|
+
this.isDirect = isDirect;
|
|
144
|
+
this.dependencyPath = dependencyPath;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Represents a segment in the dependency path.
|
|
150
|
+
*/
|
|
151
|
+
interface PathSegment {
|
|
152
|
+
name: string;
|
|
153
|
+
version: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Represents a vulnerable dependency found during scanning.
|
|
158
|
+
*/
|
|
159
|
+
interface VulnerableDependency {
|
|
160
|
+
/** The resolved dependency that is vulnerable */
|
|
161
|
+
resolved: ResolvedDependency;
|
|
162
|
+
/** The vulnerability affecting it */
|
|
163
|
+
vulnerability: Vulnerability;
|
|
164
|
+
/** Depth in dependency tree (0 = direct) */
|
|
165
|
+
depth: number;
|
|
166
|
+
/** Whether this is a direct dependency */
|
|
167
|
+
isDirect: boolean;
|
|
168
|
+
/** The scope where this dependency was found (for direct deps) */
|
|
169
|
+
scope?: DependencyScope;
|
|
170
|
+
/** Path from root to this dependency */
|
|
171
|
+
path: PathSegment[];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Represents a fix to apply for a vulnerability.
|
|
176
|
+
*/
|
|
177
|
+
interface VulnerabilityFix {
|
|
178
|
+
/** Package name to upgrade */
|
|
179
|
+
packageName: string;
|
|
180
|
+
/** Version to upgrade to */
|
|
181
|
+
newVersion: string;
|
|
182
|
+
/** Whether this is a transitive dependency fix */
|
|
183
|
+
isTransitive: boolean;
|
|
184
|
+
/** CVEs this fix resolves */
|
|
185
|
+
cves: string[];
|
|
186
|
+
/** The scope where this dependency was found (for direct deps) */
|
|
187
|
+
scope?: DependencyScope;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Project info for lock file updates.
|
|
192
|
+
*/
|
|
193
|
+
interface ProjectUpdateInfo {
|
|
194
|
+
/** Absolute path to the project directory */
|
|
195
|
+
projectDir: string;
|
|
196
|
+
/** Relative path to package.json (from source root) */
|
|
197
|
+
packageJsonPath: string;
|
|
198
|
+
/** Original package.json content */
|
|
199
|
+
originalPackageJson: string;
|
|
200
|
+
/** The package manager used by this project */
|
|
201
|
+
packageManager: PackageManager;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Accumulator for the scanning recipe.
|
|
206
|
+
*/
|
|
207
|
+
interface Accumulator extends DependencyRecipeAccumulator<ProjectUpdateInfo> {
|
|
208
|
+
/** Vulnerability database */
|
|
209
|
+
db: VulnerabilityDatabase;
|
|
210
|
+
/** Vulnerable dependencies found, grouped by package.json path */
|
|
211
|
+
vulnerableByProject: Map<string, VulnerableDependency[]>;
|
|
212
|
+
/** Fixes to apply, grouped by package.json path */
|
|
213
|
+
fixesByProject: Map<string, VulnerabilityFix[]>;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Finds and fixes vulnerable npm dependencies.
|
|
218
|
+
*
|
|
219
|
+
* This software composition analysis (SCA) tool detects and upgrades dependencies with publicly
|
|
220
|
+
* disclosed vulnerabilities. This recipe both generates a report of vulnerable dependencies and
|
|
221
|
+
* upgrades to newer versions with fixes.
|
|
222
|
+
*
|
|
223
|
+
* This recipe by default only upgrades to the latest **patch** version. If a minor or major upgrade
|
|
224
|
+
* is required to reach the fixed version, this can be controlled using the `maximumUpgradeDelta` option.
|
|
225
|
+
*
|
|
226
|
+
* Vulnerability information comes from the GitHub Security Advisory Database,
|
|
227
|
+
* which aggregates vulnerability data from several public databases, including
|
|
228
|
+
* the National Vulnerability Database maintained by the United States government.
|
|
229
|
+
*/
|
|
230
|
+
export class DependencyVulnerabilityCheck extends ScanningRecipe<Accumulator> {
|
|
231
|
+
readonly name = "org.openrewrite.node.dependency-vulnerability-check";
|
|
232
|
+
readonly displayName = "Find and fix vulnerable npm dependencies";
|
|
233
|
+
readonly description = "This software composition analysis (SCA) tool detects and upgrades dependencies with publicly " +
|
|
234
|
+
"disclosed vulnerabilities. This recipe both generates a report of vulnerable dependencies and " +
|
|
235
|
+
"upgrades to newer versions with fixes. This recipe by default only upgrades to the latest **patch** version. " +
|
|
236
|
+
"If a minor or major upgrade is required to reach the fixed version, this can be controlled using the `maximumUpgradeDelta` option. " +
|
|
237
|
+
"Vulnerability information comes from the GitHub Security Advisory Database.";
|
|
238
|
+
|
|
239
|
+
private readonly vulnerabilityReport = new DataTable<VulnerabilityReportRow>(
|
|
240
|
+
"org.openrewrite.nodejs.table.VulnerabilityReport",
|
|
241
|
+
"Vulnerability Report",
|
|
242
|
+
"Lists all vulnerabilities found in project dependencies.",
|
|
243
|
+
VulnerabilityReportRow
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
@Option({
|
|
247
|
+
displayName: "Scope",
|
|
248
|
+
description: "Match dependencies with the specified scope. Default includes all scopes. " +
|
|
249
|
+
"Use 'dependencies' for production dependencies, 'devDependencies' for development only, etc.",
|
|
250
|
+
required: false,
|
|
251
|
+
example: "dependencies",
|
|
252
|
+
valid: ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]
|
|
253
|
+
})
|
|
254
|
+
scope?: DependencyScope;
|
|
255
|
+
|
|
256
|
+
@Option({
|
|
257
|
+
displayName: "Override transitives",
|
|
258
|
+
description: "When enabled, transitive dependencies with vulnerabilities will have their versions overridden " +
|
|
259
|
+
"using npm overrides, yarn resolutions, or pnpm overrides. " +
|
|
260
|
+
"By default only direct dependencies have their version numbers upgraded.",
|
|
261
|
+
required: false,
|
|
262
|
+
example: "true"
|
|
263
|
+
})
|
|
264
|
+
overrideTransitive?: boolean;
|
|
265
|
+
|
|
266
|
+
@Option({
|
|
267
|
+
displayName: "Maximum upgrade delta",
|
|
268
|
+
description: "The maximum difference to allow when upgrading a dependency version. " +
|
|
269
|
+
"Use 'none' to only report vulnerabilities without making any changes. " +
|
|
270
|
+
"Patch version upgrades are the default and safest option. " +
|
|
271
|
+
"Minor version upgrades can introduce new features but typically no breaking changes. " +
|
|
272
|
+
"Major version upgrades may require code changes.",
|
|
273
|
+
required: false,
|
|
274
|
+
example: "patch",
|
|
275
|
+
valid: ["none", "patch", "minor", "major"]
|
|
276
|
+
})
|
|
277
|
+
maximumUpgradeDelta?: UpgradeDelta;
|
|
278
|
+
|
|
279
|
+
@Option({
|
|
280
|
+
displayName: "Minimum severity",
|
|
281
|
+
description: "Only fix vulnerabilities with a severity level equal to or higher than the specified minimum. " +
|
|
282
|
+
"Vulnerabilities are classified as LOW, MODERATE, HIGH, or CRITICAL based on their potential impact. " +
|
|
283
|
+
"Default is LOW, which includes all severity levels.",
|
|
284
|
+
required: false,
|
|
285
|
+
example: "MODERATE",
|
|
286
|
+
valid: ["LOW", "MODERATE", "HIGH", "CRITICAL"]
|
|
287
|
+
})
|
|
288
|
+
minimumSeverity?: string;
|
|
289
|
+
|
|
290
|
+
@Option({
|
|
291
|
+
displayName: "CVE pattern",
|
|
292
|
+
description: "Only fix vulnerabilities matching this regular expression pattern. " +
|
|
293
|
+
"This allows filtering to specific CVEs or CVE ranges. " +
|
|
294
|
+
"For example, 'CVE-2023-.*' will only check for CVEs from 2023. " +
|
|
295
|
+
"If not specified, all CVEs will be checked.",
|
|
296
|
+
required: false,
|
|
297
|
+
example: "CVE-2023-.*"
|
|
298
|
+
})
|
|
299
|
+
cvePattern?: string;
|
|
300
|
+
|
|
301
|
+
initialValue(_ctx: ExecutionContext): Accumulator {
|
|
302
|
+
return {
|
|
303
|
+
// DependencyRecipeAccumulator fields
|
|
304
|
+
...createDependencyRecipeAccumulator<ProjectUpdateInfo>(),
|
|
305
|
+
// Our custom fields
|
|
306
|
+
db: VulnerabilityDatabase.load(),
|
|
307
|
+
vulnerableByProject: new Map(),
|
|
308
|
+
fixesByProject: new Map()
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private getMinimumSeverity(): Severity {
|
|
313
|
+
return this.minimumSeverity ? parseSeverity(this.minimumSeverity) : Severity.LOW;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private getMaximumUpgradeDelta(): UpgradeDelta {
|
|
317
|
+
return this.maximumUpgradeDelta || 'patch';
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private isReportOnly(): boolean {
|
|
321
|
+
return this.getMaximumUpgradeDelta() === 'none';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Checks if a vulnerability matches the CVE pattern filter.
|
|
326
|
+
*/
|
|
327
|
+
private matchesCvePattern(vulnerability: Vulnerability): boolean {
|
|
328
|
+
if (!this.cvePattern) {
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
const regex = new RegExp(this.cvePattern);
|
|
333
|
+
return regex.test(vulnerability.cve);
|
|
334
|
+
} catch {
|
|
335
|
+
return true; // Invalid regex, don't filter
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Checks if a version is affected by a vulnerability.
|
|
341
|
+
*/
|
|
342
|
+
private isVersionAffected(version: string, vulnerability: Vulnerability): boolean {
|
|
343
|
+
try {
|
|
344
|
+
const v = semver.parse(version);
|
|
345
|
+
if (!v) return false;
|
|
346
|
+
|
|
347
|
+
// Check introduced version
|
|
348
|
+
if (vulnerability.introducedVersion && vulnerability.introducedVersion !== '0') {
|
|
349
|
+
const introduced = semver.parse(vulnerability.introducedVersion);
|
|
350
|
+
if (introduced && semver.lt(v, introduced)) {
|
|
351
|
+
return false; // Version is before the vulnerability was introduced
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Check if fixed
|
|
356
|
+
if (vulnerability.fixedVersion) {
|
|
357
|
+
const fixed = semver.parse(vulnerability.fixedVersion);
|
|
358
|
+
if (fixed && semver.gte(v, fixed)) {
|
|
359
|
+
return false; // Version is at or after the fix
|
|
360
|
+
}
|
|
361
|
+
return true; // Version is in the vulnerable range
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Check last affected version
|
|
365
|
+
if (vulnerability.lastAffectedVersion) {
|
|
366
|
+
const lastAffected = semver.parse(vulnerability.lastAffectedVersion);
|
|
367
|
+
if (lastAffected && semver.gt(v, lastAffected)) {
|
|
368
|
+
return false; // Version is after the last affected
|
|
369
|
+
}
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return true; // No fix information, assume vulnerable
|
|
374
|
+
} catch {
|
|
375
|
+
return false; // Invalid version, skip
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Checks if a vulnerability can be fixed within the maximum upgrade delta.
|
|
381
|
+
*/
|
|
382
|
+
private isUpgradeableWithinDelta(currentVersion: string, vulnerability: Vulnerability): boolean {
|
|
383
|
+
if (this.isReportOnly()) {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const current = semver.parse(currentVersion);
|
|
389
|
+
if (!current) return false;
|
|
390
|
+
|
|
391
|
+
const delta = this.getMaximumUpgradeDelta();
|
|
392
|
+
|
|
393
|
+
// If we have a fixed version, check if it's reachable within delta
|
|
394
|
+
if (vulnerability.fixedVersion) {
|
|
395
|
+
const fixed = semver.parse(vulnerability.fixedVersion);
|
|
396
|
+
if (!fixed) return false;
|
|
397
|
+
|
|
398
|
+
switch (delta) {
|
|
399
|
+
case 'patch':
|
|
400
|
+
return current.major === fixed.major && current.minor === fixed.minor;
|
|
401
|
+
case 'minor':
|
|
402
|
+
return current.major === fixed.major;
|
|
403
|
+
case 'major':
|
|
404
|
+
return true;
|
|
405
|
+
case 'none':
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// If we only have lastAffectedVersion, check if upgrading within delta
|
|
411
|
+
// could get us past it (i.e., to a non-vulnerable version)
|
|
412
|
+
if (vulnerability.lastAffectedVersion) {
|
|
413
|
+
const lastAffected = semver.parse(vulnerability.lastAffectedVersion);
|
|
414
|
+
if (!lastAffected) return false;
|
|
415
|
+
|
|
416
|
+
switch (delta) {
|
|
417
|
+
case 'patch':
|
|
418
|
+
return current.major === lastAffected.major &&
|
|
419
|
+
current.minor === lastAffected.minor;
|
|
420
|
+
case 'minor':
|
|
421
|
+
return current.major === lastAffected.major;
|
|
422
|
+
case 'major':
|
|
423
|
+
return true;
|
|
424
|
+
case 'none':
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return false;
|
|
430
|
+
} catch {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Gets the version to upgrade to for a vulnerability.
|
|
437
|
+
*/
|
|
438
|
+
private getUpgradeVersion(vulnerability: Vulnerability): string | undefined {
|
|
439
|
+
if (vulnerability.fixedVersion) {
|
|
440
|
+
return vulnerability.fixedVersion;
|
|
441
|
+
}
|
|
442
|
+
// For lastAffectedVersion, we need the next version after it
|
|
443
|
+
// We can't determine this exactly, so return undefined and let the
|
|
444
|
+
// upgrade recipe handle version selection
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Renders a dependency path as a string.
|
|
450
|
+
* Example: "dependencies > lodash@4.17.20 > vulnerable-pkg@1.0.0"
|
|
451
|
+
*/
|
|
452
|
+
private renderPath(scope: DependencyScope | undefined, path: PathSegment[]): string {
|
|
453
|
+
const parts: string[] = [];
|
|
454
|
+
if (scope) {
|
|
455
|
+
parts.push(scope);
|
|
456
|
+
}
|
|
457
|
+
for (const seg of path) {
|
|
458
|
+
parts.push(`${seg.name}@${seg.version}`);
|
|
459
|
+
}
|
|
460
|
+
return parts.join(' > ');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Finds vulnerabilities in a resolved dependency and its transitives.
|
|
465
|
+
*/
|
|
466
|
+
private findVulnerabilities(
|
|
467
|
+
resolved: ResolvedDependency,
|
|
468
|
+
db: VulnerabilityDatabase,
|
|
469
|
+
depth: number,
|
|
470
|
+
isDirect: boolean,
|
|
471
|
+
scope: DependencyScope | undefined,
|
|
472
|
+
path: PathSegment[],
|
|
473
|
+
visited: Set<string>,
|
|
474
|
+
results: VulnerableDependency[]
|
|
475
|
+
): void {
|
|
476
|
+
const key = `${resolved.name}@${resolved.version}`;
|
|
477
|
+
if (visited.has(key)) return;
|
|
478
|
+
visited.add(key);
|
|
479
|
+
|
|
480
|
+
// Build current path
|
|
481
|
+
const currentPath = [...path, { name: resolved.name, version: resolved.version }];
|
|
482
|
+
|
|
483
|
+
// Check for vulnerabilities in this package
|
|
484
|
+
const vulns = db.getVulnerabilities(resolved.name);
|
|
485
|
+
for (const vuln of vulns) {
|
|
486
|
+
// Filter by severity
|
|
487
|
+
if (severityOrdinal(vuln.severity) < severityOrdinal(this.getMinimumSeverity())) {
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Filter by CVE pattern
|
|
492
|
+
if (!this.matchesCvePattern(vuln)) {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Check if this version is affected
|
|
497
|
+
if (this.isVersionAffected(resolved.version, vuln)) {
|
|
498
|
+
results.push({
|
|
499
|
+
resolved,
|
|
500
|
+
vulnerability: vuln,
|
|
501
|
+
depth,
|
|
502
|
+
isDirect,
|
|
503
|
+
scope,
|
|
504
|
+
path: currentPath
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Recurse into transitive dependencies if overrideTransitive is enabled
|
|
510
|
+
if (this.overrideTransitive) {
|
|
511
|
+
const transitives = [
|
|
512
|
+
...(resolved.dependencies || []),
|
|
513
|
+
...(resolved.devDependencies || []),
|
|
514
|
+
...(resolved.peerDependencies || []),
|
|
515
|
+
...(resolved.optionalDependencies || [])
|
|
516
|
+
];
|
|
517
|
+
|
|
518
|
+
for (const dep of transitives) {
|
|
519
|
+
if (dep.resolved) {
|
|
520
|
+
this.findVulnerabilities(
|
|
521
|
+
dep.resolved,
|
|
522
|
+
db,
|
|
523
|
+
depth + 1,
|
|
524
|
+
false,
|
|
525
|
+
scope, // Keep original scope for path rendering
|
|
526
|
+
currentPath,
|
|
527
|
+
visited,
|
|
528
|
+
results
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Computes the fixes to apply for the vulnerabilities found.
|
|
537
|
+
* Groups vulnerabilities by package and determines the best fix version.
|
|
538
|
+
*/
|
|
539
|
+
private computeFixes(vulnerabilities: VulnerableDependency[]): VulnerabilityFix[] {
|
|
540
|
+
if (this.isReportOnly()) {
|
|
541
|
+
return [];
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Group vulnerabilities by package name
|
|
545
|
+
const byPackage = new Map<string, VulnerableDependency[]>();
|
|
546
|
+
for (const vuln of vulnerabilities) {
|
|
547
|
+
const existing = byPackage.get(vuln.resolved.name) || [];
|
|
548
|
+
existing.push(vuln);
|
|
549
|
+
byPackage.set(vuln.resolved.name, existing);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const fixes: VulnerabilityFix[] = [];
|
|
553
|
+
|
|
554
|
+
for (const [packageName, vulns] of byPackage) {
|
|
555
|
+
// Find the highest fix version needed across all vulnerabilities for this package
|
|
556
|
+
let highestFixVersion: string | undefined;
|
|
557
|
+
const cves: string[] = [];
|
|
558
|
+
let isTransitive = true;
|
|
559
|
+
let scope: DependencyScope | undefined;
|
|
560
|
+
|
|
561
|
+
for (const vuln of vulns) {
|
|
562
|
+
// Check if upgradeable within delta
|
|
563
|
+
if (!this.isUpgradeableWithinDelta(vuln.resolved.version, vuln.vulnerability)) {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const fixVersion = this.getUpgradeVersion(vuln.vulnerability);
|
|
568
|
+
if (fixVersion) {
|
|
569
|
+
if (!highestFixVersion || semver.gt(fixVersion, highestFixVersion)) {
|
|
570
|
+
highestFixVersion = fixVersion;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
cves.push(vuln.vulnerability.cve);
|
|
575
|
+
|
|
576
|
+
if (vuln.isDirect) {
|
|
577
|
+
isTransitive = false;
|
|
578
|
+
scope = vuln.scope;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (highestFixVersion && cves.length > 0) {
|
|
583
|
+
fixes.push({
|
|
584
|
+
packageName,
|
|
585
|
+
newVersion: highestFixVersion,
|
|
586
|
+
isTransitive,
|
|
587
|
+
cves,
|
|
588
|
+
scope
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return fixes;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async scanner(acc: Accumulator): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
597
|
+
const recipe = this;
|
|
598
|
+
|
|
599
|
+
return new class extends JsonVisitor<ExecutionContext> {
|
|
600
|
+
protected async visitDocument(doc: Json.Document, _ctx: ExecutionContext): Promise<Json | undefined> {
|
|
601
|
+
// Only process package.json files
|
|
602
|
+
if (!doc.sourcePath.endsWith('package.json')) {
|
|
603
|
+
return doc;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const marker = findNodeResolutionResult(doc);
|
|
607
|
+
if (!marker) {
|
|
608
|
+
return doc;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const vulnerabilities: VulnerableDependency[] = [];
|
|
612
|
+
const visited = new Set<string>();
|
|
613
|
+
|
|
614
|
+
// Determine which scopes to check
|
|
615
|
+
const scopesToCheck: DependencyScope[] = recipe.scope
|
|
616
|
+
? [recipe.scope]
|
|
617
|
+
: ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
|
618
|
+
|
|
619
|
+
for (const scope of scopesToCheck) {
|
|
620
|
+
const deps = marker[scope] || [];
|
|
621
|
+
for (const dep of deps) {
|
|
622
|
+
if (dep.resolved) {
|
|
623
|
+
recipe.findVulnerabilities(
|
|
624
|
+
dep.resolved,
|
|
625
|
+
acc.db,
|
|
626
|
+
0,
|
|
627
|
+
true,
|
|
628
|
+
scope,
|
|
629
|
+
[], // Start with empty path
|
|
630
|
+
visited,
|
|
631
|
+
vulnerabilities
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (vulnerabilities.length > 0) {
|
|
638
|
+
acc.vulnerableByProject.set(doc.sourcePath, vulnerabilities);
|
|
639
|
+
|
|
640
|
+
// Compute fixes
|
|
641
|
+
const fixes = recipe.computeFixes(vulnerabilities);
|
|
642
|
+
if (fixes.length > 0) {
|
|
643
|
+
acc.fixesByProject.set(doc.sourcePath, fixes);
|
|
644
|
+
|
|
645
|
+
// Store project info for lock file updates
|
|
646
|
+
const projectDir = path.dirname(path.resolve(doc.sourcePath));
|
|
647
|
+
const pm = marker.packageManager ?? PackageManager.Npm;
|
|
648
|
+
|
|
649
|
+
acc.projectsToUpdate.set(doc.sourcePath, {
|
|
650
|
+
projectDir,
|
|
651
|
+
packageJsonPath: doc.sourcePath,
|
|
652
|
+
originalPackageJson: await this.printDocument(doc),
|
|
653
|
+
packageManager: pm
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return doc;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private async printDocument(doc: Json.Document): Promise<string> {
|
|
662
|
+
return TreePrinters.print(doc);
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Returns sub-recipes to apply the fixes.
|
|
669
|
+
*/
|
|
670
|
+
async getRecipeList(): Promise<Recipe[]> {
|
|
671
|
+
// This method is called before scanning, so we can't return fix recipes here.
|
|
672
|
+
// Instead, we'll apply fixes in the editor phase.
|
|
673
|
+
return [];
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async editorWithData(acc: Accumulator): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
677
|
+
const recipe = this;
|
|
678
|
+
|
|
679
|
+
// YAML lock file names (pnpm-lock.yaml, and yarn.lock for Berry)
|
|
680
|
+
const YAML_LOCK_FILES = ['pnpm-lock.yaml', 'yarn.lock'];
|
|
681
|
+
|
|
682
|
+
// Use a TreeVisitor that handles JSON, YAML, and PlainText lock files
|
|
683
|
+
return new class extends TreeVisitor<Tree, ExecutionContext> {
|
|
684
|
+
protected async accept(tree: Tree, ctx: ExecutionContext): Promise<Tree | undefined> {
|
|
685
|
+
// Handle JSON documents (package.json and JSON lock files like bun.lock, package-lock.json)
|
|
686
|
+
if (isJson(tree) && tree.kind === Json.Kind.Document) {
|
|
687
|
+
return this.handleJsonDocument(tree as Json.Document, ctx);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Handle YAML documents (pnpm-lock.yaml and Yarn Berry yarn.lock)
|
|
691
|
+
if (isYaml(tree) && isDocuments(tree)) {
|
|
692
|
+
const basename = path.basename(tree.sourcePath);
|
|
693
|
+
if (YAML_LOCK_FILES.includes(basename)) {
|
|
694
|
+
return this.handleYamlLockFile(tree, ctx);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Handle PlainText files only for Yarn Classic yarn.lock
|
|
699
|
+
if (isPlainText(tree)) {
|
|
700
|
+
const basename = path.basename(tree.sourcePath);
|
|
701
|
+
if (basename === 'yarn.lock') {
|
|
702
|
+
return this.handlePlainTextLockFile(tree as PlainText, ctx);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return tree;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
private async handleJsonDocument(doc: Json.Document, ctx: ExecutionContext): Promise<Json | undefined> {
|
|
710
|
+
const sourcePath = doc.sourcePath;
|
|
711
|
+
|
|
712
|
+
// Handle package.json files
|
|
713
|
+
if (sourcePath.endsWith('package.json')) {
|
|
714
|
+
return this.handlePackageJson(doc, ctx);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Handle JSON lock files (bun.lock, package-lock.json)
|
|
718
|
+
const updatedLockContent = getUpdatedLockFileContent(sourcePath, acc);
|
|
719
|
+
if (updatedLockContent) {
|
|
720
|
+
return await new JsonParser({}).parseOne({
|
|
721
|
+
text: updatedLockContent,
|
|
722
|
+
sourcePath: doc.sourcePath
|
|
723
|
+
}) as Json.Document;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return doc;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
private async handleYamlLockFile(docs: Yaml.Documents, _ctx: ExecutionContext): Promise<Yaml.Documents | undefined> {
|
|
730
|
+
const sourcePath = docs.sourcePath;
|
|
731
|
+
|
|
732
|
+
// Handle YAML lock files (pnpm-lock.yaml, Yarn Berry yarn.lock)
|
|
733
|
+
const updatedLockContent = getUpdatedLockFileContent(sourcePath, acc);
|
|
734
|
+
if (updatedLockContent) {
|
|
735
|
+
return await new YamlParser({}).parseOne({
|
|
736
|
+
text: updatedLockContent,
|
|
737
|
+
sourcePath: docs.sourcePath
|
|
738
|
+
}) as Yaml.Documents;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return docs;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
private async handlePlainTextLockFile(text: PlainText, _ctx: ExecutionContext): Promise<PlainText | undefined> {
|
|
745
|
+
const sourcePath = text.sourcePath;
|
|
746
|
+
|
|
747
|
+
// Handle Yarn Classic yarn.lock (plain text format)
|
|
748
|
+
const updatedLockContent = getUpdatedLockFileContent(sourcePath, acc);
|
|
749
|
+
if (updatedLockContent) {
|
|
750
|
+
// Parse as plain text and return with new content
|
|
751
|
+
const parsed = await new PlainTextParser({}).parseOne({
|
|
752
|
+
text: updatedLockContent,
|
|
753
|
+
sourcePath: text.sourcePath
|
|
754
|
+
});
|
|
755
|
+
return parsed as PlainText;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return text;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
private async handlePackageJson(doc: Json.Document, ctx: ExecutionContext): Promise<Json | undefined> {
|
|
762
|
+
const vulnerabilities = acc.vulnerableByProject.get(doc.sourcePath);
|
|
763
|
+
if (!vulnerabilities || vulnerabilities.length === 0) {
|
|
764
|
+
return doc;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Insert rows into the data table for each vulnerability
|
|
768
|
+
for (const vuln of vulnerabilities) {
|
|
769
|
+
const upgradeable = recipe.isUpgradeableWithinDelta(
|
|
770
|
+
vuln.resolved.version,
|
|
771
|
+
vuln.vulnerability
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
recipe.vulnerabilityReport.insertRow(ctx, new VulnerabilityReportRow(
|
|
775
|
+
doc.sourcePath,
|
|
776
|
+
vuln.vulnerability.cve,
|
|
777
|
+
vuln.resolved.name,
|
|
778
|
+
vuln.resolved.version,
|
|
779
|
+
vuln.vulnerability.fixedVersion || '',
|
|
780
|
+
vuln.vulnerability.lastAffectedVersion || '',
|
|
781
|
+
upgradeable,
|
|
782
|
+
vuln.vulnerability.summary,
|
|
783
|
+
vuln.vulnerability.severity,
|
|
784
|
+
vuln.depth,
|
|
785
|
+
vuln.vulnerability.cwes,
|
|
786
|
+
vuln.isDirect,
|
|
787
|
+
recipe.renderPath(vuln.scope, vuln.path)
|
|
788
|
+
));
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Apply fixes if not in report-only mode
|
|
792
|
+
if (recipe.isReportOnly()) {
|
|
793
|
+
return doc;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const fixes = acc.fixesByProject.get(doc.sourcePath);
|
|
797
|
+
const updateInfo = acc.projectsToUpdate.get(doc.sourcePath);
|
|
798
|
+
if (!fixes || fixes.length === 0 || !updateInfo) {
|
|
799
|
+
return doc;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Run package manager install if needed
|
|
803
|
+
const failureMessage = await runInstallIfNeeded(doc.sourcePath, acc, () =>
|
|
804
|
+
recipe.runPackageManagerInstall(acc, updateInfo, fixes)
|
|
805
|
+
);
|
|
806
|
+
if (failureMessage) {
|
|
807
|
+
return markupWarn(
|
|
808
|
+
doc,
|
|
809
|
+
`Failed to fix vulnerabilities in ${doc.sourcePath}`,
|
|
810
|
+
failureMessage
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Update the dependency versions in the JSON AST (preserves formatting)
|
|
815
|
+
let result: Json.Document = doc;
|
|
816
|
+
for (const fix of fixes) {
|
|
817
|
+
if (!fix.isTransitive && fix.scope) {
|
|
818
|
+
const visitor = new UpdateVersionVisitor(
|
|
819
|
+
fix.packageName,
|
|
820
|
+
fix.newVersion,
|
|
821
|
+
fix.scope
|
|
822
|
+
);
|
|
823
|
+
result = await visitor.visit(result, undefined) as Json.Document;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Update the NodeResolutionResult marker
|
|
828
|
+
return updateNodeResolutionMarker(result, updateInfo, acc);
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Runs the package manager in a temporary directory to update the lock file.
|
|
835
|
+
* Writes a modified package.json with all new versions, then runs install.
|
|
836
|
+
*/
|
|
837
|
+
private async runPackageManagerInstall(
|
|
838
|
+
acc: Accumulator,
|
|
839
|
+
updateInfo: ProjectUpdateInfo,
|
|
840
|
+
fixes: VulnerabilityFix[]
|
|
841
|
+
): Promise<void> {
|
|
842
|
+
// Create modified package.json with all the new version constraints
|
|
843
|
+
const modifiedPackageJson = this.createModifiedPackageJson(
|
|
844
|
+
updateInfo.originalPackageJson,
|
|
845
|
+
fixes
|
|
846
|
+
);
|
|
847
|
+
|
|
848
|
+
const result = await runInstallInTempDir(
|
|
849
|
+
updateInfo.projectDir,
|
|
850
|
+
updateInfo.packageManager,
|
|
851
|
+
modifiedPackageJson
|
|
852
|
+
);
|
|
853
|
+
|
|
854
|
+
storeInstallResult(result, acc, updateInfo, modifiedPackageJson);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Creates a modified package.json with all the updated dependency versions.
|
|
859
|
+
*/
|
|
860
|
+
private createModifiedPackageJson(
|
|
861
|
+
originalContent: string,
|
|
862
|
+
fixes: VulnerabilityFix[]
|
|
863
|
+
): string {
|
|
864
|
+
const packageJson = JSON.parse(originalContent);
|
|
865
|
+
|
|
866
|
+
for (const fix of fixes) {
|
|
867
|
+
if (!fix.isTransitive && fix.scope) {
|
|
868
|
+
if (packageJson[fix.scope] && packageJson[fix.scope][fix.packageName]) {
|
|
869
|
+
packageJson[fix.scope][fix.packageName] = fix.newVersion;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return JSON.stringify(packageJson, null, 2);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Visitor that updates the version of a specific dependency in a specific scope.
|
|
880
|
+
*/
|
|
881
|
+
class UpdateVersionVisitor extends JsonVisitor<void> {
|
|
882
|
+
private readonly packageName: string;
|
|
883
|
+
private readonly newVersion: string;
|
|
884
|
+
private readonly targetScope: DependencyScope;
|
|
885
|
+
private inTargetScope = false;
|
|
886
|
+
|
|
887
|
+
constructor(packageName: string, newVersion: string, targetScope: DependencyScope) {
|
|
888
|
+
super();
|
|
889
|
+
this.packageName = packageName;
|
|
890
|
+
this.newVersion = newVersion;
|
|
891
|
+
this.targetScope = targetScope;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
protected async visitMember(member: Json.Member, p: void): Promise<Json | undefined> {
|
|
895
|
+
// Check if we're entering the target scope
|
|
896
|
+
const keyName = getMemberKeyName(member);
|
|
897
|
+
|
|
898
|
+
if (keyName === this.targetScope) {
|
|
899
|
+
// We're entering the dependencies scope
|
|
900
|
+
this.inTargetScope = true;
|
|
901
|
+
const result = await super.visitMember(member, p);
|
|
902
|
+
this.inTargetScope = false;
|
|
903
|
+
return result;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Check if this is the dependency we're looking for
|
|
907
|
+
if (this.inTargetScope && keyName === this.packageName) {
|
|
908
|
+
// Update the version value
|
|
909
|
+
return this.updateVersion(member);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
return super.visitMember(member, p);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
private updateVersion(member: Json.Member): Json.Member {
|
|
916
|
+
const value = member.value;
|
|
917
|
+
|
|
918
|
+
if (!isLiteral(value)) {
|
|
919
|
+
return member; // Not a literal value, can't update
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Create new literal with updated version
|
|
923
|
+
const newLiteral: Json.Literal = {
|
|
924
|
+
...value,
|
|
925
|
+
source: `"${this.newVersion}"`,
|
|
926
|
+
value: this.newVersion
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
return {
|
|
930
|
+
...member,
|
|
931
|
+
value: newLiteral
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
}
|