autoremediator 0.2.2 → 0.4.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,7 +1,7 @@
1
1
  // src/remediation/pipeline.ts
2
2
  import { generateText as generateText2 } from "ai";
3
3
  import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
4
- import { join as join7 } from "path";
4
+ import { join as join8 } from "path";
5
5
  import semver4 from "semver";
6
6
 
7
7
  // src/platform/config.ts
@@ -64,6 +64,19 @@ function getNvdConfig() {
64
64
  function getGitHubToken() {
65
65
  return process.env.GITHUB_TOKEN;
66
66
  }
67
+ function getIntelligenceSourceConfig() {
68
+ return {
69
+ gitLabAdvisoryApi: process.env.AUTOREMEDIATOR_GITLAB_ADVISORY_API ?? "https://advisories.gitlab.com/api/v1/advisories",
70
+ certCcSearchUrl: process.env.AUTOREMEDIATOR_CERTCC_SEARCH_URL ?? "https://www.kb.cert.org/vuls/search",
71
+ epssApi: process.env.AUTOREMEDIATOR_EPSS_API ?? "https://api.first.org/data/v1/epss",
72
+ cveServicesApi: process.env.AUTOREMEDIATOR_CVE_SERVICES_API ?? "https://cveawg.mitre.org/api/cve",
73
+ depsDevApi: process.env.AUTOREMEDIATOR_DEPSDEV_API ?? "https://api.deps.dev/v3",
74
+ scorecardApi: process.env.AUTOREMEDIATOR_SCORECARD_API ?? "https://api.securityscorecards.dev",
75
+ vendorAdvisoryFeeds: (process.env.AUTOREMEDIATOR_VENDOR_ADVISORY_FEEDS ?? "").split(",").map((v) => v.trim()).filter(Boolean),
76
+ commercialFeeds: (process.env.AUTOREMEDIATOR_COMMERCIAL_FEEDS ?? "").split(",").map((v) => v.trim()).filter(Boolean),
77
+ commercialFeedToken: process.env.AUTOREMEDIATOR_COMMERCIAL_FEED_TOKEN
78
+ };
79
+ }
67
80
 
68
81
  // src/platform/package-manager.ts
69
82
  import { existsSync } from "fs";
@@ -343,6 +356,288 @@ async function enrichWithNvd(details) {
343
356
  return details;
344
357
  }
345
358
 
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);
376
+ }
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);
390
+ }
391
+ return details;
392
+ }
393
+
394
+ // src/intelligence/sources/epss.ts
395
+ async function fetchEpss(cveId) {
396
+ const { epssApi } = getIntelligenceSourceConfig();
397
+ if (!epssApi) return void 0;
398
+ 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];
407
+ } catch {
408
+ return void 0;
409
+ }
410
+ }
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;
418
+ }
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;
432
+ }
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);
439
+ }
440
+ return Array.from(refs);
441
+ }
442
+ async function fetchCveServicesRecord(cveId) {
443
+ const { cveServicesApi } = getIntelligenceSourceConfig();
444
+ if (!cveServicesApi) return void 0;
445
+ try {
446
+ const res = await fetch(`${cveServicesApi}/${encodeURIComponent(cveId)}`, {
447
+ headers: { Accept: "application/json" }
448
+ });
449
+ if (!res.ok) return void 0;
450
+ return await res.json();
451
+ } catch {
452
+ return void 0;
453
+ }
454
+ }
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);
466
+ }
467
+ details.intelligence = {
468
+ ...details.intelligence ?? {},
469
+ cveServicesEnriched: true
470
+ };
471
+ return details;
472
+ }
473
+
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
+ );
480
+ }
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 [];
496
+ }
497
+ }
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);
506
+ }
507
+ details.intelligence = {
508
+ ...details.intelligence ?? {},
509
+ gitlabAdvisoryMatched: true
510
+ };
511
+ return details;
512
+ }
513
+
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;
531
+ }
532
+ }
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);
538
+ }
539
+ details.intelligence = {
540
+ ...details.intelligence ?? {},
541
+ certCcMatched: true
542
+ };
543
+ if (!details.references.includes(CERTCC_HOME)) {
544
+ details.references.push(CERTCC_HOME);
545
+ }
546
+ return details;
547
+ }
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;
559
+ }
560
+ }
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;
572
+ }
573
+
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;
587
+ }
588
+ }
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) {
606
+ 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 });
612
+ if (!res.ok) return void 0;
613
+ return feedUrl.toString();
614
+ } catch {
615
+ return void 0;
616
+ }
617
+ }
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;
630
+ }
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
+ return details;
639
+ }
640
+
346
641
  // src/remediation/tools/lookup-cve.ts
347
642
  var lookupCveTool = tool({
348
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.",
@@ -371,7 +666,37 @@ var lookupCveTool = tool({
371
666
  if (ghPackages.length > 0) {
372
667
  details = mergeGhDataIntoCveDetails(details, ghPackages);
373
668
  }
374
- details = await enrichWithNvd(details);
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
+ };
375
700
  if (details.affectedPackages.length === 0) {
376
701
  return {
377
702
  success: false,
@@ -574,7 +899,7 @@ var findFixedVersionTool = tool4({
574
899
  // src/remediation/tools/apply-version-bump.ts
575
900
  import { tool as tool5 } from "ai";
576
901
  import { z as z5 } from "zod";
577
- import { join as join4 } from "path";
902
+ import { join as join5 } from "path";
578
903
  import { readFileSync as readFileSync3, writeFileSync } from "fs";
579
904
  import { execa as execa2 } from "execa";
580
905
  import semver3 from "semver";
@@ -585,7 +910,11 @@ import { join as join3 } from "path";
585
910
  var DEFAULT_POLICY = {
586
911
  allowMajorBumps: false,
587
912
  denyPackages: [],
588
- allowPackages: []
913
+ allowPackages: [],
914
+ constraints: {
915
+ directDependenciesOnly: false,
916
+ preferVersionBump: false
917
+ }
589
918
  };
590
919
  function loadPolicy(cwd, explicitPath) {
591
920
  const candidate = explicitPath ?? join3(cwd, ".autoremediator.json");
@@ -595,7 +924,11 @@ function loadPolicy(cwd, explicitPath) {
595
924
  return {
596
925
  allowMajorBumps: parsed.allowMajorBumps ?? DEFAULT_POLICY.allowMajorBumps,
597
926
  denyPackages: parsed.denyPackages ?? DEFAULT_POLICY.denyPackages,
598
- allowPackages: parsed.allowPackages ?? DEFAULT_POLICY.allowPackages
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
+ }
599
932
  };
600
933
  } catch {
601
934
  return DEFAULT_POLICY;
@@ -609,6 +942,45 @@ function isPackageAllowed(policy, packageName) {
609
942
  return true;
610
943
  }
611
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
+ }
973
+ }
974
+ }
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();
981
+ }
982
+ }
983
+
612
984
  // src/remediation/tools/apply-version-bump.ts
613
985
  var applyVersionBumpTool = tool5({
614
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.",
@@ -619,8 +991,8 @@ var applyVersionBumpTool = tool5({
619
991
  fromVersion: z5.string().describe("The currently installed vulnerable version"),
620
992
  toVersion: z5.string().describe("The safe target version to upgrade to"),
621
993
  dryRun: z5.boolean().default(false).describe("If true, report changes but do not write"),
622
- policyPath: z5.string().optional().describe("Optional path to .autoremediator policy file"),
623
- skipTests: z5.boolean().default(true).describe("If true, skip test validation after applying the fix")
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")
624
996
  }),
625
997
  execute: async ({
626
998
  cwd,
@@ -629,14 +1001,14 @@ var applyVersionBumpTool = tool5({
629
1001
  fromVersion,
630
1002
  toVersion,
631
1003
  dryRun,
632
- policyPath,
633
- skipTests
1004
+ policy,
1005
+ runTests
634
1006
  }) => {
635
1007
  const pm = packageManager ?? detectPackageManager(cwd);
636
1008
  const commands = getPackageManagerCommands(pm);
637
- const pkgPath = join4(cwd, "package.json");
638
- const policy = loadPolicy(cwd, policyPath);
639
- if (!isPackageAllowed(policy, packageName)) {
1009
+ const pkgPath = join5(cwd, "package.json");
1010
+ const loadedPolicy = loadPolicy(cwd, policy);
1011
+ if (!isPackageAllowed(loadedPolicy, packageName)) {
640
1012
  return {
641
1013
  packageName,
642
1014
  strategy: "none",
@@ -648,7 +1020,7 @@ var applyVersionBumpTool = tool5({
648
1020
  };
649
1021
  }
650
1022
  const isMajorBump = semver3.valid(fromVersion) && semver3.valid(toVersion) && semver3.major(toVersion) > semver3.major(fromVersion);
651
- if (isMajorBump && !policy.allowMajorBumps) {
1023
+ if (isMajorBump && !loadedPolicy.allowMajorBumps) {
652
1024
  return {
653
1025
  packageName,
654
1026
  strategy: "none",
@@ -699,49 +1071,21 @@ var applyVersionBumpTool = tool5({
699
1071
  toVersion,
700
1072
  applied: false,
701
1073
  dryRun: true,
702
- message: `[DRY RUN] Would update ${depField}.${packageName}: "${currentRange}" \u2192 "${newRange}", then run ${installCmd}${skipTests ? "" : ` and ${testCmd}`}.`
1074
+ message: `[DRY RUN] Would update ${depField}.${packageName}: "${currentRange}" -> "${newRange}", then run ${installCmd}${runTests ? ` and ${testCmd}` : ""}.`
703
1075
  };
704
1076
  }
705
- pkgJson[depField][packageName] = newRange;
706
- writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
707
- try {
708
- const [installCmd, ...installArgs] = commands.installPreferOffline;
709
- await execa2(installCmd, installArgs, {
710
- cwd,
711
- stdio: "pipe"
712
- });
713
- } catch (err) {
714
- pkgJson[depField][packageName] = currentRange;
1077
+ return withRepoLock(cwd, async () => {
1078
+ pkgJson[depField][packageName] = newRange;
715
1079
  writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
716
- const message = err instanceof Error ? err.message : String(err);
717
- return {
718
- packageName,
719
- strategy: "version-bump",
720
- fromVersion,
721
- toVersion,
722
- applied: false,
723
- dryRun: false,
724
- message: `${commands.installPreferOffline.join(" ")} failed after updating "${packageName}" to ${toVersion}. Reverted. Error: ${message}`
725
- };
726
- }
727
- if (!skipTests) {
728
1080
  try {
729
- const [testCmd, ...testArgs] = commands.test;
730
- await execa2(testCmd, testArgs, {
1081
+ const [installCmd, ...installArgs] = commands.installPreferOffline;
1082
+ await execa2(installCmd, installArgs, {
731
1083
  cwd,
732
1084
  stdio: "pipe"
733
1085
  });
734
1086
  } catch (err) {
735
1087
  pkgJson[depField][packageName] = currentRange;
736
1088
  writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
737
- try {
738
- const [rollbackCmd, ...rollbackArgs] = commands.installPreferOffline;
739
- await execa2(rollbackCmd, rollbackArgs, {
740
- cwd,
741
- stdio: "pipe"
742
- });
743
- } catch {
744
- }
745
1089
  const message = err instanceof Error ? err.message : String(err);
746
1090
  return {
747
1091
  packageName,
@@ -750,27 +1094,57 @@ var applyVersionBumpTool = tool5({
750
1094
  toVersion,
751
1095
  applied: false,
752
1096
  dryRun: false,
753
- message: `${commands.test.join(" ")} failed after upgrading "${packageName}" to ${toVersion}. Rolled back to ${currentRange}. Error: ${message}`
1097
+ message: `${commands.installPreferOffline.join(" ")} failed after updating "${packageName}" to ${toVersion}. Reverted. Error: ${message}`
754
1098
  };
755
1099
  }
756
- }
757
- return {
758
- packageName,
759
- strategy: "version-bump",
760
- fromVersion,
761
- toVersion,
762
- applied: true,
763
- dryRun: false,
764
- message: `Successfully upgraded "${packageName}" from ${fromVersion} to ${toVersion}, ran ${commands.installPreferOffline.join(" ")}${skipTests ? "" : `, and passed ${commands.test.join(" ")}`}.`
765
- };
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
+ };
1128
+ }
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
+ };
1139
+ });
766
1140
  }
767
1141
  });
768
1142
 
769
1143
  // src/remediation/tools/fetch-package-source.ts
770
1144
  import { tool as tool6 } from "ai";
771
1145
  import { z as z6 } from "zod";
772
- import { mkdir, readdir, readFile, rm } from "fs/promises";
773
- import { join as join5 } from "path";
1146
+ import { mkdir as mkdir2, readdir, readFile, rm as rm2 } from "fs/promises";
1147
+ import { join as join6 } from "path";
774
1148
  import { execa as execa3 } from "execa";
775
1149
  var fetchPackageSourceTool = tool6({
776
1150
  description: "Download package tarball from npm and extract source files for CVE analysis. Supports custom file patterns (default: *.js, *.ts).",
@@ -787,23 +1161,23 @@ var fetchPackageSourceTool = tool6({
787
1161
  filePatterns
788
1162
  }) => {
789
1163
  const tempBaseDir = `/tmp/autoremediator-pkg-${Date.now()}`;
790
- const extractDir = join5(tempBaseDir, "out");
1164
+ const extractDir = join6(tempBaseDir, "out");
791
1165
  try {
792
1166
  const npmUrl = `https://registry.npmjs.org/${packageName}/-/${packageName.split("/").pop()}-${version}.tgz`;
793
- await mkdir(tempBaseDir, { recursive: true });
794
- const tarballPath = join5(tempBaseDir, "package.tgz");
1167
+ await mkdir2(tempBaseDir, { recursive: true });
1168
+ const tarballPath = join6(tempBaseDir, "package.tgz");
795
1169
  await execa3("curl", ["-L", "-o", tarballPath, npmUrl]);
796
- await mkdir(extractDir, { recursive: true });
1170
+ await mkdir2(extractDir, { recursive: true });
797
1171
  await execa3("tar", ["-xzf", tarballPath, "-C", extractDir]);
798
1172
  const extractedContents = await readdir(extractDir);
799
- const packageRootDir = extractedContents.includes("package") ? join5(extractDir, "package") : extractDir;
1173
+ const packageRootDir = extractedContents.includes("package") ? join6(extractDir, "package") : extractDir;
800
1174
  const sourceCode = {};
801
1175
  async function walkDir(dir, relativeBase) {
802
1176
  try {
803
1177
  const files = await readdir(dir, { withFileTypes: true });
804
1178
  for (const file of files) {
805
- const fullPath = join5(dir, file.name);
806
- const relPath = join5(relativeBase, file.name);
1179
+ const fullPath = join6(dir, file.name);
1180
+ const relPath = join6(relativeBase, file.name);
807
1181
  if (file.isDirectory()) {
808
1182
  if (![
809
1183
  "node_modules",
@@ -860,7 +1234,7 @@ var fetchPackageSourceTool = tool6({
860
1234
  error: `Failed to fetch and extract package ${packageName}@${version}: ${message}`
861
1235
  };
862
1236
  } finally {
863
- await rm(tempBaseDir, { recursive: true, force: true });
1237
+ await rm2(tempBaseDir, { recursive: true, force: true });
864
1238
  }
865
1239
  }
866
1240
  });
@@ -1076,9 +1450,9 @@ function generateUnifiedDiff(original, fixed, filePath) {
1076
1450
  import { tool as tool8 } from "ai";
1077
1451
  import { z as z8 } from "zod";
1078
1452
  import { existsSync as existsSync3 } from "fs";
1079
- import { mkdir as mkdir2, mkdtemp, readFile as readFile2, rm as rm2, writeFile } from "fs/promises";
1453
+ import { mkdir as mkdir3, mkdtemp, readFile as readFile2, rm as rm3, writeFile } from "fs/promises";
1080
1454
  import { tmpdir } from "os";
1081
- import { join as join6 } from "path";
1455
+ import { join as join7 } from "path";
1082
1456
  import { execa as execa4 } from "execa";
1083
1457
  var applyPatchFileTool = tool8({
1084
1458
  description: "Write generated patch file and apply it using package-manager-native patch flow when available, falling back to patch-package when needed.",
@@ -1126,7 +1500,7 @@ var applyPatchFileTool = tool8({
1126
1500
  };
1127
1501
  }
1128
1502
  const patchFileName = buildPatchFileName(packageName, vulnerableVersion);
1129
- const patchFilePath = join6(cwd, patchesDir, patchFileName);
1503
+ const patchFilePath = join7(cwd, patchesDir, patchFileName);
1130
1504
  if (dryRun) {
1131
1505
  return {
1132
1506
  success: true,
@@ -1139,66 +1513,68 @@ var applyPatchFileTool = tool8({
1139
1513
  patchPath: patchFilePath
1140
1514
  };
1141
1515
  }
1142
- const patchesDirPath = join6(cwd, patchesDir);
1143
- await mkdir2(patchesDirPath, { recursive: true });
1144
- await writeFile(patchFilePath, selectedPatch, "utf8");
1145
- let validationResult;
1146
- const patchMode = await resolvePatchMode(pm, cwd);
1147
- const applyResult = patchMode === "patch-package" ? await configurePatchPackagePostinstall(cwd, pm) : await applyNativePatch({
1148
- cwd,
1149
- packageName,
1150
- vulnerableVersion,
1151
- patchContent: selectedPatch,
1152
- patchMode
1153
- });
1154
- if (!applyResult.success) {
1155
- return {
1156
- success: false,
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,
1157
1524
  packageName,
1158
1525
  vulnerableVersion,
1159
- applied: false,
1160
- dryRun: false,
1161
- message: applyResult.error,
1162
- patchFilePath,
1163
- patchPath: patchFilePath,
1164
- patchMode,
1165
- postinstallConfigured: patchMode === "patch-package" ? false : void 0,
1166
- error: applyResult.error
1167
- };
1168
- }
1169
- if (validateWithTests) {
1170
- validationResult = await validatePatchWithTests(cwd, pm);
1171
- if (!validationResult.passed) {
1172
- const validationError = "Patch validation failed after apply; patch marked unresolved.";
1526
+ patchContent: selectedPatch,
1527
+ patchMode
1528
+ });
1529
+ if (!applyResult.success) {
1173
1530
  return {
1174
1531
  success: false,
1175
1532
  packageName,
1176
1533
  vulnerableVersion,
1177
1534
  applied: false,
1178
1535
  dryRun: false,
1179
- message: validationError,
1536
+ message: applyResult.error,
1180
1537
  patchFilePath,
1181
1538
  patchPath: patchFilePath,
1182
1539
  patchMode,
1183
- postinstallConfigured: patchMode === "patch-package",
1184
- validation: validationResult,
1185
- error: validationError
1540
+ postinstallConfigured: patchMode === "patch-package" ? false : void 0,
1541
+ error: applyResult.error
1186
1542
  };
1187
1543
  }
1188
- }
1189
- return {
1190
- success: true,
1191
- packageName,
1192
- vulnerableVersion,
1193
- applied: true,
1194
- dryRun: false,
1195
- message: `Patch applied successfully for ${packageName}@${vulnerableVersion}.`,
1196
- patchFilePath,
1197
- patchPath: patchFilePath,
1198
- patchMode,
1199
- postinstallConfigured: patchMode === "patch-package",
1200
- validation: validationResult
1201
- };
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
+ });
1202
1578
  } catch (err) {
1203
1579
  const message = err instanceof Error ? err.message : String(err);
1204
1580
  return {
@@ -1233,7 +1609,7 @@ function buildPatchFileName(packageName, vulnerableVersion) {
1233
1609
  return `${safeName}+${vulnerableVersion}.patch`;
1234
1610
  }
1235
1611
  async function configurePatchPackagePostinstall(cwd, packageManager) {
1236
- const pkgJsonPath = join6(cwd, "package.json");
1612
+ const pkgJsonPath = join7(cwd, "package.json");
1237
1613
  let pkgJson;
1238
1614
  try {
1239
1615
  pkgJson = JSON.parse(await readFile2(pkgJsonPath, "utf8"));
@@ -1297,8 +1673,8 @@ ${createResult.stderr}`);
1297
1673
  error: `Could not determine native patch directory for ${packageSpec}.`
1298
1674
  };
1299
1675
  }
1300
- const tempPatchDir = await mkdtemp(join6(tmpdir(), "autoremediator-native-patch-"));
1301
- const tempPatchFile = join6(tempPatchDir, "change.patch");
1676
+ const tempPatchDir = await mkdtemp(join7(tmpdir(), "autoremediator-native-patch-"));
1677
+ const tempPatchFile = join7(tempPatchDir, "change.patch");
1302
1678
  try {
1303
1679
  await writeFile(tempPatchFile, patchContent, "utf8");
1304
1680
  await execa4("patch", ["-p1", "-i", tempPatchFile], {
@@ -1317,7 +1693,7 @@ ${createResult.stderr}`);
1317
1693
  error: `Failed to apply native patch for ${packageSpec}: ${err instanceof Error ? err.message : String(err)}`
1318
1694
  };
1319
1695
  } finally {
1320
- await rm2(tempPatchDir, { recursive: true, force: true });
1696
+ await rm3(tempPatchDir, { recursive: true, force: true });
1321
1697
  }
1322
1698
  return { success: true };
1323
1699
  }
@@ -1389,17 +1765,18 @@ async function runRemediationPipeline(cveId, options = {}) {
1389
1765
  }
1390
1766
  const cwd = options.cwd ?? process.cwd();
1391
1767
  const packageManager = options.packageManager ?? detectPackageManager(cwd);
1392
- const dryRun = options.dryRun ?? false;
1393
- const skipTests = options.skipTests ?? true;
1394
- const policyPath = options.policyPath ?? "";
1768
+ const preview = options.preview ?? false;
1769
+ const dryRun = (options.dryRun ?? false) || preview;
1770
+ const runTests = options.runTests ?? false;
1771
+ const policy = options.policy ?? "";
1395
1772
  const patchesDir = options.patchesDir || "./patches";
1396
1773
  const model = await createModel(options);
1397
1774
  const systemPrompt = loadOrchestrationPrompt({
1398
1775
  cveId,
1399
1776
  cwd,
1400
1777
  dryRun,
1401
- skipTests,
1402
- policyPath,
1778
+ runTests,
1779
+ policy,
1403
1780
  patchesDir,
1404
1781
  packageManager
1405
1782
  });
@@ -1408,6 +1785,14 @@ async function runRemediationPipeline(cveId, options = {}) {
1408
1785
  const vulnerablePackages = [];
1409
1786
  let cveDetails = null;
1410
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;
1411
1796
  const result = await generateText2({
1412
1797
  model,
1413
1798
  system: systemPrompt,
@@ -1417,10 +1802,10 @@ async function runRemediationPipeline(cveId, options = {}) {
1417
1802
  "check-inventory": checkInventoryTool,
1418
1803
  "check-version-match": checkVersionMatchTool,
1419
1804
  "find-fixed-version": findFixedVersionTool,
1420
- "apply-version-bump": applyVersionBumpTool,
1805
+ "apply-version-bump": applyVersionBumpToolForRun,
1421
1806
  "fetch-package-source": fetchPackageSourceTool,
1422
1807
  "generate-patch": generatePatchTool,
1423
- "apply-patch-file": applyPatchFileTool
1808
+ "apply-patch-file": applyPatchFileToolForRun
1424
1809
  },
1425
1810
  maxSteps: 25,
1426
1811
  onStepFinish(stepResult) {
@@ -1463,15 +1848,21 @@ async function runRemediationPipeline(cveId, options = {}) {
1463
1848
  vulnerablePackages,
1464
1849
  results: collectedResults,
1465
1850
  agentSteps,
1466
- summary: result.text
1851
+ summary: result.text,
1852
+ correlation: {
1853
+ requestId: options.requestId,
1854
+ sessionId: options.sessionId,
1855
+ parentRunId: options.parentRunId
1856
+ }
1467
1857
  };
1468
1858
  }
1469
1859
  async function runLocalRemediationPipeline(cveId, options = {}) {
1470
1860
  const cwd = options.cwd ?? process.cwd();
1471
1861
  const packageManager = options.packageManager ?? detectPackageManager(cwd);
1472
- const dryRun = options.dryRun ?? false;
1473
- const skipTests = options.skipTests ?? true;
1474
- const policyPath = options.policyPath ?? "";
1862
+ const preview = options.preview ?? false;
1863
+ const dryRun = (options.dryRun ?? false) || preview;
1864
+ const runTests = options.runTests ?? false;
1865
+ const policy = options.policy ?? "";
1475
1866
  const collectedResults = [];
1476
1867
  const vulnerablePackages = [];
1477
1868
  let cveDetails = null;
@@ -1489,7 +1880,12 @@ async function runLocalRemediationPipeline(cveId, options = {}) {
1489
1880
  vulnerablePackages,
1490
1881
  results: collectedResults,
1491
1882
  agentSteps,
1492
- summary: `Local mode failed at lookup-cve: ${normalizedId} not found in OSV or GitHub advisory data.`
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
+ }
1493
1889
  };
1494
1890
  }
1495
1891
  cveDetails = osvDetails ?? {
@@ -1510,7 +1906,12 @@ async function runLocalRemediationPipeline(cveId, options = {}) {
1510
1906
  vulnerablePackages,
1511
1907
  results: collectedResults,
1512
1908
  agentSteps,
1513
- summary: `Local mode lookup succeeded but no npm affected packages were found for ${normalizedId}.`
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
+ }
1514
1915
  };
1515
1916
  }
1516
1917
  const inventory = await checkInventoryTool.execute({ cwd, packageManager });
@@ -1522,7 +1923,12 @@ async function runLocalRemediationPipeline(cveId, options = {}) {
1522
1923
  vulnerablePackages,
1523
1924
  results: collectedResults,
1524
1925
  agentSteps,
1525
- summary: `Local mode failed at check-inventory: ${inventory.error}`
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
+ }
1526
1932
  };
1527
1933
  }
1528
1934
  const installedPackages = inventory.packages ?? [];
@@ -1597,8 +2003,8 @@ async function runLocalRemediationPipeline(cveId, options = {}) {
1597
2003
  fromVersion: pkg.version,
1598
2004
  toVersion: safeVersion,
1599
2005
  dryRun,
1600
- policyPath,
1601
- skipTests
2006
+ policy,
2007
+ runTests
1602
2008
  });
1603
2009
  agentSteps += 1;
1604
2010
  collectedResults.push(applyResult);
@@ -1612,18 +2018,23 @@ async function runLocalRemediationPipeline(cveId, options = {}) {
1612
2018
  vulnerablePackages,
1613
2019
  results: collectedResults,
1614
2020
  agentSteps,
1615
- summary: `Local mode completed: vulnerable=${vulnerablePackages.length}, applied=${appliedCount}, dryRun=${dryRunCount}, unresolved=${unresolvedCount}`
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
+ }
1616
2027
  };
1617
2028
  }
1618
2029
  function loadOrchestrationPrompt(ctx) {
1619
- const promptPath = join7(process.cwd(), ".github", "instructions", "orchestration.instructions.md");
2030
+ const promptPath = join8(process.cwd(), ".github", "instructions", "orchestration.instructions.md");
1620
2031
  if (!existsSync4(promptPath)) {
1621
2032
  return `You are autoremediator, an agentic security remediation system for Node.js package dependencies.
1622
2033
  Working directory: ${ctx.cwd}
1623
2034
  Package manager: ${ctx.packageManager}
1624
2035
  Dry run: ${ctx.dryRun}
1625
- Skip tests: ${ctx.skipTests}
1626
- Policy path: ${ctx.policyPath || "undefined"}
2036
+ Run tests: ${ctx.runTests}
2037
+ Policy: ${ctx.policy || "undefined"}
1627
2038
  Patches dir: ${ctx.patchesDir}
1628
2039
 
1629
2040
  Required sequence:
@@ -1641,7 +2052,7 @@ Fallback sequence (when strategy="none"):
1641
2052
  Always respect dryRun and policy constraints.`;
1642
2053
  }
1643
2054
  const template = readFileSync4(promptPath, "utf8");
1644
- return template.replaceAll("{{cveId}}", ctx.cveId).replaceAll("{{cwd}}", ctx.cwd).replaceAll("{{packageManager}}", ctx.packageManager).replaceAll("{{dryRun}}", String(ctx.dryRun)).replaceAll("{{skipTests}}", String(ctx.skipTests)).replaceAll("{{policyPath}}", ctx.policyPath || "undefined").replaceAll("{{patchesDir}}", ctx.patchesDir);
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);
1645
2056
  }
1646
2057
 
1647
2058
  // src/scanner/index.ts
@@ -1810,10 +2221,16 @@ function uniqueCveIds(findings) {
1810
2221
 
1811
2222
  // src/platform/evidence.ts
1812
2223
  import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
1813
- import { join as join8 } from "path";
1814
- function createEvidenceLog(cwd, cveIds) {
2224
+ import { join as join9 } from "path";
2225
+ function createEvidenceLog(cwd, cveIds, context = {}) {
1815
2226
  return {
1816
- runId: `${Date.now()}`,
2227
+ runId: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
2228
+ requestId: context.requestId,
2229
+ sessionId: context.sessionId,
2230
+ parentRunId: context.parentRunId,
2231
+ actor: context.actor,
2232
+ source: context.source,
2233
+ idempotencyKey: context.idempotencyKey,
1817
2234
  cveIds,
1818
2235
  cwd,
1819
2236
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1834,21 +2251,163 @@ function finalizeEvidence(log) {
1834
2251
  return log;
1835
2252
  }
1836
2253
  function writeEvidenceLog(cwd, log) {
1837
- const dir = join8(cwd, ".autoremediator", "evidence");
2254
+ const dir = join9(cwd, ".autoremediator", "evidence");
1838
2255
  mkdirSync(dir, { recursive: true });
1839
- const filePath = join8(dir, `${log.runId}.json`);
2256
+ const filePath = join9(dir, `${log.runId}.json`);
1840
2257
  writeFileSync2(filePath, JSON.stringify(log, null, 2) + "\n", "utf8");
1841
2258
  return filePath;
1842
2259
  }
1843
2260
 
2261
+ // 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";
2264
+ var DEFAULT_INDEX = {
2265
+ schemaVersion: "1.0",
2266
+ entries: {}
2267
+ };
2268
+ function indexFilePath(cwd) {
2269
+ return join10(cwd, ".autoremediator", "state", "idempotency.json");
2270
+ }
2271
+ function entryKey(idempotencyKey, cveId) {
2272
+ return `${idempotencyKey}::${cveId.toUpperCase()}`;
2273
+ }
2274
+ function loadIndex(cwd) {
2275
+ const filePath = indexFilePath(cwd);
2276
+ if (!existsSync5(filePath)) return DEFAULT_INDEX;
2277
+ try {
2278
+ const parsed = JSON.parse(readFileSync9(filePath, "utf8"));
2279
+ if (parsed && parsed.schemaVersion === "1.0" && parsed.entries) {
2280
+ return parsed;
2281
+ }
2282
+ return DEFAULT_INDEX;
2283
+ } catch {
2284
+ return DEFAULT_INDEX;
2285
+ }
2286
+ }
2287
+ function saveIndex(cwd, index) {
2288
+ const filePath = indexFilePath(cwd);
2289
+ mkdirSync2(join10(cwd, ".autoremediator", "state"), { recursive: true });
2290
+ writeFileSync3(filePath, JSON.stringify(index, null, 2) + "\n", "utf8");
2291
+ }
2292
+ function readIdempotentReport(cwd, idempotencyKey, cveId) {
2293
+ const index = loadIndex(cwd);
2294
+ const key = entryKey(idempotencyKey, cveId);
2295
+ return index.entries[key]?.report;
2296
+ }
2297
+ function storeIdempotentReport(cwd, idempotencyKey, cveId, report) {
2298
+ const index = loadIndex(cwd);
2299
+ const key = entryKey(idempotencyKey, cveId);
2300
+ index.entries[key] = {
2301
+ key: idempotencyKey,
2302
+ cveId: cveId.toUpperCase(),
2303
+ report,
2304
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
2305
+ };
2306
+ saveIndex(cwd, index);
2307
+ }
2308
+
1844
2309
  // src/api.ts
2310
+ function buildRequestId() {
2311
+ return `req-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2312
+ }
2313
+ function resolveCorrelationContext(options) {
2314
+ return {
2315
+ requestId: options.requestId ?? buildRequestId(),
2316
+ sessionId: options.sessionId,
2317
+ parentRunId: options.parentRunId
2318
+ };
2319
+ }
2320
+ function resolveProvenanceContext(options) {
2321
+ return {
2322
+ actor: options.actor,
2323
+ source: options.source ?? "sdk"
2324
+ };
2325
+ }
2326
+ function resolveConstraints(options, cwd) {
2327
+ const policy = loadPolicy(cwd, options.policy);
2328
+ return {
2329
+ directDependenciesOnly: options.constraints?.directDependenciesOnly ?? policy.constraints?.directDependenciesOnly ?? false,
2330
+ preferVersionBump: options.constraints?.preferVersionBump ?? policy.constraints?.preferVersionBump ?? false
2331
+ };
2332
+ }
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
+ };
2353
+ }
2354
+ return result;
2355
+ });
2356
+ return {
2357
+ ...report,
2358
+ results: nextResults,
2359
+ constraints
2360
+ };
2361
+ }
1845
2362
  async function remediate(cveId, options = {}) {
1846
2363
  if (!/^CVE-\d{4}-\d+$/i.test(cveId)) {
1847
2364
  throw new Error(
1848
2365
  `Invalid CVE ID: "${cveId}". Expected format: CVE-YYYY-NNNNN (e.g. CVE-2021-23337).`
1849
2366
  );
1850
2367
  }
1851
- return runRemediationPipeline(cveId.toUpperCase(), options);
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
+ }
2384
+ }
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);
2400
+ }
2401
+ return {
2402
+ ...finalReport
2403
+ };
2404
+ }
2405
+ async function planRemediation(cveId, options = {}) {
2406
+ return remediate(cveId, {
2407
+ ...options,
2408
+ preview: true,
2409
+ dryRun: true
2410
+ });
1852
2411
  }
1853
2412
  async function remediateFromScan(inputPath, options = {}) {
1854
2413
  const cwd = options.cwd ?? process.cwd();
@@ -1856,24 +2415,36 @@ async function remediateFromScan(inputPath, options = {}) {
1856
2415
  const patchesDir = options.patchesDir ?? "./patches";
1857
2416
  const findings = parseScanInput(inputPath, format);
1858
2417
  const cveIds = uniqueCveIds(findings);
1859
- const policy = loadPolicy(cwd, options.policyPath);
1860
- const evidence = createEvidenceLog(cwd, cveIds);
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
+ });
1861
2428
  addEvidenceStep(evidence, "scan.parse", { inputPath, format }, { findingCount: findings.length, cveCount: cveIds.length });
1862
2429
  const reports = [];
1863
2430
  const errors = [];
1864
2431
  const patchValidationFailures = [];
1865
- let patchFileCount = 0;
2432
+ let patchCount = 0;
1866
2433
  for (const cveId of cveIds) {
1867
2434
  try {
1868
2435
  addEvidenceStep(evidence, "remediate.start", { cveId });
1869
2436
  const report = await remediate(cveId, {
1870
2437
  ...options,
1871
- patchesDir
2438
+ patchesDir,
2439
+ ...correlation,
2440
+ actor: provenance.actor,
2441
+ source: provenance.source,
2442
+ constraints
1872
2443
  });
1873
2444
  report.results = report.results.filter((r) => isPackageAllowed(policy, r.packageName));
1874
2445
  for (const result of report.results) {
1875
2446
  if (result.strategy === "patch-file") {
1876
- patchFileCount += 1;
2447
+ patchCount += 1;
1877
2448
  }
1878
2449
  if (result.validation?.passed === false && result.validation?.error) {
1879
2450
  patchValidationFailures.push({
@@ -1910,7 +2481,7 @@ async function remediateFromScan(inputPath, options = {}) {
1910
2481
  status = "failed";
1911
2482
  }
1912
2483
  finalizeEvidence(evidence);
1913
- const evidenceFile = options.writeEvidence === false ? void 0 : writeEvidenceLog(cwd, evidence);
2484
+ const evidenceFile = options.evidence === false ? void 0 : writeEvidenceLog(cwd, evidence);
1914
2485
  return {
1915
2486
  schemaVersion: "1.0",
1916
2487
  status,
@@ -1921,9 +2492,13 @@ async function remediateFromScan(inputPath, options = {}) {
1921
2492
  failedCount,
1922
2493
  errors,
1923
2494
  evidenceFile,
1924
- patchFileCount,
2495
+ patchCount,
1925
2496
  patchValidationFailures: patchValidationFailures.length > 0 ? patchValidationFailures : void 0,
1926
- patchStorageDir: patchFileCount > 0 ? patchesDir : void 0
2497
+ patchesDir: patchCount > 0 ? patchesDir : void 0,
2498
+ correlation,
2499
+ provenance,
2500
+ constraints,
2501
+ idempotencyKey: options.idempotencyKey
1927
2502
  };
1928
2503
  }
1929
2504
  function toCiSummary(report) {
@@ -1941,9 +2516,13 @@ function toCiSummary(report) {
1941
2516
  failedCount: report.failedCount,
1942
2517
  errors: report.errors,
1943
2518
  evidenceFile: report.evidenceFile,
1944
- patchFileCount: report.patchFileCount || 0,
2519
+ patchCount: report.patchCount || 0,
1945
2520
  patchValidationFailures: report.patchValidationFailures,
1946
- patchStorageDir: report.patchStorageDir
2521
+ patchesDir: report.patchesDir,
2522
+ correlation: report.correlation,
2523
+ provenance: report.provenance,
2524
+ constraints: report.constraints,
2525
+ idempotencyKey: report.idempotencyKey
1947
2526
  };
1948
2527
  }
1949
2528
  function ciExitCode(summary) {
@@ -1953,8 +2532,9 @@ function ciExitCode(summary) {
1953
2532
  export {
1954
2533
  runRemediationPipeline,
1955
2534
  remediate,
2535
+ planRemediation,
1956
2536
  remediateFromScan,
1957
2537
  toCiSummary,
1958
2538
  ciExitCode
1959
2539
  };
1960
- //# sourceMappingURL=chunk-DQKT2CUG.js.map
2540
+ //# sourceMappingURL=chunk-GBOD3DV6.js.map