@openrewrite/recipes-nodejs 0.37.0-20251224-170410 → 0.37.0-20260102-170441

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.
@@ -53,13 +53,15 @@ exports.extractVersionPrefix = extractVersionPrefix;
53
53
  exports.applyVersionPrefix = applyVersionPrefix;
54
54
  const rewrite_1 = require("@openrewrite/rewrite");
55
55
  const json_1 = require("@openrewrite/rewrite/json");
56
- const rewrite_2 = require("@openrewrite/rewrite");
57
56
  const text_1 = require("@openrewrite/rewrite/text");
58
57
  const yaml_1 = require("@openrewrite/rewrite/yaml");
59
58
  const javascript_1 = require("@openrewrite/rewrite/javascript");
60
59
  const semver = __importStar(require("semver"));
61
60
  const path = __importStar(require("path"));
62
61
  const vulnerability_1 = require("./vulnerability");
62
+ const ALL_DEPENDENCY_SCOPES = [
63
+ 'dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'
64
+ ];
63
65
  class VulnerabilityReportRow {
64
66
  constructor(sourcePath, cve, packageName, version, fixedVersion, lastAffectedVersion, upgradeable, summary, severity, depth, cwes, isDirect, dependencyPath) {
65
67
  this.sourcePath = sourcePath;
@@ -156,8 +158,9 @@ __decorate([
156
158
  })
157
159
  ], VulnerabilityReportRow.prototype, "dependencyPath", void 0);
158
160
  class DependencyVulnerabilityCheck extends rewrite_1.ScanningRecipe {
159
- constructor() {
160
- super(...arguments);
161
+ constructor(options) {
162
+ var _a, _b, _c;
163
+ super(options);
161
164
  this.name = "org.openrewrite.node.dependency-vulnerability-check";
162
165
  this.displayName = "Find and fix vulnerable npm dependencies";
163
166
  this.description = "This software composition analysis (SCA) tool detects and upgrades dependencies with publicly " +
@@ -166,30 +169,31 @@ class DependencyVulnerabilityCheck extends rewrite_1.ScanningRecipe {
166
169
  "If a minor or major upgrade is required to reach the fixed version, this can be controlled using the `maximumUpgradeDelta` option. " +
167
170
  "Vulnerability information comes from the GitHub Security Advisory Database.";
168
171
  this.vulnerabilityReport = new rewrite_1.DataTable("org.openrewrite.nodejs.table.VulnerabilityReport", "Vulnerability Report", "Lists all vulnerabilities found in project dependencies.", VulnerabilityReportRow);
172
+ (_a = this.maximumUpgradeDelta) !== null && _a !== void 0 ? _a : (this.maximumUpgradeDelta = 'patch');
173
+ (_b = this.minimumSeverity) !== null && _b !== void 0 ? _b : (this.minimumSeverity = vulnerability_1.Severity.LOW);
174
+ (_c = this.fixDeclaredVersions) !== null && _c !== void 0 ? _c : (this.fixDeclaredVersions = false);
175
+ if (options === null || options === void 0 ? void 0 : options.minimumSeverity) {
176
+ this.minimumSeverity = (0, vulnerability_1.parseSeverity)(options.minimumSeverity);
177
+ }
178
+ if (this.cvePattern) {
179
+ try {
180
+ this.cvePatternRegex = new RegExp(this.cvePattern);
181
+ }
182
+ catch (_d) {
183
+ }
184
+ }
169
185
  }
170
186
  initialValue(_ctx) {
171
- return Object.assign(Object.assign({}, (0, javascript_1.createDependencyRecipeAccumulator)()), { db: vulnerability_1.VulnerabilityDatabase.load(), vulnerableByProject: new Map(), fixesByProject: new Map() });
172
- }
173
- getMinimumSeverity() {
174
- return this.minimumSeverity ? (0, vulnerability_1.parseSeverity)(this.minimumSeverity) : vulnerability_1.Severity.LOW;
175
- }
176
- getMaximumUpgradeDelta() {
177
- return this.maximumUpgradeDelta || 'patch';
187
+ return Object.assign(Object.assign({}, (0, javascript_1.createDependencyRecipeAccumulator)()), { db: vulnerability_1.VulnerabilityDatabase.load(), vulnerableByProject: new Map(), fixesByProject: new Map(), originalLockFiles: new Map(), allPackageJsonContents: new Map(), workspaceRoots: new Map(), modifiedWorkspaceMemberContents: new Map(), workspaceDetectionComplete: false });
178
188
  }
179
189
  isReportOnly() {
180
- return this.getMaximumUpgradeDelta() === 'none';
190
+ return this.maximumUpgradeDelta === 'none';
181
191
  }
182
192
  matchesCvePattern(vulnerability) {
183
- if (!this.cvePattern) {
184
- return true;
185
- }
186
- try {
187
- const regex = new RegExp(this.cvePattern);
188
- return regex.test(vulnerability.cve);
189
- }
190
- catch (_a) {
193
+ if (!this.cvePatternRegex) {
191
194
  return true;
192
195
  }
196
+ return this.cvePatternRegex.test(vulnerability.cve);
193
197
  }
194
198
  isVersionAffected(version, vulnerability) {
195
199
  try {
@@ -230,7 +234,7 @@ class DependencyVulnerabilityCheck extends rewrite_1.ScanningRecipe {
230
234
  const current = semver.parse(currentVersion);
231
235
  if (!current)
232
236
  return false;
233
- const delta = this.getMaximumUpgradeDelta();
237
+ const delta = this.maximumUpgradeDelta;
234
238
  if (vulnerability.fixedVersion) {
235
239
  const fixed = semver.parse(vulnerability.fixedVersion);
236
240
  if (!fixed)
@@ -274,6 +278,17 @@ class DependencyVulnerabilityCheck extends rewrite_1.ScanningRecipe {
274
278
  }
275
279
  return undefined;
276
280
  }
281
+ getVersionPrefixForDelta() {
282
+ switch (this.maximumUpgradeDelta) {
283
+ case 'patch':
284
+ return '~';
285
+ case 'minor':
286
+ case 'major':
287
+ return '^';
288
+ default:
289
+ return '';
290
+ }
291
+ }
277
292
  renderPath(scope, path) {
278
293
  const parts = [];
279
294
  if (scope) {
@@ -292,7 +307,7 @@ class DependencyVulnerabilityCheck extends rewrite_1.ScanningRecipe {
292
307
  const currentPath = [...path, { name: resolved.name, version: resolved.version }];
293
308
  const vulns = db.getVulnerabilities(resolved.name);
294
309
  for (const vuln of vulns) {
295
- if ((0, vulnerability_1.severityOrdinal)(vuln.severity) < (0, vulnerability_1.severityOrdinal)(this.getMinimumSeverity())) {
310
+ if ((0, vulnerability_1.severityOrdinal)(vuln.severity) < (0, vulnerability_1.severityOrdinal)(this.minimumSeverity)) {
296
311
  continue;
297
312
  }
298
313
  if (!this.matchesCvePattern(vuln)) {
@@ -323,30 +338,171 @@ class DependencyVulnerabilityCheck extends rewrite_1.ScanningRecipe {
323
338
  }
324
339
  }
325
340
  }
326
- computeFixes(vulnerabilities) {
341
+ findPreventiveFixes(marker, scopes, db) {
342
+ const fixes = [];
343
+ for (const scope of scopes) {
344
+ const deps = marker[scope] || [];
345
+ for (const dep of deps) {
346
+ if (!dep.resolved)
347
+ continue;
348
+ const declaredMinVersion = this.extractMinimumVersion(dep.versionConstraint);
349
+ if (!declaredMinVersion)
350
+ continue;
351
+ if (declaredMinVersion === dep.resolved.version)
352
+ continue;
353
+ const vulns = db.getVulnerabilities(dep.name);
354
+ const affectedCves = [];
355
+ let highestFixVersion;
356
+ for (const vuln of vulns) {
357
+ if ((0, vulnerability_1.severityOrdinal)(vuln.severity) < (0, vulnerability_1.severityOrdinal)(this.minimumSeverity)) {
358
+ continue;
359
+ }
360
+ if (!this.matchesCvePattern(vuln)) {
361
+ continue;
362
+ }
363
+ if (this.isVersionAffected(declaredMinVersion, vuln) &&
364
+ !this.isVersionAffected(dep.resolved.version, vuln)) {
365
+ affectedCves.push(vuln.cve);
366
+ const fixVersion = vuln.fixedVersion;
367
+ if (fixVersion && (!highestFixVersion || semver.gt(fixVersion, highestFixVersion))) {
368
+ highestFixVersion = fixVersion;
369
+ }
370
+ }
371
+ }
372
+ if (affectedCves.length > 0 && highestFixVersion) {
373
+ if (this.isUpgradeWithinDelta(declaredMinVersion, highestFixVersion)) {
374
+ const majorVersion = semver.major(declaredMinVersion);
375
+ fixes.push({
376
+ packageName: dep.name,
377
+ newVersion: highestFixVersion,
378
+ scope,
379
+ isTransitive: false,
380
+ cves: affectedCves,
381
+ originalMajorVersion: majorVersion
382
+ });
383
+ }
384
+ }
385
+ }
386
+ }
387
+ return fixes;
388
+ }
389
+ isUpgradeWithinDelta(fromVersion, toVersion) {
390
+ if (this.isReportOnly()) {
391
+ return false;
392
+ }
393
+ return this.isVersionWithinDelta(fromVersion, toVersion);
394
+ }
395
+ extractMinimumVersion(constraint) {
396
+ if (!constraint)
397
+ return undefined;
398
+ if (semver.valid(constraint)) {
399
+ return constraint;
400
+ }
401
+ const match = constraint.match(/^[~^>=<]*\s*(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?)/);
402
+ if (match && semver.valid(match[1])) {
403
+ return match[1];
404
+ }
405
+ const coerced = semver.coerce(constraint);
406
+ return coerced === null || coerced === void 0 ? void 0 : coerced.version;
407
+ }
408
+ findHighestSafeVersion(packageName, originalVersion, initialFixVersion, db, visited = new Set()) {
409
+ if (visited.has(initialFixVersion)) {
410
+ return initialFixVersion;
411
+ }
412
+ visited.add(initialFixVersion);
413
+ const vulnsInFixVersion = db.getVulnerabilities(packageName)
414
+ .filter(v => this.isVersionAffected(initialFixVersion, v));
415
+ if (vulnsInFixVersion.length === 0) {
416
+ return initialFixVersion;
417
+ }
418
+ let highestFixVersion = initialFixVersion;
419
+ for (const vuln of vulnsInFixVersion) {
420
+ const fixVersion = this.getUpgradeVersion(vuln);
421
+ if (fixVersion && semver.valid(fixVersion)) {
422
+ if (this.isVersionWithinDelta(originalVersion, fixVersion)) {
423
+ if (semver.gt(fixVersion, highestFixVersion)) {
424
+ highestFixVersion = fixVersion;
425
+ }
426
+ }
427
+ }
428
+ }
429
+ if (highestFixVersion !== initialFixVersion) {
430
+ return this.findHighestSafeVersion(packageName, originalVersion, highestFixVersion, db, visited);
431
+ }
432
+ return initialFixVersion;
433
+ }
434
+ isVersionWithinDelta(originalVersion, targetVersion) {
435
+ try {
436
+ const original = semver.parse(originalVersion);
437
+ const target = semver.parse(targetVersion);
438
+ if (!original || !target)
439
+ return false;
440
+ switch (this.maximumUpgradeDelta) {
441
+ case 'patch':
442
+ return original.major === target.major && original.minor === target.minor;
443
+ case 'minor':
444
+ return original.major === target.major;
445
+ case 'major':
446
+ return true;
447
+ case 'none':
448
+ return false;
449
+ default:
450
+ return false;
451
+ }
452
+ }
453
+ catch (_a) {
454
+ return false;
455
+ }
456
+ }
457
+ computeFixes(vulnerabilities, db) {
458
+ var _a, _b, _c, _d, _e, _f, _g;
327
459
  if (this.isReportOnly()) {
328
460
  return [];
329
461
  }
330
- const byPackage = new Map();
462
+ const byPackageAndMajor = new Map();
331
463
  for (const vuln of vulnerabilities) {
332
- const existing = byPackage.get(vuln.resolved.name) || [];
464
+ const parsed = semver.parse(vuln.resolved.version);
465
+ const major = (_a = parsed === null || parsed === void 0 ? void 0 : parsed.major) !== null && _a !== void 0 ? _a : 0;
466
+ const key = `${vuln.resolved.name}@${major}`;
467
+ const existing = byPackageAndMajor.get(key) || [];
333
468
  existing.push(vuln);
334
- byPackage.set(vuln.resolved.name, existing);
469
+ byPackageAndMajor.set(key, existing);
470
+ }
471
+ const majorVersionsByPackage = new Map();
472
+ for (const vuln of vulnerabilities) {
473
+ const parsed = semver.parse(vuln.resolved.version);
474
+ const major = (_b = parsed === null || parsed === void 0 ? void 0 : parsed.major) !== null && _b !== void 0 ? _b : 0;
475
+ const existing = majorVersionsByPackage.get(vuln.resolved.name) || new Set();
476
+ existing.add(major);
477
+ majorVersionsByPackage.set(vuln.resolved.name, existing);
335
478
  }
336
479
  const fixes = [];
337
- for (const [packageName, vulns] of byPackage) {
480
+ for (const [key, vulns] of byPackageAndMajor) {
481
+ const packageName = vulns[0].resolved.name;
482
+ const originalMajor = (_d = (_c = semver.parse(vulns[0].resolved.version)) === null || _c === void 0 ? void 0 : _c.major) !== null && _d !== void 0 ? _d : 0;
483
+ const hasMultipleMajorVersions = ((_f = (_e = majorVersionsByPackage.get(packageName)) === null || _e === void 0 ? void 0 : _e.size) !== null && _f !== void 0 ? _f : 0) > 1;
338
484
  let highestFixVersion;
339
485
  const cves = [];
340
486
  let isTransitive = true;
341
487
  let scope;
488
+ let originalVersion;
342
489
  for (const vuln of vulns) {
490
+ if (!originalVersion) {
491
+ originalVersion = vuln.resolved.version;
492
+ }
343
493
  if (!this.isUpgradeableWithinDelta(vuln.resolved.version, vuln.vulnerability)) {
344
494
  continue;
345
495
  }
346
496
  const fixVersion = this.getUpgradeVersion(vuln.vulnerability);
347
497
  if (fixVersion) {
348
- if (!highestFixVersion || semver.gt(fixVersion, highestFixVersion)) {
349
- highestFixVersion = fixVersion;
498
+ const fixMajor = (_g = semver.parse(fixVersion)) === null || _g === void 0 ? void 0 : _g.major;
499
+ const shouldConsiderFix = hasMultipleMajorVersions
500
+ ? fixMajor === originalMajor
501
+ : true;
502
+ if (shouldConsiderFix) {
503
+ if (!highestFixVersion || semver.gt(fixVersion, highestFixVersion)) {
504
+ highestFixVersion = fixVersion;
505
+ }
350
506
  }
351
507
  }
352
508
  cves.push(vuln.vulnerability.cve);
@@ -355,13 +511,15 @@ class DependencyVulnerabilityCheck extends rewrite_1.ScanningRecipe {
355
511
  scope = vuln.scope;
356
512
  }
357
513
  }
358
- if (highestFixVersion && cves.length > 0) {
514
+ if (highestFixVersion && cves.length > 0 && originalVersion) {
515
+ const safeVersion = this.findHighestSafeVersion(packageName, originalVersion, highestFixVersion, db);
359
516
  fixes.push({
360
517
  packageName,
361
- newVersion: highestFixVersion,
518
+ newVersion: safeVersion || highestFixVersion,
362
519
  isTransitive,
363
520
  cves,
364
- scope
521
+ scope,
522
+ originalMajorVersion: originalMajor
365
523
  });
366
524
  }
367
525
  }
@@ -370,22 +528,47 @@ class DependencyVulnerabilityCheck extends rewrite_1.ScanningRecipe {
370
528
  scanner(acc) {
371
529
  return __awaiter(this, void 0, void 0, function* () {
372
530
  const recipe = this;
373
- return new class extends json_1.JsonVisitor {
374
- visitDocument(doc, _ctx) {
531
+ const LOCK_FILE_NAMES = ['pnpm-lock.yaml', 'yarn.lock', 'package-lock.json', 'bun.lock'];
532
+ return new class extends rewrite_1.TreeVisitor {
533
+ accept(tree, ctx) {
375
534
  return __awaiter(this, void 0, void 0, function* () {
376
- var _a;
535
+ if ((0, json_1.isJson)(tree) && tree.kind === json_1.Json.Kind.Document) {
536
+ return this.handleJsonDocument(tree, ctx);
537
+ }
538
+ if ((0, yaml_1.isYaml)(tree) && (0, yaml_1.isDocuments)(tree)) {
539
+ return this.handleYamlDocument(tree, ctx);
540
+ }
541
+ if ((0, text_1.isPlainText)(tree)) {
542
+ return this.handlePlainTextDocument(tree, ctx);
543
+ }
544
+ return tree;
545
+ });
546
+ }
547
+ handleJsonDocument(doc, _ctx) {
548
+ return __awaiter(this, void 0, void 0, function* () {
549
+ var _a, _b;
550
+ const basename = path.basename(doc.sourcePath);
551
+ if (LOCK_FILE_NAMES.includes(basename)) {
552
+ acc.originalLockFiles.set(doc.sourcePath, yield rewrite_1.TreePrinters.print(doc));
553
+ return doc;
554
+ }
377
555
  if (!doc.sourcePath.endsWith('package.json')) {
378
556
  return doc;
379
557
  }
558
+ const packageJsonContent = yield rewrite_1.TreePrinters.print(doc);
559
+ acc.allPackageJsonContents.set(doc.sourcePath, packageJsonContent);
380
560
  const marker = (0, javascript_1.findNodeResolutionResult)(doc);
381
561
  if (!marker) {
382
562
  return doc;
383
563
  }
564
+ if (marker.workspacePackagePaths && marker.workspacePackagePaths.length > 0) {
565
+ acc.workspaceRoots.set(doc.sourcePath, [...marker.workspacePackagePaths]);
566
+ }
384
567
  const vulnerabilities = [];
385
568
  const visited = new Set();
386
569
  const scopesToCheck = recipe.scope
387
570
  ? [recipe.scope]
388
- : ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
571
+ : ALL_DEPENDENCY_SCOPES;
389
572
  for (const scope of scopesToCheck) {
390
573
  const deps = marker[scope] || [];
391
574
  for (const dep of deps) {
@@ -394,37 +577,81 @@ class DependencyVulnerabilityCheck extends rewrite_1.ScanningRecipe {
394
577
  }
395
578
  }
396
579
  }
580
+ const pm = (_a = marker.packageManager) !== null && _a !== void 0 ? _a : "Npm";
581
+ const configFiles = {};
582
+ const projectNpmrc = (_b = marker.npmrcConfigs) === null || _b === void 0 ? void 0 : _b.find(c => c.scope === "Project");
583
+ if (projectNpmrc) {
584
+ const lines = Object.entries(projectNpmrc.properties)
585
+ .map(([key, value]) => `${key}=${value}`);
586
+ configFiles['.npmrc'] = lines.join('\n');
587
+ }
588
+ const storedContent = acc.allPackageJsonContents.get(doc.sourcePath);
589
+ let isWorkspaceRoot = false;
590
+ if (storedContent) {
591
+ try {
592
+ isWorkspaceRoot = JSON.parse(storedContent).workspaces !== undefined;
593
+ }
594
+ catch (_c) {
595
+ }
596
+ }
597
+ let fixes = [];
397
598
  if (vulnerabilities.length > 0) {
398
599
  acc.vulnerableByProject.set(doc.sourcePath, vulnerabilities);
399
- const fixes = recipe.computeFixes(vulnerabilities);
400
- if (fixes.length > 0) {
401
- acc.fixesByProject.set(doc.sourcePath, fixes);
402
- const projectDir = path.dirname(path.resolve(doc.sourcePath));
403
- const pm = (_a = marker.packageManager) !== null && _a !== void 0 ? _a : "Npm";
404
- acc.projectsToUpdate.set(doc.sourcePath, {
405
- projectDir,
406
- packageJsonPath: doc.sourcePath,
407
- originalPackageJson: yield this.printDocument(doc),
408
- packageManager: pm
409
- });
600
+ fixes = recipe.computeFixes(vulnerabilities, acc.db);
601
+ }
602
+ if (recipe.fixDeclaredVersions) {
603
+ const preventiveFixes = recipe.findPreventiveFixes(marker, scopesToCheck, acc.db);
604
+ const existingPackages = new Set(fixes.map(f => `${f.packageName}@${f.scope}`));
605
+ for (const fix of preventiveFixes) {
606
+ const key = `${fix.packageName}@${fix.scope}`;
607
+ if (!existingPackages.has(key)) {
608
+ fixes.push(fix);
609
+ existingPackages.add(key);
610
+ }
410
611
  }
411
612
  }
613
+ if (fixes.length > 0) {
614
+ acc.fixesByProject.set(doc.sourcePath, fixes);
615
+ acc.projectsToUpdate.set(doc.sourcePath, {
616
+ packageJsonPath: doc.sourcePath,
617
+ originalPackageJson: yield rewrite_1.TreePrinters.print(doc),
618
+ packageManager: pm,
619
+ configFiles: Object.keys(configFiles).length > 0 ? configFiles : undefined
620
+ });
621
+ }
622
+ else if (isWorkspaceRoot && !acc.projectsToUpdate.has(doc.sourcePath)) {
623
+ acc.projectsToUpdate.set(doc.sourcePath, {
624
+ packageJsonPath: doc.sourcePath,
625
+ originalPackageJson: yield rewrite_1.TreePrinters.print(doc),
626
+ packageManager: pm,
627
+ configFiles: Object.keys(configFiles).length > 0 ? configFiles : undefined
628
+ });
629
+ }
412
630
  return doc;
413
631
  });
414
632
  }
415
- printDocument(doc) {
633
+ handleYamlDocument(docs, _ctx) {
416
634
  return __awaiter(this, void 0, void 0, function* () {
417
- return rewrite_2.TreePrinters.print(doc);
635
+ const basename = path.basename(docs.sourcePath);
636
+ if (LOCK_FILE_NAMES.includes(basename)) {
637
+ acc.originalLockFiles.set(docs.sourcePath, yield rewrite_1.TreePrinters.print(docs));
638
+ }
639
+ return docs;
640
+ });
641
+ }
642
+ handlePlainTextDocument(text, _ctx) {
643
+ return __awaiter(this, void 0, void 0, function* () {
644
+ const basename = path.basename(text.sourcePath);
645
+ if (LOCK_FILE_NAMES.includes(basename)) {
646
+ acc.originalLockFiles.set(text.sourcePath, yield rewrite_1.TreePrinters.print(text));
647
+ }
648
+ return text;
418
649
  });
419
650
  }
420
651
  };
421
652
  });
422
653
  }
423
- getRecipeList() {
424
- return __awaiter(this, void 0, void 0, function* () {
425
- return [];
426
- });
427
- }
654
+ ;
428
655
  editorWithData(acc) {
429
656
  return __awaiter(this, void 0, void 0, function* () {
430
657
  const recipe = this;
@@ -498,14 +725,65 @@ class DependencyVulnerabilityCheck extends rewrite_1.ScanningRecipe {
498
725
  handlePackageJson(doc, ctx) {
499
726
  return __awaiter(this, void 0, void 0, function* () {
500
727
  const vulnerabilities = acc.vulnerableByProject.get(doc.sourcePath);
501
- if (!vulnerabilities || vulnerabilities.length === 0) {
728
+ if (vulnerabilities && vulnerabilities.length > 0) {
729
+ for (const vuln of vulnerabilities) {
730
+ const upgradeable = recipe.isUpgradeableWithinDelta(vuln.resolved.version, vuln.vulnerability);
731
+ recipe.vulnerabilityReport.insertRow(ctx, new VulnerabilityReportRow(doc.sourcePath, vuln.vulnerability.cve, vuln.resolved.name, vuln.resolved.version, vuln.vulnerability.fixedVersion || '', vuln.vulnerability.lastAffectedVersion || '', upgradeable, vuln.vulnerability.summary, vuln.vulnerability.severity, vuln.depth, vuln.vulnerability.cwes, vuln.isDirect, recipe.renderPath(vuln.scope, vuln.path)));
732
+ }
733
+ }
734
+ if (recipe.isReportOnly()) {
502
735
  return doc;
503
736
  }
504
- for (const vuln of vulnerabilities) {
505
- const upgradeable = recipe.isUpgradeableWithinDelta(vuln.resolved.version, vuln.vulnerability);
506
- recipe.vulnerabilityReport.insertRow(ctx, new VulnerabilityReportRow(doc.sourcePath, vuln.vulnerability.cve, vuln.resolved.name, vuln.resolved.version, vuln.vulnerability.fixedVersion || '', vuln.vulnerability.lastAffectedVersion || '', upgradeable, vuln.vulnerability.summary, vuln.vulnerability.severity, vuln.depth, vuln.vulnerability.cwes, vuln.isDirect, recipe.renderPath(vuln.scope, vuln.path)));
737
+ if (!acc.workspaceDetectionComplete) {
738
+ this.detectWorkspacesFromContents();
739
+ acc.workspaceDetectionComplete = true;
507
740
  }
508
- if (recipe.isReportOnly()) {
741
+ const preModifiedContent = acc.modifiedWorkspaceMemberContents.get(doc.sourcePath)
742
+ || acc.updatedPackageJsons.get(doc.sourcePath);
743
+ if (preModifiedContent) {
744
+ return this.applyModifiedContent(doc, preModifiedContent);
745
+ }
746
+ const isWorkspaceRoot = acc.workspaceRoots.has(doc.sourcePath);
747
+ const workspaceRootPath = this.findWorkspaceRootFor(doc.sourcePath);
748
+ const isWorkspaceMember = workspaceRootPath !== undefined;
749
+ if (isWorkspaceMember && !isWorkspaceRoot) {
750
+ const needsProcessing = this.workspaceNeedsProcessing(workspaceRootPath);
751
+ if (needsProcessing && !acc.processedProjects.has(workspaceRootPath)) {
752
+ const rootUpdateInfo = acc.projectsToUpdate.get(workspaceRootPath);
753
+ if (rootUpdateInfo) {
754
+ const failureMessage = yield (0, javascript_1.runInstallIfNeeded)(workspaceRootPath, acc, () => recipe.runWorkspacePackageManagerInstall(acc, workspaceRootPath, rootUpdateInfo));
755
+ if (failureMessage) {
756
+ return (0, rewrite_1.markupWarn)(doc, `Failed to fix vulnerabilities in workspace`, failureMessage);
757
+ }
758
+ }
759
+ }
760
+ const modifiedContent = acc.modifiedWorkspaceMemberContents.get(doc.sourcePath);
761
+ if (modifiedContent) {
762
+ return this.applyModifiedContent(doc, modifiedContent);
763
+ }
764
+ return doc;
765
+ }
766
+ if (isWorkspaceRoot) {
767
+ const updateInfo = acc.projectsToUpdate.get(doc.sourcePath);
768
+ if (!updateInfo) {
769
+ return doc;
770
+ }
771
+ const needsProcessing = this.workspaceNeedsProcessing(doc.sourcePath);
772
+ if (!needsProcessing) {
773
+ return doc;
774
+ }
775
+ const failureMessage = yield (0, javascript_1.runInstallIfNeeded)(doc.sourcePath, acc, () => recipe.runWorkspacePackageManagerInstall(acc, doc.sourcePath, updateInfo));
776
+ if (failureMessage) {
777
+ return (0, rewrite_1.markupWarn)(doc, `Failed to fix vulnerabilities in ${doc.sourcePath}`, failureMessage);
778
+ }
779
+ const rootFixes = acc.fixesByProject.get(doc.sourcePath) || [];
780
+ if (rootFixes.length > 0) {
781
+ const modifiedContent = acc.updatedPackageJsons.get(doc.sourcePath);
782
+ if (modifiedContent) {
783
+ const result = yield this.applyModifiedContent(doc, modifiedContent);
784
+ return (0, javascript_1.updateNodeResolutionMarker)(result, updateInfo, acc);
785
+ }
786
+ }
509
787
  return doc;
510
788
  }
511
789
  const fixes = acc.fixesByProject.get(doc.sourcePath);
@@ -517,28 +795,159 @@ class DependencyVulnerabilityCheck extends rewrite_1.ScanningRecipe {
517
795
  if (failureMessage) {
518
796
  return (0, rewrite_1.markupWarn)(doc, `Failed to fix vulnerabilities in ${doc.sourcePath}`, failureMessage);
519
797
  }
520
- let result = doc;
521
- for (const fix of fixes) {
522
- if (!fix.isTransitive && fix.scope) {
523
- const visitor = new UpdateVersionVisitor(fix.packageName, fix.newVersion, fix.scope);
524
- result = (yield visitor.visit(result, undefined));
525
- }
798
+ const modifiedPackageJson = acc.updatedPackageJsons.get(doc.sourcePath);
799
+ if (!modifiedPackageJson) {
800
+ return doc;
526
801
  }
802
+ const result = yield this.applyModifiedContent(doc, modifiedPackageJson);
527
803
  return (0, javascript_1.updateNodeResolutionMarker)(result, updateInfo, acc);
528
804
  });
529
805
  }
806
+ findWorkspaceRootFor(packageJsonPath) {
807
+ for (const [rootPath, memberPaths] of acc.workspaceRoots) {
808
+ if (memberPaths.includes(packageJsonPath)) {
809
+ return rootPath;
810
+ }
811
+ }
812
+ return undefined;
813
+ }
814
+ workspaceNeedsProcessing(rootPath) {
815
+ if (acc.fixesByProject.has(rootPath)) {
816
+ return true;
817
+ }
818
+ const memberPaths = acc.workspaceRoots.get(rootPath) || [];
819
+ for (const memberPath of memberPaths) {
820
+ if (acc.fixesByProject.has(memberPath)) {
821
+ return true;
822
+ }
823
+ }
824
+ return false;
825
+ }
826
+ detectWorkspacesFromContents() {
827
+ for (const [pkgPath, content] of acc.allPackageJsonContents) {
828
+ if (acc.workspaceRoots.has(pkgPath)) {
829
+ continue;
830
+ }
831
+ try {
832
+ const pkgJson = JSON.parse(content);
833
+ if (!pkgJson.workspaces) {
834
+ continue;
835
+ }
836
+ const patterns = Array.isArray(pkgJson.workspaces)
837
+ ? pkgJson.workspaces
838
+ : pkgJson.workspaces.packages || [];
839
+ if (patterns.length === 0) {
840
+ continue;
841
+ }
842
+ const rootDir = path.dirname(pkgPath);
843
+ const memberPaths = [];
844
+ for (const [otherPath] of acc.allPackageJsonContents) {
845
+ if (otherPath === pkgPath)
846
+ continue;
847
+ const relativePath = rootDir === '.'
848
+ ? otherPath
849
+ : otherPath.startsWith(rootDir + '/')
850
+ ? otherPath.slice(rootDir.length + 1)
851
+ : null;
852
+ if (relativePath && this.matchesWorkspacePattern(relativePath, patterns)) {
853
+ memberPaths.push(otherPath);
854
+ }
855
+ }
856
+ if (memberPaths.length > 0) {
857
+ acc.workspaceRoots.set(pkgPath, memberPaths);
858
+ }
859
+ }
860
+ catch (_a) {
861
+ }
862
+ }
863
+ }
864
+ matchesWorkspacePattern(relativePath, patterns) {
865
+ for (const pattern of patterns) {
866
+ if (pattern.endsWith('/*')) {
867
+ const baseDir = pattern.slice(0, -2);
868
+ const pathParts = relativePath.split('/');
869
+ if (pathParts.length === 3 &&
870
+ pathParts[0] === baseDir &&
871
+ pathParts[2] === 'package.json') {
872
+ return true;
873
+ }
874
+ }
875
+ else if (!pattern.includes('*')) {
876
+ if (relativePath === `${pattern}/package.json`) {
877
+ return true;
878
+ }
879
+ }
880
+ }
881
+ return false;
882
+ }
883
+ applyModifiedContent(doc, content) {
884
+ return __awaiter(this, void 0, void 0, function* () {
885
+ const parsedModified = yield new json_1.JsonParser({}).parseOne({
886
+ text: content,
887
+ sourcePath: doc.sourcePath
888
+ });
889
+ return Object.assign(Object.assign({}, doc), { value: parsedModified.value, eof: parsedModified.eof });
890
+ });
891
+ }
530
892
  };
531
893
  });
532
894
  }
533
895
  runPackageManagerInstall(acc, updateInfo, fixes) {
534
896
  return __awaiter(this, void 0, void 0, function* () {
535
- const modifiedPackageJson = this.createModifiedPackageJson(updateInfo.originalPackageJson, fixes);
536
- const result = yield (0, javascript_1.runInstallInTempDir)(updateInfo.projectDir, updateInfo.packageManager, modifiedPackageJson);
897
+ const modifiedPackageJson = this.createModifiedPackageJson(updateInfo.originalPackageJson, fixes, updateInfo.packageManager);
898
+ const result = yield (0, javascript_1.runInstallInTempDir)(updateInfo.packageManager, modifiedPackageJson, {
899
+ configFiles: updateInfo.configFiles
900
+ });
537
901
  (0, javascript_1.storeInstallResult)(result, acc, updateInfo, modifiedPackageJson);
538
902
  });
539
903
  }
540
- createModifiedPackageJson(originalContent, fixes) {
541
- const packageJson = JSON.parse(originalContent);
904
+ runWorkspacePackageManagerInstall(acc, rootPath, rootUpdateInfo) {
905
+ return __awaiter(this, void 0, void 0, function* () {
906
+ const memberPaths = acc.workspaceRoots.get(rootPath) || [];
907
+ const pm = rootUpdateInfo.packageManager;
908
+ const allTransitiveFixes = [];
909
+ const rootFixes = acc.fixesByProject.get(rootPath) || [];
910
+ const rootDirectFixes = rootFixes.filter(f => !f.isTransitive);
911
+ const rootTransitiveFixes = rootFixes.filter(f => f.isTransitive);
912
+ allTransitiveFixes.push(...rootTransitiveFixes);
913
+ const memberDirectFixes = new Map();
914
+ for (const memberPath of memberPaths) {
915
+ const memberFixes = acc.fixesByProject.get(memberPath) || [];
916
+ const directFixes = memberFixes.filter(f => !f.isTransitive);
917
+ const transitiveFixes = memberFixes.filter(f => f.isTransitive);
918
+ if (directFixes.length > 0) {
919
+ memberDirectFixes.set(memberPath, directFixes);
920
+ }
921
+ allTransitiveFixes.push(...transitiveFixes);
922
+ }
923
+ const rootOriginalContent = acc.allPackageJsonContents.get(rootPath) || rootUpdateInfo.originalPackageJson;
924
+ const modifiedRootPackageJson = this.createModifiedPackageJson(rootOriginalContent, [...rootDirectFixes, ...allTransitiveFixes], pm);
925
+ const workspacePackages = {};
926
+ for (const memberPath of memberPaths) {
927
+ const originalContent = acc.allPackageJsonContents.get(memberPath);
928
+ if (!originalContent) {
929
+ continue;
930
+ }
931
+ const directFixes = memberDirectFixes.get(memberPath);
932
+ let modifiedContent;
933
+ if (directFixes && directFixes.length > 0) {
934
+ modifiedContent = this.createModifiedPackageJsonDirectOnly(originalContent, directFixes);
935
+ acc.modifiedWorkspaceMemberContents.set(memberPath, modifiedContent);
936
+ }
937
+ else {
938
+ modifiedContent = originalContent;
939
+ }
940
+ workspacePackages[memberPath] = modifiedContent;
941
+ }
942
+ const result = yield (0, javascript_1.runWorkspaceInstallInTempDir)(pm, modifiedRootPackageJson, {
943
+ configFiles: rootUpdateInfo.configFiles,
944
+ workspacePackages
945
+ });
946
+ (0, javascript_1.storeInstallResult)(result, acc, rootUpdateInfo, modifiedRootPackageJson);
947
+ });
948
+ }
949
+ createModifiedPackageJsonDirectOnly(originalContent, fixes) {
950
+ let packageJson = JSON.parse(originalContent);
542
951
  for (const fix of fixes) {
543
952
  if (!fix.isTransitive && fix.scope) {
544
953
  if (packageJson[fix.scope] && packageJson[fix.scope][fix.packageName]) {
@@ -549,6 +958,75 @@ class DependencyVulnerabilityCheck extends rewrite_1.ScanningRecipe {
549
958
  }
550
959
  return JSON.stringify(packageJson, null, 2);
551
960
  }
961
+ createModifiedPackageJson(originalContent, fixes, packageManager) {
962
+ var _a, _b, _c;
963
+ let packageJson = JSON.parse(originalContent);
964
+ const fixesByPackage = new Map();
965
+ for (const fix of fixes) {
966
+ const existing = fixesByPackage.get(fix.packageName) || [];
967
+ existing.push(fix);
968
+ fixesByPackage.set(fix.packageName, existing);
969
+ }
970
+ for (const fix of fixes) {
971
+ if (!fix.isTransitive && fix.scope) {
972
+ if (packageJson[fix.scope] && packageJson[fix.scope][fix.packageName]) {
973
+ const originalVersion = packageJson[fix.scope][fix.packageName];
974
+ packageJson[fix.scope][fix.packageName] = applyVersionPrefix(originalVersion, fix.newVersion);
975
+ }
976
+ }
977
+ else if (fix.isTransitive) {
978
+ const directDepScope = findDirectDependencyScope(packageJson, fix.packageName);
979
+ if (directDepScope) {
980
+ const directVersion = packageJson[directDepScope][fix.packageName];
981
+ const directMajor = semver.major(semver.coerce(directVersion) || '0.0.0');
982
+ if (fix.originalMajorVersion !== undefined && directMajor !== fix.originalMajorVersion) {
983
+ const isYarn = packageManager === "YarnClassic" ||
984
+ packageManager === "YarnBerry";
985
+ if (!isYarn) {
986
+ const versionSpecificKey = `${fix.packageName}@^${fix.originalMajorVersion}`;
987
+ packageJson = (0, javascript_1.applyOverrideToPackageJson)(packageJson, packageManager, versionSpecificKey, fix.newVersion);
988
+ }
989
+ else {
990
+ }
991
+ continue;
992
+ }
993
+ packageJson[directDepScope][fix.packageName] = applyVersionPrefix(directVersion, fix.newVersion);
994
+ continue;
995
+ }
996
+ const packageFixes = fixesByPackage.get(fix.packageName) || [];
997
+ const hasMultipleMajorVersions = packageFixes.length > 1 &&
998
+ new Set(packageFixes.map(f => f.originalMajorVersion)).size > 1;
999
+ const existingOverrides = getOverridesFromPackageJson(packageJson, packageManager);
1000
+ const hasExistingVersionSpecificOverrides = existingOverrides &&
1001
+ Object.keys(existingOverrides).some(key => key.startsWith(`${fix.packageName}@`));
1002
+ const isYarn = packageManager === "YarnClassic" ||
1003
+ packageManager === "YarnBerry";
1004
+ const useVersionSpecificOverride = !isYarn &&
1005
+ (hasMultipleMajorVersions || hasExistingVersionSpecificOverrides) &&
1006
+ fix.originalMajorVersion !== undefined;
1007
+ if (useVersionSpecificOverride) {
1008
+ const versionSpecificKey = `${fix.packageName}@^${fix.originalMajorVersion}`;
1009
+ packageJson = (0, javascript_1.applyOverrideToPackageJson)(packageJson, packageManager, versionSpecificKey, fix.newVersion);
1010
+ }
1011
+ else {
1012
+ packageJson = (0, javascript_1.applyOverrideToPackageJson)(packageJson, packageManager, fix.packageName, fix.newVersion);
1013
+ }
1014
+ if (packageManager === "Pnpm") {
1015
+ if (!packageJson.devDependencies) {
1016
+ packageJson.devDependencies = {};
1017
+ }
1018
+ if (!((_a = packageJson.dependencies) === null || _a === void 0 ? void 0 : _a[fix.packageName]) &&
1019
+ !packageJson.devDependencies[fix.packageName] &&
1020
+ !((_b = packageJson.peerDependencies) === null || _b === void 0 ? void 0 : _b[fix.packageName]) &&
1021
+ !((_c = packageJson.optionalDependencies) === null || _c === void 0 ? void 0 : _c[fix.packageName])) {
1022
+ const prefix = this.getVersionPrefixForDelta();
1023
+ packageJson.devDependencies[fix.packageName] = prefix + fix.newVersion;
1024
+ }
1025
+ }
1026
+ }
1027
+ }
1028
+ return JSON.stringify(packageJson, null, 2);
1029
+ }
552
1030
  }
553
1031
  exports.DependencyVulnerabilityCheck = DependencyVulnerabilityCheck;
554
1032
  __decorate([
@@ -606,6 +1084,18 @@ __decorate([
606
1084
  example: "CVE-2023-.*"
607
1085
  })
608
1086
  ], DependencyVulnerabilityCheck.prototype, "cvePattern", void 0);
1087
+ __decorate([
1088
+ (0, rewrite_1.Option)({
1089
+ displayName: "Fix declared versions",
1090
+ description: "When enabled, also upgrades version specifiers declared in package.json that specify vulnerable versions, " +
1091
+ "even if the lock file already resolves to a safe version. This is a preventive measure to ensure that " +
1092
+ "future installs (e.g., on a different machine or after lock file changes) won't install vulnerable versions. " +
1093
+ "These preventive upgrades are NOT reported in the vulnerability data table since there's no actual vulnerability. " +
1094
+ "Default is false.",
1095
+ required: false,
1096
+ example: "true"
1097
+ })
1098
+ ], DependencyVulnerabilityCheck.prototype, "fixDeclaredVersions", void 0);
609
1099
  function extractVersionPrefix(versionString) {
610
1100
  const match = versionString.match(/^([~^]|>=?|<=?|=)?(.*)$/);
611
1101
  if (match) {
@@ -620,41 +1110,28 @@ function applyVersionPrefix(originalVersion, newVersion) {
620
1110
  const { prefix } = extractVersionPrefix(originalVersion);
621
1111
  return prefix + newVersion;
622
1112
  }
623
- class UpdateVersionVisitor extends json_1.JsonVisitor {
624
- constructor(packageName, newVersion, targetScope) {
625
- super();
626
- this.inTargetScope = false;
627
- this.packageName = packageName;
628
- this.newVersion = newVersion;
629
- this.targetScope = targetScope;
630
- }
631
- visitMember(member, p) {
632
- const _super = Object.create(null, {
633
- visitMember: { get: () => super.visitMember }
634
- });
635
- return __awaiter(this, void 0, void 0, function* () {
636
- const keyName = (0, json_1.getMemberKeyName)(member);
637
- if (keyName === this.targetScope) {
638
- this.inTargetScope = true;
639
- const result = yield _super.visitMember.call(this, member, p);
640
- this.inTargetScope = false;
641
- return result;
642
- }
643
- if (this.inTargetScope && keyName === this.packageName) {
644
- return this.updateVersion(member);
645
- }
646
- return _super.visitMember.call(this, member, p);
647
- });
1113
+ function findDirectDependencyScope(packageJson, packageName) {
1114
+ var _a;
1115
+ for (const scope of ALL_DEPENDENCY_SCOPES) {
1116
+ if ((_a = packageJson[scope]) === null || _a === void 0 ? void 0 : _a[packageName]) {
1117
+ return scope;
1118
+ }
648
1119
  }
649
- updateVersion(member) {
650
- const value = member.value;
651
- if (!(0, json_1.isLiteral)(value)) {
652
- return member;
653
- }
654
- const originalVersion = String(value.value);
655
- const newVersionWithPrefix = applyVersionPrefix(originalVersion, this.newVersion);
656
- const newLiteral = Object.assign(Object.assign({}, value), { source: `"${newVersionWithPrefix}"`, value: newVersionWithPrefix });
657
- return Object.assign(Object.assign({}, member), { value: newLiteral });
1120
+ return undefined;
1121
+ }
1122
+ function getOverridesFromPackageJson(packageJson, packageManager) {
1123
+ var _a;
1124
+ switch (packageManager) {
1125
+ case "Npm":
1126
+ case "Bun":
1127
+ return packageJson.overrides;
1128
+ case "Pnpm":
1129
+ return (_a = packageJson.pnpm) === null || _a === void 0 ? void 0 : _a.overrides;
1130
+ case "YarnClassic":
1131
+ case "YarnBerry":
1132
+ return packageJson.resolutions;
1133
+ default:
1134
+ return undefined;
658
1135
  }
659
1136
  }
660
1137
  //# sourceMappingURL=dependency-vulnerability-check.js.map