autoremediator 0.5.0 → 0.7.0

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.
@@ -1,8 +1,5 @@
1
1
  // src/remediation/pipeline.ts
2
2
  import { generateText as generateText2 } from "ai";
3
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
4
- import { join as join8 } from "path";
5
- import semver4 from "semver";
6
3
 
7
4
  // src/platform/config.ts
8
5
  function resolveProvider(options = {}) {
@@ -162,567 +159,1108 @@ function parseListOutput(pm, stdout) {
162
159
  return versions;
163
160
  }
164
161
 
165
- // src/remediation/tools/lookup-cve.ts
162
+ // src/remediation/tools/apply-version-bump.ts
166
163
  import { tool } from "ai";
167
164
  import { z } from "zod";
165
+ import { join as join4 } from "path";
166
+ import { readFileSync as readFileSync2, writeFileSync } from "fs";
167
+ import { execa } from "execa";
168
+ import semver from "semver";
168
169
 
169
- // src/intelligence/sources/osv.ts
170
- var OSV_BASE = "https://api.osv.dev/v1";
171
- async function fetchOsvVuln(cveId) {
172
- const url = `${OSV_BASE}/vulns/${encodeURIComponent(cveId)}`;
173
- const res = await fetch(url, {
174
- headers: { Accept: "application/json" }
175
- });
176
- if (res.status === 404) return null;
177
- if (!res.ok) {
178
- throw new Error(`OSV API error ${res.status} for ${cveId}: ${await res.text()}`);
179
- }
180
- return res.json();
181
- }
182
- function osvEventsToSemverRange(events) {
183
- const parts = [];
184
- for (const event of events) {
185
- if (event.introduced !== void 0) {
186
- const v = event.introduced === "0" ? "0.0.0" : event.introduced;
187
- parts.push(`>=${v}`);
188
- }
189
- if (event.fixed !== void 0) {
190
- parts.push(`<${event.fixed}`);
191
- }
192
- if (event.last_affected !== void 0) {
193
- parts.push(`<=${event.last_affected}`);
194
- }
170
+ // src/platform/policy.ts
171
+ import { existsSync as existsSync2, readFileSync } from "fs";
172
+ import { join as join2 } from "path";
173
+ var DEFAULT_POLICY = {
174
+ allowMajorBumps: false,
175
+ denyPackages: [],
176
+ allowPackages: [],
177
+ constraints: {
178
+ directDependenciesOnly: false,
179
+ preferVersionBump: false
195
180
  }
196
- return parts.join(" ") || ">=0.0.0";
197
- }
198
- function parseOsvVuln(vuln) {
199
- const npmAffected = [];
200
- for (const affected of vuln.affected ?? []) {
201
- const ecosystem = affected.package?.ecosystem;
202
- const packageName = affected.package?.name;
203
- if (!ecosystem || typeof ecosystem !== "string") continue;
204
- if (!packageName || typeof packageName !== "string") continue;
205
- if (ecosystem.toLowerCase() !== "npm") continue;
206
- const semverRange = affected.ranges?.find((r) => r.type === "SEMVER");
207
- const vulnerableRange = semverRange ? osvEventsToSemverRange(semverRange.events) : ">=0.0.0";
208
- const fixedEvent = semverRange?.events.find((e) => e.fixed !== void 0);
209
- npmAffected.push({
210
- name: packageName,
211
- ecosystem: "npm",
212
- vulnerableRange,
213
- firstPatchedVersion: fixedEvent?.fixed,
214
- source: "osv"
215
- });
181
+ };
182
+ function loadPolicy(cwd, explicitPath) {
183
+ const candidate = explicitPath ?? join2(cwd, ".autoremediator.json");
184
+ if (!existsSync2(candidate)) return DEFAULT_POLICY;
185
+ try {
186
+ const parsed = JSON.parse(readFileSync(candidate, "utf8"));
187
+ return {
188
+ allowMajorBumps: parsed.allowMajorBumps ?? DEFAULT_POLICY.allowMajorBumps,
189
+ denyPackages: parsed.denyPackages ?? DEFAULT_POLICY.denyPackages,
190
+ allowPackages: parsed.allowPackages ?? DEFAULT_POLICY.allowPackages,
191
+ constraints: {
192
+ directDependenciesOnly: parsed.constraints?.directDependenciesOnly ?? DEFAULT_POLICY.constraints?.directDependenciesOnly ?? false,
193
+ preferVersionBump: parsed.constraints?.preferVersionBump ?? DEFAULT_POLICY.constraints?.preferVersionBump ?? false
194
+ }
195
+ };
196
+ } catch {
197
+ return DEFAULT_POLICY;
216
198
  }
217
- const severity = deriveSeverity(vuln.severity);
218
- return {
219
- id: vuln.id,
220
- summary: vuln.summary ?? vuln.details ?? "No summary available.",
221
- severity,
222
- references: vuln.references?.map((r) => r.url) ?? [],
223
- affectedPackages: npmAffected
224
- };
225
199
  }
226
- function deriveSeverity(severityEntries) {
227
- if (!severityEntries?.length) return "UNKNOWN";
228
- const cvssEntry = severityEntries.find((s) => s.type === "CVSS_V3") ?? severityEntries[0];
229
- const scoreMatch = cvssEntry.score.match(/(\d+\.\d+)$/);
230
- if (scoreMatch) {
231
- const score = parseFloat(scoreMatch[1]);
232
- if (score >= 9) return "CRITICAL";
233
- if (score >= 7) return "HIGH";
234
- if (score >= 4) return "MEDIUM";
235
- return "LOW";
200
+ function isPackageAllowed(policy, packageName) {
201
+ if (policy.denyPackages.includes(packageName)) return false;
202
+ if (policy.allowPackages.length > 0 && !policy.allowPackages.includes(packageName)) {
203
+ return false;
236
204
  }
237
- return "UNKNOWN";
238
- }
239
- async function lookupCveOsv(cveId) {
240
- const vuln = await fetchOsvVuln(cveId);
241
- if (!vuln) return null;
242
- return parseOsvVuln(vuln);
205
+ return true;
243
206
  }
244
207
 
245
- // src/intelligence/sources/github-advisory.ts
246
- var GH_ADVISORY_BASE = "https://api.github.com/advisories";
247
- function buildHeaders() {
248
- const headers = {
249
- Accept: "application/vnd.github+json",
250
- "X-GitHub-Api-Version": "2022-11-28"
251
- };
252
- const token = getGitHubToken();
253
- if (token) {
254
- headers.Authorization = `Bearer ${token}`;
255
- }
256
- return headers;
257
- }
258
- async function fetchGhAdvisories(cveId) {
259
- const url = new URL(GH_ADVISORY_BASE);
260
- url.searchParams.set("cve_id", cveId);
261
- url.searchParams.set("ecosystem", "npm");
262
- url.searchParams.set("type", "reviewed");
263
- url.searchParams.set("per_page", "10");
264
- const res = await fetch(url.toString(), { headers: buildHeaders() });
265
- if (res.status === 404) return [];
266
- if (!res.ok) {
267
- console.warn(
268
- `[autoremediator] GitHub Advisory API returned ${res.status} for ${cveId} \u2014 skipping.`
269
- );
270
- return [];
271
- }
272
- return res.json();
273
- }
274
- function parseGhAdvisories(advisories) {
275
- const packages = [];
276
- for (const advisory of advisories) {
277
- for (const vuln of advisory.vulnerabilities) {
278
- if (vuln.package.ecosystem.toLowerCase() !== "npm") continue;
279
- packages.push({
280
- name: vuln.package.name,
281
- ecosystem: "npm",
282
- vulnerableRange: vuln.vulnerable_version_range ?? ">=0.0.0",
283
- firstPatchedVersion: vuln.first_patched_version ?? void 0,
284
- source: "github-advisory"
285
- });
286
- }
287
- }
288
- return packages;
208
+ // src/platform/repo-lock.ts
209
+ import { mkdir, rm } from "fs/promises";
210
+ import { join as join3 } from "path";
211
+ async function sleep(ms) {
212
+ await new Promise((resolve) => setTimeout(resolve, ms));
289
213
  }
290
- function mergeGhDataIntoCveDetails(details, ghPackages) {
291
- const enriched = { ...details };
292
- for (const ghPkg of ghPackages) {
293
- const existing = enriched.affectedPackages.find(
294
- (p) => p.name === ghPkg.name
295
- );
296
- if (existing) {
297
- if (!existing.firstPatchedVersion && ghPkg.firstPatchedVersion) {
298
- existing.firstPatchedVersion = ghPkg.firstPatchedVersion;
214
+ async function acquireRepoLock(cwd, options = {}) {
215
+ const timeoutMs = options.timeoutMs ?? 15e3;
216
+ const retryDelayMs = options.retryDelayMs ?? 125;
217
+ const lockRoot = join3(cwd, ".autoremediator", "locks");
218
+ const lockPath = join3(cwd, ".autoremediator", "locks", "remediation.lock");
219
+ const startedAt = Date.now();
220
+ await mkdir(lockRoot, { recursive: true });
221
+ while (true) {
222
+ try {
223
+ await mkdir(lockPath, { recursive: false });
224
+ return {
225
+ lockPath,
226
+ release: async () => {
227
+ await rm(lockPath, { recursive: true, force: true });
228
+ }
229
+ };
230
+ } catch {
231
+ if (Date.now() - startedAt > timeoutMs) {
232
+ throw new Error(`Timed out waiting for repository lock at ${lockPath}.`);
299
233
  }
300
- } else {
301
- enriched.affectedPackages.push(ghPkg);
234
+ await sleep(retryDelayMs);
302
235
  }
303
236
  }
304
- return enriched;
305
- }
306
- async function lookupCveGitHub(cveId) {
307
- const advisories = await fetchGhAdvisories(cveId);
308
- return parseGhAdvisories(advisories);
309
- }
310
-
311
- // src/intelligence/sources/nvd.ts
312
- var NVD_BASE = "https://services.nvd.nist.gov/rest/json/cves/2.0";
313
- function buildNvdHeaders() {
314
- const { apiKey } = getNvdConfig();
315
- const headers = { Accept: "application/json" };
316
- if (apiKey) {
317
- headers.apiKey = apiKey;
318
- }
319
- return headers;
320
237
  }
321
- async function fetchNvdCvss(cveId) {
322
- const url = `${NVD_BASE}?cveId=${encodeURIComponent(cveId)}`;
238
+ async function withRepoLock(cwd, fn, options) {
239
+ const lock = await acquireRepoLock(cwd, options);
323
240
  try {
324
- const res = await fetch(url, { headers: buildNvdHeaders() });
325
- if (!res.ok) return void 0;
326
- const data = await res.json();
327
- const vuln = data.vulnerabilities?.[0];
328
- if (!vuln) return void 0;
329
- const metrics = vuln.cve.metrics;
330
- const metric = metrics?.cvssMetricV31?.[0] ?? metrics?.cvssMetricV30?.[0] ?? metrics?.cvssMetricV2?.[0];
331
- if (!metric) return void 0;
332
- const score = metric.cvssData.baseScore;
333
- const rawSeverity = metric.cvssData.baseSeverity.toUpperCase();
334
- const severityMap = {
335
- CRITICAL: "CRITICAL",
336
- HIGH: "HIGH",
337
- MEDIUM: "MEDIUM",
338
- LOW: "LOW"
339
- };
340
- return {
341
- score,
342
- severity: severityMap[rawSeverity] ?? "UNKNOWN"
343
- };
344
- } catch {
345
- return void 0;
346
- }
347
- }
348
- async function enrichWithNvd(details) {
349
- const cvss = await fetchNvdCvss(details.id);
350
- if (cvss) {
351
- details.cvssScore = cvss.score;
352
- if (details.severity === "UNKNOWN") {
353
- details.severity = cvss.severity;
354
- }
241
+ return await fn();
242
+ } finally {
243
+ await lock.release();
355
244
  }
356
- return details;
357
245
  }
358
246
 
359
- // src/intelligence/sources/cisa-kev.ts
360
- var CISA_KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json";
361
- async function fetchCisaKevFeed() {
362
- try {
363
- const res = await fetch(CISA_KEV_URL, {
364
- headers: { Accept: "application/json" }
365
- });
366
- if (!res.ok) return void 0;
367
- return await res.json();
368
- } catch {
369
- return void 0;
370
- }
371
- }
372
- function findKevEntry(feed, cveId) {
373
- if (!feed?.vulnerabilities?.length) return void 0;
374
- const normalized = cveId.toUpperCase();
375
- return feed.vulnerabilities.find((v) => v.cveID.toUpperCase() === normalized);
247
+ // src/remediation/tools/apply-version-bump.ts
248
+ var applyVersionBumpTool = tool({
249
+ description: "Update package.json to use the safe version of a vulnerable package and run the project's package manager install. In dry-run mode, only reports what would change.",
250
+ parameters: z.object({
251
+ cwd: z.string().describe("Absolute path to the consumer project root"),
252
+ packageManager: z.enum(["npm", "pnpm", "yarn"]).optional().describe("Package manager used by the target project (auto-detected if omitted)"),
253
+ packageName: z.string().describe("The npm package to upgrade"),
254
+ fromVersion: z.string().describe("The currently installed vulnerable version"),
255
+ toVersion: z.string().describe("The safe target version to upgrade to"),
256
+ dryRun: z.boolean().default(false).describe("If true, report changes but do not write"),
257
+ policy: z.string().optional().describe("Optional path to .autoremediator policy file"),
258
+ runTests: z.boolean().default(false).describe("If true, run test validation after applying the fix")
259
+ }),
260
+ execute: async ({
261
+ cwd,
262
+ packageManager,
263
+ packageName,
264
+ fromVersion,
265
+ toVersion,
266
+ dryRun,
267
+ policy,
268
+ runTests
269
+ }) => {
270
+ const pm = packageManager ?? detectPackageManager(cwd);
271
+ const commands = getPackageManagerCommands(pm);
272
+ const pkgPath = join4(cwd, "package.json");
273
+ const loadedPolicy = loadPolicy(cwd, policy);
274
+ if (!isPackageAllowed(loadedPolicy, packageName)) {
275
+ return {
276
+ packageName,
277
+ strategy: "none",
278
+ fromVersion,
279
+ toVersion,
280
+ applied: false,
281
+ dryRun,
282
+ unresolvedReason: "policy-blocked",
283
+ message: `Policy blocked changes for package "${packageName}".`
284
+ };
285
+ }
286
+ const isMajorBump = semver.valid(fromVersion) && semver.valid(toVersion) && semver.major(toVersion) > semver.major(fromVersion);
287
+ if (isMajorBump && !loadedPolicy.allowMajorBumps) {
288
+ return {
289
+ packageName,
290
+ strategy: "none",
291
+ fromVersion,
292
+ toVersion,
293
+ applied: false,
294
+ dryRun,
295
+ unresolvedReason: "major-bump-required",
296
+ message: `Policy blocked major bump for "${packageName}" (${fromVersion} -> ${toVersion}).`
297
+ };
298
+ }
299
+ let pkgJson;
300
+ try {
301
+ pkgJson = JSON.parse(readFileSync2(pkgPath, "utf8"));
302
+ } catch {
303
+ return {
304
+ packageName,
305
+ strategy: "none",
306
+ fromVersion,
307
+ applied: false,
308
+ dryRun,
309
+ unresolvedReason: "package-json-not-found",
310
+ message: `Could not read package.json at "${pkgPath}".`
311
+ };
312
+ }
313
+ const depField = ["dependencies", "devDependencies", "peerDependencies"].find(
314
+ (f) => pkgJson[f]?.[packageName] !== void 0
315
+ );
316
+ if (!depField) {
317
+ return {
318
+ packageName,
319
+ strategy: "none",
320
+ fromVersion,
321
+ applied: false,
322
+ dryRun,
323
+ unresolvedReason: "indirect-dependency",
324
+ message: `"${packageName}" was not found in package.json dependencies (it may be a transitive dep). Cannot auto-bump.`
325
+ };
326
+ }
327
+ const currentRange = pkgJson[depField][packageName];
328
+ const prefixMatch = currentRange.match(/^([~^]?)/);
329
+ const prefix = prefixMatch?.[1] ?? "";
330
+ const newRange = `${prefix}${toVersion}`;
331
+ if (dryRun) {
332
+ const installCmd = commands.installPreferOffline.join(" ");
333
+ const testCmd = commands.test.join(" ");
334
+ return {
335
+ packageName,
336
+ strategy: "version-bump",
337
+ fromVersion,
338
+ toVersion,
339
+ applied: false,
340
+ dryRun: true,
341
+ message: `[DRY RUN] Would update ${depField}.${packageName}: "${currentRange}" -> "${newRange}", then run ${installCmd}${runTests ? ` and ${testCmd}` : ""}.`
342
+ };
343
+ }
344
+ return withRepoLock(cwd, async () => {
345
+ pkgJson[depField][packageName] = newRange;
346
+ writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
347
+ try {
348
+ const [installCmd, ...installArgs] = commands.installPreferOffline;
349
+ await execa(installCmd, installArgs, {
350
+ cwd,
351
+ stdio: "pipe"
352
+ });
353
+ } catch (err) {
354
+ pkgJson[depField][packageName] = currentRange;
355
+ writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
356
+ const message = err instanceof Error ? err.message : String(err);
357
+ return {
358
+ packageName,
359
+ strategy: "version-bump",
360
+ fromVersion,
361
+ toVersion,
362
+ applied: false,
363
+ dryRun: false,
364
+ unresolvedReason: "install-failed",
365
+ message: `${commands.installPreferOffline.join(" ")} failed after updating "${packageName}" to ${toVersion}. Reverted. Error: ${message}`
366
+ };
367
+ }
368
+ if (runTests) {
369
+ try {
370
+ const [testCmd, ...testArgs] = commands.test;
371
+ await execa(testCmd, testArgs, {
372
+ cwd,
373
+ stdio: "pipe"
374
+ });
375
+ } catch (err) {
376
+ pkgJson[depField][packageName] = currentRange;
377
+ writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
378
+ try {
379
+ const [rollbackCmd, ...rollbackArgs] = commands.installPreferOffline;
380
+ await execa(rollbackCmd, rollbackArgs, {
381
+ cwd,
382
+ stdio: "pipe"
383
+ });
384
+ } catch {
385
+ }
386
+ const message = err instanceof Error ? err.message : String(err);
387
+ return {
388
+ packageName,
389
+ strategy: "version-bump",
390
+ fromVersion,
391
+ toVersion,
392
+ applied: false,
393
+ dryRun: false,
394
+ unresolvedReason: "validation-failed",
395
+ message: `${commands.test.join(" ")} failed after upgrading "${packageName}" to ${toVersion}. Rolled back to ${currentRange}. Error: ${message}`
396
+ };
397
+ }
398
+ }
399
+ return {
400
+ packageName,
401
+ strategy: "version-bump",
402
+ fromVersion,
403
+ toVersion,
404
+ applied: true,
405
+ dryRun: false,
406
+ message: `Successfully upgraded "${packageName}" from ${fromVersion} to ${toVersion}, ran ${commands.installPreferOffline.join(" ")}${runTests ? `, and passed ${commands.test.join(" ")}` : ""}.`
407
+ };
408
+ });
409
+ }
410
+ });
411
+
412
+ // src/remediation/tools/apply-package-override.ts
413
+ import { tool as tool2 } from "ai";
414
+ import { z as z2 } from "zod";
415
+ import { join as join5 } from "path";
416
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
417
+ import { execa as execa2 } from "execa";
418
+ import semver2 from "semver";
419
+ var applyPackageOverrideTool = tool2({
420
+ description: "Apply a package-manager-native package.json override for a vulnerable transitive dependency and reinstall. Uses npm overrides, pnpm.overrides, or yarn resolutions.",
421
+ parameters: z2.object({
422
+ cwd: z2.string().describe("Absolute path to the consumer project root"),
423
+ packageManager: z2.enum(["npm", "pnpm", "yarn"]).optional().describe("Package manager used by the target project (auto-detected if omitted)"),
424
+ packageName: z2.string().describe("The npm package to override"),
425
+ fromVersion: z2.string().describe("The currently installed vulnerable version"),
426
+ toVersion: z2.string().describe("The safe target version to override to"),
427
+ dryRun: z2.boolean().default(false).describe("If true, report changes but do not write"),
428
+ policy: z2.string().optional().describe("Optional path to .autoremediator policy file"),
429
+ runTests: z2.boolean().default(false).describe("If true, run test validation after applying the override")
430
+ }),
431
+ execute: async ({
432
+ cwd,
433
+ packageManager,
434
+ packageName,
435
+ fromVersion,
436
+ toVersion,
437
+ dryRun,
438
+ policy,
439
+ runTests
440
+ }) => {
441
+ const pm = packageManager ?? detectPackageManager(cwd);
442
+ const commands = getPackageManagerCommands(pm);
443
+ const pkgPath = join5(cwd, "package.json");
444
+ const loadedPolicy = loadPolicy(cwd, policy);
445
+ if (!isPackageAllowed(loadedPolicy, packageName)) {
446
+ return {
447
+ packageName,
448
+ strategy: "none",
449
+ fromVersion,
450
+ toVersion,
451
+ applied: false,
452
+ dryRun,
453
+ unresolvedReason: "policy-blocked",
454
+ message: `Policy blocked changes for package "${packageName}".`
455
+ };
456
+ }
457
+ const isMajorBump = semver2.valid(fromVersion) && semver2.valid(toVersion) && semver2.major(toVersion) > semver2.major(fromVersion);
458
+ if (isMajorBump && !loadedPolicy.allowMajorBumps) {
459
+ return {
460
+ packageName,
461
+ strategy: "none",
462
+ fromVersion,
463
+ toVersion,
464
+ applied: false,
465
+ dryRun,
466
+ unresolvedReason: "major-bump-required",
467
+ message: `Policy blocked major override for "${packageName}" (${fromVersion} -> ${toVersion}).`
468
+ };
469
+ }
470
+ let pkgJson;
471
+ try {
472
+ pkgJson = JSON.parse(readFileSync3(pkgPath, "utf8"));
473
+ } catch {
474
+ return {
475
+ packageName,
476
+ strategy: "none",
477
+ fromVersion,
478
+ toVersion,
479
+ applied: false,
480
+ dryRun,
481
+ unresolvedReason: "package-json-not-found",
482
+ message: `Could not read package.json at "${pkgPath}".`
483
+ };
484
+ }
485
+ const overrideLabel = describeOverrideField(pm);
486
+ const previousValue = getOverrideValue(pkgJson, pm, packageName);
487
+ if (dryRun) {
488
+ return {
489
+ packageName,
490
+ strategy: "override",
491
+ fromVersion,
492
+ toVersion,
493
+ applied: false,
494
+ dryRun: true,
495
+ message: `[DRY RUN] Would set ${overrideLabel}.${packageName} to "${toVersion}", then run ${commands.installPreferOffline.join(" ")}${runTests ? ` and ${commands.test.join(" ")}` : ""}.`
496
+ };
497
+ }
498
+ return withRepoLock(cwd, async () => {
499
+ setOverrideValue(pkgJson, pm, packageName, toVersion);
500
+ writeFileSync2(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
501
+ try {
502
+ const [installCmd, ...installArgs] = commands.installPreferOffline;
503
+ await execa2(installCmd, installArgs, { cwd, stdio: "pipe" });
504
+ } catch (err) {
505
+ restoreOverrideValue(pkgJson, pm, packageName, previousValue);
506
+ writeFileSync2(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
507
+ const message = err instanceof Error ? err.message : String(err);
508
+ return {
509
+ packageName,
510
+ strategy: "override",
511
+ fromVersion,
512
+ toVersion,
513
+ applied: false,
514
+ dryRun: false,
515
+ unresolvedReason: "override-apply-failed",
516
+ message: `${commands.installPreferOffline.join(" ")} failed after applying ${overrideLabel} for "${packageName}" to ${toVersion}. Reverted. Error: ${message}`
517
+ };
518
+ }
519
+ if (runTests) {
520
+ try {
521
+ const [testCmd, ...testArgs] = commands.test;
522
+ await execa2(testCmd, testArgs, { cwd, stdio: "pipe" });
523
+ } catch (err) {
524
+ restoreOverrideValue(pkgJson, pm, packageName, previousValue);
525
+ writeFileSync2(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
526
+ try {
527
+ const [rollbackCmd, ...rollbackArgs] = commands.installPreferOffline;
528
+ await execa2(rollbackCmd, rollbackArgs, { cwd, stdio: "pipe" });
529
+ } catch {
530
+ }
531
+ const message = err instanceof Error ? err.message : String(err);
532
+ return {
533
+ packageName,
534
+ strategy: "override",
535
+ fromVersion,
536
+ toVersion,
537
+ applied: false,
538
+ dryRun: false,
539
+ unresolvedReason: "validation-failed",
540
+ message: `${commands.test.join(" ")} failed after applying ${overrideLabel} for "${packageName}" to ${toVersion}. Reverted. Error: ${message}`
541
+ };
542
+ }
543
+ }
544
+ return {
545
+ packageName,
546
+ strategy: "override",
547
+ fromVersion,
548
+ toVersion,
549
+ applied: true,
550
+ dryRun: false,
551
+ message: `Successfully applied ${overrideLabel} for "${packageName}" from ${fromVersion} to ${toVersion}, then ran ${commands.installPreferOffline.join(" ")}${runTests ? ` and passed ${commands.test.join(" ")}` : ""}.`
552
+ };
553
+ });
554
+ }
555
+ });
556
+ function describeOverrideField(packageManager) {
557
+ if (packageManager === "npm") return "overrides";
558
+ if (packageManager === "pnpm") return "pnpm.overrides";
559
+ return "resolutions";
560
+ }
561
+ function getOverrideValue(pkgJson, packageManager, packageName) {
562
+ if (packageManager === "npm") return pkgJson.overrides?.[packageName];
563
+ if (packageManager === "pnpm") return pkgJson.pnpm?.overrides?.[packageName];
564
+ return pkgJson.resolutions?.[packageName];
565
+ }
566
+ function setOverrideValue(pkgJson, packageManager, packageName, version) {
567
+ if (packageManager === "npm") {
568
+ pkgJson.overrides = { ...pkgJson.overrides ?? {}, [packageName]: version };
569
+ return;
570
+ }
571
+ if (packageManager === "pnpm") {
572
+ pkgJson.pnpm = {
573
+ ...pkgJson.pnpm ?? {},
574
+ overrides: {
575
+ ...pkgJson.pnpm?.overrides ?? {},
576
+ [packageName]: version
577
+ }
578
+ };
579
+ return;
580
+ }
581
+ pkgJson.resolutions = { ...pkgJson.resolutions ?? {}, [packageName]: version };
582
+ }
583
+ function restoreOverrideValue(pkgJson, packageManager, packageName, previousValue) {
584
+ if (packageManager === "npm") {
585
+ pkgJson.overrides = restoreRecord(pkgJson.overrides, packageName, previousValue);
586
+ return;
587
+ }
588
+ if (packageManager === "pnpm") {
589
+ pkgJson.pnpm = {
590
+ ...pkgJson.pnpm ?? {},
591
+ overrides: restoreRecord(pkgJson.pnpm?.overrides, packageName, previousValue)
592
+ };
593
+ if (!pkgJson.pnpm.overrides) {
594
+ delete pkgJson.pnpm.overrides;
595
+ }
596
+ if (Object.keys(pkgJson.pnpm).length === 0) {
597
+ delete pkgJson.pnpm;
598
+ }
599
+ return;
600
+ }
601
+ pkgJson.resolutions = restoreRecord(pkgJson.resolutions, packageName, previousValue);
602
+ }
603
+ function restoreRecord(record, key, previousValue) {
604
+ const nextRecord = { ...record ?? {} };
605
+ if (previousValue === void 0) {
606
+ delete nextRecord[key];
607
+ } else {
608
+ nextRecord[key] = previousValue;
609
+ }
610
+ return Object.keys(nextRecord).length > 0 ? nextRecord : void 0;
611
+ }
612
+
613
+ // src/remediation/tools/apply-patch-file.ts
614
+ import { tool as tool3 } from "ai";
615
+ import { z as z3 } from "zod";
616
+ import { existsSync as existsSync4 } from "fs";
617
+ import { mkdir as mkdir2, mkdtemp, readFile, rm as rm2, writeFile } from "fs/promises";
618
+ import { tmpdir } from "os";
619
+ import { join as join7 } from "path";
620
+ import { execa as execa4 } from "execa";
621
+
622
+ // src/remediation/strategies/patch-utils.ts
623
+ import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync3, readFileSync as readFileSync4 } from "fs";
624
+ import { join as join6 } from "path";
625
+ import { execa as execa3 } from "execa";
626
+ function validatePatchDiff(patchContent) {
627
+ if (!patchContent || typeof patchContent !== "string") {
628
+ return {
629
+ valid: false,
630
+ error: "Patch content must be a non-empty string"
631
+ };
632
+ }
633
+ const hasFromLine = /^---\s+\S+/m.test(patchContent);
634
+ const hasToLine = /^\+\+\+\s+\S+/m.test(patchContent);
635
+ const hasHunkHeader = /^@@\s+-\d+/m.test(patchContent);
636
+ if (!hasFromLine) {
637
+ return {
638
+ valid: false,
639
+ error: 'Missing "---" line in patch format'
640
+ };
641
+ }
642
+ if (!hasToLine) {
643
+ return {
644
+ valid: false,
645
+ error: 'Missing "+++" line in patch format'
646
+ };
647
+ }
648
+ if (!hasHunkHeader) {
649
+ return {
650
+ valid: false,
651
+ error: "No hunk headers (@@...) found in patch"
652
+ };
653
+ }
654
+ return { valid: true };
655
+ }
656
+
657
+ // src/remediation/tools/apply-patch-file.ts
658
+ var applyPatchFileTool = tool3({
659
+ description: "Write generated patch file and apply it using package-manager-native patch flow when available, falling back to patch-package when needed.",
660
+ parameters: z3.object({
661
+ packageName: z3.string().min(1).describe("The npm package name"),
662
+ vulnerableVersion: z3.string().describe("The vulnerable version string"),
663
+ patchContent: z3.string().min(10).optional().describe("Unified diff patch content from generate-patch"),
664
+ patches: z3.array(
665
+ z3.object({
666
+ filePath: z3.string().min(1),
667
+ unifiedDiff: z3.string().min(10)
668
+ })
669
+ ).optional().describe("Patch list from generate-patch; first patch is applied"),
670
+ patchesDir: z3.string().optional().default("./patches").describe("Directory to store patch files"),
671
+ cwd: z3.string().describe("Project root directory (for package.json)"),
672
+ packageManager: z3.enum(["npm", "pnpm", "yarn"]).optional().describe("Package manager used by the target project (auto-detected if omitted)"),
673
+ validateWithTests: z3.boolean().optional().default(true).describe("Run package manager test command to validate patch doesn't break anything"),
674
+ dryRun: z3.boolean().optional().default(false).describe("If true, report but do not mutate files")
675
+ }).refine((value) => Boolean(value.patchContent || value.patches && value.patches.length > 0), {
676
+ message: "Either patchContent or patches must be provided"
677
+ }),
678
+ execute: async ({
679
+ packageName,
680
+ vulnerableVersion,
681
+ patchContent,
682
+ patches,
683
+ patchesDir,
684
+ cwd,
685
+ packageManager,
686
+ validateWithTests,
687
+ dryRun
688
+ }) => {
689
+ try {
690
+ const pm = packageManager ?? detectPackageManager(cwd);
691
+ const selectedPatch = patchContent ?? patches?.[0]?.unifiedDiff;
692
+ if (!selectedPatch) {
693
+ return {
694
+ success: false,
695
+ packageName,
696
+ vulnerableVersion,
697
+ applied: false,
698
+ dryRun,
699
+ message: "No patch content provided.",
700
+ error: "No patch content provided."
701
+ };
702
+ }
703
+ const patchValidation = validatePatchDiff(selectedPatch);
704
+ if (!patchValidation.valid) {
705
+ return {
706
+ success: false,
707
+ packageName,
708
+ vulnerableVersion,
709
+ applied: false,
710
+ dryRun,
711
+ message: patchValidation.error ?? "Patch content is not a valid unified diff.",
712
+ error: patchValidation.error ?? "Patch content is not a valid unified diff."
713
+ };
714
+ }
715
+ const patchFileName = buildPatchFileName(packageName, vulnerableVersion);
716
+ const patchFilePath = join7(cwd, patchesDir, patchFileName);
717
+ if (dryRun) {
718
+ return {
719
+ success: true,
720
+ packageName,
721
+ vulnerableVersion,
722
+ applied: false,
723
+ dryRun: true,
724
+ message: `[DRY RUN] Would write and configure patch at ${patchFilePath}.`,
725
+ patchFilePath,
726
+ patchPath: patchFilePath
727
+ };
728
+ }
729
+ return withRepoLock(cwd, async () => {
730
+ const packageJsonSnapshot = patchModeRequiresPackageJsonSnapshot(pm, cwd) ? await capturePackageJsonSnapshot(cwd) : void 0;
731
+ const patchesDirPath = join7(cwd, patchesDir);
732
+ await mkdir2(patchesDirPath, { recursive: true });
733
+ await writeFile(patchFilePath, selectedPatch, "utf8");
734
+ let validationResult;
735
+ const patchMode = await resolvePatchMode(pm, cwd);
736
+ const commands = getPackageManagerCommands(pm);
737
+ const applyResult = patchMode === "patch-package" ? await configurePatchPackagePostinstall(cwd, pm) : await applyNativePatch({
738
+ cwd,
739
+ packageName,
740
+ vulnerableVersion,
741
+ patchContent: selectedPatch,
742
+ patchMode
743
+ });
744
+ if (!applyResult.success) {
745
+ await cleanupPatchArtifacts({
746
+ cwd,
747
+ packageManager: pm,
748
+ patchFilePath,
749
+ patchMode,
750
+ packageJsonSnapshot,
751
+ rerunInstall: patchMode === "patch-package"
752
+ });
753
+ return {
754
+ success: false,
755
+ packageName,
756
+ vulnerableVersion,
757
+ applied: false,
758
+ dryRun: false,
759
+ message: applyResult.error,
760
+ patchFilePath,
761
+ patchPath: patchFilePath,
762
+ patchMode,
763
+ postinstallConfigured: patchMode === "patch-package" ? false : void 0,
764
+ error: applyResult.error
765
+ };
766
+ }
767
+ if (patchMode === "patch-package") {
768
+ try {
769
+ const [installCmd, ...installArgs] = commands.installPreferOffline;
770
+ await execa4(installCmd, installArgs, {
771
+ cwd,
772
+ stdio: "pipe"
773
+ });
774
+ } catch (err) {
775
+ await cleanupPatchArtifacts({
776
+ cwd,
777
+ packageManager: pm,
778
+ patchFilePath,
779
+ patchMode,
780
+ packageJsonSnapshot,
781
+ rerunInstall: true
782
+ });
783
+ const error = err instanceof Error ? err.message : String(err);
784
+ return {
785
+ success: false,
786
+ packageName,
787
+ vulnerableVersion,
788
+ applied: false,
789
+ dryRun: false,
790
+ message: `Failed to apply patch-package workflow for ${packageName}@${vulnerableVersion}: ${error}`,
791
+ patchFilePath,
792
+ patchPath: patchFilePath,
793
+ patchMode,
794
+ postinstallConfigured: false,
795
+ error: `Failed to apply patch-package workflow for ${packageName}@${vulnerableVersion}: ${error}`
796
+ };
797
+ }
798
+ }
799
+ if (validateWithTests) {
800
+ validationResult = await validatePatchWithTests(cwd, pm);
801
+ if (!validationResult.passed) {
802
+ await cleanupPatchArtifacts({
803
+ cwd,
804
+ packageManager: pm,
805
+ patchFilePath,
806
+ patchMode,
807
+ packageJsonSnapshot,
808
+ rerunInstall: patchMode === "patch-package"
809
+ });
810
+ const validationError = "Patch validation failed after apply; patch marked unresolved.";
811
+ return {
812
+ success: false,
813
+ packageName,
814
+ vulnerableVersion,
815
+ applied: false,
816
+ dryRun: false,
817
+ message: validationError,
818
+ patchFilePath,
819
+ patchPath: patchFilePath,
820
+ patchMode,
821
+ postinstallConfigured: false,
822
+ validation: validationResult,
823
+ error: validationError
824
+ };
825
+ }
826
+ }
827
+ return {
828
+ success: true,
829
+ packageName,
830
+ vulnerableVersion,
831
+ applied: true,
832
+ dryRun: false,
833
+ message: `Patch applied successfully for ${packageName}@${vulnerableVersion}.`,
834
+ patchFilePath,
835
+ patchPath: patchFilePath,
836
+ patchMode,
837
+ postinstallConfigured: patchMode === "patch-package",
838
+ validation: validationResult
839
+ };
840
+ });
841
+ } catch (err) {
842
+ const message = err instanceof Error ? err.message : String(err);
843
+ return {
844
+ success: false,
845
+ packageName,
846
+ vulnerableVersion,
847
+ applied: false,
848
+ dryRun,
849
+ message: `Failed to apply patch file: ${message}`,
850
+ error: `Failed to apply patch file: ${message}`
851
+ };
852
+ }
853
+ }
854
+ });
855
+ async function resolvePatchMode(packageManager, cwd) {
856
+ if (packageManager === "npm") return "patch-package";
857
+ if (packageManager === "pnpm") return "native-pnpm";
858
+ try {
859
+ const result = await execa4("yarn", ["--version"], {
860
+ cwd,
861
+ stdio: "pipe"
862
+ });
863
+ const version = result.stdout.trim();
864
+ const major = Number.parseInt(version.split(".")[0] || "0", 10);
865
+ return major >= 2 ? "native-yarn" : "patch-package";
866
+ } catch {
867
+ return "patch-package";
868
+ }
376
869
  }
377
- async function enrichWithCisaKev(details) {
378
- const feed = await fetchCisaKevFeed();
379
- const entry = findKevEntry(feed, details.id);
380
- if (!entry) return details;
381
- details.kev = {
382
- knownExploited: true,
383
- dateAdded: entry.dateAdded,
384
- dueDate: entry.dueDate,
385
- requiredAction: entry.requiredAction,
386
- knownRansomwareCampaignUse: entry.knownRansomwareCampaignUse
387
- };
388
- if (!details.references.includes(CISA_KEV_URL)) {
389
- details.references.push(CISA_KEV_URL);
870
+ function patchModeRequiresPackageJsonSnapshot(packageManager, cwd) {
871
+ if (packageManager === "npm") return true;
872
+ if (packageManager === "pnpm") return false;
873
+ return true;
874
+ }
875
+ function buildPatchFileName(packageName, vulnerableVersion) {
876
+ const safeName = packageName.replace(/^@/, "").replace(/\//g, "+");
877
+ return `${safeName}+${vulnerableVersion}.patch`;
878
+ }
879
+ async function configurePatchPackagePostinstall(cwd, packageManager) {
880
+ const pkgJsonPath = join7(cwd, "package.json");
881
+ let pkgJson;
882
+ try {
883
+ pkgJson = JSON.parse(await readFile(pkgJsonPath, "utf8"));
884
+ } catch {
885
+ return {
886
+ success: false,
887
+ error: `Could not read package.json at ${pkgJsonPath}`
888
+ };
390
889
  }
391
- return details;
890
+ const devDependencies = pkgJson.devDependencies ?? {};
891
+ if (!devDependencies["patch-package"]) {
892
+ try {
893
+ const commands = getPackageManagerCommands(packageManager);
894
+ const [cmd, ...args] = commands.installDev("patch-package");
895
+ await execa4(cmd, args, {
896
+ cwd,
897
+ stdio: "pipe"
898
+ });
899
+ } catch (err) {
900
+ return {
901
+ success: false,
902
+ error: `Failed to install patch-package: ${err instanceof Error ? err.message : String(err)}`
903
+ };
904
+ }
905
+ }
906
+ if (!pkgJson.scripts) {
907
+ pkgJson.scripts = {};
908
+ }
909
+ const patchApplyCmd = "patch-package";
910
+ const currentPostinstall = pkgJson.scripts.postinstall || "";
911
+ if (currentPostinstall && !currentPostinstall.includes("patch-package")) {
912
+ pkgJson.scripts.postinstall = `${currentPostinstall} && ${patchApplyCmd}`;
913
+ } else if (!currentPostinstall) {
914
+ pkgJson.scripts.postinstall = patchApplyCmd;
915
+ }
916
+ await writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
917
+ return { success: true };
392
918
  }
393
-
394
- // src/intelligence/sources/epss.ts
395
- async function fetchEpss(cveId) {
396
- const { epssApi } = getIntelligenceSourceConfig();
397
- if (!epssApi) return void 0;
919
+ async function capturePackageJsonSnapshot(cwd) {
920
+ const path = join7(cwd, "package.json");
398
921
  try {
399
- const url = new URL(epssApi);
400
- url.searchParams.set("cve", cveId);
401
- const res = await fetch(url.toString(), {
402
- headers: { Accept: "application/json" }
403
- });
404
- if (!res.ok) return void 0;
405
- const body = await res.json();
406
- return body.data?.[0];
922
+ const content = await readFile(path, "utf8");
923
+ return { path, content };
407
924
  } catch {
408
925
  return void 0;
409
926
  }
410
927
  }
411
- async function enrichWithEpss(details) {
412
- const row = await fetchEpss(details.id);
413
- if (!row) return details;
414
- const score = Number.parseFloat(row.epss);
415
- const percentile = Number.parseFloat(row.percentile);
416
- if (!Number.isFinite(score) || !Number.isFinite(percentile)) {
417
- return details;
928
+ async function cleanupPatchArtifacts(params) {
929
+ const { cwd, packageManager, patchFilePath, patchMode, packageJsonSnapshot, rerunInstall } = params;
930
+ await rm2(patchFilePath, { force: true }).catch(() => void 0);
931
+ if (patchMode === "patch-package" && packageJsonSnapshot) {
932
+ await writeFile(packageJsonSnapshot.path, packageJsonSnapshot.content, "utf8").catch(() => void 0);
933
+ }
934
+ if (!rerunInstall) return;
935
+ try {
936
+ const commands = getPackageManagerCommands(packageManager);
937
+ const [installCmd, ...installArgs] = commands.installPreferOffline;
938
+ await execa4(installCmd, installArgs, {
939
+ cwd,
940
+ stdio: "pipe"
941
+ });
942
+ } catch {
943
+ }
944
+ }
945
+ async function applyNativePatch(params) {
946
+ const { cwd, packageName, vulnerableVersion, patchContent, patchMode } = params;
947
+ const packageSpec = `${packageName}@${vulnerableVersion}`;
948
+ const createCommand = patchMode === "native-pnpm" ? "pnpm" : "yarn";
949
+ const createArgs = ["patch", packageSpec];
950
+ let patchDir;
951
+ try {
952
+ const createResult = await execa4(createCommand, createArgs, {
953
+ cwd,
954
+ stdio: "pipe"
955
+ });
956
+ patchDir = extractPatchDirectory(`${createResult.stdout}
957
+ ${createResult.stderr}`);
958
+ } catch (err) {
959
+ return {
960
+ success: false,
961
+ error: `Failed to create native patch workspace for ${packageSpec}: ${err instanceof Error ? err.message : String(err)}`
962
+ };
963
+ }
964
+ if (!patchDir) {
965
+ return {
966
+ success: false,
967
+ error: `Could not determine native patch directory for ${packageSpec}.`
968
+ };
969
+ }
970
+ const tempPatchDir = await mkdtemp(join7(tmpdir(), "autoremediator-native-patch-"));
971
+ const tempPatchFile = join7(tempPatchDir, "change.patch");
972
+ try {
973
+ await writeFile(tempPatchFile, patchContent, "utf8");
974
+ await execa4("patch", ["-p1", "-i", tempPatchFile], {
975
+ cwd: patchDir,
976
+ stdio: "pipe"
977
+ });
978
+ const commitCommand = patchMode === "native-pnpm" ? "pnpm" : "yarn";
979
+ const commitArgs = patchMode === "native-pnpm" ? ["patch-commit", patchDir] : ["patch-commit", "-s", patchDir];
980
+ await execa4(commitCommand, commitArgs, {
981
+ cwd,
982
+ stdio: "pipe"
983
+ });
984
+ } catch (err) {
985
+ return {
986
+ success: false,
987
+ error: `Failed to apply native patch for ${packageSpec}: ${err instanceof Error ? err.message : String(err)}`
988
+ };
989
+ } finally {
990
+ await rm2(tempPatchDir, { recursive: true, force: true });
418
991
  }
419
- details.epss = {
420
- score,
421
- percentile,
422
- date: row.date
423
- };
424
- return details;
425
- }
426
-
427
- // src/intelligence/sources/cve-services.ts
428
- function pickEnglishDescription(container) {
429
- if (!container?.descriptions?.length) return void 0;
430
- const en = container.descriptions.find((d) => d.lang === "en" && d.value);
431
- return (en?.value ?? container.descriptions[0]?.value)?.trim() || void 0;
992
+ return { success: true };
432
993
  }
433
- function collectReferences(record) {
434
- const refs = /* @__PURE__ */ new Set();
435
- const cnaRefs = record.containers?.cna?.references ?? [];
436
- const adpRefs = (record.containers?.adp ?? []).flatMap((c) => c.references ?? []);
437
- for (const ref of [...cnaRefs, ...adpRefs]) {
438
- if (ref.url) refs.add(ref.url);
994
+ function extractPatchDirectory(output) {
995
+ const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
996
+ for (const line of lines) {
997
+ if (existsSync4(line)) {
998
+ return line;
999
+ }
1000
+ const tokens = line.split(/\s+/).map((token) => token.replace(/^['"]|['"]$/g, ""));
1001
+ for (const token of tokens) {
1002
+ if (token.startsWith("/") && existsSync4(token)) {
1003
+ return token;
1004
+ }
1005
+ }
439
1006
  }
440
- return Array.from(refs);
1007
+ return "";
441
1008
  }
442
- async function fetchCveServicesRecord(cveId) {
443
- const { cveServicesApi } = getIntelligenceSourceConfig();
444
- if (!cveServicesApi) return void 0;
1009
+ async function validatePatchWithTests(cwd, packageManager) {
445
1010
  try {
446
- const res = await fetch(`${cveServicesApi}/${encodeURIComponent(cveId)}`, {
447
- headers: { Accept: "application/json" }
1011
+ const commands = getPackageManagerCommands(packageManager);
1012
+ const [cmd, ...args] = commands.test;
1013
+ const result = await execa4(cmd, args, {
1014
+ cwd,
1015
+ timeout: 6e4,
1016
+ // 60 second timeout
1017
+ stdio: "pipe"
448
1018
  });
449
- if (!res.ok) return void 0;
450
- return await res.json();
451
- } catch {
452
- return void 0;
1019
+ return {
1020
+ passed: true,
1021
+ output: result.stdout
1022
+ };
1023
+ } catch (err) {
1024
+ const errorOutput = typeof err === "object" && err !== null && "stdout" in err ? String(err.stdout ?? "") : "";
1025
+ const failedTests = extractFailedTests(errorOutput);
1026
+ return {
1027
+ passed: false,
1028
+ error: failedTests.length > 0 ? `Failed tests: ${failedTests.join(", ")}` : "Package-manager test validation failed.",
1029
+ output: errorOutput,
1030
+ failedTests
1031
+ };
453
1032
  }
454
1033
  }
455
- async function enrichWithCveServices(details) {
456
- const record = await fetchCveServicesRecord(details.id);
457
- if (!record) return details;
458
- const summary = pickEnglishDescription(record.containers?.cna);
459
- if (summary && (!details.summary || details.summary.includes("No summary available"))) {
460
- details.summary = summary;
461
- }
462
- const refs = collectReferences(record);
463
- if (refs.length > 0) {
464
- const merged = /* @__PURE__ */ new Set([...details.references, ...refs]);
465
- details.references = Array.from(merged);
1034
+ function extractFailedTests(output) {
1035
+ const failedTests = [];
1036
+ const patterns = [
1037
+ /✖\s+(.+?)(?:\n|$)/g,
1038
+ // Mocha style
1039
+ /●\s+(.+)(?:\n|$)/g,
1040
+ // Jest style
1041
+ /^FAIL\s+(.+?)(?:\n|$)/gm
1042
+ // Generic FAIL
1043
+ ];
1044
+ for (const pattern of patterns) {
1045
+ let match;
1046
+ while ((match = pattern.exec(output)) !== null) {
1047
+ if (match[1]) {
1048
+ failedTests.push(match[1].trim());
1049
+ }
1050
+ }
466
1051
  }
467
- details.intelligence = {
468
- ...details.intelligence ?? {},
469
- cveServicesEnriched: true
470
- };
471
- return details;
1052
+ return failedTests.slice(0, 5);
472
1053
  }
473
1054
 
474
- // src/intelligence/sources/gitlab-advisory.ts
475
- function advisoryMatchesCve(advisory, cveId) {
476
- const normalized = cveId.toUpperCase();
477
- return (advisory.identifiers ?? []).some(
478
- (id) => id.type?.toUpperCase() === "CVE" && id.value?.toUpperCase() === normalized
479
- );
1055
+ // src/remediation/local/run.ts
1056
+ import semver4 from "semver";
1057
+
1058
+ // src/intelligence/sources/osv.ts
1059
+ var OSV_BASE = "https://api.osv.dev/v1";
1060
+ async function fetchOsvVuln(cveId) {
1061
+ const url = `${OSV_BASE}/vulns/${encodeURIComponent(cveId)}`;
1062
+ const res = await fetch(url, {
1063
+ headers: { Accept: "application/json" }
1064
+ });
1065
+ if (res.status === 404) return null;
1066
+ if (!res.ok) {
1067
+ throw new Error(`OSV API error ${res.status} for ${cveId}: ${await res.text()}`);
1068
+ }
1069
+ return res.json();
480
1070
  }
481
- async function fetchGitLabAdvisories(cveId) {
482
- const { gitLabAdvisoryApi } = getIntelligenceSourceConfig();
483
- if (!gitLabAdvisoryApi) return [];
484
- try {
485
- const url = new URL(gitLabAdvisoryApi);
486
- url.searchParams.set("identifier", cveId);
487
- url.searchParams.set("ecosystem", "npm");
488
- const res = await fetch(url.toString(), {
489
- headers: { Accept: "application/json" }
490
- });
491
- if (!res.ok) return [];
492
- const body = await res.json();
493
- return Array.isArray(body) ? body : [];
494
- } catch {
495
- return [];
1071
+ function osvEventsToSemverRange(events) {
1072
+ const parts = [];
1073
+ for (const event of events) {
1074
+ if (event.introduced !== void 0) {
1075
+ const v = event.introduced === "0" ? "0.0.0" : event.introduced;
1076
+ parts.push(`>=${v}`);
1077
+ }
1078
+ if (event.fixed !== void 0) {
1079
+ parts.push(`<${event.fixed}`);
1080
+ }
1081
+ if (event.last_affected !== void 0) {
1082
+ parts.push(`<=${event.last_affected}`);
1083
+ }
496
1084
  }
1085
+ return parts.join(" ") || ">=0.0.0";
497
1086
  }
498
- async function enrichWithGitLabAdvisory(details) {
499
- const advisories = await fetchGitLabAdvisories(details.id);
500
- const matched = advisories.filter((a) => advisoryMatchesCve(a, details.id));
501
- if (matched.length === 0) return details;
502
- const refs = matched.flatMap((m) => m.references ?? []);
503
- if (refs.length > 0) {
504
- const merged = /* @__PURE__ */ new Set([...details.references, ...refs]);
505
- details.references = Array.from(merged);
1087
+ function parseOsvVuln(vuln) {
1088
+ const npmAffected = [];
1089
+ for (const affected of vuln.affected ?? []) {
1090
+ const ecosystem = affected.package?.ecosystem;
1091
+ const packageName = affected.package?.name;
1092
+ if (!ecosystem || typeof ecosystem !== "string") continue;
1093
+ if (!packageName || typeof packageName !== "string") continue;
1094
+ if (ecosystem.toLowerCase() !== "npm") continue;
1095
+ const semverRange = affected.ranges?.find((r) => r.type === "SEMVER");
1096
+ const vulnerableRange = semverRange ? osvEventsToSemverRange(semverRange.events) : ">=0.0.0";
1097
+ const fixedEvent = semverRange?.events.find((e) => e.fixed !== void 0);
1098
+ npmAffected.push({
1099
+ name: packageName,
1100
+ ecosystem: "npm",
1101
+ vulnerableRange,
1102
+ firstPatchedVersion: fixedEvent?.fixed,
1103
+ source: "osv"
1104
+ });
506
1105
  }
507
- details.intelligence = {
508
- ...details.intelligence ?? {},
509
- gitlabAdvisoryMatched: true
1106
+ const severity = deriveSeverity(vuln.severity);
1107
+ return {
1108
+ id: vuln.id,
1109
+ summary: vuln.summary ?? vuln.details ?? "No summary available.",
1110
+ severity,
1111
+ references: vuln.references?.map((r) => r.url) ?? [],
1112
+ affectedPackages: npmAffected
510
1113
  };
511
- return details;
1114
+ }
1115
+ function deriveSeverity(severityEntries) {
1116
+ if (!severityEntries?.length) return "UNKNOWN";
1117
+ const cvssEntry = severityEntries.find((s) => s.type === "CVSS_V3") ?? severityEntries[0];
1118
+ const scoreMatch = cvssEntry.score.match(/(\d+\.\d+)$/);
1119
+ if (scoreMatch) {
1120
+ const score = parseFloat(scoreMatch[1]);
1121
+ if (score >= 9) return "CRITICAL";
1122
+ if (score >= 7) return "HIGH";
1123
+ if (score >= 4) return "MEDIUM";
1124
+ return "LOW";
1125
+ }
1126
+ return "UNKNOWN";
1127
+ }
1128
+ async function lookupCveOsv(cveId) {
1129
+ const vuln = await fetchOsvVuln(cveId);
1130
+ if (!vuln) return null;
1131
+ return parseOsvVuln(vuln);
512
1132
  }
513
1133
 
514
- // src/intelligence/sources/certcc.ts
515
- var CERTCC_HOME = "https://www.kb.cert.org/vuls/";
516
- async function findCertCcReference(cveId) {
517
- const { certCcSearchUrl } = getIntelligenceSourceConfig();
518
- if (!certCcSearchUrl) return void 0;
519
- try {
520
- const url = new URL(certCcSearchUrl);
521
- url.searchParams.set("query", cveId);
522
- const res = await fetch(url.toString(), {
523
- headers: { Accept: "text/html" }
524
- });
525
- if (!res.ok) return void 0;
526
- const html = await res.text();
527
- const match = html.match(/https:\/\/www\.kb\.cert\.org\/vuls\/id\/\d+/i);
528
- return match?.[0] ?? void 0;
529
- } catch {
530
- return void 0;
1134
+ // src/intelligence/sources/github-advisory.ts
1135
+ var GH_ADVISORY_BASE = "https://api.github.com/advisories";
1136
+ function buildHeaders() {
1137
+ const headers = {
1138
+ Accept: "application/vnd.github+json",
1139
+ "X-GitHub-Api-Version": "2022-11-28"
1140
+ };
1141
+ const token = getGitHubToken();
1142
+ if (token) {
1143
+ headers.Authorization = `Bearer ${token}`;
531
1144
  }
1145
+ return headers;
532
1146
  }
533
- async function enrichWithCertCc(details) {
534
- const ref = await findCertCcReference(details.id);
535
- if (!ref) return details;
536
- if (!details.references.includes(ref)) {
537
- details.references.push(ref);
1147
+ async function fetchGhAdvisories(cveId) {
1148
+ const url = new URL(GH_ADVISORY_BASE);
1149
+ url.searchParams.set("cve_id", cveId);
1150
+ url.searchParams.set("ecosystem", "npm");
1151
+ url.searchParams.set("type", "reviewed");
1152
+ url.searchParams.set("per_page", "10");
1153
+ const res = await fetch(url.toString(), { headers: buildHeaders() });
1154
+ if (res.status === 404) return [];
1155
+ if (!res.ok) {
1156
+ console.warn(
1157
+ `[autoremediator] GitHub Advisory API returned ${res.status} for ${cveId} \u2014 skipping.`
1158
+ );
1159
+ return [];
538
1160
  }
539
- details.intelligence = {
540
- ...details.intelligence ?? {},
541
- certCcMatched: true
542
- };
543
- if (!details.references.includes(CERTCC_HOME)) {
544
- details.references.push(CERTCC_HOME);
1161
+ return res.json();
1162
+ }
1163
+ function parseGhAdvisories(advisories) {
1164
+ const packages = [];
1165
+ for (const advisory of advisories) {
1166
+ for (const vuln of advisory.vulnerabilities) {
1167
+ if (vuln.package.ecosystem.toLowerCase() !== "npm") continue;
1168
+ packages.push({
1169
+ name: vuln.package.name,
1170
+ ecosystem: "npm",
1171
+ vulnerableRange: vuln.vulnerable_version_range ?? ">=0.0.0",
1172
+ firstPatchedVersion: vuln.first_patched_version ?? void 0,
1173
+ source: "github-advisory"
1174
+ });
1175
+ }
545
1176
  }
546
- return details;
1177
+ return packages;
547
1178
  }
548
-
549
- // src/intelligence/sources/deps-dev.ts
550
- async function fetchDepsDevPackage(name) {
551
- const { depsDevApi } = getIntelligenceSourceConfig();
552
- if (!depsDevApi) return false;
553
- try {
554
- const url = `${depsDevApi}/systems/npm/packages/${encodeURIComponent(name)}`;
555
- const res = await fetch(url, { headers: { Accept: "application/json" } });
556
- return res.ok;
557
- } catch {
558
- return false;
1179
+ function mergeGhDataIntoCveDetails(details, ghPackages) {
1180
+ const enriched = { ...details };
1181
+ for (const ghPkg of ghPackages) {
1182
+ const existing = enriched.affectedPackages.find(
1183
+ (p) => p.name === ghPkg.name
1184
+ );
1185
+ if (existing) {
1186
+ if (!existing.firstPatchedVersion && ghPkg.firstPatchedVersion) {
1187
+ existing.firstPatchedVersion = ghPkg.firstPatchedVersion;
1188
+ }
1189
+ } else {
1190
+ enriched.affectedPackages.push(ghPkg);
1191
+ }
559
1192
  }
1193
+ return enriched;
560
1194
  }
561
- async function enrichWithDepsDev(details) {
562
- const names = Array.from(new Set(details.affectedPackages.map((p) => p.name))).slice(0, 20);
563
- if (names.length === 0) return details;
564
- const checks = await Promise.all(names.map((name) => fetchDepsDevPackage(name)));
565
- const matched = checks.filter(Boolean).length;
566
- if (matched === 0) return details;
567
- details.intelligence = {
568
- ...details.intelligence ?? {},
569
- depsDevEnrichedPackages: matched
570
- };
571
- return details;
1195
+ async function lookupCveGitHub(cveId) {
1196
+ const advisories = await fetchGhAdvisories(cveId);
1197
+ return parseGhAdvisories(advisories);
572
1198
  }
573
1199
 
574
- // src/intelligence/sources/ossf-scorecard.ts
575
- async function checkProject(project) {
576
- const { scorecardApi } = getIntelligenceSourceConfig();
577
- if (!scorecardApi) return false;
578
- try {
579
- const url = new URL(`${scorecardApi}/projects`);
580
- url.searchParams.set("project", project);
581
- const res = await fetch(url.toString(), {
582
- headers: { Accept: "application/json" }
583
- });
584
- return res.ok;
585
- } catch {
586
- return false;
1200
+ // src/intelligence/sources/nvd.ts
1201
+ var NVD_BASE = "https://services.nvd.nist.gov/rest/json/cves/2.0";
1202
+ function buildNvdHeaders() {
1203
+ const { apiKey } = getNvdConfig();
1204
+ const headers = { Accept: "application/json" };
1205
+ if (apiKey) {
1206
+ headers.apiKey = apiKey;
587
1207
  }
1208
+ return headers;
588
1209
  }
589
- async function enrichWithOssfScorecard(details) {
590
- const projects = Array.from(
591
- new Set(details.affectedPackages.map((p) => `github.com/${p.name}/${p.name}`))
592
- ).slice(0, 10);
593
- if (projects.length === 0) return details;
594
- const checks = await Promise.all(projects.map((project) => checkProject(project)));
595
- const matched = checks.filter(Boolean).length;
596
- if (matched === 0) return details;
597
- details.intelligence = {
598
- ...details.intelligence ?? {},
599
- scorecardProjects: matched
600
- };
601
- return details;
602
- }
603
-
604
- // src/intelligence/sources/external-feeds.ts
605
- async function probeFeed(url, cveId, token) {
1210
+ async function fetchNvdCvss(cveId) {
1211
+ const url = `${NVD_BASE}?cveId=${encodeURIComponent(cveId)}`;
606
1212
  try {
607
- const feedUrl = new URL(url);
608
- feedUrl.searchParams.set("cve", cveId);
609
- const headers = { Accept: "application/json" };
610
- if (token) headers.Authorization = `Bearer ${token}`;
611
- const res = await fetch(feedUrl.toString(), { headers });
1213
+ const res = await fetch(url, { headers: buildNvdHeaders() });
612
1214
  if (!res.ok) return void 0;
613
- return feedUrl.toString();
1215
+ const data = await res.json();
1216
+ const vuln = data.vulnerabilities?.[0];
1217
+ if (!vuln) return void 0;
1218
+ const metrics = vuln.cve.metrics;
1219
+ const metric = metrics?.cvssMetricV31?.[0] ?? metrics?.cvssMetricV30?.[0] ?? metrics?.cvssMetricV2?.[0];
1220
+ if (!metric) return void 0;
1221
+ const score = metric.cvssData.baseScore;
1222
+ const rawSeverity = metric.cvssData.baseSeverity.toUpperCase();
1223
+ const severityMap = {
1224
+ CRITICAL: "CRITICAL",
1225
+ HIGH: "HIGH",
1226
+ MEDIUM: "MEDIUM",
1227
+ LOW: "LOW"
1228
+ };
1229
+ return {
1230
+ score,
1231
+ severity: severityMap[rawSeverity] ?? "UNKNOWN"
1232
+ };
614
1233
  } catch {
615
1234
  return void 0;
616
1235
  }
617
1236
  }
618
- async function enrichWithExternalFeeds(details) {
619
- const {
620
- vendorAdvisoryFeeds,
621
- commercialFeeds,
622
- commercialFeedToken
623
- } = getIntelligenceSourceConfig();
624
- const vendorHits = (await Promise.all(vendorAdvisoryFeeds.map((url) => probeFeed(url, details.id)))).filter((v) => Boolean(v));
625
- const commercialHits = (await Promise.all(
626
- commercialFeeds.map((url) => probeFeed(url, details.id, commercialFeedToken))
627
- )).filter((v) => Boolean(v));
628
- if (vendorHits.length === 0 && commercialHits.length === 0) {
629
- return details;
1237
+ async function enrichWithNvd(details) {
1238
+ const cvss = await fetchNvdCvss(details.id);
1239
+ if (cvss) {
1240
+ details.cvssScore = cvss.score;
1241
+ if (details.severity === "UNKNOWN") {
1242
+ details.severity = cvss.severity;
1243
+ }
630
1244
  }
631
- details.intelligence = {
632
- ...details.intelligence ?? {},
633
- vendorAdvisories: vendorHits.length > 0 ? vendorHits : details.intelligence?.vendorAdvisories,
634
- commercialFeeds: commercialHits.length > 0 ? commercialHits : details.intelligence?.commercialFeeds
635
- };
636
- const mergedRefs = /* @__PURE__ */ new Set([...details.references, ...vendorHits, ...commercialHits]);
637
- details.references = Array.from(mergedRefs);
638
1245
  return details;
639
1246
  }
640
1247
 
641
- // src/remediation/tools/lookup-cve.ts
642
- var lookupCveTool = tool({
643
- description: "Look up a CVE ID and return the list of affected npm packages, their vulnerable version ranges, and the first patched version. Always call this first.",
644
- parameters: z.object({
645
- cveId: z.string().regex(/^CVE-\d{4}-\d+$/i, "Must be a valid CVE ID like CVE-2021-23337")
646
- }),
647
- execute: async ({ cveId }) => {
648
- const normalizedId = cveId.toUpperCase();
649
- const [osvDetails, ghPackages] = await Promise.all([
650
- lookupCveOsv(normalizedId),
651
- lookupCveGitHub(normalizedId)
652
- ]);
653
- if (!osvDetails && ghPackages.length === 0) {
654
- return {
655
- success: false,
656
- error: `CVE "${normalizedId}" was not found in OSV or GitHub Advisory databases. It may be too new, or not affect npm packages.`
657
- };
658
- }
659
- let details = osvDetails ?? {
660
- id: normalizedId,
661
- summary: "Details sourced from GitHub Advisory Database.",
662
- severity: "UNKNOWN",
663
- references: [],
664
- affectedPackages: []
665
- };
666
- if (ghPackages.length > 0) {
667
- details = mergeGhDataIntoCveDetails(details, ghPackages);
668
- }
669
- const sourceHealth = {};
670
- const applyEnricher = async (sourceName, enricher) => {
671
- const before = JSON.stringify(details);
672
- try {
673
- details = await enricher(details);
674
- const after = JSON.stringify(details);
675
- sourceHealth[sourceName] = {
676
- attempted: true,
677
- changed: before !== after
678
- };
679
- } catch (error) {
680
- sourceHealth[sourceName] = {
681
- attempted: true,
682
- changed: false,
683
- error: error instanceof Error ? error.message : String(error)
684
- };
685
- }
686
- };
687
- await applyEnricher("nvd", enrichWithNvd);
688
- await applyEnricher("cisa-kev", enrichWithCisaKev);
689
- await applyEnricher("epss", enrichWithEpss);
690
- await applyEnricher("cve-services", enrichWithCveServices);
691
- await applyEnricher("gitlab-advisory", enrichWithGitLabAdvisory);
692
- await applyEnricher("certcc", enrichWithCertCc);
693
- await applyEnricher("deps-dev", enrichWithDepsDev);
694
- await applyEnricher("ossf-scorecard", enrichWithOssfScorecard);
695
- await applyEnricher("external-feeds", enrichWithExternalFeeds);
696
- details.intelligence = {
697
- ...details.intelligence ?? {},
698
- sourceHealth
699
- };
700
- if (details.affectedPackages.length === 0) {
701
- return {
702
- success: false,
703
- error: `CVE "${normalizedId}" was found but has no npm-specific affected packages listed. It may affect a different ecosystem.`
704
- };
705
- }
706
- return { success: true, data: details };
707
- }
708
- });
709
-
710
1248
  // src/remediation/tools/check-inventory.ts
711
- import { tool as tool2 } from "ai";
712
- import { z as z2 } from "zod";
713
- import { readFileSync } from "fs";
714
- import { join as join2 } from "path";
715
- import { execa } from "execa";
716
- var checkInventoryTool = tool2({
1249
+ import { tool as tool4 } from "ai";
1250
+ import { z as z4 } from "zod";
1251
+ import { readFileSync as readFileSync5 } from "fs";
1252
+ import { join as join8 } from "path";
1253
+ import { execa as execa5 } from "execa";
1254
+ var checkInventoryTool = tool4({
717
1255
  description: "Read the project's package.json and installed dependencies to list packages and exact versions. Must be called before checking version matches.",
718
- parameters: z2.object({
719
- cwd: z2.string().describe("Absolute path to the consumer project's root directory"),
720
- packageManager: z2.enum(["npm", "pnpm", "yarn"]).optional().describe("Package manager used by the target project (auto-detected if omitted)")
1256
+ parameters: z4.object({
1257
+ cwd: z4.string().describe("Absolute path to the consumer project's root directory"),
1258
+ packageManager: z4.enum(["npm", "pnpm", "yarn"]).optional().describe("Package manager used by the target project (auto-detected if omitted)")
721
1259
  }),
722
1260
  execute: async ({ cwd, packageManager }) => {
723
1261
  let pkgJson;
724
1262
  try {
725
- pkgJson = JSON.parse(readFileSync(join2(cwd, "package.json"), "utf8"));
1263
+ pkgJson = JSON.parse(readFileSync5(join8(cwd, "package.json"), "utf8"));
726
1264
  } catch {
727
1265
  return {
728
1266
  packages: [],
@@ -734,7 +1272,7 @@ var checkInventoryTool = tool2({
734
1272
  let installedVersions = /* @__PURE__ */ new Map();
735
1273
  try {
736
1274
  const [cmd, ...args] = commands.list;
737
- const listResult = await execa(cmd, args, {
1275
+ const listResult = await execa5(cmd, args, {
738
1276
  cwd,
739
1277
  stdio: "pipe",
740
1278
  reject: false
@@ -761,66 +1299,12 @@ var checkInventoryTool = tool2({
761
1299
  packages.push({ name, version: cleaned, type: "direct" });
762
1300
  }
763
1301
  }
764
- return { packages };
765
- }
766
- });
767
-
768
- // src/remediation/tools/check-version-match.ts
769
- import { tool as tool3 } from "ai";
770
- import { z as z3 } from "zod";
771
- import semver from "semver";
772
- var affectedPackageSchema = z3.object({
773
- name: z3.string(),
774
- ecosystem: z3.literal("npm"),
775
- vulnerableRange: z3.string(),
776
- firstPatchedVersion: z3.string().optional(),
777
- source: z3.enum(["osv", "github-advisory"])
778
- });
779
- var inventoryPackageSchema = z3.object({
780
- name: z3.string(),
781
- version: z3.string(),
782
- type: z3.enum(["direct", "indirect"])
783
- });
784
- var checkVersionMatchTool = tool3({
785
- description: "Check which of the project's installed packages fall within the CVE's vulnerable version ranges. Returns only the packages that are actually vulnerable.",
786
- parameters: z3.object({
787
- installedPackages: z3.array(inventoryPackageSchema).describe("Output from the check-inventory tool"),
788
- affectedPackages: z3.array(affectedPackageSchema).describe("affectedPackages array from the lookup-cve tool result")
789
- }),
790
- execute: async ({ installedPackages, affectedPackages }) => {
791
- const vulnerable = [];
792
- for (const affected of affectedPackages) {
793
- const matches = installedPackages.filter(
794
- (p) => p.name === affected.name
795
- );
796
- for (const installed of matches) {
797
- if (!semver.valid(installed.version)) continue;
798
- let isVulnerable = false;
799
- try {
800
- isVulnerable = semver.satisfies(installed.version, affected.vulnerableRange, {
801
- includePrerelease: false
802
- });
803
- } catch {
804
- continue;
805
- }
806
- if (isVulnerable) {
807
- vulnerable.push({ installed, affected });
808
- }
809
- }
810
- }
811
- return {
812
- vulnerablePackages: vulnerable,
813
- checkedCount: installedPackages.length
814
- };
1302
+ return { packages };
815
1303
  }
816
1304
  });
817
1305
 
818
- // src/remediation/tools/find-fixed-version.ts
819
- import { tool as tool4 } from "ai";
820
- import { z as z4 } from "zod";
821
-
822
1306
  // src/intelligence/sources/registry.ts
823
- import semver2 from "semver";
1307
+ import semver3 from "semver";
824
1308
  var NPM_REGISTRY = "https://registry.npmjs.org";
825
1309
  async function fetchPackageVersions(packageName) {
826
1310
  const url = `${NPM_REGISTRY}/${encodeURIComponent(packageName)}`;
@@ -836,322 +1320,218 @@ async function fetchPackageVersions(packageName) {
836
1320
  const data = await res.json();
837
1321
  return Object.keys(data.versions);
838
1322
  }
839
- async function findSafeUpgradeVersion(packageName, installedVersion, firstPatchedVersion, vulnerableRange) {
1323
+ async function resolveSafeUpgradeVersion(packageName, installedVersion, firstPatchedVersion, vulnerableRange) {
840
1324
  const versions = await fetchPackageVersions(packageName);
841
- if (!versions.length) return void 0;
842
- const installedMajor = semver2.major(installedVersion);
843
- const candidates = versions.filter((v) => semver2.valid(v) && semver2.gte(v, firstPatchedVersion)).filter((v) => {
1325
+ if (!versions.length) {
1326
+ return {
1327
+ candidates: {},
1328
+ majorOnlyFixAvailable: false
1329
+ };
1330
+ }
1331
+ const installed = semver3.parse(installedVersion);
1332
+ const candidates = versions.filter((v) => semver3.valid(v) && semver3.gte(v, firstPatchedVersion)).filter((v) => {
844
1333
  if (!vulnerableRange) return true;
845
1334
  try {
846
- return !semver2.satisfies(v, vulnerableRange, { includePrerelease: false });
1335
+ return !semver3.satisfies(v, vulnerableRange, { includePrerelease: false });
847
1336
  } catch {
848
1337
  return true;
849
1338
  }
850
- }).sort(semver2.compare);
851
- if (!candidates.length) return void 0;
852
- const sameMajor = candidates.find(
853
- (v) => semver2.major(v) === installedMajor
854
- );
855
- if (sameMajor) return sameMajor;
856
- return candidates[0];
857
- }
858
-
859
- // src/remediation/tools/find-fixed-version.ts
860
- var findFixedVersionTool = tool4({
861
- description: "Query the npm registry to find the lowest published version of a package that is >= the first patched version. Prefer same-major upgrades. Returns undefined if no safe version exists.",
862
- parameters: z4.object({
863
- packageName: z4.string().describe("The npm package name"),
864
- installedVersion: z4.string().describe("The currently installed version (exact semver)"),
865
- firstPatchedVersion: z4.string().describe(
866
- "The first version that is NOT vulnerable (from lookup-cve). Use this as the floor."
867
- ),
868
- vulnerableRange: z4.string().optional().describe("Optional vulnerable semver range used to exclude still-vulnerable versions")
869
- }),
870
- execute: async ({
871
- packageName,
872
- installedVersion,
873
- firstPatchedVersion,
874
- vulnerableRange
875
- }) => {
876
- const safeVersion = await findSafeUpgradeVersion(
877
- packageName,
878
- installedVersion,
879
- firstPatchedVersion,
880
- vulnerableRange
881
- );
882
- if (!safeVersion) {
883
- return {
884
- isMajorBump: false,
885
- message: `No safe upgrade version found for "${packageName}". The patch-file path will be needed.`
886
- };
887
- }
888
- const installedMajor = parseInt(installedVersion.split(".")[0] ?? "0", 10);
889
- const safeMajor = parseInt(safeVersion.split(".")[0] ?? "0", 10);
890
- const isMajorBump = safeMajor > installedMajor;
1339
+ }).sort(semver3.compare);
1340
+ if (!candidates.length) {
891
1341
  return {
892
- safeVersion,
893
- isMajorBump,
894
- message: isMajorBump ? `Found safe version ${safeVersion} for "${packageName}", but it is a major bump from ${installedVersion}. Applying anyway \u2014 consumer should review for breaking changes.` : `Found safe version ${safeVersion} for "${packageName}" (from ${installedVersion}).`
1342
+ candidates: {},
1343
+ majorOnlyFixAvailable: false
895
1344
  };
896
1345
  }
897
- });
898
-
899
- // src/remediation/tools/apply-version-bump.ts
900
- import { tool as tool5 } from "ai";
901
- import { z as z5 } from "zod";
902
- import { join as join5 } from "path";
903
- import { readFileSync as readFileSync3, writeFileSync } from "fs";
904
- import { execa as execa2 } from "execa";
905
- import semver3 from "semver";
906
-
907
- // src/platform/policy.ts
908
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
909
- import { join as join3 } from "path";
910
- var DEFAULT_POLICY = {
911
- allowMajorBumps: false,
912
- denyPackages: [],
913
- allowPackages: [],
914
- constraints: {
915
- directDependenciesOnly: false,
916
- preferVersionBump: false
1346
+ const categorizedCandidates = {};
1347
+ for (const candidate of candidates) {
1348
+ const level = classifyUpgradeLevel(installedVersion, candidate);
1349
+ if (!level) continue;
1350
+ if (!categorizedCandidates[level]) {
1351
+ categorizedCandidates[level] = candidate;
1352
+ }
917
1353
  }
918
- };
919
- function loadPolicy(cwd, explicitPath) {
920
- const candidate = explicitPath ?? join3(cwd, ".autoremediator.json");
921
- if (!existsSync2(candidate)) return DEFAULT_POLICY;
922
- try {
923
- const parsed = JSON.parse(readFileSync2(candidate, "utf8"));
1354
+ const safeVersion = categorizedCandidates.patch ?? categorizedCandidates.minor ?? categorizedCandidates.major;
1355
+ if (!safeVersion) {
924
1356
  return {
925
- allowMajorBumps: parsed.allowMajorBumps ?? DEFAULT_POLICY.allowMajorBumps,
926
- denyPackages: parsed.denyPackages ?? DEFAULT_POLICY.denyPackages,
927
- allowPackages: parsed.allowPackages ?? DEFAULT_POLICY.allowPackages,
928
- constraints: {
929
- directDependenciesOnly: parsed.constraints?.directDependenciesOnly ?? DEFAULT_POLICY.constraints?.directDependenciesOnly ?? false,
930
- preferVersionBump: parsed.constraints?.preferVersionBump ?? DEFAULT_POLICY.constraints?.preferVersionBump ?? false
931
- }
1357
+ candidates: categorizedCandidates,
1358
+ majorOnlyFixAvailable: false
932
1359
  };
933
- } catch {
934
- return DEFAULT_POLICY;
935
- }
936
- }
937
- function isPackageAllowed(policy, packageName) {
938
- if (policy.denyPackages.includes(packageName)) return false;
939
- if (policy.allowPackages.length > 0 && !policy.allowPackages.includes(packageName)) {
940
- return false;
941
1360
  }
942
- return true;
943
- }
944
-
945
- // src/platform/repo-lock.ts
946
- import { mkdir, rm } from "fs/promises";
947
- import { join as join4 } from "path";
948
- async function sleep(ms) {
949
- await new Promise((resolve) => setTimeout(resolve, ms));
950
- }
951
- async function acquireRepoLock(cwd, options = {}) {
952
- const timeoutMs = options.timeoutMs ?? 15e3;
953
- const retryDelayMs = options.retryDelayMs ?? 125;
954
- const lockRoot = join4(cwd, ".autoremediator", "locks");
955
- const lockPath = join4(cwd, ".autoremediator", "locks", "remediation.lock");
956
- const startedAt = Date.now();
957
- await mkdir(lockRoot, { recursive: true });
958
- while (true) {
959
- try {
960
- await mkdir(lockPath, { recursive: false });
961
- return {
962
- lockPath,
963
- release: async () => {
964
- await rm(lockPath, { recursive: true, force: true });
965
- }
966
- };
967
- } catch {
968
- if (Date.now() - startedAt > timeoutMs) {
969
- throw new Error(`Timed out waiting for repository lock at ${lockPath}.`);
970
- }
971
- await sleep(retryDelayMs);
972
- }
1361
+ const upgradeLevel = classifyUpgradeLevel(installedVersion, safeVersion);
1362
+ const majorOnlyFixAvailable = !categorizedCandidates.patch && !categorizedCandidates.minor && Boolean(categorizedCandidates.major);
1363
+ if (!installed || !upgradeLevel) {
1364
+ return {
1365
+ safeVersion,
1366
+ upgradeLevel,
1367
+ candidates: categorizedCandidates,
1368
+ majorOnlyFixAvailable
1369
+ };
973
1370
  }
1371
+ return {
1372
+ safeVersion,
1373
+ upgradeLevel,
1374
+ candidates: categorizedCandidates,
1375
+ majorOnlyFixAvailable
1376
+ };
974
1377
  }
975
- async function withRepoLock(cwd, fn, options) {
976
- const lock = await acquireRepoLock(cwd, options);
977
- try {
978
- return await fn();
979
- } finally {
980
- await lock.release();
1378
+ function classifyUpgradeLevel(installedVersion, candidateVersion) {
1379
+ const installed = semver3.parse(installedVersion);
1380
+ const candidate = semver3.parse(candidateVersion);
1381
+ if (!installed || !candidate) return void 0;
1382
+ if (candidate.major > installed.major) return "major";
1383
+ if (candidate.minor > installed.minor) return "minor";
1384
+ if (candidate.patch > installed.patch || candidate.version === installed.version) {
1385
+ return "patch";
981
1386
  }
1387
+ return void 0;
982
1388
  }
983
1389
 
984
- // src/remediation/tools/apply-version-bump.ts
985
- var applyVersionBumpTool = tool5({
986
- description: "Update package.json to use the safe version of a vulnerable package and run the project's package manager install. In dry-run mode, only reports what would change.",
987
- parameters: z5.object({
988
- cwd: z5.string().describe("Absolute path to the consumer project root"),
989
- packageManager: z5.enum(["npm", "pnpm", "yarn"]).optional().describe("Package manager used by the target project (auto-detected if omitted)"),
990
- packageName: z5.string().describe("The npm package to upgrade"),
991
- fromVersion: z5.string().describe("The currently installed vulnerable version"),
992
- toVersion: z5.string().describe("The safe target version to upgrade to"),
993
- dryRun: z5.boolean().default(false).describe("If true, report changes but do not write"),
994
- policy: z5.string().optional().describe("Optional path to .autoremediator policy file"),
995
- runTests: z5.boolean().default(false).describe("If true, run test validation after applying the fix")
996
- }),
997
- execute: async ({
998
- cwd,
999
- packageManager,
1000
- packageName,
1001
- fromVersion,
1002
- toVersion,
1003
- dryRun,
1004
- policy,
1005
- runTests
1006
- }) => {
1007
- const pm = packageManager ?? detectPackageManager(cwd);
1008
- const commands = getPackageManagerCommands(pm);
1009
- const pkgPath = join5(cwd, "package.json");
1010
- const loadedPolicy = loadPolicy(cwd, policy);
1011
- if (!isPackageAllowed(loadedPolicy, packageName)) {
1390
+ // src/remediation/local/primary-strategy.ts
1391
+ async function resolvePrimaryResult(params) {
1392
+ const { vulnerable, cwd, packageManager, dryRun, policy, runTests, constraints } = params;
1393
+ const pkg = vulnerable.installed;
1394
+ const firstPatchedVersion = vulnerable.affected.firstPatchedVersion;
1395
+ if (pkg.type === "indirect") {
1396
+ if (constraints.directDependenciesOnly) {
1012
1397
  return {
1013
- packageName,
1014
- strategy: "none",
1015
- fromVersion,
1016
- toVersion,
1017
- applied: false,
1018
- dryRun,
1019
- message: `Policy blocked changes for package "${packageName}".`
1398
+ steps: 0,
1399
+ result: {
1400
+ packageName: pkg.name,
1401
+ strategy: "none",
1402
+ fromVersion: pkg.version,
1403
+ applied: false,
1404
+ dryRun,
1405
+ unresolvedReason: "constraint-blocked",
1406
+ message: `Constraint blocked remediation for indirect dependency "${pkg.name}".`
1407
+ }
1020
1408
  };
1021
1409
  }
1022
- const isMajorBump = semver3.valid(fromVersion) && semver3.valid(toVersion) && semver3.major(toVersion) > semver3.major(fromVersion);
1023
- if (isMajorBump && !loadedPolicy.allowMajorBumps) {
1410
+ if (constraints.preferVersionBump) {
1024
1411
  return {
1025
- packageName,
1026
- strategy: "none",
1027
- fromVersion,
1028
- toVersion,
1029
- applied: false,
1030
- dryRun,
1031
- message: `Policy blocked major bump for "${packageName}" (${fromVersion} -> ${toVersion}).`
1412
+ steps: 0,
1413
+ result: {
1414
+ packageName: pkg.name,
1415
+ strategy: "none",
1416
+ fromVersion: pkg.version,
1417
+ applied: false,
1418
+ dryRun,
1419
+ unresolvedReason: "constraint-blocked",
1420
+ message: `Constraint prefers version-bump and rejected override remediation for "${pkg.name}".`
1421
+ }
1032
1422
  };
1033
1423
  }
1034
- let pkgJson;
1035
- try {
1036
- pkgJson = JSON.parse(readFileSync3(pkgPath, "utf8"));
1037
- } catch {
1424
+ if (!firstPatchedVersion) {
1038
1425
  return {
1039
- packageName,
1040
- strategy: "none",
1041
- fromVersion,
1042
- applied: false,
1043
- dryRun,
1044
- message: `Could not read package.json at "${pkgPath}".`
1426
+ steps: 0,
1427
+ result: {
1428
+ packageName: pkg.name,
1429
+ strategy: "none",
1430
+ fromVersion: pkg.version,
1431
+ applied: false,
1432
+ dryRun,
1433
+ unresolvedReason: "no-safe-version",
1434
+ message: `No firstPatchedVersion available for ${pkg.name}; cannot resolve deterministic override in local mode.`
1435
+ }
1045
1436
  };
1046
1437
  }
1047
- const depField = ["dependencies", "devDependencies", "peerDependencies"].find(
1048
- (f) => pkgJson[f]?.[packageName] !== void 0
1438
+ const safeUpgrade2 = await resolveSafeUpgradeVersion(
1439
+ pkg.name,
1440
+ pkg.version,
1441
+ firstPatchedVersion,
1442
+ vulnerable.affected.vulnerableRange
1049
1443
  );
1050
- if (!depField) {
1051
- return {
1052
- packageName,
1053
- strategy: "none",
1054
- fromVersion,
1055
- applied: false,
1056
- dryRun,
1057
- message: `"${packageName}" was not found in package.json dependencies (it may be a transitive dep). Cannot auto-bump.`
1058
- };
1059
- }
1060
- const currentRange = pkgJson[depField][packageName];
1061
- const prefixMatch = currentRange.match(/^([~^]?)/);
1062
- const prefix = prefixMatch?.[1] ?? "";
1063
- const newRange = `${prefix}${toVersion}`;
1064
- if (dryRun) {
1065
- const installCmd = commands.installPreferOffline.join(" ");
1066
- const testCmd = commands.test.join(" ");
1444
+ if (!safeUpgrade2.safeVersion) {
1067
1445
  return {
1068
- packageName,
1069
- strategy: "version-bump",
1070
- fromVersion,
1071
- toVersion,
1072
- applied: false,
1073
- dryRun: true,
1074
- message: `[DRY RUN] Would update ${depField}.${packageName}: "${currentRange}" -> "${newRange}", then run ${installCmd}${runTests ? ` and ${testCmd}` : ""}.`
1075
- };
1076
- }
1077
- return withRepoLock(cwd, async () => {
1078
- pkgJson[depField][packageName] = newRange;
1079
- writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
1080
- try {
1081
- const [installCmd, ...installArgs] = commands.installPreferOffline;
1082
- await execa2(installCmd, installArgs, {
1083
- cwd,
1084
- stdio: "pipe"
1085
- });
1086
- } catch (err) {
1087
- pkgJson[depField][packageName] = currentRange;
1088
- writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
1089
- const message = err instanceof Error ? err.message : String(err);
1090
- return {
1091
- packageName,
1092
- strategy: "version-bump",
1093
- fromVersion,
1094
- toVersion,
1446
+ steps: 1,
1447
+ result: {
1448
+ packageName: pkg.name,
1449
+ strategy: "none",
1450
+ fromVersion: pkg.version,
1095
1451
  applied: false,
1096
- dryRun: false,
1097
- message: `${commands.installPreferOffline.join(" ")} failed after updating "${packageName}" to ${toVersion}. Reverted. Error: ${message}`
1098
- };
1099
- }
1100
- if (runTests) {
1101
- try {
1102
- const [testCmd, ...testArgs] = commands.test;
1103
- await execa2(testCmd, testArgs, {
1104
- cwd,
1105
- stdio: "pipe"
1106
- });
1107
- } catch (err) {
1108
- pkgJson[depField][packageName] = currentRange;
1109
- writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
1110
- try {
1111
- const [rollbackCmd, ...rollbackArgs] = commands.installPreferOffline;
1112
- await execa2(rollbackCmd, rollbackArgs, {
1113
- cwd,
1114
- stdio: "pipe"
1115
- });
1116
- } catch {
1117
- }
1118
- const message = err instanceof Error ? err.message : String(err);
1119
- return {
1120
- packageName,
1121
- strategy: "version-bump",
1122
- fromVersion,
1123
- toVersion,
1124
- applied: false,
1125
- dryRun: false,
1126
- message: `${commands.test.join(" ")} failed after upgrading "${packageName}" to ${toVersion}. Rolled back to ${currentRange}. Error: ${message}`
1127
- };
1452
+ dryRun,
1453
+ unresolvedReason: "no-safe-version",
1454
+ message: `No safe override version found for ${pkg.name}.`
1128
1455
  }
1129
- }
1130
- return {
1131
- packageName,
1132
- strategy: "version-bump",
1133
- fromVersion,
1134
- toVersion,
1135
- applied: true,
1136
- dryRun: false,
1137
- message: `Successfully upgraded "${packageName}" from ${fromVersion} to ${toVersion}, ran ${commands.installPreferOffline.join(" ")}${runTests ? `, and passed ${commands.test.join(" ")}` : ""}.`
1138
1456
  };
1457
+ }
1458
+ const overrideResult = await applyPackageOverrideTool.execute({
1459
+ cwd,
1460
+ packageManager,
1461
+ packageName: pkg.name,
1462
+ fromVersion: pkg.version,
1463
+ toVersion: safeUpgrade2.safeVersion,
1464
+ dryRun,
1465
+ policy,
1466
+ runTests
1139
1467
  });
1468
+ return {
1469
+ steps: 2,
1470
+ result: overrideResult
1471
+ };
1140
1472
  }
1141
- });
1473
+ if (!firstPatchedVersion) {
1474
+ return {
1475
+ steps: 0,
1476
+ result: {
1477
+ packageName: pkg.name,
1478
+ strategy: "none",
1479
+ fromVersion: pkg.version,
1480
+ applied: false,
1481
+ dryRun,
1482
+ unresolvedReason: "no-safe-version",
1483
+ message: `No firstPatchedVersion available for ${pkg.name}; cannot resolve deterministic upgrade in local mode.`
1484
+ }
1485
+ };
1486
+ }
1487
+ const safeUpgrade = await resolveSafeUpgradeVersion(
1488
+ pkg.name,
1489
+ pkg.version,
1490
+ firstPatchedVersion,
1491
+ vulnerable.affected.vulnerableRange
1492
+ );
1493
+ if (!safeUpgrade.safeVersion) {
1494
+ return {
1495
+ steps: 1,
1496
+ result: {
1497
+ packageName: pkg.name,
1498
+ strategy: "none",
1499
+ fromVersion: pkg.version,
1500
+ applied: false,
1501
+ dryRun,
1502
+ unresolvedReason: "no-safe-version",
1503
+ message: `No safe upgrade version found for ${pkg.name}.`
1504
+ }
1505
+ };
1506
+ }
1507
+ const applyResult = await applyVersionBumpTool.execute({
1508
+ cwd,
1509
+ packageManager,
1510
+ packageName: pkg.name,
1511
+ fromVersion: pkg.version,
1512
+ toVersion: safeUpgrade.safeVersion,
1513
+ dryRun,
1514
+ policy,
1515
+ runTests
1516
+ });
1517
+ return {
1518
+ steps: 2,
1519
+ result: applyResult
1520
+ };
1521
+ }
1142
1522
 
1143
1523
  // src/remediation/tools/fetch-package-source.ts
1144
- import { tool as tool6 } from "ai";
1145
- import { z as z6 } from "zod";
1146
- import { mkdir as mkdir2, readdir, readFile, rm as rm2 } from "fs/promises";
1147
- import { join as join6 } from "path";
1148
- import { execa as execa3 } from "execa";
1149
- var fetchPackageSourceTool = tool6({
1524
+ import { tool as tool5 } from "ai";
1525
+ import { z as z5 } from "zod";
1526
+ import { mkdir as mkdir3, readdir, readFile as readFile2, rm as rm3 } from "fs/promises";
1527
+ import { join as join9 } from "path";
1528
+ import { execa as execa6 } from "execa";
1529
+ var fetchPackageSourceTool = tool5({
1150
1530
  description: "Download package tarball from npm and extract source files for CVE analysis. Supports custom file patterns (default: *.js, *.ts).",
1151
- parameters: z6.object({
1152
- packageName: z6.string().min(1).describe("The npm package name (e.g., 'lodash', '@scope/package')"),
1153
- version: z6.string().regex(/^\d+\.\d+\.\d+/, "Must be a valid semver version").describe("Exact package version to download"),
1154
- filePatterns: z6.array(z6.string()).optional().default(["*.js", "*.ts"]).describe(
1531
+ parameters: z5.object({
1532
+ packageName: z5.string().min(1).describe("The npm package name (e.g., 'lodash', '@scope/package')"),
1533
+ version: z5.string().regex(/^\d+\.\d+\.\d+/, "Must be a valid semver version").describe("Exact package version to download"),
1534
+ filePatterns: z5.array(z5.string()).optional().default(["*.js", "*.ts"]).describe(
1155
1535
  "File patterns to extract (glob patterns, default: *.js, *.ts)"
1156
1536
  )
1157
1537
  }),
@@ -1161,23 +1541,23 @@ var fetchPackageSourceTool = tool6({
1161
1541
  filePatterns
1162
1542
  }) => {
1163
1543
  const tempBaseDir = `/tmp/autoremediator-pkg-${Date.now()}`;
1164
- const extractDir = join6(tempBaseDir, "out");
1544
+ const extractDir = join9(tempBaseDir, "out");
1165
1545
  try {
1166
1546
  const npmUrl = `https://registry.npmjs.org/${packageName}/-/${packageName.split("/").pop()}-${version}.tgz`;
1167
- await mkdir2(tempBaseDir, { recursive: true });
1168
- const tarballPath = join6(tempBaseDir, "package.tgz");
1169
- await execa3("curl", ["-L", "-o", tarballPath, npmUrl]);
1170
- await mkdir2(extractDir, { recursive: true });
1171
- await execa3("tar", ["-xzf", tarballPath, "-C", extractDir]);
1547
+ await mkdir3(tempBaseDir, { recursive: true });
1548
+ const tarballPath = join9(tempBaseDir, "package.tgz");
1549
+ await execa6("curl", ["-L", "-o", tarballPath, npmUrl]);
1550
+ await mkdir3(extractDir, { recursive: true });
1551
+ await execa6("tar", ["-xzf", tarballPath, "-C", extractDir]);
1172
1552
  const extractedContents = await readdir(extractDir);
1173
- const packageRootDir = extractedContents.includes("package") ? join6(extractDir, "package") : extractDir;
1553
+ const packageRootDir = extractedContents.includes("package") ? join9(extractDir, "package") : extractDir;
1174
1554
  const sourceCode = {};
1175
1555
  async function walkDir(dir, relativeBase) {
1176
1556
  try {
1177
1557
  const files = await readdir(dir, { withFileTypes: true });
1178
1558
  for (const file of files) {
1179
- const fullPath = join6(dir, file.name);
1180
- const relPath = join6(relativeBase, file.name);
1559
+ const fullPath = join9(dir, file.name);
1560
+ const relPath = join9(relativeBase, file.name);
1181
1561
  if (file.isDirectory()) {
1182
1562
  if (![
1183
1563
  "node_modules",
@@ -1199,7 +1579,7 @@ var fetchPackageSourceTool = tool6({
1199
1579
  });
1200
1580
  if (matches) {
1201
1581
  try {
1202
- const content = await readFile(fullPath, "utf8");
1582
+ const content = await readFile2(fullPath, "utf8");
1203
1583
  sourceCode[relPath] = content;
1204
1584
  } catch {
1205
1585
  }
@@ -1234,14 +1614,14 @@ var fetchPackageSourceTool = tool6({
1234
1614
  error: `Failed to fetch and extract package ${packageName}@${version}: ${message}`
1235
1615
  };
1236
1616
  } finally {
1237
- await rm2(tempBaseDir, { recursive: true, force: true });
1617
+ await rm3(tempBaseDir, { recursive: true, force: true });
1238
1618
  }
1239
1619
  }
1240
1620
  });
1241
1621
 
1242
1622
  // src/remediation/tools/generate-patch.ts
1243
- import { tool as tool7 } from "ai";
1244
- import { z as z7 } from "zod";
1623
+ import { tool as tool6 } from "ai";
1624
+ import { z as z6 } from "zod";
1245
1625
  import { generateText } from "ai";
1246
1626
  var VULNERABILITY_DESCRIPTIONS = {
1247
1627
  redos: "Regular Expression Denial of Service (ReDoS): The vulnerability is caused by poorly constructed regular expressions that cause excessive backtracking when processing certain inputs. The fix should optimize the regex to avoid catastrophic backtracking or replace it with a safer alternative.",
@@ -1249,18 +1629,18 @@ var VULNERABILITY_DESCRIPTIONS = {
1249
1629
  "path-traversal": "Path Traversal: The vulnerability allows access to files outside intended directories through path traversal sequences (../, etc.). The fix must validate and normalize file paths, use path.resolve() and path.relative() checks.",
1250
1630
  unknown: "Unknown vulnerability type: Analyze the CVE summary carefully and implement the most appropriate fix for the security issue described."
1251
1631
  };
1252
- var generatePatchTool = tool7({
1632
+ var generatePatchTool = tool6({
1253
1633
  description: "Generate a unified diff patch for a CVE using LLM analysis of vulnerable source code.",
1254
- parameters: z7.object({
1255
- packageName: z7.string().min(1).describe("The npm package name"),
1256
- vulnerableVersion: z7.string().describe("The vulnerable version string"),
1257
- cveId: z7.string().regex(/^CVE-\d{4}-\d+$/i).describe("CVE ID (e.g., CVE-2021-23337)"),
1258
- cveSummary: z7.string().min(10).describe("CVE description and impact"),
1259
- sourceFiles: z7.record(z7.string()).describe(
1634
+ parameters: z6.object({
1635
+ packageName: z6.string().min(1).describe("The npm package name"),
1636
+ vulnerableVersion: z6.string().describe("The vulnerable version string"),
1637
+ cveId: z6.string().regex(/^CVE-\d{4}-\d+$/i).describe("CVE ID (e.g., CVE-2021-23337)"),
1638
+ cveSummary: z6.string().min(10).describe("CVE description and impact"),
1639
+ sourceFiles: z6.record(z6.string()).describe(
1260
1640
  "Map of file paths to source code contents from fetch-package-source"
1261
1641
  ),
1262
- vulnerabilityCategory: z7.enum(["redos", "code-injection", "path-traversal", "unknown"]).optional().default("unknown").describe("Category of the vulnerability for better context"),
1263
- dryRun: z7.boolean().optional().default(false).describe("If true, return analysis without generating patches")
1642
+ vulnerabilityCategory: z6.enum(["redos", "code-injection", "path-traversal", "unknown"]).optional().default("unknown").describe("Category of the vulnerability for better context"),
1643
+ dryRun: z6.boolean().optional().default(false).describe("If true, return analysis without generating patches")
1264
1644
  }),
1265
1645
  execute: async ({
1266
1646
  packageName,
@@ -1446,589 +1826,730 @@ function generateUnifiedDiff(original, fixed, filePath) {
1446
1826
  return diff.join("\n");
1447
1827
  }
1448
1828
 
1449
- // src/remediation/tools/apply-patch-file.ts
1450
- import { tool as tool8 } from "ai";
1451
- import { z as z8 } from "zod";
1452
- import { existsSync as existsSync3 } from "fs";
1453
- import { mkdir as mkdir3, mkdtemp, readFile as readFile2, rm as rm3, writeFile } from "fs/promises";
1454
- import { tmpdir } from "os";
1455
- import { join as join7 } from "path";
1456
- import { execa as execa4 } from "execa";
1457
- var applyPatchFileTool = tool8({
1458
- description: "Write generated patch file and apply it using package-manager-native patch flow when available, falling back to patch-package when needed.",
1459
- parameters: z8.object({
1460
- packageName: z8.string().min(1).describe("The npm package name"),
1461
- vulnerableVersion: z8.string().describe("The vulnerable version string"),
1462
- patchContent: z8.string().min(10).optional().describe("Unified diff patch content from generate-patch"),
1463
- patches: z8.array(
1464
- z8.object({
1465
- filePath: z8.string().min(1),
1466
- unifiedDiff: z8.string().min(10)
1467
- })
1468
- ).optional().describe("Patch list from generate-patch; first patch is applied"),
1469
- patchesDir: z8.string().optional().default("./patches").describe("Directory to store patch files"),
1470
- cwd: z8.string().describe("Project root directory (for package.json)"),
1471
- packageManager: z8.enum(["npm", "pnpm", "yarn"]).optional().describe("Package manager used by the target project (auto-detected if omitted)"),
1472
- validateWithTests: z8.boolean().optional().default(true).describe("Run package manager test command to validate patch doesn't break anything"),
1473
- dryRun: z8.boolean().optional().default(false).describe("If true, report but do not mutate files")
1474
- }).refine((value) => Boolean(value.patchContent || value.patches && value.patches.length > 0), {
1475
- message: "Either patchContent or patches must be provided"
1476
- }),
1477
- execute: async ({
1478
- packageName,
1479
- vulnerableVersion,
1480
- patchContent,
1481
- patches,
1482
- patchesDir,
1483
- cwd,
1484
- packageManager,
1485
- validateWithTests,
1486
- dryRun
1487
- }) => {
1488
- try {
1489
- const pm = packageManager ?? detectPackageManager(cwd);
1490
- const selectedPatch = patchContent ?? patches?.[0]?.unifiedDiff;
1491
- if (!selectedPatch) {
1492
- return {
1493
- success: false,
1494
- packageName,
1495
- vulnerableVersion,
1496
- applied: false,
1497
- dryRun,
1498
- message: "No patch content provided.",
1499
- error: "No patch content provided."
1500
- };
1829
+ // src/remediation/local/fallback.ts
1830
+ function shouldAttemptPatchFallback(result, preferVersionBump) {
1831
+ if (preferVersionBump) return false;
1832
+ if (result.applied || result.dryRun) return false;
1833
+ return result.unresolvedReason === "no-safe-version" || result.unresolvedReason === "install-failed" || result.unresolvedReason === "override-apply-failed" || result.unresolvedReason === "validation-failed" || result.unresolvedReason === "major-bump-required" || result.unresolvedReason === "indirect-dependency";
1834
+ }
1835
+ async function tryLocalPatchFallback(params) {
1836
+ let steps = 0;
1837
+ const sourceResult = await fetchPackageSourceTool.execute({
1838
+ packageName: params.packageName,
1839
+ version: params.vulnerableVersion
1840
+ });
1841
+ steps += 1;
1842
+ if (!sourceResult?.success || !sourceResult.sourceFiles) {
1843
+ return {
1844
+ steps,
1845
+ result: {
1846
+ packageName: params.packageName,
1847
+ strategy: "none",
1848
+ fromVersion: params.vulnerableVersion,
1849
+ applied: false,
1850
+ dryRun: params.dryRun,
1851
+ unresolvedReason: "source-fetch-failed",
1852
+ message: sourceResult?.error ?? `Failed to fetch source for ${params.packageName}@${params.vulnerableVersion}.`
1501
1853
  }
1502
- const patchFileName = buildPatchFileName(packageName, vulnerableVersion);
1503
- const patchFilePath = join7(cwd, patchesDir, patchFileName);
1504
- if (dryRun) {
1505
- return {
1506
- success: true,
1507
- packageName,
1508
- vulnerableVersion,
1509
- applied: false,
1510
- dryRun: true,
1511
- message: `[DRY RUN] Would write and configure patch at ${patchFilePath}.`,
1512
- patchFilePath,
1513
- patchPath: patchFilePath
1514
- };
1854
+ };
1855
+ }
1856
+ const patchResult = await generatePatchTool.execute({
1857
+ packageName: params.packageName,
1858
+ vulnerableVersion: params.vulnerableVersion,
1859
+ cveId: params.cveId,
1860
+ cveSummary: params.cveSummary,
1861
+ sourceFiles: sourceResult.sourceFiles,
1862
+ vulnerabilityCategory: "unknown",
1863
+ dryRun: params.dryRun
1864
+ });
1865
+ steps += 1;
1866
+ if (!patchResult?.success) {
1867
+ const error = patchResult?.error ?? "Patch generation failed.";
1868
+ const unresolvedReason = error.includes("API_KEY") || error.includes("does not create a language model") ? "requires-llm-fallback" : "patch-generation-failed";
1869
+ return {
1870
+ steps,
1871
+ result: {
1872
+ packageName: params.packageName,
1873
+ strategy: "none",
1874
+ fromVersion: params.vulnerableVersion,
1875
+ applied: false,
1876
+ dryRun: params.dryRun,
1877
+ unresolvedReason,
1878
+ message: error
1515
1879
  }
1516
- return withRepoLock(cwd, async () => {
1517
- const patchesDirPath = join7(cwd, patchesDir);
1518
- await mkdir3(patchesDirPath, { recursive: true });
1519
- await writeFile(patchFilePath, selectedPatch, "utf8");
1520
- let validationResult;
1521
- const patchMode = await resolvePatchMode(pm, cwd);
1522
- const applyResult = patchMode === "patch-package" ? await configurePatchPackagePostinstall(cwd, pm) : await applyNativePatch({
1523
- cwd,
1524
- packageName,
1525
- vulnerableVersion,
1526
- patchContent: selectedPatch,
1527
- patchMode
1528
- });
1529
- if (!applyResult.success) {
1530
- return {
1531
- success: false,
1532
- packageName,
1533
- vulnerableVersion,
1534
- applied: false,
1535
- dryRun: false,
1536
- message: applyResult.error,
1537
- patchFilePath,
1538
- patchPath: patchFilePath,
1539
- patchMode,
1540
- postinstallConfigured: patchMode === "patch-package" ? false : void 0,
1541
- error: applyResult.error
1542
- };
1543
- }
1544
- if (validateWithTests) {
1545
- validationResult = await validatePatchWithTests(cwd, pm);
1546
- if (!validationResult.passed) {
1547
- const validationError = "Patch validation failed after apply; patch marked unresolved.";
1548
- return {
1549
- success: false,
1550
- packageName,
1551
- vulnerableVersion,
1552
- applied: false,
1553
- dryRun: false,
1554
- message: validationError,
1555
- patchFilePath,
1556
- patchPath: patchFilePath,
1557
- patchMode,
1558
- postinstallConfigured: patchMode === "patch-package",
1559
- validation: validationResult,
1560
- error: validationError
1561
- };
1562
- }
1563
- }
1564
- return {
1565
- success: true,
1566
- packageName,
1567
- vulnerableVersion,
1568
- applied: true,
1569
- dryRun: false,
1570
- message: `Patch applied successfully for ${packageName}@${vulnerableVersion}.`,
1571
- patchFilePath,
1572
- patchPath: patchFilePath,
1573
- patchMode,
1574
- postinstallConfigured: patchMode === "patch-package",
1575
- validation: validationResult
1576
- };
1577
- });
1578
- } catch (err) {
1579
- const message = err instanceof Error ? err.message : String(err);
1580
- return {
1581
- success: false,
1582
- packageName,
1583
- vulnerableVersion,
1880
+ };
1881
+ }
1882
+ if (typeof patchResult.confidence === "number" && patchResult.confidence < 0.7) {
1883
+ return {
1884
+ steps,
1885
+ result: {
1886
+ packageName: params.packageName,
1887
+ strategy: "none",
1888
+ fromVersion: params.vulnerableVersion,
1584
1889
  applied: false,
1890
+ dryRun: params.dryRun,
1891
+ unresolvedReason: "patch-confidence-too-low",
1892
+ message: `Patch confidence ${patchResult.confidence.toFixed(2)} is below threshold 0.70.`
1893
+ }
1894
+ };
1895
+ }
1896
+ const applyResult = await applyPatchFileTool.execute({
1897
+ packageName: params.packageName,
1898
+ vulnerableVersion: params.vulnerableVersion,
1899
+ patchContent: patchResult.patchContent,
1900
+ patches: patchResult.patches,
1901
+ patchesDir: params.patchesDir,
1902
+ cwd: params.cwd,
1903
+ packageManager: params.packageManager,
1904
+ validateWithTests: params.runTests,
1905
+ dryRun: params.dryRun
1906
+ });
1907
+ steps += 1;
1908
+ return {
1909
+ steps,
1910
+ result: {
1911
+ packageName: params.packageName,
1912
+ strategy: "patch-file",
1913
+ fromVersion: params.vulnerableVersion,
1914
+ patchFilePath: applyResult.patchFilePath ?? applyResult.patchPath,
1915
+ applied: Boolean(applyResult.applied),
1916
+ dryRun: Boolean(applyResult.dryRun),
1917
+ unresolvedReason: !Boolean(applyResult.applied) && !Boolean(applyResult.dryRun) ? applyResult.validation?.passed === false ? "patch-validation-failed" : "patch-apply-failed" : void 0,
1918
+ message: applyResult.message ?? applyResult.error ?? "Patch-file strategy finished.",
1919
+ validation: typeof applyResult.validation?.passed === "boolean" ? {
1920
+ passed: applyResult.validation.passed,
1921
+ error: applyResult.validation.error
1922
+ } : void 0
1923
+ }
1924
+ };
1925
+ }
1926
+
1927
+ // src/remediation/local/run.ts
1928
+ async function runLocalRemediationPipeline(cveId, options = {}) {
1929
+ const cwd = options.cwd ?? process.cwd();
1930
+ const packageManager = options.packageManager ?? detectPackageManager(cwd);
1931
+ const preview = options.preview ?? false;
1932
+ const dryRun = (options.dryRun ?? false) || preview;
1933
+ const runTests = options.runTests ?? false;
1934
+ const policy = options.policy ?? "";
1935
+ const patchesDir = options.patchesDir || "./patches";
1936
+ const constraints = options.constraints ?? {};
1937
+ const collectedResults = [];
1938
+ const vulnerablePackages = [];
1939
+ let cveDetails = null;
1940
+ let agentSteps = 0;
1941
+ const normalizedId = cveId.toUpperCase();
1942
+ const [osvDetails, ghPackages] = await Promise.all([
1943
+ lookupCveOsv(normalizedId),
1944
+ lookupCveGitHub(normalizedId).catch(() => [])
1945
+ ]);
1946
+ agentSteps += 2;
1947
+ if (!osvDetails && ghPackages.length === 0) {
1948
+ return {
1949
+ cveId,
1950
+ cveDetails: null,
1951
+ vulnerablePackages,
1952
+ results: collectedResults,
1953
+ agentSteps,
1954
+ summary: `Local mode failed at lookup-cve: ${normalizedId} not found in OSV or GitHub advisory data.`,
1955
+ correlation: {
1956
+ requestId: options.requestId,
1957
+ sessionId: options.sessionId,
1958
+ parentRunId: options.parentRunId
1959
+ }
1960
+ };
1961
+ }
1962
+ cveDetails = osvDetails ?? {
1963
+ id: normalizedId,
1964
+ summary: "Details sourced from GitHub Advisory Database.",
1965
+ severity: "UNKNOWN",
1966
+ references: [],
1967
+ affectedPackages: []
1968
+ };
1969
+ if (ghPackages.length > 0) {
1970
+ cveDetails = mergeGhDataIntoCveDetails(cveDetails, ghPackages);
1971
+ }
1972
+ cveDetails = await enrichWithNvd(cveDetails);
1973
+ if (cveDetails.affectedPackages.length === 0) {
1974
+ return {
1975
+ cveId,
1976
+ cveDetails,
1977
+ vulnerablePackages,
1978
+ results: collectedResults,
1979
+ agentSteps,
1980
+ summary: `Local mode lookup succeeded but no npm affected packages were found for ${normalizedId}.`,
1981
+ correlation: {
1982
+ requestId: options.requestId,
1983
+ sessionId: options.sessionId,
1984
+ parentRunId: options.parentRunId
1985
+ }
1986
+ };
1987
+ }
1988
+ const inventory = await checkInventoryTool.execute({ cwd, packageManager });
1989
+ agentSteps += 1;
1990
+ if (inventory?.error) {
1991
+ return {
1992
+ cveId,
1993
+ cveDetails,
1994
+ vulnerablePackages,
1995
+ results: collectedResults,
1996
+ agentSteps,
1997
+ summary: `Local mode failed at check-inventory: ${inventory.error}`,
1998
+ correlation: {
1999
+ requestId: options.requestId,
2000
+ sessionId: options.sessionId,
2001
+ parentRunId: options.parentRunId
2002
+ }
2003
+ };
2004
+ }
2005
+ const installedPackages = inventory.packages ?? [];
2006
+ for (const affected of cveDetails.affectedPackages) {
2007
+ if (!affected || typeof affected !== "object") continue;
2008
+ if (!affected.name || !affected.vulnerableRange) continue;
2009
+ if (affected.ecosystem !== "npm") continue;
2010
+ const matches = installedPackages.filter((pkg) => pkg.name === affected.name);
2011
+ for (const installed of matches) {
2012
+ if (!semver4.valid(installed.version)) continue;
2013
+ let isVulnerable = false;
2014
+ try {
2015
+ isVulnerable = semver4.satisfies(installed.version, affected.vulnerableRange, {
2016
+ includePrerelease: false
2017
+ });
2018
+ } catch {
2019
+ continue;
2020
+ }
2021
+ if (isVulnerable) {
2022
+ vulnerablePackages.push({ installed, affected });
2023
+ }
2024
+ }
2025
+ }
2026
+ agentSteps += 1;
2027
+ for (const vulnerable of vulnerablePackages) {
2028
+ const primary = await resolvePrimaryResult({
2029
+ vulnerable,
2030
+ cwd,
2031
+ packageManager,
2032
+ dryRun,
2033
+ policy,
2034
+ runTests,
2035
+ constraints
2036
+ });
2037
+ agentSteps += primary.steps;
2038
+ if (shouldAttemptPatchFallback(primary.result, constraints.preferVersionBump ?? false)) {
2039
+ const fallback = await tryLocalPatchFallback({
2040
+ cwd,
2041
+ packageManager,
2042
+ packageName: vulnerable.installed.name,
2043
+ vulnerableVersion: vulnerable.installed.version,
2044
+ cveId: normalizedId,
2045
+ cveSummary: cveDetails?.summary ?? normalizedId,
1585
2046
  dryRun,
1586
- message: `Failed to apply patch file: ${message}`,
1587
- error: `Failed to apply patch file: ${message}`
1588
- };
2047
+ runTests,
2048
+ patchesDir
2049
+ });
2050
+ agentSteps += fallback.steps;
2051
+ collectedResults.push(fallback.result);
2052
+ continue;
1589
2053
  }
2054
+ collectedResults.push(primary.result);
1590
2055
  }
1591
- });
1592
- async function resolvePatchMode(packageManager, cwd) {
1593
- if (packageManager === "npm") return "patch-package";
1594
- if (packageManager === "pnpm") return "native-pnpm";
2056
+ const appliedCount = collectedResults.filter((result) => result.applied).length;
2057
+ const unresolvedCount = collectedResults.filter((result) => !result.applied && !result.dryRun).length;
2058
+ const dryRunCount = collectedResults.filter((result) => result.dryRun).length;
2059
+ return {
2060
+ cveId,
2061
+ cveDetails,
2062
+ vulnerablePackages,
2063
+ results: collectedResults,
2064
+ agentSteps,
2065
+ summary: `Local mode completed: vulnerable=${vulnerablePackages.length}, applied=${appliedCount}, dryRun=${dryRunCount}, unresolved=${unresolvedCount}`,
2066
+ correlation: {
2067
+ requestId: options.requestId,
2068
+ sessionId: options.sessionId,
2069
+ parentRunId: options.parentRunId
2070
+ }
2071
+ };
2072
+ }
2073
+
2074
+ // src/remediation/tools/lookup-cve.ts
2075
+ import { tool as tool7 } from "ai";
2076
+ import { z as z7 } from "zod";
2077
+
2078
+ // src/intelligence/sources/cisa-kev.ts
2079
+ var CISA_KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json";
2080
+ async function fetchCisaKevFeed() {
1595
2081
  try {
1596
- const result = await execa4("yarn", ["--version"], {
1597
- cwd,
1598
- stdio: "pipe"
2082
+ const res = await fetch(CISA_KEV_URL, {
2083
+ headers: { Accept: "application/json" }
1599
2084
  });
1600
- const version = result.stdout.trim();
1601
- const major = Number.parseInt(version.split(".")[0] || "0", 10);
1602
- return major >= 2 ? "native-yarn" : "patch-package";
2085
+ if (!res.ok) return void 0;
2086
+ return await res.json();
1603
2087
  } catch {
1604
- return "patch-package";
2088
+ return void 0;
1605
2089
  }
1606
2090
  }
1607
- function buildPatchFileName(packageName, vulnerableVersion) {
1608
- const safeName = packageName.replace(/^@/, "").replace(/\//g, "+");
1609
- return `${safeName}+${vulnerableVersion}.patch`;
2091
+ function findKevEntry(feed, cveId) {
2092
+ if (!feed?.vulnerabilities?.length) return void 0;
2093
+ const normalized = cveId.toUpperCase();
2094
+ return feed.vulnerabilities.find((v) => v.cveID.toUpperCase() === normalized);
1610
2095
  }
1611
- async function configurePatchPackagePostinstall(cwd, packageManager) {
1612
- const pkgJsonPath = join7(cwd, "package.json");
1613
- let pkgJson;
2096
+ async function enrichWithCisaKev(details) {
2097
+ const feed = await fetchCisaKevFeed();
2098
+ const entry = findKevEntry(feed, details.id);
2099
+ if (!entry) return details;
2100
+ details.kev = {
2101
+ knownExploited: true,
2102
+ dateAdded: entry.dateAdded,
2103
+ dueDate: entry.dueDate,
2104
+ requiredAction: entry.requiredAction,
2105
+ knownRansomwareCampaignUse: entry.knownRansomwareCampaignUse
2106
+ };
2107
+ if (!details.references.includes(CISA_KEV_URL)) {
2108
+ details.references.push(CISA_KEV_URL);
2109
+ }
2110
+ return details;
2111
+ }
2112
+
2113
+ // src/intelligence/sources/epss.ts
2114
+ async function fetchEpss(cveId) {
2115
+ const { epssApi } = getIntelligenceSourceConfig();
2116
+ if (!epssApi) return void 0;
1614
2117
  try {
1615
- pkgJson = JSON.parse(await readFile2(pkgJsonPath, "utf8"));
2118
+ const url = new URL(epssApi);
2119
+ url.searchParams.set("cve", cveId);
2120
+ const res = await fetch(url.toString(), {
2121
+ headers: { Accept: "application/json" }
2122
+ });
2123
+ if (!res.ok) return void 0;
2124
+ const body = await res.json();
2125
+ return body.data?.[0];
1616
2126
  } catch {
1617
- return {
1618
- success: false,
1619
- error: `Could not read package.json at ${pkgJsonPath}`
1620
- };
1621
- }
1622
- const devDependencies = pkgJson.devDependencies ?? {};
1623
- if (!devDependencies["patch-package"]) {
1624
- try {
1625
- const commands = getPackageManagerCommands(packageManager);
1626
- const [cmd, ...args] = commands.installDev("patch-package");
1627
- await execa4(cmd, args, {
1628
- cwd,
1629
- stdio: "pipe"
1630
- });
1631
- } catch (err) {
1632
- return {
1633
- success: false,
1634
- error: `Failed to install patch-package: ${err instanceof Error ? err.message : String(err)}`
1635
- };
1636
- }
2127
+ return void 0;
1637
2128
  }
1638
- if (!pkgJson.scripts) {
1639
- pkgJson.scripts = {};
2129
+ }
2130
+ async function enrichWithEpss(details) {
2131
+ const row = await fetchEpss(details.id);
2132
+ if (!row) return details;
2133
+ const score = Number.parseFloat(row.epss);
2134
+ const percentile = Number.parseFloat(row.percentile);
2135
+ if (!Number.isFinite(score) || !Number.isFinite(percentile)) {
2136
+ return details;
1640
2137
  }
1641
- const patchApplyCmd = "patch-package";
1642
- const currentPostinstall = pkgJson.scripts.postinstall || "";
1643
- if (currentPostinstall && !currentPostinstall.includes("patch-package")) {
1644
- pkgJson.scripts.postinstall = `${currentPostinstall} && ${patchApplyCmd}`;
1645
- } else if (!currentPostinstall) {
1646
- pkgJson.scripts.postinstall = patchApplyCmd;
2138
+ details.epss = {
2139
+ score,
2140
+ percentile,
2141
+ date: row.date
2142
+ };
2143
+ return details;
2144
+ }
2145
+
2146
+ // src/intelligence/sources/cve-services.ts
2147
+ function pickEnglishDescription(container) {
2148
+ if (!container?.descriptions?.length) return void 0;
2149
+ const en = container.descriptions.find((d) => d.lang === "en" && d.value);
2150
+ return (en?.value ?? container.descriptions[0]?.value)?.trim() || void 0;
2151
+ }
2152
+ function collectReferences(record) {
2153
+ const refs = /* @__PURE__ */ new Set();
2154
+ const cnaRefs = record.containers?.cna?.references ?? [];
2155
+ const adpRefs = (record.containers?.adp ?? []).flatMap((c) => c.references ?? []);
2156
+ for (const ref of [...cnaRefs, ...adpRefs]) {
2157
+ if (ref.url) refs.add(ref.url);
1647
2158
  }
1648
- await writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
1649
- return { success: true };
2159
+ return Array.from(refs);
1650
2160
  }
1651
- async function applyNativePatch(params) {
1652
- const { cwd, packageName, vulnerableVersion, patchContent, patchMode } = params;
1653
- const packageSpec = `${packageName}@${vulnerableVersion}`;
1654
- const createCommand = patchMode === "native-pnpm" ? "pnpm" : "yarn";
1655
- const createArgs = ["patch", packageSpec];
1656
- let patchDir;
2161
+ async function fetchCveServicesRecord(cveId) {
2162
+ const { cveServicesApi } = getIntelligenceSourceConfig();
2163
+ if (!cveServicesApi) return void 0;
1657
2164
  try {
1658
- const createResult = await execa4(createCommand, createArgs, {
1659
- cwd,
1660
- stdio: "pipe"
2165
+ const res = await fetch(`${cveServicesApi}/${encodeURIComponent(cveId)}`, {
2166
+ headers: { Accept: "application/json" }
1661
2167
  });
1662
- patchDir = extractPatchDirectory(`${createResult.stdout}
1663
- ${createResult.stderr}`);
1664
- } catch (err) {
1665
- return {
1666
- success: false,
1667
- error: `Failed to create native patch workspace for ${packageSpec}: ${err instanceof Error ? err.message : String(err)}`
1668
- };
2168
+ if (!res.ok) return void 0;
2169
+ return await res.json();
2170
+ } catch {
2171
+ return void 0;
1669
2172
  }
1670
- if (!patchDir) {
1671
- return {
1672
- success: false,
1673
- error: `Could not determine native patch directory for ${packageSpec}.`
1674
- };
2173
+ }
2174
+ async function enrichWithCveServices(details) {
2175
+ const record = await fetchCveServicesRecord(details.id);
2176
+ if (!record) return details;
2177
+ const summary = pickEnglishDescription(record.containers?.cna);
2178
+ if (summary && (!details.summary || details.summary.includes("No summary available"))) {
2179
+ details.summary = summary;
1675
2180
  }
1676
- const tempPatchDir = await mkdtemp(join7(tmpdir(), "autoremediator-native-patch-"));
1677
- const tempPatchFile = join7(tempPatchDir, "change.patch");
2181
+ const refs = collectReferences(record);
2182
+ if (refs.length > 0) {
2183
+ const merged = /* @__PURE__ */ new Set([...details.references, ...refs]);
2184
+ details.references = Array.from(merged);
2185
+ }
2186
+ details.intelligence = {
2187
+ ...details.intelligence ?? {},
2188
+ cveServicesEnriched: true
2189
+ };
2190
+ return details;
2191
+ }
2192
+
2193
+ // src/intelligence/sources/gitlab-advisory.ts
2194
+ function advisoryMatchesCve(advisory, cveId) {
2195
+ const normalized = cveId.toUpperCase();
2196
+ return (advisory.identifiers ?? []).some(
2197
+ (id) => id.type?.toUpperCase() === "CVE" && id.value?.toUpperCase() === normalized
2198
+ );
2199
+ }
2200
+ async function fetchGitLabAdvisories(cveId) {
2201
+ const { gitLabAdvisoryApi } = getIntelligenceSourceConfig();
2202
+ if (!gitLabAdvisoryApi) return [];
1678
2203
  try {
1679
- await writeFile(tempPatchFile, patchContent, "utf8");
1680
- await execa4("patch", ["-p1", "-i", tempPatchFile], {
1681
- cwd: patchDir,
1682
- stdio: "pipe"
1683
- });
1684
- const commitCommand = patchMode === "native-pnpm" ? "pnpm" : "yarn";
1685
- const commitArgs = patchMode === "native-pnpm" ? ["patch-commit", patchDir] : ["patch-commit", "-s", patchDir];
1686
- await execa4(commitCommand, commitArgs, {
1687
- cwd,
1688
- stdio: "pipe"
2204
+ const url = new URL(gitLabAdvisoryApi);
2205
+ url.searchParams.set("identifier", cveId);
2206
+ url.searchParams.set("ecosystem", "npm");
2207
+ const res = await fetch(url.toString(), {
2208
+ headers: { Accept: "application/json" }
1689
2209
  });
1690
- } catch (err) {
1691
- return {
1692
- success: false,
1693
- error: `Failed to apply native patch for ${packageSpec}: ${err instanceof Error ? err.message : String(err)}`
1694
- };
1695
- } finally {
1696
- await rm3(tempPatchDir, { recursive: true, force: true });
2210
+ if (!res.ok) return [];
2211
+ const body = await res.json();
2212
+ return Array.isArray(body) ? body : [];
2213
+ } catch {
2214
+ return [];
1697
2215
  }
1698
- return { success: true };
1699
2216
  }
1700
- function extractPatchDirectory(output) {
1701
- const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1702
- for (const line of lines) {
1703
- if (existsSync3(line)) {
1704
- return line;
1705
- }
1706
- const tokens = line.split(/\s+/).map((token) => token.replace(/^['"]|['"]$/g, ""));
1707
- for (const token of tokens) {
1708
- if (token.startsWith("/") && existsSync3(token)) {
1709
- return token;
1710
- }
1711
- }
2217
+ async function enrichWithGitLabAdvisory(details) {
2218
+ const advisories = await fetchGitLabAdvisories(details.id);
2219
+ const matched = advisories.filter((a) => advisoryMatchesCve(a, details.id));
2220
+ if (matched.length === 0) return details;
2221
+ const refs = matched.flatMap((m) => m.references ?? []);
2222
+ if (refs.length > 0) {
2223
+ const merged = /* @__PURE__ */ new Set([...details.references, ...refs]);
2224
+ details.references = Array.from(merged);
1712
2225
  }
1713
- return "";
2226
+ details.intelligence = {
2227
+ ...details.intelligence ?? {},
2228
+ gitlabAdvisoryMatched: true
2229
+ };
2230
+ return details;
1714
2231
  }
1715
- async function validatePatchWithTests(cwd, packageManager) {
2232
+
2233
+ // src/intelligence/sources/certcc.ts
2234
+ var CERTCC_HOME = "https://www.kb.cert.org/vuls/";
2235
+ async function findCertCcReference(cveId) {
2236
+ const { certCcSearchUrl } = getIntelligenceSourceConfig();
2237
+ if (!certCcSearchUrl) return void 0;
1716
2238
  try {
1717
- const commands = getPackageManagerCommands(packageManager);
1718
- const [cmd, ...args] = commands.test;
1719
- const result = await execa4(cmd, args, {
1720
- cwd,
1721
- timeout: 6e4,
1722
- // 60 second timeout
1723
- stdio: "pipe"
2239
+ const url = new URL(certCcSearchUrl);
2240
+ url.searchParams.set("query", cveId);
2241
+ const res = await fetch(url.toString(), {
2242
+ headers: { Accept: "text/html" }
1724
2243
  });
1725
- return {
1726
- passed: true,
1727
- output: result.stdout
1728
- };
1729
- } catch (err) {
1730
- const errorOutput = err instanceof Error && "stdout" in err ? err.stdout : "";
1731
- const failedTests = extractFailedTests(errorOutput);
1732
- return {
1733
- passed: false,
1734
- output: errorOutput,
1735
- failedTests
1736
- };
2244
+ if (!res.ok) return void 0;
2245
+ const html = await res.text();
2246
+ const match = html.match(/https:\/\/www\.kb\.cert\.org\/vuls\/id\/\d+/i);
2247
+ return match?.[0] ?? void 0;
2248
+ } catch {
2249
+ return void 0;
1737
2250
  }
1738
2251
  }
1739
- function extractFailedTests(output) {
1740
- const failedTests = [];
1741
- const patterns = [
1742
- /✖\s+(.+?)(?:\n|$)/g,
1743
- // Mocha style
1744
- /●\s+(.+)(?:\n|$)/g,
1745
- // Jest style
1746
- /FAIL.*?(.+?)(?:\n|$)/g
1747
- // Generic FAIL
1748
- ];
1749
- for (const pattern of patterns) {
1750
- let match;
1751
- while ((match = pattern.exec(output)) !== null) {
1752
- if (match[1]) {
1753
- failedTests.push(match[1].trim());
1754
- }
1755
- }
2252
+ async function enrichWithCertCc(details) {
2253
+ const ref = await findCertCcReference(details.id);
2254
+ if (!ref) return details;
2255
+ if (!details.references.includes(ref)) {
2256
+ details.references.push(ref);
1756
2257
  }
1757
- return failedTests.slice(0, 5);
2258
+ details.intelligence = {
2259
+ ...details.intelligence ?? {},
2260
+ certCcMatched: true
2261
+ };
2262
+ if (!details.references.includes(CERTCC_HOME)) {
2263
+ details.references.push(CERTCC_HOME);
2264
+ }
2265
+ return details;
1758
2266
  }
1759
2267
 
1760
- // src/remediation/pipeline.ts
1761
- async function runRemediationPipeline(cveId, options = {}) {
1762
- const provider = resolveProvider(options);
1763
- if (provider === "local") {
1764
- return runLocalRemediationPipeline(cveId, options);
2268
+ // src/intelligence/sources/deps-dev.ts
2269
+ async function fetchDepsDevPackage(name) {
2270
+ const { depsDevApi } = getIntelligenceSourceConfig();
2271
+ if (!depsDevApi) return false;
2272
+ try {
2273
+ const url = `${depsDevApi}/systems/npm/packages/${encodeURIComponent(name)}`;
2274
+ const res = await fetch(url, { headers: { Accept: "application/json" } });
2275
+ return res.ok;
2276
+ } catch {
2277
+ return false;
1765
2278
  }
1766
- const cwd = options.cwd ?? process.cwd();
1767
- const packageManager = options.packageManager ?? detectPackageManager(cwd);
1768
- const preview = options.preview ?? false;
1769
- const dryRun = (options.dryRun ?? false) || preview;
1770
- const runTests = options.runTests ?? false;
1771
- const policy = options.policy ?? "";
1772
- const patchesDir = options.patchesDir || "./patches";
1773
- const model = await createModel(options);
1774
- const systemPrompt = loadOrchestrationPrompt({
1775
- cveId,
1776
- cwd,
1777
- dryRun,
1778
- runTests,
1779
- policy,
1780
- patchesDir,
1781
- packageManager
1782
- });
1783
- const prompt = `Patch vulnerable dependencies affected by ${cveId} in the project at: ${cwd}. Package manager: ${packageManager}.`;
1784
- const collectedResults = [];
1785
- const vulnerablePackages = [];
1786
- let cveDetails = null;
1787
- let agentSteps = 0;
1788
- const applyVersionBumpToolForRun = preview ? {
1789
- ...applyVersionBumpTool,
1790
- execute: async (input) => applyVersionBumpTool.execute({ ...input, dryRun: true })
1791
- } : applyVersionBumpTool;
1792
- const applyPatchFileToolForRun = preview ? {
1793
- ...applyPatchFileTool,
1794
- execute: async (input) => applyPatchFileTool.execute({ ...input, dryRun: true })
1795
- } : applyPatchFileTool;
1796
- const result = await generateText2({
1797
- model,
1798
- system: systemPrompt,
1799
- prompt,
1800
- tools: {
1801
- "lookup-cve": lookupCveTool,
1802
- "check-inventory": checkInventoryTool,
1803
- "check-version-match": checkVersionMatchTool,
1804
- "find-fixed-version": findFixedVersionTool,
1805
- "apply-version-bump": applyVersionBumpToolForRun,
1806
- "fetch-package-source": fetchPackageSourceTool,
1807
- "generate-patch": generatePatchTool,
1808
- "apply-patch-file": applyPatchFileToolForRun
1809
- },
1810
- maxSteps: 25,
1811
- onStepFinish(stepResult) {
1812
- agentSteps += 1;
1813
- const { toolResults } = stepResult;
1814
- for (const tr of toolResults ?? []) {
1815
- const toolResult = tr.result;
1816
- if (tr.toolName === "lookup-cve" && toolResult?.data) {
1817
- cveDetails = toolResult.data;
1818
- }
1819
- if (tr.toolName === "check-version-match" && toolResult?.vulnerablePackages) {
1820
- vulnerablePackages.push(...toolResult.vulnerablePackages);
1821
- }
1822
- if (tr.toolName === "apply-version-bump") {
1823
- collectedResults.push(toolResult);
1824
- }
1825
- if (tr.toolName === "apply-patch-file" && toolResult) {
1826
- const validation = toolResult.validation;
1827
- const message = typeof toolResult.message === "string" ? toolResult.message : typeof toolResult.error === "string" ? toolResult.error : "Patch-file strategy finished.";
1828
- collectedResults.push({
1829
- packageName: typeof toolResult.packageName === "string" ? toolResult.packageName : "unknown-package",
1830
- strategy: "patch-file",
1831
- fromVersion: typeof toolResult.vulnerableVersion === "string" ? toolResult.vulnerableVersion : "unknown",
1832
- patchFilePath: typeof toolResult.patchFilePath === "string" ? toolResult.patchFilePath : typeof toolResult.patchPath === "string" ? toolResult.patchPath : void 0,
1833
- applied: Boolean(toolResult.applied),
1834
- dryRun: Boolean(toolResult.dryRun),
1835
- message,
1836
- validation: validation && typeof validation.passed === "boolean" ? {
1837
- passed: validation.passed,
1838
- error: typeof validation.error === "string" ? validation.error : void 0
1839
- } : void 0
1840
- });
1841
- }
1842
- }
1843
- }
1844
- });
1845
- return {
1846
- cveId,
1847
- cveDetails,
1848
- vulnerablePackages,
1849
- results: collectedResults,
1850
- agentSteps,
1851
- summary: result.text,
1852
- correlation: {
1853
- requestId: options.requestId,
1854
- sessionId: options.sessionId,
1855
- parentRunId: options.parentRunId
1856
- }
2279
+ }
2280
+ async function enrichWithDepsDev(details) {
2281
+ const names = Array.from(new Set(details.affectedPackages.map((p) => p.name))).slice(0, 20);
2282
+ if (names.length === 0) return details;
2283
+ const checks = await Promise.all(names.map((name) => fetchDepsDevPackage(name)));
2284
+ const matched = checks.filter(Boolean).length;
2285
+ if (matched === 0) return details;
2286
+ details.intelligence = {
2287
+ ...details.intelligence ?? {},
2288
+ depsDevEnrichedPackages: matched
1857
2289
  };
2290
+ return details;
1858
2291
  }
1859
- async function runLocalRemediationPipeline(cveId, options = {}) {
1860
- const cwd = options.cwd ?? process.cwd();
1861
- const packageManager = options.packageManager ?? detectPackageManager(cwd);
1862
- const preview = options.preview ?? false;
1863
- const dryRun = (options.dryRun ?? false) || preview;
1864
- const runTests = options.runTests ?? false;
1865
- const policy = options.policy ?? "";
1866
- const collectedResults = [];
1867
- const vulnerablePackages = [];
1868
- let cveDetails = null;
1869
- let agentSteps = 0;
1870
- const normalizedId = cveId.toUpperCase();
1871
- const [osvDetails, ghPackages] = await Promise.all([
1872
- lookupCveOsv(normalizedId),
1873
- lookupCveGitHub(normalizedId).catch(() => [])
1874
- ]);
1875
- agentSteps += 2;
1876
- if (!osvDetails && ghPackages.length === 0) {
1877
- return {
1878
- cveId,
1879
- cveDetails: null,
1880
- vulnerablePackages,
1881
- results: collectedResults,
1882
- agentSteps,
1883
- summary: `Local mode failed at lookup-cve: ${normalizedId} not found in OSV or GitHub advisory data.`,
1884
- correlation: {
1885
- requestId: options.requestId,
1886
- sessionId: options.sessionId,
1887
- parentRunId: options.parentRunId
1888
- }
1889
- };
2292
+
2293
+ // src/intelligence/sources/ossf-scorecard.ts
2294
+ async function checkProject(project) {
2295
+ const { scorecardApi } = getIntelligenceSourceConfig();
2296
+ if (!scorecardApi) return false;
2297
+ try {
2298
+ const url = new URL(`${scorecardApi}/projects`);
2299
+ url.searchParams.set("project", project);
2300
+ const res = await fetch(url.toString(), {
2301
+ headers: { Accept: "application/json" }
2302
+ });
2303
+ return res.ok;
2304
+ } catch {
2305
+ return false;
1890
2306
  }
1891
- cveDetails = osvDetails ?? {
1892
- id: normalizedId,
1893
- summary: "Details sourced from GitHub Advisory Database.",
1894
- severity: "UNKNOWN",
1895
- references: [],
1896
- affectedPackages: []
2307
+ }
2308
+ async function enrichWithOssfScorecard(details) {
2309
+ const projects = Array.from(
2310
+ new Set(details.affectedPackages.map((p) => `github.com/${p.name}/${p.name}`))
2311
+ ).slice(0, 10);
2312
+ if (projects.length === 0) return details;
2313
+ const checks = await Promise.all(projects.map((project) => checkProject(project)));
2314
+ const matched = checks.filter(Boolean).length;
2315
+ if (matched === 0) return details;
2316
+ details.intelligence = {
2317
+ ...details.intelligence ?? {},
2318
+ scorecardProjects: matched
1897
2319
  };
1898
- if (ghPackages.length > 0) {
1899
- cveDetails = mergeGhDataIntoCveDetails(cveDetails, ghPackages);
2320
+ return details;
2321
+ }
2322
+
2323
+ // src/intelligence/sources/external-feeds.ts
2324
+ async function probeFeed(url, cveId, token) {
2325
+ try {
2326
+ const feedUrl = new URL(url);
2327
+ feedUrl.searchParams.set("cve", cveId);
2328
+ const headers = { Accept: "application/json" };
2329
+ if (token) headers.Authorization = `Bearer ${token}`;
2330
+ const res = await fetch(feedUrl.toString(), { headers });
2331
+ if (!res.ok) return void 0;
2332
+ return feedUrl.toString();
2333
+ } catch {
2334
+ return void 0;
1900
2335
  }
1901
- cveDetails = await enrichWithNvd(cveDetails);
1902
- if (cveDetails.affectedPackages.length === 0) {
1903
- return {
1904
- cveId,
1905
- cveDetails,
1906
- vulnerablePackages,
1907
- results: collectedResults,
1908
- agentSteps,
1909
- summary: `Local mode lookup succeeded but no npm affected packages were found for ${normalizedId}.`,
1910
- correlation: {
1911
- requestId: options.requestId,
1912
- sessionId: options.sessionId,
1913
- parentRunId: options.parentRunId
1914
- }
1915
- };
2336
+ }
2337
+ async function enrichWithExternalFeeds(details) {
2338
+ const {
2339
+ vendorAdvisoryFeeds,
2340
+ commercialFeeds,
2341
+ commercialFeedToken
2342
+ } = getIntelligenceSourceConfig();
2343
+ const vendorHits = (await Promise.all(vendorAdvisoryFeeds.map((url) => probeFeed(url, details.id)))).filter((v) => Boolean(v));
2344
+ const commercialHits = (await Promise.all(
2345
+ commercialFeeds.map((url) => probeFeed(url, details.id, commercialFeedToken))
2346
+ )).filter((v) => Boolean(v));
2347
+ if (vendorHits.length === 0 && commercialHits.length === 0) {
2348
+ return details;
1916
2349
  }
1917
- const inventory = await checkInventoryTool.execute({ cwd, packageManager });
1918
- agentSteps += 1;
1919
- if (inventory?.error) {
1920
- return {
1921
- cveId,
1922
- cveDetails,
1923
- vulnerablePackages,
1924
- results: collectedResults,
1925
- agentSteps,
1926
- summary: `Local mode failed at check-inventory: ${inventory.error}`,
1927
- correlation: {
1928
- requestId: options.requestId,
1929
- sessionId: options.sessionId,
1930
- parentRunId: options.parentRunId
1931
- }
2350
+ details.intelligence = {
2351
+ ...details.intelligence ?? {},
2352
+ vendorAdvisories: vendorHits.length > 0 ? vendorHits : details.intelligence?.vendorAdvisories,
2353
+ commercialFeeds: commercialHits.length > 0 ? commercialHits : details.intelligence?.commercialFeeds
2354
+ };
2355
+ const mergedRefs = /* @__PURE__ */ new Set([...details.references, ...vendorHits, ...commercialHits]);
2356
+ details.references = Array.from(mergedRefs);
2357
+ return details;
2358
+ }
2359
+
2360
+ // src/remediation/tools/lookup-cve.ts
2361
+ var lookupCveTool = tool7({
2362
+ description: "Look up a CVE ID and return the list of affected npm packages, their vulnerable version ranges, and the first patched version. Always call this first.",
2363
+ parameters: z7.object({
2364
+ cveId: z7.string().regex(/^CVE-\d{4}-\d+$/i, "Must be a valid CVE ID like CVE-2021-23337")
2365
+ }),
2366
+ execute: async ({ cveId }) => {
2367
+ const normalizedId = cveId.toUpperCase();
2368
+ const [osvDetails, ghPackages] = await Promise.all([
2369
+ lookupCveOsv(normalizedId),
2370
+ lookupCveGitHub(normalizedId)
2371
+ ]);
2372
+ if (!osvDetails && ghPackages.length === 0) {
2373
+ return {
2374
+ success: false,
2375
+ error: `CVE "${normalizedId}" was not found in OSV or GitHub Advisory databases. It may be too new, or not affect npm packages.`
2376
+ };
2377
+ }
2378
+ let details = osvDetails ?? {
2379
+ id: normalizedId,
2380
+ summary: "Details sourced from GitHub Advisory Database.",
2381
+ severity: "UNKNOWN",
2382
+ references: [],
2383
+ affectedPackages: []
1932
2384
  };
1933
- }
1934
- const installedPackages = inventory.packages ?? [];
1935
- for (const affected of cveDetails.affectedPackages) {
1936
- if (!affected || typeof affected !== "object") continue;
1937
- if (!affected.name || !affected.vulnerableRange) continue;
1938
- if (affected.ecosystem !== "npm") continue;
1939
- const matches = installedPackages.filter((p) => p.name === affected.name);
1940
- for (const installed of matches) {
1941
- if (!semver4.valid(installed.version)) continue;
1942
- let isVulnerable = false;
2385
+ if (ghPackages.length > 0) {
2386
+ details = mergeGhDataIntoCveDetails(details, ghPackages);
2387
+ }
2388
+ const sourceHealth = {};
2389
+ const applyEnricher = async (sourceName, enricher) => {
2390
+ const before = JSON.stringify(details);
1943
2391
  try {
1944
- isVulnerable = semver4.satisfies(installed.version, affected.vulnerableRange, {
1945
- includePrerelease: false
1946
- });
1947
- } catch {
1948
- continue;
1949
- }
1950
- if (isVulnerable) {
1951
- vulnerablePackages.push({ installed, affected });
2392
+ details = await enricher(details);
2393
+ const after = JSON.stringify(details);
2394
+ sourceHealth[sourceName] = {
2395
+ attempted: true,
2396
+ changed: before !== after
2397
+ };
2398
+ } catch (error) {
2399
+ sourceHealth[sourceName] = {
2400
+ attempted: true,
2401
+ changed: false,
2402
+ error: error instanceof Error ? error.message : String(error)
2403
+ };
1952
2404
  }
2405
+ };
2406
+ await applyEnricher("nvd", enrichWithNvd);
2407
+ await applyEnricher("cisa-kev", enrichWithCisaKev);
2408
+ await applyEnricher("epss", enrichWithEpss);
2409
+ await applyEnricher("cve-services", enrichWithCveServices);
2410
+ await applyEnricher("gitlab-advisory", enrichWithGitLabAdvisory);
2411
+ await applyEnricher("certcc", enrichWithCertCc);
2412
+ await applyEnricher("deps-dev", enrichWithDepsDev);
2413
+ await applyEnricher("ossf-scorecard", enrichWithOssfScorecard);
2414
+ await applyEnricher("external-feeds", enrichWithExternalFeeds);
2415
+ details.intelligence = {
2416
+ ...details.intelligence ?? {},
2417
+ sourceHealth
2418
+ };
2419
+ if (details.affectedPackages.length === 0) {
2420
+ return {
2421
+ success: false,
2422
+ error: `CVE "${normalizedId}" was found but has no npm-specific affected packages listed. It may affect a different ecosystem.`
2423
+ };
1953
2424
  }
2425
+ return { success: true, data: details };
1954
2426
  }
1955
- agentSteps += 1;
1956
- for (const vulnerable of vulnerablePackages) {
1957
- const pkg = vulnerable.installed;
1958
- const firstPatchedVersion = vulnerable.affected.firstPatchedVersion;
1959
- if (pkg.type === "indirect") {
1960
- collectedResults.push({
1961
- packageName: pkg.name,
1962
- strategy: "none",
1963
- fromVersion: pkg.version,
1964
- applied: false,
1965
- dryRun,
1966
- message: `"${pkg.name}" is an indirect dependency; automatic version bump is limited to direct dependencies in local mode.`
1967
- });
1968
- continue;
1969
- }
1970
- if (!firstPatchedVersion) {
1971
- collectedResults.push({
1972
- packageName: pkg.name,
1973
- strategy: "none",
1974
- fromVersion: pkg.version,
1975
- applied: false,
1976
- dryRun,
1977
- message: `No firstPatchedVersion available for ${pkg.name}; cannot resolve deterministic upgrade in local mode.`
1978
- });
1979
- continue;
2427
+ });
2428
+
2429
+ // src/remediation/tools/check-version-match.ts
2430
+ import { tool as tool8 } from "ai";
2431
+ import { z as z8 } from "zod";
2432
+ import semver5 from "semver";
2433
+ var affectedPackageSchema = z8.object({
2434
+ name: z8.string(),
2435
+ ecosystem: z8.literal("npm"),
2436
+ vulnerableRange: z8.string(),
2437
+ firstPatchedVersion: z8.string().optional(),
2438
+ source: z8.enum(["osv", "github-advisory"])
2439
+ });
2440
+ var inventoryPackageSchema = z8.object({
2441
+ name: z8.string(),
2442
+ version: z8.string(),
2443
+ type: z8.enum(["direct", "indirect"])
2444
+ });
2445
+ var checkVersionMatchTool = tool8({
2446
+ description: "Check which of the project's installed packages fall within the CVE's vulnerable version ranges. Returns only the packages that are actually vulnerable.",
2447
+ parameters: z8.object({
2448
+ installedPackages: z8.array(inventoryPackageSchema).describe("Output from the check-inventory tool"),
2449
+ affectedPackages: z8.array(affectedPackageSchema).describe("affectedPackages array from the lookup-cve tool result")
2450
+ }),
2451
+ execute: async ({ installedPackages, affectedPackages }) => {
2452
+ const vulnerable = [];
2453
+ for (const affected of affectedPackages) {
2454
+ const matches = installedPackages.filter(
2455
+ (p) => p.name === affected.name
2456
+ );
2457
+ for (const installed of matches) {
2458
+ if (!semver5.valid(installed.version)) continue;
2459
+ let isVulnerable = false;
2460
+ try {
2461
+ isVulnerable = semver5.satisfies(installed.version, affected.vulnerableRange, {
2462
+ includePrerelease: false
2463
+ });
2464
+ } catch {
2465
+ continue;
2466
+ }
2467
+ if (isVulnerable) {
2468
+ vulnerable.push({ installed, affected });
2469
+ }
2470
+ }
1980
2471
  }
1981
- const safeVersion = await findSafeUpgradeVersion(
1982
- pkg.name,
1983
- pkg.version,
2472
+ return {
2473
+ vulnerablePackages: vulnerable,
2474
+ checkedCount: installedPackages.length
2475
+ };
2476
+ }
2477
+ });
2478
+
2479
+ // src/remediation/tools/find-fixed-version.ts
2480
+ import { tool as tool9 } from "ai";
2481
+ import { z as z9 } from "zod";
2482
+ var findFixedVersionTool = tool9({
2483
+ description: "Query the npm registry to find the safest published upgrade version for a package that is >= the first patched version. Prefer patch upgrades first, then minor, and only fall back to major when no same-major fix exists.",
2484
+ parameters: z9.object({
2485
+ packageName: z9.string().describe("The npm package name"),
2486
+ installedVersion: z9.string().describe("The currently installed version (exact semver)"),
2487
+ firstPatchedVersion: z9.string().describe(
2488
+ "The first version that is NOT vulnerable (from lookup-cve). Use this as the floor."
2489
+ ),
2490
+ vulnerableRange: z9.string().optional().describe("Optional vulnerable semver range used to exclude still-vulnerable versions")
2491
+ }),
2492
+ execute: async ({
2493
+ packageName,
2494
+ installedVersion,
2495
+ firstPatchedVersion,
2496
+ vulnerableRange
2497
+ }) => {
2498
+ const resolution = await resolveSafeUpgradeVersion(
2499
+ packageName,
2500
+ installedVersion,
1984
2501
  firstPatchedVersion,
1985
- vulnerable.affected.vulnerableRange
2502
+ vulnerableRange
1986
2503
  );
1987
- agentSteps += 1;
2504
+ const { safeVersion, upgradeLevel, candidates, majorOnlyFixAvailable } = resolution;
1988
2505
  if (!safeVersion) {
1989
- collectedResults.push({
1990
- packageName: pkg.name,
1991
- strategy: "none",
1992
- fromVersion: pkg.version,
1993
- applied: false,
1994
- dryRun,
1995
- message: `No safe upgrade version found for ${pkg.name}.`
1996
- });
1997
- continue;
2506
+ return {
2507
+ candidates,
2508
+ isMajorBump: false,
2509
+ majorOnlyFixAvailable: false,
2510
+ message: `No safe upgrade version found for "${packageName}". The patch-file path will be needed.`
2511
+ };
1998
2512
  }
1999
- const applyResult = await applyVersionBumpTool.execute({
2000
- cwd,
2001
- packageManager,
2002
- packageName: pkg.name,
2003
- fromVersion: pkg.version,
2004
- toVersion: safeVersion,
2005
- dryRun,
2006
- policy,
2007
- runTests
2008
- });
2009
- agentSteps += 1;
2010
- collectedResults.push(applyResult);
2513
+ const installedMajor = parseInt(installedVersion.split(".")[0] ?? "0", 10);
2514
+ const safeMajor = parseInt(safeVersion.split(".")[0] ?? "0", 10);
2515
+ const isMajorBump = safeMajor > installedMajor;
2516
+ return {
2517
+ safeVersion,
2518
+ upgradeLevel,
2519
+ candidates,
2520
+ isMajorBump,
2521
+ majorOnlyFixAvailable,
2522
+ message: isMajorBump ? `Found safe version ${safeVersion} for "${packageName}", but only a major upgrade is available from ${installedVersion}. This should remain blocked unless policy explicitly allows major bumps.` : `Found ${upgradeLevel ?? "safe"} upgrade ${safeVersion} for "${packageName}" (from ${installedVersion}).`
2523
+ };
2011
2524
  }
2012
- const appliedCount = collectedResults.filter((r) => r.applied).length;
2013
- const unresolvedCount = collectedResults.filter((r) => !r.applied && !r.dryRun).length;
2014
- const dryRunCount = collectedResults.filter((r) => r.dryRun).length;
2015
- return {
2016
- cveId,
2017
- cveDetails,
2018
- vulnerablePackages,
2019
- results: collectedResults,
2020
- agentSteps,
2021
- summary: `Local mode completed: vulnerable=${vulnerablePackages.length}, applied=${appliedCount}, dryRun=${dryRunCount}, unresolved=${unresolvedCount}`,
2022
- correlation: {
2023
- requestId: options.requestId,
2024
- sessionId: options.sessionId,
2025
- parentRunId: options.parentRunId
2026
- }
2525
+ });
2526
+
2527
+ // src/remediation/runtime-tools.ts
2528
+ function buildRuntimeTools(ctx) {
2529
+ const tools = {
2530
+ "lookup-cve": lookupCveTool,
2531
+ "check-inventory": checkInventoryTool,
2532
+ "check-version-match": checkVersionMatchTool,
2533
+ "find-fixed-version": findFixedVersionTool,
2534
+ "apply-version-bump": ctx.applyVersionBumpToolForRun
2027
2535
  };
2536
+ if (!ctx.constraints.directDependenciesOnly && !ctx.constraints.preferVersionBump) {
2537
+ tools["apply-package-override"] = ctx.applyPackageOverrideToolForRun;
2538
+ }
2539
+ if (!ctx.constraints.preferVersionBump) {
2540
+ tools["fetch-package-source"] = fetchPackageSourceTool;
2541
+ tools["generate-patch"] = generatePatchTool;
2542
+ tools["apply-patch-file"] = ctx.applyPatchFileToolForRun;
2543
+ }
2544
+ return tools;
2028
2545
  }
2546
+
2547
+ // src/remediation/orchestration-prompt.ts
2548
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
2549
+ import { join as join10 } from "path";
2029
2550
  function loadOrchestrationPrompt(ctx) {
2030
- const promptPath = join8(process.cwd(), ".github", "instructions", "orchestration.instructions.md");
2031
- if (!existsSync4(promptPath)) {
2551
+ const promptPath = join10(process.cwd(), ".github", "instructions", "orchestration.instructions.md");
2552
+ if (!existsSync5(promptPath)) {
2032
2553
  return `You are autoremediator, an agentic security remediation system for Node.js package dependencies.
2033
2554
  Working directory: ${ctx.cwd}
2034
2555
  Package manager: ${ctx.packageManager}
@@ -2036,6 +2557,8 @@ Dry run: ${ctx.dryRun}
2036
2557
  Run tests: ${ctx.runTests}
2037
2558
  Policy: ${ctx.policy || "undefined"}
2038
2559
  Patches dir: ${ctx.patchesDir}
2560
+ Direct dependencies only: ${String(ctx.constraints.directDependenciesOnly ?? false)}
2561
+ Prefer version bump: ${String(ctx.constraints.preferVersionBump ?? false)}
2039
2562
 
2040
2563
  Required sequence:
2041
2564
  1. lookup-cve
@@ -2043,185 +2566,364 @@ Required sequence:
2043
2566
  3. check-version-match
2044
2567
  4. find-fixed-version
2045
2568
  5. apply-version-bump
2569
+ 6. apply-package-override
2046
2570
 
2047
- Fallback sequence (when strategy="none"):
2571
+ Fallback sequence (when neither version bump nor override can be applied):
2048
2572
  1. fetch-package-source
2049
2573
  2. generate-patch
2050
2574
  3. apply-patch-file
2051
2575
 
2052
2576
  Always respect dryRun and policy constraints.`;
2053
2577
  }
2054
- const template = readFileSync4(promptPath, "utf8");
2055
- return template.replaceAll("{{cveId}}", ctx.cveId).replaceAll("{{cwd}}", ctx.cwd).replaceAll("{{packageManager}}", ctx.packageManager).replaceAll("{{dryRun}}", String(ctx.dryRun)).replaceAll("{{runTests}}", String(ctx.runTests)).replaceAll("{{policy}}", ctx.policy || "undefined").replaceAll("{{patchesDir}}", ctx.patchesDir);
2578
+ const template = readFileSync6(promptPath, "utf8");
2579
+ return template.replaceAll("{{cveId}}", ctx.cveId).replaceAll("{{cwd}}", ctx.cwd).replaceAll("{{packageManager}}", ctx.packageManager).replaceAll("{{dryRun}}", String(ctx.dryRun)).replaceAll("{{runTests}}", String(ctx.runTests)).replaceAll("{{policy}}", ctx.policy || "undefined").replaceAll("{{patchesDir}}", ctx.patchesDir).replaceAll("{{directDependenciesOnly}}", String(ctx.constraints.directDependenciesOnly ?? false)).replaceAll("{{preferVersionBump}}", String(ctx.constraints.preferVersionBump ?? false));
2056
2580
  }
2057
2581
 
2058
- // src/scanner/index.ts
2059
- import { extname } from "path";
2060
- import { readFileSync as readFileSync8 } from "fs";
2061
-
2062
- // src/scanner/adapters/npm-audit.ts
2063
- import { readFileSync as readFileSync5 } from "fs";
2064
- var CVE_REGEX = /CVE-\d{4}-\d+/gi;
2065
- function normalizeSeverity(raw) {
2066
- if (!raw) return "UNKNOWN";
2067
- const up = raw.toUpperCase();
2068
- if (up === "CRITICAL" || up === "HIGH" || up === "MEDIUM" || up === "LOW") {
2069
- return up;
2582
+ // src/remediation/pipeline.ts
2583
+ async function runRemediationPipeline(cveId, options = {}) {
2584
+ const provider = resolveProvider(options);
2585
+ if (provider === "local") {
2586
+ return runLocalRemediationPipeline(cveId, options);
2070
2587
  }
2071
- return "UNKNOWN";
2072
- }
2073
- function parseNpmAuditJsonFromString(content) {
2074
- const report = JSON.parse(content);
2075
- const findings = [];
2076
- const seen = /* @__PURE__ */ new Set();
2077
- for (const vuln of Object.values(report.vulnerabilities ?? {})) {
2078
- for (const viaEntry of vuln.via ?? []) {
2079
- const text = typeof viaEntry === "string" ? viaEntry : `${viaEntry.url ?? ""} ${viaEntry.name ?? ""}`;
2080
- const matches = text.match(CVE_REGEX) ?? [];
2081
- for (const match of matches) {
2082
- const cveId = match.toUpperCase();
2083
- const key = `${cveId}:${vuln.name}`;
2084
- if (seen.has(key)) continue;
2085
- seen.add(key);
2086
- findings.push({
2087
- cveId,
2088
- source: "npm-audit",
2089
- packageName: vuln.name,
2090
- severity: normalizeSeverity(vuln.severity)
2091
- });
2588
+ const cwd = options.cwd ?? process.cwd();
2589
+ const packageManager = options.packageManager ?? detectPackageManager(cwd);
2590
+ const preview = options.preview ?? false;
2591
+ const dryRun = (options.dryRun ?? false) || preview;
2592
+ const runTests = options.runTests ?? false;
2593
+ const policy = options.policy ?? "";
2594
+ const patchesDir = options.patchesDir || "./patches";
2595
+ const constraints = options.constraints ?? {};
2596
+ const model = await createModel(options);
2597
+ const systemPrompt = loadOrchestrationPrompt({
2598
+ cveId,
2599
+ cwd,
2600
+ dryRun,
2601
+ runTests,
2602
+ policy,
2603
+ patchesDir,
2604
+ packageManager,
2605
+ constraints
2606
+ });
2607
+ const prompt = `Patch vulnerable dependencies affected by ${cveId} in the project at: ${cwd}. Package manager: ${packageManager}.`;
2608
+ const collectedResults = [];
2609
+ const vulnerablePackages = [];
2610
+ let cveDetails = null;
2611
+ let agentSteps = 0;
2612
+ const applyVersionBumpToolForRun = preview ? {
2613
+ ...applyVersionBumpTool,
2614
+ execute: async (input) => applyVersionBumpTool.execute({ ...input, dryRun: true })
2615
+ } : applyVersionBumpTool;
2616
+ const applyPackageOverrideToolForRun = preview ? {
2617
+ ...applyPackageOverrideTool,
2618
+ execute: async (input) => applyPackageOverrideTool.execute({ ...input, dryRun: true })
2619
+ } : applyPackageOverrideTool;
2620
+ const applyPatchFileToolForRun = preview ? {
2621
+ ...applyPatchFileTool,
2622
+ execute: async (input) => applyPatchFileTool.execute({ ...input, dryRun: true })
2623
+ } : applyPatchFileTool;
2624
+ const tools = buildRuntimeTools({
2625
+ applyVersionBumpToolForRun,
2626
+ applyPackageOverrideToolForRun,
2627
+ applyPatchFileToolForRun,
2628
+ constraints
2629
+ });
2630
+ const result = await generateText2({
2631
+ model,
2632
+ system: systemPrompt,
2633
+ prompt,
2634
+ tools,
2635
+ maxSteps: 25,
2636
+ onStepFinish(stepResult) {
2637
+ agentSteps += 1;
2638
+ const toolResults = stepResult.toolResults ?? [];
2639
+ for (const tr of toolResults) {
2640
+ const toolResult = tr.result;
2641
+ if (tr.toolName === "lookup-cve" && toolResult?.data) {
2642
+ cveDetails = toolResult.data;
2643
+ }
2644
+ if (tr.toolName === "check-version-match" && toolResult?.vulnerablePackages) {
2645
+ vulnerablePackages.push(...toolResult.vulnerablePackages);
2646
+ }
2647
+ if (tr.toolName === "apply-version-bump") {
2648
+ collectedResults.push(toolResult);
2649
+ }
2650
+ if (tr.toolName === "apply-package-override") {
2651
+ collectedResults.push(toolResult);
2652
+ }
2653
+ if (tr.toolName === "apply-patch-file" && toolResult) {
2654
+ const validation = toolResult.validation;
2655
+ const message = typeof toolResult.message === "string" ? toolResult.message : typeof toolResult.error === "string" ? toolResult.error : "Patch-file strategy finished.";
2656
+ collectedResults.push({
2657
+ packageName: typeof toolResult.packageName === "string" ? toolResult.packageName : "unknown-package",
2658
+ strategy: "patch-file",
2659
+ fromVersion: typeof toolResult.vulnerableVersion === "string" ? toolResult.vulnerableVersion : "unknown",
2660
+ patchFilePath: typeof toolResult.patchFilePath === "string" ? toolResult.patchFilePath : typeof toolResult.patchPath === "string" ? toolResult.patchPath : void 0,
2661
+ applied: Boolean(toolResult.applied),
2662
+ dryRun: Boolean(toolResult.dryRun),
2663
+ unresolvedReason: !Boolean(toolResult.applied) && !Boolean(toolResult.dryRun) ? validation && validation.passed === false ? "patch-validation-failed" : "patch-apply-failed" : void 0,
2664
+ message,
2665
+ validation: validation && typeof validation.passed === "boolean" ? {
2666
+ passed: validation.passed,
2667
+ error: typeof validation.error === "string" ? validation.error : void 0
2668
+ } : void 0
2669
+ });
2670
+ }
2092
2671
  }
2093
2672
  }
2094
- }
2095
- return findings;
2096
- }
2097
- function parseNpmAuditJsonFile(filePath) {
2098
- const content = readFileSync5(filePath, "utf8");
2099
- return parseNpmAuditJsonFromString(content);
2673
+ });
2674
+ return {
2675
+ cveId,
2676
+ cveDetails,
2677
+ vulnerablePackages,
2678
+ results: collectedResults,
2679
+ agentSteps,
2680
+ summary: result.text,
2681
+ correlation: {
2682
+ requestId: options.requestId,
2683
+ sessionId: options.sessionId,
2684
+ parentRunId: options.parentRunId
2685
+ }
2686
+ };
2100
2687
  }
2101
2688
 
2102
- // src/scanner/adapters/yarn-audit.ts
2103
- import { readFileSync as readFileSync6 } from "fs";
2104
- var CVE_REGEX2 = /CVE-\d{4}-\d+/gi;
2105
- function normalizeSeverity2(raw) {
2106
- if (!raw) return "UNKNOWN";
2107
- const up = raw.toUpperCase();
2108
- if (up === "CRITICAL" || up === "HIGH" || up === "MEDIUM" || up === "LOW") {
2109
- return up;
2110
- }
2111
- return "UNKNOWN";
2689
+ // src/api/options-schema.ts
2690
+ var PACKAGE_MANAGER_VALUES = ["npm", "pnpm", "yarn"];
2691
+ var LLM_PROVIDER_VALUES = ["openai", "anthropic", "local"];
2692
+ var PROVENANCE_SOURCE_VALUES = ["cli", "sdk", "mcp", "openapi", "unknown"];
2693
+ var OPTION_DESCRIPTIONS = {
2694
+ cveId: "CVE ID, e.g. CVE-2021-23337",
2695
+ inputPath: "Absolute path to the scanner output file",
2696
+ cwd: "Absolute path to the project root (default: process.cwd())",
2697
+ packageManager: "Package manager override (auto-detected by default)",
2698
+ dryRun: "If true, plan changes but write nothing",
2699
+ preview: "If true, enforce non-mutating preview mode",
2700
+ runTests: "Run package-manager test command after applying fix",
2701
+ llmProvider: "LLM provider override",
2702
+ patchesDir: "Directory to write .patch files (default: ./patches)",
2703
+ policy: "Optional path to .autoremediator policy file",
2704
+ requestId: "Request correlation ID",
2705
+ sessionId: "Session correlation ID",
2706
+ parentRunId: "Parent run correlation ID",
2707
+ idempotencyKey: "Idempotency key for replay-safe execution",
2708
+ resume: "Return cached result for matching idempotency key when available",
2709
+ actor: "Actor identity for evidence provenance",
2710
+ source: "Source system for provenance",
2711
+ format: "Scanner format (default: auto)",
2712
+ evidence: "Write evidence JSON to .autoremediator/evidence/ (default: true)",
2713
+ directDependenciesOnly: "Restrict remediation to direct dependencies only",
2714
+ preferVersionBump: "Reject override and patch remediation when version-bump-only policy is required"
2715
+ };
2716
+ function createConstraintSchemaProperties() {
2717
+ return {
2718
+ directDependenciesOnly: { type: "boolean", description: OPTION_DESCRIPTIONS.directDependenciesOnly },
2719
+ preferVersionBump: { type: "boolean", description: OPTION_DESCRIPTIONS.preferVersionBump }
2720
+ };
2112
2721
  }
2113
- function parseYarnAuditJsonFromString(content) {
2114
- const findings = [];
2115
- const seen = /* @__PURE__ */ new Set();
2116
- const lines = content.split("\n").map((line) => line.trim()).filter(Boolean);
2117
- for (const line of lines) {
2118
- let parsed;
2119
- try {
2120
- parsed = JSON.parse(line);
2121
- } catch {
2122
- continue;
2123
- }
2124
- const event = parsed;
2125
- if (event.type !== "auditAdvisory") continue;
2126
- const advisory = event.data?.advisory;
2127
- const packageName = advisory?.module_name;
2128
- const severity = normalizeSeverity2(advisory?.severity);
2129
- const text = `${advisory?.url ?? ""} ${(advisory?.cves ?? []).join(" ")}`;
2130
- const matches = text.match(CVE_REGEX2) ?? [];
2131
- for (const match of matches) {
2132
- const cveId = match.toUpperCase();
2133
- const key = `${cveId}:${packageName ?? ""}`;
2134
- if (seen.has(key)) continue;
2135
- seen.add(key);
2136
- findings.push({
2137
- cveId,
2138
- source: "yarn-audit",
2139
- packageName,
2140
- severity
2141
- });
2722
+ function createRemediateOptionSchemaProperties(options) {
2723
+ const includeDryRun = options?.includeDryRun ?? true;
2724
+ const includePreview = options?.includePreview ?? true;
2725
+ const includeEvidence = options?.includeEvidence ?? true;
2726
+ return {
2727
+ cwd: { type: "string", description: OPTION_DESCRIPTIONS.cwd },
2728
+ packageManager: { type: "string", enum: [...PACKAGE_MANAGER_VALUES], description: OPTION_DESCRIPTIONS.packageManager },
2729
+ ...includeDryRun ? { dryRun: { type: "boolean", description: OPTION_DESCRIPTIONS.dryRun } } : {},
2730
+ ...includePreview ? { preview: { type: "boolean", description: OPTION_DESCRIPTIONS.preview } } : {},
2731
+ runTests: { type: "boolean", description: OPTION_DESCRIPTIONS.runTests },
2732
+ llmProvider: { type: "string", enum: [...LLM_PROVIDER_VALUES], description: OPTION_DESCRIPTIONS.llmProvider },
2733
+ patchesDir: { type: "string", description: OPTION_DESCRIPTIONS.patchesDir },
2734
+ policy: { type: "string", description: OPTION_DESCRIPTIONS.policy },
2735
+ ...includeEvidence ? { evidence: { type: "boolean", description: OPTION_DESCRIPTIONS.evidence } } : {},
2736
+ requestId: { type: "string", description: OPTION_DESCRIPTIONS.requestId },
2737
+ sessionId: { type: "string", description: OPTION_DESCRIPTIONS.sessionId },
2738
+ parentRunId: { type: "string", description: OPTION_DESCRIPTIONS.parentRunId },
2739
+ idempotencyKey: { type: "string", description: OPTION_DESCRIPTIONS.idempotencyKey },
2740
+ resume: { type: "boolean", description: OPTION_DESCRIPTIONS.resume },
2741
+ actor: { type: "string", description: OPTION_DESCRIPTIONS.actor },
2742
+ source: { type: "string", enum: [...PROVENANCE_SOURCE_VALUES], description: OPTION_DESCRIPTIONS.source },
2743
+ constraints: {
2744
+ type: "object",
2745
+ properties: createConstraintSchemaProperties()
2142
2746
  }
2143
- }
2144
- return findings;
2747
+ };
2145
2748
  }
2146
- function parseYarnAuditJsonFile(filePath) {
2147
- const content = readFileSync6(filePath, "utf8");
2148
- return parseYarnAuditJsonFromString(content);
2749
+ function createScanOptionSchemaProperties() {
2750
+ return {
2751
+ ...createRemediateOptionSchemaProperties({ includeEvidence: true }),
2752
+ format: { type: "string", enum: ["npm-audit", "yarn-audit", "sarif", "auto"], description: OPTION_DESCRIPTIONS.format }
2753
+ };
2754
+ }
2755
+ function createScanReportSchemaProperties() {
2756
+ return {
2757
+ schemaVersion: { type: "string" },
2758
+ status: { type: "string", enum: ["ok", "partial", "failed"] },
2759
+ generatedAt: { type: "string" },
2760
+ cveIds: { type: "array", items: { type: "string" } },
2761
+ reports: { type: "array", items: { type: "object" } },
2762
+ successCount: { type: "number" },
2763
+ failedCount: { type: "number" },
2764
+ errors: { type: "array", items: { type: "object" } },
2765
+ evidenceFile: { type: "string" },
2766
+ patchCount: { type: "number" },
2767
+ patchValidationFailures: { type: "array", items: { type: "object" } },
2768
+ strategyCounts: {
2769
+ type: "object",
2770
+ additionalProperties: { type: "number" }
2771
+ },
2772
+ dependencyScopeCounts: {
2773
+ type: "object",
2774
+ additionalProperties: { type: "number" }
2775
+ },
2776
+ unresolvedByReason: {
2777
+ type: "object",
2778
+ additionalProperties: { type: "number" }
2779
+ },
2780
+ patchesDir: { type: "string" },
2781
+ correlation: { type: "object" },
2782
+ provenance: { type: "object" },
2783
+ constraints: { type: "object" },
2784
+ idempotencyKey: { type: "string" }
2785
+ };
2149
2786
  }
2150
2787
 
2151
- // src/scanner/adapters/sarif.ts
2152
- import { readFileSync as readFileSync7 } from "fs";
2153
- var CVE_REGEX3 = /CVE-\d{4}-\d+/gi;
2154
- function extractPackageName(result) {
2155
- const pkg = result.properties?.["packageName"];
2156
- return typeof pkg === "string" ? pkg : void 0;
2788
+ // src/api/reporting.ts
2789
+ function buildStrategyCounts(reports) {
2790
+ const counts = {};
2791
+ for (const report of reports) {
2792
+ for (const result of report.results) {
2793
+ counts[result.strategy] = (counts[result.strategy] ?? 0) + 1;
2794
+ }
2795
+ }
2796
+ return Object.keys(counts).length > 0 ? counts : void 0;
2157
2797
  }
2158
- function parseSarifFromString(content) {
2159
- const report = JSON.parse(content);
2160
- const findings = [];
2161
- const seen = /* @__PURE__ */ new Set();
2162
- for (const run of report.runs ?? []) {
2163
- for (const result of run.results ?? []) {
2164
- const combined = `${result.ruleId ?? ""} ${result.message?.text ?? ""}`;
2165
- const matches = combined.match(CVE_REGEX3) ?? [];
2166
- for (const match of matches) {
2167
- const cveId = match.toUpperCase();
2168
- const pkg = extractPackageName(result);
2169
- const key = `${cveId}:${pkg ?? ""}`;
2170
- if (seen.has(key)) continue;
2171
- seen.add(key);
2172
- findings.push({
2173
- cveId,
2174
- source: "sarif",
2175
- packageName: pkg,
2176
- severity: "UNKNOWN"
2177
- });
2798
+ function toDependencyScope(installedType) {
2799
+ return installedType === "direct" ? "direct" : "transitive";
2800
+ }
2801
+ function buildDependencyScopeCounts(reports) {
2802
+ const counts = {};
2803
+ for (const report of reports) {
2804
+ const packageScopes = /* @__PURE__ */ new Map();
2805
+ for (const vulnerablePackage of report.vulnerablePackages) {
2806
+ const scope = toDependencyScope(vulnerablePackage.installed.type);
2807
+ const current = packageScopes.get(vulnerablePackage.installed.name);
2808
+ if (!current || current !== "direct") {
2809
+ packageScopes.set(vulnerablePackage.installed.name, scope);
2178
2810
  }
2179
2811
  }
2812
+ for (const result of report.results) {
2813
+ const scope = packageScopes.get(result.packageName);
2814
+ if (!scope) continue;
2815
+ counts[scope] = (counts[scope] ?? 0) + 1;
2816
+ }
2817
+ }
2818
+ return Object.keys(counts).length > 0 ? counts : void 0;
2819
+ }
2820
+ function buildUnresolvedReasonCounts(reports) {
2821
+ const counts = {};
2822
+ for (const report of reports) {
2823
+ for (const result of report.results) {
2824
+ if (!result.unresolvedReason) continue;
2825
+ counts[result.unresolvedReason] = (counts[result.unresolvedReason] ?? 0) + 1;
2826
+ }
2827
+ }
2828
+ return Object.keys(counts).length > 0 ? counts : void 0;
2829
+ }
2830
+ function toCiSummary(report) {
2831
+ let remediationCount = 0;
2832
+ for (const cveReport of report.reports) {
2833
+ remediationCount += cveReport.results.length;
2180
2834
  }
2181
- return findings;
2835
+ return {
2836
+ schemaVersion: report.schemaVersion,
2837
+ status: report.status,
2838
+ generatedAt: report.generatedAt,
2839
+ cveCount: report.cveIds.length,
2840
+ remediationCount,
2841
+ successCount: report.successCount,
2842
+ failedCount: report.failedCount,
2843
+ errors: report.errors,
2844
+ evidenceFile: report.evidenceFile,
2845
+ patchCount: report.patchCount || 0,
2846
+ patchValidationFailures: report.patchValidationFailures,
2847
+ strategyCounts: report.strategyCounts,
2848
+ dependencyScopeCounts: report.dependencyScopeCounts,
2849
+ unresolvedByReason: report.unresolvedByReason,
2850
+ patchesDir: report.patchesDir,
2851
+ correlation: report.correlation,
2852
+ provenance: report.provenance,
2853
+ constraints: report.constraints,
2854
+ idempotencyKey: report.idempotencyKey
2855
+ };
2182
2856
  }
2183
- function parseSarifFile(filePath) {
2184
- const content = readFileSync7(filePath, "utf8");
2185
- return parseSarifFromString(content);
2857
+ function ciExitCode(summary) {
2858
+ return summary.failedCount > 0 ? 1 : 0;
2186
2859
  }
2187
2860
 
2188
- // src/scanner/index.ts
2189
- function parseScanInput(filePath, format) {
2190
- const resolved = format === "auto" ? inferFormat(filePath) : format;
2191
- if (resolved === "npm-audit") {
2192
- return parseNpmAuditJsonFile(filePath);
2193
- }
2194
- if (resolved === "yarn-audit") {
2195
- return parseYarnAuditJsonFile(filePath);
2196
- }
2197
- if (resolved === "sarif") {
2198
- return parseSarifFile(filePath);
2199
- }
2200
- throw new Error(`Unsupported input format: ${resolved}`);
2861
+ // src/api/sarif.ts
2862
+ function severityToSarifLevel(severity) {
2863
+ if (severity === "CRITICAL" || severity === "HIGH") return "error";
2864
+ if (severity === "MEDIUM") return "warning";
2865
+ if (severity === "LOW") return "note";
2866
+ return "warning";
2201
2867
  }
2202
- function inferFormat(filePath) {
2203
- const ext = extname(filePath).toLowerCase();
2204
- if (ext === ".sarif") return "sarif";
2205
- try {
2206
- const content = readFileSync8(filePath, "utf8");
2207
- const firstLine = content.split("\n").find((line) => line.trim().startsWith("{"));
2208
- if (firstLine) {
2209
- const parsed = JSON.parse(firstLine);
2210
- if (parsed.type === "auditAdvisory" || parsed.type === "auditSummary") {
2211
- return "yarn-audit";
2212
- }
2868
+ function toSarifOutput(report) {
2869
+ const rules = [];
2870
+ const results = [];
2871
+ const seenRules = /* @__PURE__ */ new Set();
2872
+ for (const cveReport of report.reports) {
2873
+ const severity = cveReport.cveDetails?.severity ?? "UNKNOWN";
2874
+ const level = severityToSarifLevel(severity);
2875
+ const summary = cveReport.cveDetails?.summary ?? cveReport.cveId;
2876
+ if (!seenRules.has(cveReport.cveId)) {
2877
+ seenRules.add(cveReport.cveId);
2878
+ rules.push({
2879
+ id: cveReport.cveId,
2880
+ name: "VulnerableDependency",
2881
+ shortDescription: { text: cveReport.cveId },
2882
+ fullDescription: { text: summary },
2883
+ defaultConfiguration: { level },
2884
+ helpUri: `https://osv.dev/vulnerability/${cveReport.cveId}`,
2885
+ properties: { severity }
2886
+ });
2887
+ }
2888
+ for (const vulnerablePackage of cveReport.vulnerablePackages) {
2889
+ const fixText = vulnerablePackage.affected.firstPatchedVersion ? ` Fix: upgrade to ${vulnerablePackage.affected.firstPatchedVersion}.` : " No fixed version available.";
2890
+ results.push({
2891
+ ruleId: cveReport.cveId,
2892
+ level,
2893
+ message: {
2894
+ text: `${vulnerablePackage.installed.name}@${vulnerablePackage.installed.version} is vulnerable to ${cveReport.cveId}: ${summary}${fixText}`
2895
+ },
2896
+ locations: [
2897
+ {
2898
+ physicalLocation: {
2899
+ artifactLocation: { uri: "package.json", uriBaseId: "%SRCROOT%" }
2900
+ }
2901
+ }
2902
+ ]
2903
+ });
2213
2904
  }
2214
- } catch {
2215
2905
  }
2216
- return "npm-audit";
2217
- }
2218
- function uniqueCveIds(findings) {
2219
- return [...new Set(findings.map((f) => f.cveId.toUpperCase()))];
2906
+ return {
2907
+ version: "2.1.0",
2908
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Documents/CommitteeSpecifications/2.1.0/sarif-schema-2.1.0.json",
2909
+ runs: [
2910
+ {
2911
+ tool: {
2912
+ driver: {
2913
+ name: "autoremediator",
2914
+ informationUri: "https://github.com/Rawlings/autoremediator",
2915
+ rules
2916
+ }
2917
+ },
2918
+ results
2919
+ }
2920
+ ]
2921
+ };
2220
2922
  }
2221
2923
 
2222
2924
  // src/platform/evidence.ts
2223
- import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
2224
- import { join as join9 } from "path";
2925
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
2926
+ import { join as join11 } from "path";
2225
2927
  function createEvidenceLog(cwd, cveIds, context = {}) {
2226
2928
  return {
2227
2929
  runId: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
@@ -2251,31 +2953,31 @@ function finalizeEvidence(log) {
2251
2953
  return log;
2252
2954
  }
2253
2955
  function writeEvidenceLog(cwd, log) {
2254
- const dir = join9(cwd, ".autoremediator", "evidence");
2255
- mkdirSync(dir, { recursive: true });
2256
- const filePath = join9(dir, `${log.runId}.json`);
2257
- writeFileSync2(filePath, JSON.stringify(log, null, 2) + "\n", "utf8");
2956
+ const dir = join11(cwd, ".autoremediator", "evidence");
2957
+ mkdirSync2(dir, { recursive: true });
2958
+ const filePath = join11(dir, `${log.runId}.json`);
2959
+ writeFileSync4(filePath, JSON.stringify(log, null, 2) + "\n", "utf8");
2258
2960
  return filePath;
2259
2961
  }
2260
2962
 
2261
2963
  // src/platform/idempotency.ts
2262
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "fs";
2263
- import { join as join10 } from "path";
2964
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
2965
+ import { join as join12 } from "path";
2264
2966
  var DEFAULT_INDEX = {
2265
2967
  schemaVersion: "1.0",
2266
2968
  entries: {}
2267
2969
  };
2268
2970
  function indexFilePath(cwd) {
2269
- return join10(cwd, ".autoremediator", "state", "idempotency.json");
2971
+ return join12(cwd, ".autoremediator", "state", "idempotency.json");
2270
2972
  }
2271
2973
  function entryKey(idempotencyKey, cveId) {
2272
2974
  return `${idempotencyKey}::${cveId.toUpperCase()}`;
2273
2975
  }
2274
2976
  function loadIndex(cwd) {
2275
2977
  const filePath = indexFilePath(cwd);
2276
- if (!existsSync5(filePath)) return DEFAULT_INDEX;
2978
+ if (!existsSync6(filePath)) return DEFAULT_INDEX;
2277
2979
  try {
2278
- const parsed = JSON.parse(readFileSync9(filePath, "utf8"));
2980
+ const parsed = JSON.parse(readFileSync7(filePath, "utf8"));
2279
2981
  if (parsed && parsed.schemaVersion === "1.0" && parsed.entries) {
2280
2982
  return parsed;
2281
2983
  }
@@ -2286,8 +2988,8 @@ function loadIndex(cwd) {
2286
2988
  }
2287
2989
  function saveIndex(cwd, index) {
2288
2990
  const filePath = indexFilePath(cwd);
2289
- mkdirSync2(join10(cwd, ".autoremediator", "state"), { recursive: true });
2290
- writeFileSync3(filePath, JSON.stringify(index, null, 2) + "\n", "utf8");
2991
+ mkdirSync3(join12(cwd, ".autoremediator", "state"), { recursive: true });
2992
+ writeFileSync5(filePath, JSON.stringify(index, null, 2) + "\n", "utf8");
2291
2993
  }
2292
2994
  function readIdempotentReport(cwd, idempotencyKey, cveId) {
2293
2995
  const index = loadIndex(cwd);
@@ -2306,7 +3008,7 @@ function storeIdempotentReport(cwd, idempotencyKey, cveId, report) {
2306
3008
  saveIndex(cwd, index);
2307
3009
  }
2308
3010
 
2309
- // src/api.ts
3011
+ // src/api/context.ts
2310
3012
  function buildRequestId() {
2311
3013
  return `req-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2312
3014
  }
@@ -2330,118 +3032,312 @@ function resolveConstraints(options, cwd) {
2330
3032
  preferVersionBump: options.constraints?.preferVersionBump ?? policy.constraints?.preferVersionBump ?? false
2331
3033
  };
2332
3034
  }
2333
- function enforceConstraints(report, constraints) {
2334
- const indirectPackages = new Set(
2335
- report.vulnerablePackages.filter((vp) => vp.installed.type === "indirect").map((vp) => vp.installed.name)
2336
- );
2337
- const nextResults = report.results.map((result) => {
2338
- if (constraints.directDependenciesOnly && indirectPackages.has(result.packageName)) {
2339
- return {
2340
- ...result,
2341
- strategy: "none",
2342
- applied: false,
2343
- message: `Constraint blocked remediation for indirect dependency "${result.packageName}".`
2344
- };
2345
- }
2346
- if (constraints.preferVersionBump && result.strategy === "patch-file") {
2347
- return {
2348
- ...result,
2349
- strategy: "none",
2350
- applied: false,
2351
- message: `Constraint prefers version-bump and rejected patch-file remediation for "${result.packageName}".`
2352
- };
3035
+
3036
+ // src/api/remediate.ts
3037
+ async function remediate(cveId, options = {}) {
3038
+ if (!/^CVE-\d{4}-\d+$/i.test(cveId)) {
3039
+ throw new Error(
3040
+ `Invalid CVE ID: "${cveId}". Expected format: CVE-YYYY-NNNNN (e.g. CVE-2021-23337).`
3041
+ );
3042
+ }
3043
+ const normalizedCveId = cveId.toUpperCase();
3044
+ const cwd = options.cwd ?? process.cwd();
3045
+ const constraints = resolveConstraints(options, cwd);
3046
+ const provenance = resolveProvenanceContext(options);
3047
+ const correlation = resolveCorrelationContext(options);
3048
+ const evidenceEnabled = options.evidence !== false;
3049
+ const evidence = evidenceEnabled ? createEvidenceLog(cwd, [normalizedCveId], {
3050
+ ...correlation,
3051
+ actor: provenance.actor,
3052
+ source: provenance.source,
3053
+ idempotencyKey: options.idempotencyKey
3054
+ }) : void 0;
3055
+ if (options.resume && options.idempotencyKey) {
3056
+ const cached = readIdempotentReport(cwd, options.idempotencyKey, normalizedCveId);
3057
+ if (cached) {
3058
+ if (evidence) {
3059
+ addEvidenceStep(evidence, "remediate.resume-cache", { cveId: normalizedCveId });
3060
+ finalizeEvidence(evidence);
3061
+ }
3062
+ const evidenceFile2 = evidence ? writeEvidenceLog(cwd, evidence) : void 0;
3063
+ return {
3064
+ ...cached,
3065
+ summary: `${cached.summary} (resumed from idempotency cache)`,
3066
+ evidenceFile: evidenceFile2,
3067
+ correlation,
3068
+ provenance,
3069
+ constraints,
3070
+ resumedFromCache: true
3071
+ };
3072
+ }
3073
+ }
3074
+ if (evidence) {
3075
+ addEvidenceStep(
3076
+ evidence,
3077
+ "remediate.start",
3078
+ {
3079
+ cveId: normalizedCveId,
3080
+ dryRun: Boolean(options.dryRun),
3081
+ preview: Boolean(options.preview)
3082
+ },
3083
+ {
3084
+ directDependenciesOnly: Boolean(constraints.directDependenciesOnly),
3085
+ preferVersionBump: Boolean(constraints.preferVersionBump)
3086
+ }
3087
+ );
3088
+ }
3089
+ let report;
3090
+ try {
3091
+ report = await runRemediationPipeline(normalizedCveId, {
3092
+ ...options,
3093
+ ...correlation,
3094
+ constraints
3095
+ });
3096
+ } catch (error) {
3097
+ if (evidence) {
3098
+ const message = error instanceof Error ? error.message : String(error);
3099
+ addEvidenceStep(evidence, "remediate.error", { cveId: normalizedCveId }, void 0, message);
3100
+ finalizeEvidence(evidence);
3101
+ writeEvidenceLog(cwd, evidence);
3102
+ }
3103
+ throw error;
3104
+ }
3105
+ if (evidence) {
3106
+ for (const result of report.results) {
3107
+ addEvidenceStep(
3108
+ evidence,
3109
+ "remediate.package-result",
3110
+ {
3111
+ packageName: result.packageName,
3112
+ strategy: result.strategy,
3113
+ fromVersion: result.fromVersion,
3114
+ toVersion: result.toVersion
3115
+ },
3116
+ {
3117
+ applied: result.applied,
3118
+ dryRun: result.dryRun,
3119
+ unresolvedReason: result.unresolvedReason
3120
+ }
3121
+ );
3122
+ }
3123
+ addEvidenceStep(
3124
+ evidence,
3125
+ "remediate.finish",
3126
+ { cveId: normalizedCveId },
3127
+ {
3128
+ resultCount: report.results.length,
3129
+ vulnerableCount: report.vulnerablePackages.length
3130
+ }
3131
+ );
3132
+ finalizeEvidence(evidence);
3133
+ }
3134
+ const evidenceFile = evidence ? writeEvidenceLog(cwd, evidence) : void 0;
3135
+ const finalReport = {
3136
+ ...report,
3137
+ evidenceFile,
3138
+ correlation,
3139
+ provenance,
3140
+ constraints,
3141
+ resumedFromCache: false
3142
+ };
3143
+ if (options.idempotencyKey && !options.dryRun && !options.preview) {
3144
+ storeIdempotentReport(cwd, options.idempotencyKey, normalizedCveId, finalReport);
3145
+ }
3146
+ return finalReport;
3147
+ }
3148
+ async function planRemediation(cveId, options = {}) {
3149
+ return remediate(cveId, {
3150
+ ...options,
3151
+ preview: true,
3152
+ dryRun: true
3153
+ });
3154
+ }
3155
+
3156
+ // src/scanner/parse-input.ts
3157
+ import { extname } from "path";
3158
+ import { readFileSync as readFileSync11 } from "fs";
3159
+
3160
+ // src/scanner/adapters/npm-audit.ts
3161
+ import { readFileSync as readFileSync8 } from "fs";
3162
+ var CVE_REGEX = /CVE-\d{4}-\d+/gi;
3163
+ function normalizeSeverity(raw) {
3164
+ if (!raw) return "UNKNOWN";
3165
+ const up = raw.toUpperCase();
3166
+ if (up === "CRITICAL" || up === "HIGH" || up === "MEDIUM" || up === "LOW") {
3167
+ return up;
3168
+ }
3169
+ return "UNKNOWN";
3170
+ }
3171
+ function parseNpmAuditJsonFromString(content) {
3172
+ const report = JSON.parse(content);
3173
+ const findings = [];
3174
+ const seen = /* @__PURE__ */ new Set();
3175
+ for (const vuln of Object.values(report.vulnerabilities ?? {})) {
3176
+ for (const viaEntry of vuln.via ?? []) {
3177
+ const text = typeof viaEntry === "string" ? viaEntry : `${viaEntry.url ?? ""} ${viaEntry.name ?? ""}`;
3178
+ const matches = text.match(CVE_REGEX) ?? [];
3179
+ for (const match of matches) {
3180
+ const cveId = match.toUpperCase();
3181
+ const key = `${cveId}:${vuln.name}`;
3182
+ if (seen.has(key)) continue;
3183
+ seen.add(key);
3184
+ findings.push({
3185
+ cveId,
3186
+ source: "npm-audit",
3187
+ packageName: vuln.name,
3188
+ severity: normalizeSeverity(vuln.severity)
3189
+ });
3190
+ }
3191
+ }
3192
+ }
3193
+ return findings;
3194
+ }
3195
+ function parseNpmAuditJsonFile(filePath) {
3196
+ const content = readFileSync8(filePath, "utf8");
3197
+ return parseNpmAuditJsonFromString(content);
3198
+ }
3199
+
3200
+ // src/scanner/adapters/yarn-audit.ts
3201
+ import { readFileSync as readFileSync9 } from "fs";
3202
+ var CVE_REGEX2 = /CVE-\d{4}-\d+/gi;
3203
+ function normalizeSeverity2(raw) {
3204
+ if (!raw) return "UNKNOWN";
3205
+ const up = raw.toUpperCase();
3206
+ if (up === "CRITICAL" || up === "HIGH" || up === "MEDIUM" || up === "LOW") {
3207
+ return up;
3208
+ }
3209
+ return "UNKNOWN";
3210
+ }
3211
+ function parseYarnAuditJsonFromString(content) {
3212
+ const findings = [];
3213
+ const seen = /* @__PURE__ */ new Set();
3214
+ const lines = content.split("\n").map((line) => line.trim()).filter(Boolean);
3215
+ for (const line of lines) {
3216
+ let parsed;
3217
+ try {
3218
+ parsed = JSON.parse(line);
3219
+ } catch {
3220
+ continue;
3221
+ }
3222
+ const event = parsed;
3223
+ if (event.type !== "auditAdvisory") continue;
3224
+ const advisory = event.data?.advisory;
3225
+ const packageName = advisory?.module_name;
3226
+ const severity = normalizeSeverity2(advisory?.severity);
3227
+ const text = `${advisory?.url ?? ""} ${(advisory?.cves ?? []).join(" ")}`;
3228
+ const matches = text.match(CVE_REGEX2) ?? [];
3229
+ for (const match of matches) {
3230
+ const cveId = match.toUpperCase();
3231
+ const key = `${cveId}:${packageName ?? ""}`;
3232
+ if (seen.has(key)) continue;
3233
+ seen.add(key);
3234
+ findings.push({
3235
+ cveId,
3236
+ source: "yarn-audit",
3237
+ packageName,
3238
+ severity
3239
+ });
3240
+ }
3241
+ }
3242
+ return findings;
3243
+ }
3244
+ function parseYarnAuditJsonFile(filePath) {
3245
+ const content = readFileSync9(filePath, "utf8");
3246
+ return parseYarnAuditJsonFromString(content);
3247
+ }
3248
+
3249
+ // src/scanner/adapters/sarif.ts
3250
+ import { readFileSync as readFileSync10 } from "fs";
3251
+ var CVE_REGEX3 = /CVE-\d{4}-\d+/gi;
3252
+ function extractPackageName(result) {
3253
+ const pkg = result.properties?.["packageName"];
3254
+ return typeof pkg === "string" ? pkg : void 0;
3255
+ }
3256
+ function parseSarifFromString(content) {
3257
+ const report = JSON.parse(content);
3258
+ const findings = [];
3259
+ const seen = /* @__PURE__ */ new Set();
3260
+ for (const run of report.runs ?? []) {
3261
+ for (const result of run.results ?? []) {
3262
+ const combined = `${result.ruleId ?? ""} ${result.message?.text ?? ""}`;
3263
+ const matches = combined.match(CVE_REGEX3) ?? [];
3264
+ for (const match of matches) {
3265
+ const cveId = match.toUpperCase();
3266
+ const pkg = extractPackageName(result);
3267
+ const key = `${cveId}:${pkg ?? ""}`;
3268
+ if (seen.has(key)) continue;
3269
+ seen.add(key);
3270
+ findings.push({
3271
+ cveId,
3272
+ source: "sarif",
3273
+ packageName: pkg,
3274
+ severity: "UNKNOWN"
3275
+ });
3276
+ }
2353
3277
  }
2354
- return result;
2355
- });
2356
- return {
2357
- ...report,
2358
- results: nextResults,
2359
- constraints
2360
- };
3278
+ }
3279
+ return findings;
2361
3280
  }
2362
- async function remediate(cveId, options = {}) {
2363
- if (!/^CVE-\d{4}-\d+$/i.test(cveId)) {
2364
- throw new Error(
2365
- `Invalid CVE ID: "${cveId}". Expected format: CVE-YYYY-NNNNN (e.g. CVE-2021-23337).`
2366
- );
3281
+ function parseSarifFile(filePath) {
3282
+ const content = readFileSync10(filePath, "utf8");
3283
+ return parseSarifFromString(content);
3284
+ }
3285
+
3286
+ // src/scanner/parse-input.ts
3287
+ function parseScanInput(filePath, format) {
3288
+ const resolved = format === "auto" ? inferFormat(filePath) : format;
3289
+ if (resolved === "npm-audit") {
3290
+ return parseNpmAuditJsonFile(filePath);
2367
3291
  }
2368
- const cwd = options.cwd ?? process.cwd();
2369
- const constraints = resolveConstraints(options, cwd);
2370
- const provenance = resolveProvenanceContext(options);
2371
- const correlation = resolveCorrelationContext(options);
2372
- if (options.resume && options.idempotencyKey) {
2373
- const cached = readIdempotentReport(cwd, options.idempotencyKey, cveId.toUpperCase());
2374
- if (cached) {
2375
- return {
2376
- ...cached,
2377
- summary: `${cached.summary} (resumed from idempotency cache)`,
2378
- correlation,
2379
- provenance,
2380
- constraints,
2381
- resumedFromCache: true
2382
- };
2383
- }
3292
+ if (resolved === "yarn-audit") {
3293
+ return parseYarnAuditJsonFile(filePath);
2384
3294
  }
2385
- const report = await runRemediationPipeline(cveId.toUpperCase(), {
2386
- ...options,
2387
- ...correlation,
2388
- constraints
2389
- });
2390
- const constrainedReport = enforceConstraints(report, constraints);
2391
- const finalReport = {
2392
- ...constrainedReport,
2393
- correlation,
2394
- provenance,
2395
- constraints,
2396
- resumedFromCache: false
2397
- };
2398
- if (options.idempotencyKey && !options.dryRun && !options.preview) {
2399
- storeIdempotentReport(cwd, options.idempotencyKey, cveId.toUpperCase(), finalReport);
3295
+ if (resolved === "sarif") {
3296
+ return parseSarifFile(filePath);
2400
3297
  }
2401
- return {
2402
- ...finalReport
2403
- };
3298
+ throw new Error(`Unsupported input format: ${resolved}`);
2404
3299
  }
2405
- async function planRemediation(cveId, options = {}) {
2406
- return remediate(cveId, {
2407
- ...options,
2408
- preview: true,
2409
- dryRun: true
2410
- });
3300
+ function inferFormat(filePath) {
3301
+ const ext = extname(filePath).toLowerCase();
3302
+ if (ext === ".sarif") return "sarif";
3303
+ try {
3304
+ const content = readFileSync11(filePath, "utf8");
3305
+ const firstLine = content.split("\n").find((line) => line.trim().startsWith("{"));
3306
+ if (firstLine) {
3307
+ const parsed = JSON.parse(firstLine);
3308
+ if (parsed.type === "auditAdvisory" || parsed.type === "auditSummary") {
3309
+ return "yarn-audit";
3310
+ }
3311
+ }
3312
+ } catch {
3313
+ }
3314
+ return "npm-audit";
2411
3315
  }
2412
- async function remediateFromScan(inputPath, options = {}) {
2413
- const cwd = options.cwd ?? process.cwd();
2414
- const format = options.format ?? "auto";
2415
- const patchesDir = options.patchesDir ?? "./patches";
2416
- const findings = parseScanInput(inputPath, format);
2417
- const cveIds = uniqueCveIds(findings);
2418
- const policy = loadPolicy(cwd, options.policy);
2419
- const correlation = resolveCorrelationContext(options);
2420
- const provenance = resolveProvenanceContext(options);
2421
- const constraints = resolveConstraints(options, cwd);
2422
- const evidence = createEvidenceLog(cwd, cveIds, {
2423
- ...correlation,
2424
- actor: provenance.actor,
2425
- source: provenance.source,
2426
- idempotencyKey: options.idempotencyKey
2427
- });
2428
- addEvidenceStep(evidence, "scan.parse", { inputPath, format }, { findingCount: findings.length, cveCount: cveIds.length });
3316
+
3317
+ // src/scanner/unique-cve-ids.ts
3318
+ function uniqueCveIds(findings) {
3319
+ return [...new Set(findings.map((f) => f.cveId.toUpperCase()))];
3320
+ }
3321
+
3322
+ // src/api/scan-execution.ts
3323
+ async function executeScanRemediations(params) {
2429
3324
  const reports = [];
2430
3325
  const errors = [];
2431
3326
  const patchValidationFailures = [];
2432
3327
  let patchCount = 0;
2433
- for (const cveId of cveIds) {
3328
+ for (const cveId of params.cveIds) {
2434
3329
  try {
2435
- addEvidenceStep(evidence, "remediate.start", { cveId });
3330
+ addEvidenceStep(params.evidence, "remediate.start", { cveId });
2436
3331
  const report = await remediate(cveId, {
2437
- ...options,
2438
- patchesDir,
2439
- ...correlation,
2440
- actor: provenance.actor,
2441
- source: provenance.source,
2442
- constraints
3332
+ ...params.options,
3333
+ patchesDir: params.patchesDir,
3334
+ evidence: false,
3335
+ ...params.correlation,
3336
+ actor: params.provenance.actor,
3337
+ source: params.provenance.source,
3338
+ constraints: params.constraints
2443
3339
  });
2444
- report.results = report.results.filter((r) => isPackageAllowed(policy, r.packageName));
3340
+ report.results = report.results.filter((result) => isPackageAllowed(params.policy, result.packageName));
2445
3341
  for (const result of report.results) {
2446
3342
  if (result.strategy === "patch-file") {
2447
3343
  patchCount += 1;
@@ -2455,16 +3351,26 @@ async function remediateFromScan(inputPath, options = {}) {
2455
3351
  }
2456
3352
  }
2457
3353
  reports.push(report);
2458
- addEvidenceStep(evidence, "remediate.finish", { cveId }, { results: report.results.length });
3354
+ addEvidenceStep(params.evidence, "remediate.finish", { cveId }, { results: report.results.length });
2459
3355
  } catch (error) {
2460
3356
  const message = error instanceof Error ? error.message : String(error);
2461
3357
  errors.push({ cveId, message });
2462
- addEvidenceStep(evidence, "remediate.error", { cveId }, void 0, message);
3358
+ addEvidenceStep(params.evidence, "remediate.error", { cveId }, void 0, message);
2463
3359
  }
2464
3360
  }
3361
+ return {
3362
+ reports,
3363
+ errors,
3364
+ patchCount,
3365
+ patchValidationFailures
3366
+ };
3367
+ }
3368
+
3369
+ // src/api/scan-outcome.ts
3370
+ function buildScanOutcome(params) {
2465
3371
  let successCount = 0;
2466
3372
  let failedCount = 0;
2467
- for (const report of reports) {
3373
+ for (const report of params.reports) {
2468
3374
  for (const result of report.results) {
2469
3375
  if (result.applied || result.dryRun) {
2470
3376
  successCount += 1;
@@ -2473,13 +3379,75 @@ async function remediateFromScan(inputPath, options = {}) {
2473
3379
  }
2474
3380
  }
2475
3381
  }
2476
- failedCount += errors.length;
3382
+ failedCount += params.errors.length;
2477
3383
  let status = "ok";
2478
3384
  if (failedCount > 0 && successCount > 0) {
2479
3385
  status = "partial";
2480
3386
  } else if (failedCount > 0 && successCount === 0) {
2481
3387
  status = "failed";
2482
3388
  }
3389
+ const strategyCounts = buildStrategyCounts(params.reports);
3390
+ const dependencyScopeCounts = buildDependencyScopeCounts(params.reports);
3391
+ const unresolvedByReason = buildUnresolvedReasonCounts(params.reports);
3392
+ const remediationCount = params.reports.reduce((sum, report) => sum + report.results.length, 0);
3393
+ return {
3394
+ status,
3395
+ successCount,
3396
+ failedCount,
3397
+ strategyCounts,
3398
+ dependencyScopeCounts,
3399
+ unresolvedByReason,
3400
+ remediationCount
3401
+ };
3402
+ }
3403
+
3404
+ // src/api/remediate-from-scan.ts
3405
+ async function remediateFromScan(inputPath, options = {}) {
3406
+ const cwd = options.cwd ?? process.cwd();
3407
+ const format = options.format ?? "auto";
3408
+ const patchesDir = options.patchesDir ?? "./patches";
3409
+ const findings = parseScanInput(inputPath, format);
3410
+ const cveIds = uniqueCveIds(findings);
3411
+ const policy = loadPolicy(cwd, options.policy);
3412
+ const correlation = resolveCorrelationContext(options);
3413
+ const provenance = resolveProvenanceContext(options);
3414
+ const constraints = resolveConstraints(options, cwd);
3415
+ const evidence = createEvidenceLog(cwd, cveIds, {
3416
+ ...correlation,
3417
+ actor: provenance.actor,
3418
+ source: provenance.source,
3419
+ idempotencyKey: options.idempotencyKey
3420
+ });
3421
+ addEvidenceStep(evidence, "scan.parse", { inputPath, format }, { findingCount: findings.length, cveCount: cveIds.length });
3422
+ const execution = await executeScanRemediations({
3423
+ cveIds,
3424
+ options,
3425
+ patchesDir,
3426
+ policy,
3427
+ correlation,
3428
+ provenance,
3429
+ constraints,
3430
+ evidence
3431
+ });
3432
+ const reports = execution.reports;
3433
+ const errors = execution.errors;
3434
+ const patchCount = execution.patchCount;
3435
+ const patchValidationFailures = execution.patchValidationFailures;
3436
+ const outcome = buildScanOutcome({ reports, errors });
3437
+ const { status, successCount, failedCount, strategyCounts, dependencyScopeCounts, unresolvedByReason, remediationCount } = outcome;
3438
+ evidence.summary = {
3439
+ status,
3440
+ cveCount: cveIds.length,
3441
+ remediationCount,
3442
+ successCount,
3443
+ failedCount,
3444
+ patchCount,
3445
+ patchValidationFailures: patchValidationFailures.length > 0 ? patchValidationFailures : void 0,
3446
+ strategyCounts,
3447
+ dependencyScopeCounts,
3448
+ unresolvedByReason,
3449
+ patchesDir: patchCount > 0 ? patchesDir : void 0
3450
+ };
2483
3451
  finalizeEvidence(evidence);
2484
3452
  const evidenceFile = options.evidence === false ? void 0 : writeEvidenceLog(cwd, evidence);
2485
3453
  return {
@@ -2494,6 +3462,9 @@ async function remediateFromScan(inputPath, options = {}) {
2494
3462
  evidenceFile,
2495
3463
  patchCount,
2496
3464
  patchValidationFailures: patchValidationFailures.length > 0 ? patchValidationFailures : void 0,
3465
+ strategyCounts,
3466
+ dependencyScopeCounts,
3467
+ unresolvedByReason,
2497
3468
  patchesDir: patchCount > 0 ? patchesDir : void 0,
2498
3469
  correlation,
2499
3470
  provenance,
@@ -2501,102 +3472,22 @@ async function remediateFromScan(inputPath, options = {}) {
2501
3472
  idempotencyKey: options.idempotencyKey
2502
3473
  };
2503
3474
  }
2504
- function toCiSummary(report) {
2505
- let remediationCount = 0;
2506
- for (const cveReport of report.reports) {
2507
- remediationCount += cveReport.results.length;
2508
- }
2509
- return {
2510
- schemaVersion: report.schemaVersion,
2511
- status: report.status,
2512
- generatedAt: report.generatedAt,
2513
- cveCount: report.cveIds.length,
2514
- remediationCount,
2515
- successCount: report.successCount,
2516
- failedCount: report.failedCount,
2517
- errors: report.errors,
2518
- evidenceFile: report.evidenceFile,
2519
- patchCount: report.patchCount || 0,
2520
- patchValidationFailures: report.patchValidationFailures,
2521
- patchesDir: report.patchesDir,
2522
- correlation: report.correlation,
2523
- provenance: report.provenance,
2524
- constraints: report.constraints,
2525
- idempotencyKey: report.idempotencyKey
2526
- };
2527
- }
2528
- function ciExitCode(summary) {
2529
- return summary.failedCount > 0 ? 1 : 0;
2530
- }
2531
- function severityToSarifLevel(severity) {
2532
- if (severity === "CRITICAL" || severity === "HIGH") return "error";
2533
- if (severity === "MEDIUM") return "warning";
2534
- if (severity === "LOW") return "note";
2535
- return "warning";
2536
- }
2537
- function toSarifOutput(report) {
2538
- const rules = [];
2539
- const results = [];
2540
- const seenRules = /* @__PURE__ */ new Set();
2541
- for (const r of report.reports) {
2542
- const severity = r.cveDetails?.severity ?? "UNKNOWN";
2543
- const level = severityToSarifLevel(severity);
2544
- const summary = r.cveDetails?.summary ?? r.cveId;
2545
- if (!seenRules.has(r.cveId)) {
2546
- seenRules.add(r.cveId);
2547
- rules.push({
2548
- id: r.cveId,
2549
- name: "VulnerableDependency",
2550
- shortDescription: { text: r.cveId },
2551
- fullDescription: { text: summary },
2552
- defaultConfiguration: { level },
2553
- helpUri: `https://osv.dev/vulnerability/${r.cveId}`,
2554
- properties: { severity }
2555
- });
2556
- }
2557
- for (const vp of r.vulnerablePackages) {
2558
- const fixText = vp.affected.firstPatchedVersion ? ` Fix: upgrade to ${vp.affected.firstPatchedVersion}.` : " No fixed version available.";
2559
- results.push({
2560
- ruleId: r.cveId,
2561
- level,
2562
- message: {
2563
- text: `${vp.installed.name}@${vp.installed.version} is vulnerable to ${r.cveId}: ${summary}${fixText}`
2564
- },
2565
- locations: [
2566
- {
2567
- physicalLocation: {
2568
- artifactLocation: { uri: "package.json", uriBaseId: "%SRCROOT%" }
2569
- }
2570
- }
2571
- ]
2572
- });
2573
- }
2574
- }
2575
- return {
2576
- version: "2.1.0",
2577
- $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Documents/CommitteeSpecifications/2.1.0/sarif-schema-2.1.0.json",
2578
- runs: [
2579
- {
2580
- tool: {
2581
- driver: {
2582
- name: "autoremediator",
2583
- informationUri: "https://github.com/Rawlings/autoremediator",
2584
- rules
2585
- }
2586
- },
2587
- results
2588
- }
2589
- ]
2590
- };
2591
- }
2592
3475
 
2593
3476
  export {
2594
3477
  runRemediationPipeline,
2595
- remediate,
2596
- planRemediation,
2597
- remediateFromScan,
3478
+ PACKAGE_MANAGER_VALUES,
3479
+ LLM_PROVIDER_VALUES,
3480
+ PROVENANCE_SOURCE_VALUES,
3481
+ OPTION_DESCRIPTIONS,
3482
+ createConstraintSchemaProperties,
3483
+ createRemediateOptionSchemaProperties,
3484
+ createScanOptionSchemaProperties,
3485
+ createScanReportSchemaProperties,
2598
3486
  toCiSummary,
2599
3487
  ciExitCode,
2600
- toSarifOutput
3488
+ toSarifOutput,
3489
+ remediate,
3490
+ planRemediation,
3491
+ remediateFromScan
2601
3492
  };
2602
- //# sourceMappingURL=chunk-VLXGEH7U.js.map
3493
+ //# sourceMappingURL=chunk-MUFP2DQX.js.map