autoremediator 0.4.1 → 0.6.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,8 @@
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";
3
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
4
+ import { join as join10 } from "path";
5
+ import semver5 from "semver";
6
6
 
7
7
  // src/platform/config.ts
8
8
  function resolveProvider(options = {}) {
@@ -836,10 +836,15 @@ async function fetchPackageVersions(packageName) {
836
836
  const data = await res.json();
837
837
  return Object.keys(data.versions);
838
838
  }
839
- async function findSafeUpgradeVersion(packageName, installedVersion, firstPatchedVersion, vulnerableRange) {
839
+ async function resolveSafeUpgradeVersion(packageName, installedVersion, firstPatchedVersion, vulnerableRange) {
840
840
  const versions = await fetchPackageVersions(packageName);
841
- if (!versions.length) return void 0;
842
- const installedMajor = semver2.major(installedVersion);
841
+ if (!versions.length) {
842
+ return {
843
+ candidates: {},
844
+ majorOnlyFixAvailable: false
845
+ };
846
+ }
847
+ const installed = semver2.parse(installedVersion);
843
848
  const candidates = versions.filter((v) => semver2.valid(v) && semver2.gte(v, firstPatchedVersion)).filter((v) => {
844
849
  if (!vulnerableRange) return true;
845
850
  try {
@@ -848,17 +853,59 @@ async function findSafeUpgradeVersion(packageName, installedVersion, firstPatche
848
853
  return true;
849
854
  }
850
855
  }).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];
856
+ if (!candidates.length) {
857
+ return {
858
+ candidates: {},
859
+ majorOnlyFixAvailable: false
860
+ };
861
+ }
862
+ const categorizedCandidates = {};
863
+ for (const candidate of candidates) {
864
+ const level = classifyUpgradeLevel(installedVersion, candidate);
865
+ if (!level) continue;
866
+ if (!categorizedCandidates[level]) {
867
+ categorizedCandidates[level] = candidate;
868
+ }
869
+ }
870
+ const safeVersion = categorizedCandidates.patch ?? categorizedCandidates.minor ?? categorizedCandidates.major;
871
+ if (!safeVersion) {
872
+ return {
873
+ candidates: categorizedCandidates,
874
+ majorOnlyFixAvailable: false
875
+ };
876
+ }
877
+ const upgradeLevel = classifyUpgradeLevel(installedVersion, safeVersion);
878
+ const majorOnlyFixAvailable = !categorizedCandidates.patch && !categorizedCandidates.minor && Boolean(categorizedCandidates.major);
879
+ if (!installed || !upgradeLevel) {
880
+ return {
881
+ safeVersion,
882
+ upgradeLevel,
883
+ candidates: categorizedCandidates,
884
+ majorOnlyFixAvailable
885
+ };
886
+ }
887
+ return {
888
+ safeVersion,
889
+ upgradeLevel,
890
+ candidates: categorizedCandidates,
891
+ majorOnlyFixAvailable
892
+ };
893
+ }
894
+ function classifyUpgradeLevel(installedVersion, candidateVersion) {
895
+ const installed = semver2.parse(installedVersion);
896
+ const candidate = semver2.parse(candidateVersion);
897
+ if (!installed || !candidate) return void 0;
898
+ if (candidate.major > installed.major) return "major";
899
+ if (candidate.minor > installed.minor) return "minor";
900
+ if (candidate.patch > installed.patch || candidate.version === installed.version) {
901
+ return "patch";
902
+ }
903
+ return void 0;
857
904
  }
858
905
 
859
906
  // src/remediation/tools/find-fixed-version.ts
860
907
  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.",
908
+ 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.",
862
909
  parameters: z4.object({
863
910
  packageName: z4.string().describe("The npm package name"),
864
911
  installedVersion: z4.string().describe("The currently installed version (exact semver)"),
@@ -873,15 +920,18 @@ var findFixedVersionTool = tool4({
873
920
  firstPatchedVersion,
874
921
  vulnerableRange
875
922
  }) => {
876
- const safeVersion = await findSafeUpgradeVersion(
923
+ const resolution = await resolveSafeUpgradeVersion(
877
924
  packageName,
878
925
  installedVersion,
879
926
  firstPatchedVersion,
880
927
  vulnerableRange
881
928
  );
929
+ const { safeVersion, upgradeLevel, candidates, majorOnlyFixAvailable } = resolution;
882
930
  if (!safeVersion) {
883
931
  return {
932
+ candidates,
884
933
  isMajorBump: false,
934
+ majorOnlyFixAvailable: false,
885
935
  message: `No safe upgrade version found for "${packageName}". The patch-file path will be needed.`
886
936
  };
887
937
  }
@@ -890,8 +940,11 @@ var findFixedVersionTool = tool4({
890
940
  const isMajorBump = safeMajor > installedMajor;
891
941
  return {
892
942
  safeVersion,
943
+ upgradeLevel,
944
+ candidates,
893
945
  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}).`
946
+ majorOnlyFixAvailable,
947
+ 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}).`
895
948
  };
896
949
  }
897
950
  });
@@ -1016,6 +1069,7 @@ var applyVersionBumpTool = tool5({
1016
1069
  toVersion,
1017
1070
  applied: false,
1018
1071
  dryRun,
1072
+ unresolvedReason: "policy-blocked",
1019
1073
  message: `Policy blocked changes for package "${packageName}".`
1020
1074
  };
1021
1075
  }
@@ -1028,6 +1082,7 @@ var applyVersionBumpTool = tool5({
1028
1082
  toVersion,
1029
1083
  applied: false,
1030
1084
  dryRun,
1085
+ unresolvedReason: "major-bump-required",
1031
1086
  message: `Policy blocked major bump for "${packageName}" (${fromVersion} -> ${toVersion}).`
1032
1087
  };
1033
1088
  }
@@ -1041,6 +1096,7 @@ var applyVersionBumpTool = tool5({
1041
1096
  fromVersion,
1042
1097
  applied: false,
1043
1098
  dryRun,
1099
+ unresolvedReason: "package-json-not-found",
1044
1100
  message: `Could not read package.json at "${pkgPath}".`
1045
1101
  };
1046
1102
  }
@@ -1054,6 +1110,7 @@ var applyVersionBumpTool = tool5({
1054
1110
  fromVersion,
1055
1111
  applied: false,
1056
1112
  dryRun,
1113
+ unresolvedReason: "indirect-dependency",
1057
1114
  message: `"${packageName}" was not found in package.json dependencies (it may be a transitive dep). Cannot auto-bump.`
1058
1115
  };
1059
1116
  }
@@ -1094,6 +1151,7 @@ var applyVersionBumpTool = tool5({
1094
1151
  toVersion,
1095
1152
  applied: false,
1096
1153
  dryRun: false,
1154
+ unresolvedReason: "install-failed",
1097
1155
  message: `${commands.installPreferOffline.join(" ")} failed after updating "${packageName}" to ${toVersion}. Reverted. Error: ${message}`
1098
1156
  };
1099
1157
  }
@@ -1123,6 +1181,7 @@ var applyVersionBumpTool = tool5({
1123
1181
  toVersion,
1124
1182
  applied: false,
1125
1183
  dryRun: false,
1184
+ unresolvedReason: "validation-failed",
1126
1185
  message: `${commands.test.join(" ")} failed after upgrading "${packageName}" to ${toVersion}. Rolled back to ${currentRange}. Error: ${message}`
1127
1186
  };
1128
1187
  }
@@ -1140,18 +1199,219 @@ var applyVersionBumpTool = tool5({
1140
1199
  }
1141
1200
  });
1142
1201
 
1143
- // src/remediation/tools/fetch-package-source.ts
1202
+ // src/remediation/tools/apply-package-override.ts
1144
1203
  import { tool as tool6 } from "ai";
1145
1204
  import { z as z6 } from "zod";
1146
- import { mkdir as mkdir2, readdir, readFile, rm as rm2 } from "fs/promises";
1147
1205
  import { join as join6 } from "path";
1206
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
1148
1207
  import { execa as execa3 } from "execa";
1149
- var fetchPackageSourceTool = tool6({
1150
- description: "Download package tarball from npm and extract source files for CVE analysis. Supports custom file patterns (default: *.js, *.ts).",
1208
+ import semver4 from "semver";
1209
+ var applyPackageOverrideTool = tool6({
1210
+ description: "Apply a package-manager-native package.json override for a vulnerable transitive dependency and reinstall. Uses npm overrides, pnpm.overrides, or yarn resolutions.",
1151
1211
  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(
1212
+ cwd: z6.string().describe("Absolute path to the consumer project root"),
1213
+ packageManager: z6.enum(["npm", "pnpm", "yarn"]).optional().describe("Package manager used by the target project (auto-detected if omitted)"),
1214
+ packageName: z6.string().describe("The npm package to override"),
1215
+ fromVersion: z6.string().describe("The currently installed vulnerable version"),
1216
+ toVersion: z6.string().describe("The safe target version to override to"),
1217
+ dryRun: z6.boolean().default(false).describe("If true, report changes but do not write"),
1218
+ policy: z6.string().optional().describe("Optional path to .autoremediator policy file"),
1219
+ runTests: z6.boolean().default(false).describe("If true, run test validation after applying the override")
1220
+ }),
1221
+ execute: async ({
1222
+ cwd,
1223
+ packageManager,
1224
+ packageName,
1225
+ fromVersion,
1226
+ toVersion,
1227
+ dryRun,
1228
+ policy,
1229
+ runTests
1230
+ }) => {
1231
+ const pm = packageManager ?? detectPackageManager(cwd);
1232
+ const commands = getPackageManagerCommands(pm);
1233
+ const pkgPath = join6(cwd, "package.json");
1234
+ const loadedPolicy = loadPolicy(cwd, policy);
1235
+ if (!isPackageAllowed(loadedPolicy, packageName)) {
1236
+ return {
1237
+ packageName,
1238
+ strategy: "none",
1239
+ fromVersion,
1240
+ toVersion,
1241
+ applied: false,
1242
+ dryRun,
1243
+ unresolvedReason: "policy-blocked",
1244
+ message: `Policy blocked changes for package "${packageName}".`
1245
+ };
1246
+ }
1247
+ const isMajorBump = semver4.valid(fromVersion) && semver4.valid(toVersion) && semver4.major(toVersion) > semver4.major(fromVersion);
1248
+ if (isMajorBump && !loadedPolicy.allowMajorBumps) {
1249
+ return {
1250
+ packageName,
1251
+ strategy: "none",
1252
+ fromVersion,
1253
+ toVersion,
1254
+ applied: false,
1255
+ dryRun,
1256
+ unresolvedReason: "major-bump-required",
1257
+ message: `Policy blocked major override for "${packageName}" (${fromVersion} -> ${toVersion}).`
1258
+ };
1259
+ }
1260
+ let pkgJson;
1261
+ try {
1262
+ pkgJson = JSON.parse(readFileSync4(pkgPath, "utf8"));
1263
+ } catch {
1264
+ return {
1265
+ packageName,
1266
+ strategy: "none",
1267
+ fromVersion,
1268
+ toVersion,
1269
+ applied: false,
1270
+ dryRun,
1271
+ unresolvedReason: "package-json-not-found",
1272
+ message: `Could not read package.json at "${pkgPath}".`
1273
+ };
1274
+ }
1275
+ const overrideLabel = describeOverrideField(pm);
1276
+ const previousValue = getOverrideValue(pkgJson, pm, packageName);
1277
+ if (dryRun) {
1278
+ return {
1279
+ packageName,
1280
+ strategy: "override",
1281
+ fromVersion,
1282
+ toVersion,
1283
+ applied: false,
1284
+ dryRun: true,
1285
+ message: `[DRY RUN] Would set ${overrideLabel}.${packageName} to "${toVersion}", then run ${commands.installPreferOffline.join(" ")}${runTests ? ` and ${commands.test.join(" ")}` : ""}.`
1286
+ };
1287
+ }
1288
+ return withRepoLock(cwd, async () => {
1289
+ setOverrideValue(pkgJson, pm, packageName, toVersion);
1290
+ writeFileSync2(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
1291
+ try {
1292
+ const [installCmd, ...installArgs] = commands.installPreferOffline;
1293
+ await execa3(installCmd, installArgs, { cwd, stdio: "pipe" });
1294
+ } catch (err) {
1295
+ restoreOverrideValue(pkgJson, pm, packageName, previousValue);
1296
+ writeFileSync2(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
1297
+ const message = err instanceof Error ? err.message : String(err);
1298
+ return {
1299
+ packageName,
1300
+ strategy: "override",
1301
+ fromVersion,
1302
+ toVersion,
1303
+ applied: false,
1304
+ dryRun: false,
1305
+ unresolvedReason: "override-apply-failed",
1306
+ message: `${commands.installPreferOffline.join(" ")} failed after applying ${overrideLabel} for "${packageName}" to ${toVersion}. Reverted. Error: ${message}`
1307
+ };
1308
+ }
1309
+ if (runTests) {
1310
+ try {
1311
+ const [testCmd, ...testArgs] = commands.test;
1312
+ await execa3(testCmd, testArgs, { cwd, stdio: "pipe" });
1313
+ } catch (err) {
1314
+ restoreOverrideValue(pkgJson, pm, packageName, previousValue);
1315
+ writeFileSync2(pkgPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
1316
+ try {
1317
+ const [rollbackCmd, ...rollbackArgs] = commands.installPreferOffline;
1318
+ await execa3(rollbackCmd, rollbackArgs, { cwd, stdio: "pipe" });
1319
+ } catch {
1320
+ }
1321
+ const message = err instanceof Error ? err.message : String(err);
1322
+ return {
1323
+ packageName,
1324
+ strategy: "override",
1325
+ fromVersion,
1326
+ toVersion,
1327
+ applied: false,
1328
+ dryRun: false,
1329
+ unresolvedReason: "validation-failed",
1330
+ message: `${commands.test.join(" ")} failed after applying ${overrideLabel} for "${packageName}" to ${toVersion}. Reverted. Error: ${message}`
1331
+ };
1332
+ }
1333
+ }
1334
+ return {
1335
+ packageName,
1336
+ strategy: "override",
1337
+ fromVersion,
1338
+ toVersion,
1339
+ applied: true,
1340
+ dryRun: false,
1341
+ message: `Successfully applied ${overrideLabel} for "${packageName}" from ${fromVersion} to ${toVersion}, then ran ${commands.installPreferOffline.join(" ")}${runTests ? ` and passed ${commands.test.join(" ")}` : ""}.`
1342
+ };
1343
+ });
1344
+ }
1345
+ });
1346
+ function describeOverrideField(packageManager) {
1347
+ if (packageManager === "npm") return "overrides";
1348
+ if (packageManager === "pnpm") return "pnpm.overrides";
1349
+ return "resolutions";
1350
+ }
1351
+ function getOverrideValue(pkgJson, packageManager, packageName) {
1352
+ if (packageManager === "npm") return pkgJson.overrides?.[packageName];
1353
+ if (packageManager === "pnpm") return pkgJson.pnpm?.overrides?.[packageName];
1354
+ return pkgJson.resolutions?.[packageName];
1355
+ }
1356
+ function setOverrideValue(pkgJson, packageManager, packageName, version) {
1357
+ if (packageManager === "npm") {
1358
+ pkgJson.overrides = { ...pkgJson.overrides ?? {}, [packageName]: version };
1359
+ return;
1360
+ }
1361
+ if (packageManager === "pnpm") {
1362
+ pkgJson.pnpm = {
1363
+ ...pkgJson.pnpm ?? {},
1364
+ overrides: {
1365
+ ...pkgJson.pnpm?.overrides ?? {},
1366
+ [packageName]: version
1367
+ }
1368
+ };
1369
+ return;
1370
+ }
1371
+ pkgJson.resolutions = { ...pkgJson.resolutions ?? {}, [packageName]: version };
1372
+ }
1373
+ function restoreOverrideValue(pkgJson, packageManager, packageName, previousValue) {
1374
+ if (packageManager === "npm") {
1375
+ pkgJson.overrides = restoreRecord(pkgJson.overrides, packageName, previousValue);
1376
+ return;
1377
+ }
1378
+ if (packageManager === "pnpm") {
1379
+ pkgJson.pnpm = {
1380
+ ...pkgJson.pnpm ?? {},
1381
+ overrides: restoreRecord(pkgJson.pnpm?.overrides, packageName, previousValue)
1382
+ };
1383
+ if (!pkgJson.pnpm.overrides) {
1384
+ delete pkgJson.pnpm.overrides;
1385
+ }
1386
+ if (Object.keys(pkgJson.pnpm).length === 0) {
1387
+ delete pkgJson.pnpm;
1388
+ }
1389
+ return;
1390
+ }
1391
+ pkgJson.resolutions = restoreRecord(pkgJson.resolutions, packageName, previousValue);
1392
+ }
1393
+ function restoreRecord(record, key, previousValue) {
1394
+ const nextRecord = { ...record ?? {} };
1395
+ if (previousValue === void 0) {
1396
+ delete nextRecord[key];
1397
+ } else {
1398
+ nextRecord[key] = previousValue;
1399
+ }
1400
+ return Object.keys(nextRecord).length > 0 ? nextRecord : void 0;
1401
+ }
1402
+
1403
+ // src/remediation/tools/fetch-package-source.ts
1404
+ import { tool as tool7 } from "ai";
1405
+ import { z as z7 } from "zod";
1406
+ import { mkdir as mkdir2, readdir, readFile, rm as rm2 } from "fs/promises";
1407
+ import { join as join7 } from "path";
1408
+ import { execa as execa4 } from "execa";
1409
+ var fetchPackageSourceTool = tool7({
1410
+ description: "Download package tarball from npm and extract source files for CVE analysis. Supports custom file patterns (default: *.js, *.ts).",
1411
+ parameters: z7.object({
1412
+ packageName: z7.string().min(1).describe("The npm package name (e.g., 'lodash', '@scope/package')"),
1413
+ version: z7.string().regex(/^\d+\.\d+\.\d+/, "Must be a valid semver version").describe("Exact package version to download"),
1414
+ filePatterns: z7.array(z7.string()).optional().default(["*.js", "*.ts"]).describe(
1155
1415
  "File patterns to extract (glob patterns, default: *.js, *.ts)"
1156
1416
  )
1157
1417
  }),
@@ -1161,23 +1421,23 @@ var fetchPackageSourceTool = tool6({
1161
1421
  filePatterns
1162
1422
  }) => {
1163
1423
  const tempBaseDir = `/tmp/autoremediator-pkg-${Date.now()}`;
1164
- const extractDir = join6(tempBaseDir, "out");
1424
+ const extractDir = join7(tempBaseDir, "out");
1165
1425
  try {
1166
1426
  const npmUrl = `https://registry.npmjs.org/${packageName}/-/${packageName.split("/").pop()}-${version}.tgz`;
1167
1427
  await mkdir2(tempBaseDir, { recursive: true });
1168
- const tarballPath = join6(tempBaseDir, "package.tgz");
1169
- await execa3("curl", ["-L", "-o", tarballPath, npmUrl]);
1428
+ const tarballPath = join7(tempBaseDir, "package.tgz");
1429
+ await execa4("curl", ["-L", "-o", tarballPath, npmUrl]);
1170
1430
  await mkdir2(extractDir, { recursive: true });
1171
- await execa3("tar", ["-xzf", tarballPath, "-C", extractDir]);
1431
+ await execa4("tar", ["-xzf", tarballPath, "-C", extractDir]);
1172
1432
  const extractedContents = await readdir(extractDir);
1173
- const packageRootDir = extractedContents.includes("package") ? join6(extractDir, "package") : extractDir;
1433
+ const packageRootDir = extractedContents.includes("package") ? join7(extractDir, "package") : extractDir;
1174
1434
  const sourceCode = {};
1175
1435
  async function walkDir(dir, relativeBase) {
1176
1436
  try {
1177
1437
  const files = await readdir(dir, { withFileTypes: true });
1178
1438
  for (const file of files) {
1179
- const fullPath = join6(dir, file.name);
1180
- const relPath = join6(relativeBase, file.name);
1439
+ const fullPath = join7(dir, file.name);
1440
+ const relPath = join7(relativeBase, file.name);
1181
1441
  if (file.isDirectory()) {
1182
1442
  if (![
1183
1443
  "node_modules",
@@ -1240,8 +1500,8 @@ var fetchPackageSourceTool = tool6({
1240
1500
  });
1241
1501
 
1242
1502
  // src/remediation/tools/generate-patch.ts
1243
- import { tool as tool7 } from "ai";
1244
- import { z as z7 } from "zod";
1503
+ import { tool as tool8 } from "ai";
1504
+ import { z as z8 } from "zod";
1245
1505
  import { generateText } from "ai";
1246
1506
  var VULNERABILITY_DESCRIPTIONS = {
1247
1507
  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 +1509,18 @@ var VULNERABILITY_DESCRIPTIONS = {
1249
1509
  "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
1510
  unknown: "Unknown vulnerability type: Analyze the CVE summary carefully and implement the most appropriate fix for the security issue described."
1251
1511
  };
1252
- var generatePatchTool = tool7({
1512
+ var generatePatchTool = tool8({
1253
1513
  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(
1514
+ parameters: z8.object({
1515
+ packageName: z8.string().min(1).describe("The npm package name"),
1516
+ vulnerableVersion: z8.string().describe("The vulnerable version string"),
1517
+ cveId: z8.string().regex(/^CVE-\d{4}-\d+$/i).describe("CVE ID (e.g., CVE-2021-23337)"),
1518
+ cveSummary: z8.string().min(10).describe("CVE description and impact"),
1519
+ sourceFiles: z8.record(z8.string()).describe(
1260
1520
  "Map of file paths to source code contents from fetch-package-source"
1261
1521
  ),
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")
1522
+ vulnerabilityCategory: z8.enum(["redos", "code-injection", "path-traversal", "unknown"]).optional().default("unknown").describe("Category of the vulnerability for better context"),
1523
+ dryRun: z8.boolean().optional().default(false).describe("If true, return analysis without generating patches")
1264
1524
  }),
1265
1525
  execute: async ({
1266
1526
  packageName,
@@ -1447,30 +1707,67 @@ function generateUnifiedDiff(original, fixed, filePath) {
1447
1707
  }
1448
1708
 
1449
1709
  // 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";
1710
+ import { tool as tool9 } from "ai";
1711
+ import { z as z9 } from "zod";
1712
+ import { existsSync as existsSync4 } from "fs";
1453
1713
  import { mkdir as mkdir3, mkdtemp, readFile as readFile2, rm as rm3, writeFile } from "fs/promises";
1454
1714
  import { tmpdir } from "os";
1455
- import { join as join7 } from "path";
1456
- import { execa as execa4 } from "execa";
1457
- var applyPatchFileTool = tool8({
1715
+ import { join as join9 } from "path";
1716
+ import { execa as execa6 } from "execa";
1717
+
1718
+ // src/remediation/strategies/patch-utils.ts
1719
+ import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync3, readFileSync as readFileSync5 } from "fs";
1720
+ import { join as join8 } from "path";
1721
+ import { execa as execa5 } from "execa";
1722
+ function validatePatchDiff(patchContent) {
1723
+ if (!patchContent || typeof patchContent !== "string") {
1724
+ return {
1725
+ valid: false,
1726
+ error: "Patch content must be a non-empty string"
1727
+ };
1728
+ }
1729
+ const hasFromLine = /^---\s+\S+/m.test(patchContent);
1730
+ const hasToLine = /^\+\+\+\s+\S+/m.test(patchContent);
1731
+ const hasHunkHeader = /^@@\s+-\d+/m.test(patchContent);
1732
+ if (!hasFromLine) {
1733
+ return {
1734
+ valid: false,
1735
+ error: 'Missing "---" line in patch format'
1736
+ };
1737
+ }
1738
+ if (!hasToLine) {
1739
+ return {
1740
+ valid: false,
1741
+ error: 'Missing "+++" line in patch format'
1742
+ };
1743
+ }
1744
+ if (!hasHunkHeader) {
1745
+ return {
1746
+ valid: false,
1747
+ error: "No hunk headers (@@...) found in patch"
1748
+ };
1749
+ }
1750
+ return { valid: true };
1751
+ }
1752
+
1753
+ // src/remediation/tools/apply-patch-file.ts
1754
+ var applyPatchFileTool = tool9({
1458
1755
  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)
1756
+ parameters: z9.object({
1757
+ packageName: z9.string().min(1).describe("The npm package name"),
1758
+ vulnerableVersion: z9.string().describe("The vulnerable version string"),
1759
+ patchContent: z9.string().min(10).optional().describe("Unified diff patch content from generate-patch"),
1760
+ patches: z9.array(
1761
+ z9.object({
1762
+ filePath: z9.string().min(1),
1763
+ unifiedDiff: z9.string().min(10)
1467
1764
  })
1468
1765
  ).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")
1766
+ patchesDir: z9.string().optional().default("./patches").describe("Directory to store patch files"),
1767
+ cwd: z9.string().describe("Project root directory (for package.json)"),
1768
+ packageManager: z9.enum(["npm", "pnpm", "yarn"]).optional().describe("Package manager used by the target project (auto-detected if omitted)"),
1769
+ validateWithTests: z9.boolean().optional().default(true).describe("Run package manager test command to validate patch doesn't break anything"),
1770
+ dryRun: z9.boolean().optional().default(false).describe("If true, report but do not mutate files")
1474
1771
  }).refine((value) => Boolean(value.patchContent || value.patches && value.patches.length > 0), {
1475
1772
  message: "Either patchContent or patches must be provided"
1476
1773
  }),
@@ -1499,8 +1796,20 @@ var applyPatchFileTool = tool8({
1499
1796
  error: "No patch content provided."
1500
1797
  };
1501
1798
  }
1799
+ const patchValidation = validatePatchDiff(selectedPatch);
1800
+ if (!patchValidation.valid) {
1801
+ return {
1802
+ success: false,
1803
+ packageName,
1804
+ vulnerableVersion,
1805
+ applied: false,
1806
+ dryRun,
1807
+ message: patchValidation.error ?? "Patch content is not a valid unified diff.",
1808
+ error: patchValidation.error ?? "Patch content is not a valid unified diff."
1809
+ };
1810
+ }
1502
1811
  const patchFileName = buildPatchFileName(packageName, vulnerableVersion);
1503
- const patchFilePath = join7(cwd, patchesDir, patchFileName);
1812
+ const patchFilePath = join9(cwd, patchesDir, patchFileName);
1504
1813
  if (dryRun) {
1505
1814
  return {
1506
1815
  success: true,
@@ -1514,11 +1823,13 @@ var applyPatchFileTool = tool8({
1514
1823
  };
1515
1824
  }
1516
1825
  return withRepoLock(cwd, async () => {
1517
- const patchesDirPath = join7(cwd, patchesDir);
1826
+ const packageJsonSnapshot = patchModeRequiresPackageJsonSnapshot(pm, cwd) ? await capturePackageJsonSnapshot(cwd) : void 0;
1827
+ const patchesDirPath = join9(cwd, patchesDir);
1518
1828
  await mkdir3(patchesDirPath, { recursive: true });
1519
1829
  await writeFile(patchFilePath, selectedPatch, "utf8");
1520
1830
  let validationResult;
1521
1831
  const patchMode = await resolvePatchMode(pm, cwd);
1832
+ const commands = getPackageManagerCommands(pm);
1522
1833
  const applyResult = patchMode === "patch-package" ? await configurePatchPackagePostinstall(cwd, pm) : await applyNativePatch({
1523
1834
  cwd,
1524
1835
  packageName,
@@ -1527,6 +1838,14 @@ var applyPatchFileTool = tool8({
1527
1838
  patchMode
1528
1839
  });
1529
1840
  if (!applyResult.success) {
1841
+ await cleanupPatchArtifacts({
1842
+ cwd,
1843
+ packageManager: pm,
1844
+ patchFilePath,
1845
+ patchMode,
1846
+ packageJsonSnapshot,
1847
+ rerunInstall: patchMode === "patch-package"
1848
+ });
1530
1849
  return {
1531
1850
  success: false,
1532
1851
  packageName,
@@ -1541,9 +1860,49 @@ var applyPatchFileTool = tool8({
1541
1860
  error: applyResult.error
1542
1861
  };
1543
1862
  }
1863
+ if (patchMode === "patch-package") {
1864
+ try {
1865
+ const [installCmd, ...installArgs] = commands.installPreferOffline;
1866
+ await execa6(installCmd, installArgs, {
1867
+ cwd,
1868
+ stdio: "pipe"
1869
+ });
1870
+ } catch (err) {
1871
+ await cleanupPatchArtifacts({
1872
+ cwd,
1873
+ packageManager: pm,
1874
+ patchFilePath,
1875
+ patchMode,
1876
+ packageJsonSnapshot,
1877
+ rerunInstall: true
1878
+ });
1879
+ const error = err instanceof Error ? err.message : String(err);
1880
+ return {
1881
+ success: false,
1882
+ packageName,
1883
+ vulnerableVersion,
1884
+ applied: false,
1885
+ dryRun: false,
1886
+ message: `Failed to apply patch-package workflow for ${packageName}@${vulnerableVersion}: ${error}`,
1887
+ patchFilePath,
1888
+ patchPath: patchFilePath,
1889
+ patchMode,
1890
+ postinstallConfigured: false,
1891
+ error: `Failed to apply patch-package workflow for ${packageName}@${vulnerableVersion}: ${error}`
1892
+ };
1893
+ }
1894
+ }
1544
1895
  if (validateWithTests) {
1545
1896
  validationResult = await validatePatchWithTests(cwd, pm);
1546
1897
  if (!validationResult.passed) {
1898
+ await cleanupPatchArtifacts({
1899
+ cwd,
1900
+ packageManager: pm,
1901
+ patchFilePath,
1902
+ patchMode,
1903
+ packageJsonSnapshot,
1904
+ rerunInstall: patchMode === "patch-package"
1905
+ });
1547
1906
  const validationError = "Patch validation failed after apply; patch marked unresolved.";
1548
1907
  return {
1549
1908
  success: false,
@@ -1555,7 +1914,7 @@ var applyPatchFileTool = tool8({
1555
1914
  patchFilePath,
1556
1915
  patchPath: patchFilePath,
1557
1916
  patchMode,
1558
- postinstallConfigured: patchMode === "patch-package",
1917
+ postinstallConfigured: false,
1559
1918
  validation: validationResult,
1560
1919
  error: validationError
1561
1920
  };
@@ -1593,7 +1952,7 @@ async function resolvePatchMode(packageManager, cwd) {
1593
1952
  if (packageManager === "npm") return "patch-package";
1594
1953
  if (packageManager === "pnpm") return "native-pnpm";
1595
1954
  try {
1596
- const result = await execa4("yarn", ["--version"], {
1955
+ const result = await execa6("yarn", ["--version"], {
1597
1956
  cwd,
1598
1957
  stdio: "pipe"
1599
1958
  });
@@ -1604,12 +1963,17 @@ async function resolvePatchMode(packageManager, cwd) {
1604
1963
  return "patch-package";
1605
1964
  }
1606
1965
  }
1966
+ function patchModeRequiresPackageJsonSnapshot(packageManager, cwd) {
1967
+ if (packageManager === "npm") return true;
1968
+ if (packageManager === "pnpm") return false;
1969
+ return true;
1970
+ }
1607
1971
  function buildPatchFileName(packageName, vulnerableVersion) {
1608
1972
  const safeName = packageName.replace(/^@/, "").replace(/\//g, "+");
1609
1973
  return `${safeName}+${vulnerableVersion}.patch`;
1610
1974
  }
1611
1975
  async function configurePatchPackagePostinstall(cwd, packageManager) {
1612
- const pkgJsonPath = join7(cwd, "package.json");
1976
+ const pkgJsonPath = join9(cwd, "package.json");
1613
1977
  let pkgJson;
1614
1978
  try {
1615
1979
  pkgJson = JSON.parse(await readFile2(pkgJsonPath, "utf8"));
@@ -1624,7 +1988,7 @@ async function configurePatchPackagePostinstall(cwd, packageManager) {
1624
1988
  try {
1625
1989
  const commands = getPackageManagerCommands(packageManager);
1626
1990
  const [cmd, ...args] = commands.installDev("patch-package");
1627
- await execa4(cmd, args, {
1991
+ await execa6(cmd, args, {
1628
1992
  cwd,
1629
1993
  stdio: "pipe"
1630
1994
  });
@@ -1648,6 +2012,32 @@ async function configurePatchPackagePostinstall(cwd, packageManager) {
1648
2012
  await writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf8");
1649
2013
  return { success: true };
1650
2014
  }
2015
+ async function capturePackageJsonSnapshot(cwd) {
2016
+ const path = join9(cwd, "package.json");
2017
+ try {
2018
+ const content = await readFile2(path, "utf8");
2019
+ return { path, content };
2020
+ } catch {
2021
+ return void 0;
2022
+ }
2023
+ }
2024
+ async function cleanupPatchArtifacts(params) {
2025
+ const { cwd, packageManager, patchFilePath, patchMode, packageJsonSnapshot, rerunInstall } = params;
2026
+ await rm3(patchFilePath, { force: true }).catch(() => void 0);
2027
+ if (patchMode === "patch-package" && packageJsonSnapshot) {
2028
+ await writeFile(packageJsonSnapshot.path, packageJsonSnapshot.content, "utf8").catch(() => void 0);
2029
+ }
2030
+ if (!rerunInstall) return;
2031
+ try {
2032
+ const commands = getPackageManagerCommands(packageManager);
2033
+ const [installCmd, ...installArgs] = commands.installPreferOffline;
2034
+ await execa6(installCmd, installArgs, {
2035
+ cwd,
2036
+ stdio: "pipe"
2037
+ });
2038
+ } catch {
2039
+ }
2040
+ }
1651
2041
  async function applyNativePatch(params) {
1652
2042
  const { cwd, packageName, vulnerableVersion, patchContent, patchMode } = params;
1653
2043
  const packageSpec = `${packageName}@${vulnerableVersion}`;
@@ -1655,7 +2045,7 @@ async function applyNativePatch(params) {
1655
2045
  const createArgs = ["patch", packageSpec];
1656
2046
  let patchDir;
1657
2047
  try {
1658
- const createResult = await execa4(createCommand, createArgs, {
2048
+ const createResult = await execa6(createCommand, createArgs, {
1659
2049
  cwd,
1660
2050
  stdio: "pipe"
1661
2051
  });
@@ -1673,17 +2063,17 @@ ${createResult.stderr}`);
1673
2063
  error: `Could not determine native patch directory for ${packageSpec}.`
1674
2064
  };
1675
2065
  }
1676
- const tempPatchDir = await mkdtemp(join7(tmpdir(), "autoremediator-native-patch-"));
1677
- const tempPatchFile = join7(tempPatchDir, "change.patch");
2066
+ const tempPatchDir = await mkdtemp(join9(tmpdir(), "autoremediator-native-patch-"));
2067
+ const tempPatchFile = join9(tempPatchDir, "change.patch");
1678
2068
  try {
1679
2069
  await writeFile(tempPatchFile, patchContent, "utf8");
1680
- await execa4("patch", ["-p1", "-i", tempPatchFile], {
2070
+ await execa6("patch", ["-p1", "-i", tempPatchFile], {
1681
2071
  cwd: patchDir,
1682
2072
  stdio: "pipe"
1683
2073
  });
1684
2074
  const commitCommand = patchMode === "native-pnpm" ? "pnpm" : "yarn";
1685
2075
  const commitArgs = patchMode === "native-pnpm" ? ["patch-commit", patchDir] : ["patch-commit", "-s", patchDir];
1686
- await execa4(commitCommand, commitArgs, {
2076
+ await execa6(commitCommand, commitArgs, {
1687
2077
  cwd,
1688
2078
  stdio: "pipe"
1689
2079
  });
@@ -1700,12 +2090,12 @@ ${createResult.stderr}`);
1700
2090
  function extractPatchDirectory(output) {
1701
2091
  const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1702
2092
  for (const line of lines) {
1703
- if (existsSync3(line)) {
2093
+ if (existsSync4(line)) {
1704
2094
  return line;
1705
2095
  }
1706
2096
  const tokens = line.split(/\s+/).map((token) => token.replace(/^['"]|['"]$/g, ""));
1707
2097
  for (const token of tokens) {
1708
- if (token.startsWith("/") && existsSync3(token)) {
2098
+ if (token.startsWith("/") && existsSync4(token)) {
1709
2099
  return token;
1710
2100
  }
1711
2101
  }
@@ -1716,7 +2106,7 @@ async function validatePatchWithTests(cwd, packageManager) {
1716
2106
  try {
1717
2107
  const commands = getPackageManagerCommands(packageManager);
1718
2108
  const [cmd, ...args] = commands.test;
1719
- const result = await execa4(cmd, args, {
2109
+ const result = await execa6(cmd, args, {
1720
2110
  cwd,
1721
2111
  timeout: 6e4,
1722
2112
  // 60 second timeout
@@ -1727,10 +2117,11 @@ async function validatePatchWithTests(cwd, packageManager) {
1727
2117
  output: result.stdout
1728
2118
  };
1729
2119
  } catch (err) {
1730
- const errorOutput = err instanceof Error && "stdout" in err ? err.stdout : "";
2120
+ const errorOutput = typeof err === "object" && err !== null && "stdout" in err ? String(err.stdout ?? "") : "";
1731
2121
  const failedTests = extractFailedTests(errorOutput);
1732
2122
  return {
1733
2123
  passed: false,
2124
+ error: failedTests.length > 0 ? `Failed tests: ${failedTests.join(", ")}` : "Package-manager test validation failed.",
1734
2125
  output: errorOutput,
1735
2126
  failedTests
1736
2127
  };
@@ -1743,7 +2134,7 @@ function extractFailedTests(output) {
1743
2134
  // Mocha style
1744
2135
  /●\s+(.+)(?:\n|$)/g,
1745
2136
  // Jest style
1746
- /FAIL.*?(.+?)(?:\n|$)/g
2137
+ /^FAIL\s+(.+?)(?:\n|$)/gm
1747
2138
  // Generic FAIL
1748
2139
  ];
1749
2140
  for (const pattern of patterns) {
@@ -1770,6 +2161,7 @@ async function runRemediationPipeline(cveId, options = {}) {
1770
2161
  const runTests = options.runTests ?? false;
1771
2162
  const policy = options.policy ?? "";
1772
2163
  const patchesDir = options.patchesDir || "./patches";
2164
+ const constraints = options.constraints ?? {};
1773
2165
  const model = await createModel(options);
1774
2166
  const systemPrompt = loadOrchestrationPrompt({
1775
2167
  cveId,
@@ -1778,7 +2170,8 @@ async function runRemediationPipeline(cveId, options = {}) {
1778
2170
  runTests,
1779
2171
  policy,
1780
2172
  patchesDir,
1781
- packageManager
2173
+ packageManager,
2174
+ constraints
1782
2175
  });
1783
2176
  const prompt = `Patch vulnerable dependencies affected by ${cveId} in the project at: ${cwd}. Package manager: ${packageManager}.`;
1784
2177
  const collectedResults = [];
@@ -1789,29 +2182,30 @@ async function runRemediationPipeline(cveId, options = {}) {
1789
2182
  ...applyVersionBumpTool,
1790
2183
  execute: async (input) => applyVersionBumpTool.execute({ ...input, dryRun: true })
1791
2184
  } : applyVersionBumpTool;
2185
+ const applyPackageOverrideToolForRun = preview ? {
2186
+ ...applyPackageOverrideTool,
2187
+ execute: async (input) => applyPackageOverrideTool.execute({ ...input, dryRun: true })
2188
+ } : applyPackageOverrideTool;
1792
2189
  const applyPatchFileToolForRun = preview ? {
1793
2190
  ...applyPatchFileTool,
1794
2191
  execute: async (input) => applyPatchFileTool.execute({ ...input, dryRun: true })
1795
2192
  } : applyPatchFileTool;
2193
+ const tools = buildRuntimeTools({
2194
+ applyVersionBumpToolForRun,
2195
+ applyPackageOverrideToolForRun,
2196
+ applyPatchFileToolForRun,
2197
+ constraints
2198
+ });
1796
2199
  const result = await generateText2({
1797
2200
  model,
1798
2201
  system: systemPrompt,
1799
2202
  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
- },
2203
+ tools,
1810
2204
  maxSteps: 25,
1811
2205
  onStepFinish(stepResult) {
1812
2206
  agentSteps += 1;
1813
- const { toolResults } = stepResult;
1814
- for (const tr of toolResults ?? []) {
2207
+ const toolResults = stepResult.toolResults ?? [];
2208
+ for (const tr of toolResults) {
1815
2209
  const toolResult = tr.result;
1816
2210
  if (tr.toolName === "lookup-cve" && toolResult?.data) {
1817
2211
  cveDetails = toolResult.data;
@@ -1822,6 +2216,9 @@ async function runRemediationPipeline(cveId, options = {}) {
1822
2216
  if (tr.toolName === "apply-version-bump") {
1823
2217
  collectedResults.push(toolResult);
1824
2218
  }
2219
+ if (tr.toolName === "apply-package-override") {
2220
+ collectedResults.push(toolResult);
2221
+ }
1825
2222
  if (tr.toolName === "apply-patch-file" && toolResult) {
1826
2223
  const validation = toolResult.validation;
1827
2224
  const message = typeof toolResult.message === "string" ? toolResult.message : typeof toolResult.error === "string" ? toolResult.error : "Patch-file strategy finished.";
@@ -1832,6 +2229,7 @@ async function runRemediationPipeline(cveId, options = {}) {
1832
2229
  patchFilePath: typeof toolResult.patchFilePath === "string" ? toolResult.patchFilePath : typeof toolResult.patchPath === "string" ? toolResult.patchPath : void 0,
1833
2230
  applied: Boolean(toolResult.applied),
1834
2231
  dryRun: Boolean(toolResult.dryRun),
2232
+ unresolvedReason: !Boolean(toolResult.applied) && !Boolean(toolResult.dryRun) ? validation && validation.passed === false ? "patch-validation-failed" : "patch-apply-failed" : void 0,
1835
2233
  message,
1836
2234
  validation: validation && typeof validation.passed === "boolean" ? {
1837
2235
  passed: validation.passed,
@@ -1863,6 +2261,7 @@ async function runLocalRemediationPipeline(cveId, options = {}) {
1863
2261
  const dryRun = (options.dryRun ?? false) || preview;
1864
2262
  const runTests = options.runTests ?? false;
1865
2263
  const policy = options.policy ?? "";
2264
+ const constraints = options.constraints ?? {};
1866
2265
  const collectedResults = [];
1867
2266
  const vulnerablePackages = [];
1868
2267
  let cveDetails = null;
@@ -1938,10 +2337,10 @@ async function runLocalRemediationPipeline(cveId, options = {}) {
1938
2337
  if (affected.ecosystem !== "npm") continue;
1939
2338
  const matches = installedPackages.filter((p) => p.name === affected.name);
1940
2339
  for (const installed of matches) {
1941
- if (!semver4.valid(installed.version)) continue;
2340
+ if (!semver5.valid(installed.version)) continue;
1942
2341
  let isVulnerable = false;
1943
2342
  try {
1944
- isVulnerable = semver4.satisfies(installed.version, affected.vulnerableRange, {
2343
+ isVulnerable = semver5.satisfies(installed.version, affected.vulnerableRange, {
1945
2344
  includePrerelease: false
1946
2345
  });
1947
2346
  } catch {
@@ -1957,14 +2356,73 @@ async function runLocalRemediationPipeline(cveId, options = {}) {
1957
2356
  const pkg = vulnerable.installed;
1958
2357
  const firstPatchedVersion = vulnerable.affected.firstPatchedVersion;
1959
2358
  if (pkg.type === "indirect") {
1960
- collectedResults.push({
2359
+ if (constraints.directDependenciesOnly) {
2360
+ collectedResults.push({
2361
+ packageName: pkg.name,
2362
+ strategy: "none",
2363
+ fromVersion: pkg.version,
2364
+ applied: false,
2365
+ dryRun,
2366
+ unresolvedReason: "constraint-blocked",
2367
+ message: `Constraint blocked remediation for indirect dependency "${pkg.name}".`
2368
+ });
2369
+ continue;
2370
+ }
2371
+ if (constraints.preferVersionBump) {
2372
+ collectedResults.push({
2373
+ packageName: pkg.name,
2374
+ strategy: "none",
2375
+ fromVersion: pkg.version,
2376
+ applied: false,
2377
+ dryRun,
2378
+ unresolvedReason: "constraint-blocked",
2379
+ message: `Constraint prefers version-bump and rejected override remediation for "${pkg.name}".`
2380
+ });
2381
+ continue;
2382
+ }
2383
+ if (!firstPatchedVersion) {
2384
+ collectedResults.push({
2385
+ packageName: pkg.name,
2386
+ strategy: "none",
2387
+ fromVersion: pkg.version,
2388
+ applied: false,
2389
+ dryRun,
2390
+ unresolvedReason: "no-safe-version",
2391
+ message: `No firstPatchedVersion available for ${pkg.name}; cannot resolve deterministic override in local mode.`
2392
+ });
2393
+ continue;
2394
+ }
2395
+ const safeUpgrade2 = await resolveSafeUpgradeVersion(
2396
+ pkg.name,
2397
+ pkg.version,
2398
+ firstPatchedVersion,
2399
+ vulnerable.affected.vulnerableRange
2400
+ );
2401
+ agentSteps += 1;
2402
+ if (!safeUpgrade2.safeVersion) {
2403
+ collectedResults.push({
2404
+ packageName: pkg.name,
2405
+ strategy: "none",
2406
+ fromVersion: pkg.version,
2407
+ applied: false,
2408
+ dryRun,
2409
+ unresolvedReason: "no-safe-version",
2410
+ message: `No safe override version found for ${pkg.name}.`
2411
+ });
2412
+ continue;
2413
+ }
2414
+ const overrideResult = await applyPackageOverrideTool.execute({
2415
+ cwd,
2416
+ packageManager,
1961
2417
  packageName: pkg.name,
1962
- strategy: "none",
1963
2418
  fromVersion: pkg.version,
1964
- applied: false,
2419
+ toVersion: safeUpgrade2.safeVersion,
1965
2420
  dryRun,
1966
- message: `"${pkg.name}" is an indirect dependency; automatic version bump is limited to direct dependencies in local mode.`
2421
+ policy,
2422
+ runTests
1967
2423
  });
2424
+ agentSteps += 1;
2425
+ collectedResults.push(overrideResult);
1968
2426
  continue;
1969
2427
  }
1970
2428
  if (!firstPatchedVersion) {
@@ -1974,16 +2432,18 @@ async function runLocalRemediationPipeline(cveId, options = {}) {
1974
2432
  fromVersion: pkg.version,
1975
2433
  applied: false,
1976
2434
  dryRun,
2435
+ unresolvedReason: "no-safe-version",
1977
2436
  message: `No firstPatchedVersion available for ${pkg.name}; cannot resolve deterministic upgrade in local mode.`
1978
2437
  });
1979
2438
  continue;
1980
2439
  }
1981
- const safeVersion = await findSafeUpgradeVersion(
2440
+ const safeUpgrade = await resolveSafeUpgradeVersion(
1982
2441
  pkg.name,
1983
2442
  pkg.version,
1984
2443
  firstPatchedVersion,
1985
2444
  vulnerable.affected.vulnerableRange
1986
2445
  );
2446
+ const safeVersion = safeUpgrade.safeVersion;
1987
2447
  agentSteps += 1;
1988
2448
  if (!safeVersion) {
1989
2449
  collectedResults.push({
@@ -1992,6 +2452,7 @@ async function runLocalRemediationPipeline(cveId, options = {}) {
1992
2452
  fromVersion: pkg.version,
1993
2453
  applied: false,
1994
2454
  dryRun,
2455
+ unresolvedReason: "no-safe-version",
1995
2456
  message: `No safe upgrade version found for ${pkg.name}.`
1996
2457
  });
1997
2458
  continue;
@@ -2026,9 +2487,27 @@ async function runLocalRemediationPipeline(cveId, options = {}) {
2026
2487
  }
2027
2488
  };
2028
2489
  }
2490
+ function buildRuntimeTools(ctx) {
2491
+ const tools = {
2492
+ "lookup-cve": lookupCveTool,
2493
+ "check-inventory": checkInventoryTool,
2494
+ "check-version-match": checkVersionMatchTool,
2495
+ "find-fixed-version": findFixedVersionTool,
2496
+ "apply-version-bump": ctx.applyVersionBumpToolForRun
2497
+ };
2498
+ if (!ctx.constraints.directDependenciesOnly && !ctx.constraints.preferVersionBump) {
2499
+ tools["apply-package-override"] = ctx.applyPackageOverrideToolForRun;
2500
+ }
2501
+ if (!ctx.constraints.preferVersionBump) {
2502
+ tools["fetch-package-source"] = fetchPackageSourceTool;
2503
+ tools["generate-patch"] = generatePatchTool;
2504
+ tools["apply-patch-file"] = ctx.applyPatchFileToolForRun;
2505
+ }
2506
+ return tools;
2507
+ }
2029
2508
  function loadOrchestrationPrompt(ctx) {
2030
- const promptPath = join8(process.cwd(), ".github", "instructions", "orchestration.instructions.md");
2031
- if (!existsSync4(promptPath)) {
2509
+ const promptPath = join10(process.cwd(), ".github", "instructions", "orchestration.instructions.md");
2510
+ if (!existsSync5(promptPath)) {
2032
2511
  return `You are autoremediator, an agentic security remediation system for Node.js package dependencies.
2033
2512
  Working directory: ${ctx.cwd}
2034
2513
  Package manager: ${ctx.packageManager}
@@ -2036,6 +2515,8 @@ Dry run: ${ctx.dryRun}
2036
2515
  Run tests: ${ctx.runTests}
2037
2516
  Policy: ${ctx.policy || "undefined"}
2038
2517
  Patches dir: ${ctx.patchesDir}
2518
+ Direct dependencies only: ${String(ctx.constraints.directDependenciesOnly ?? false)}
2519
+ Prefer version bump: ${String(ctx.constraints.preferVersionBump ?? false)}
2039
2520
 
2040
2521
  Required sequence:
2041
2522
  1. lookup-cve
@@ -2043,24 +2524,25 @@ Required sequence:
2043
2524
  3. check-version-match
2044
2525
  4. find-fixed-version
2045
2526
  5. apply-version-bump
2527
+ 6. apply-package-override
2046
2528
 
2047
- Fallback sequence (when strategy="none"):
2529
+ Fallback sequence (when neither version bump nor override can be applied):
2048
2530
  1. fetch-package-source
2049
2531
  2. generate-patch
2050
2532
  3. apply-patch-file
2051
2533
 
2052
2534
  Always respect dryRun and policy constraints.`;
2053
2535
  }
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);
2536
+ const template = readFileSync6(promptPath, "utf8");
2537
+ 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
2538
  }
2057
2539
 
2058
2540
  // src/scanner/index.ts
2059
2541
  import { extname } from "path";
2060
- import { readFileSync as readFileSync8 } from "fs";
2542
+ import { readFileSync as readFileSync10 } from "fs";
2061
2543
 
2062
2544
  // src/scanner/adapters/npm-audit.ts
2063
- import { readFileSync as readFileSync5 } from "fs";
2545
+ import { readFileSync as readFileSync7 } from "fs";
2064
2546
  var CVE_REGEX = /CVE-\d{4}-\d+/gi;
2065
2547
  function normalizeSeverity(raw) {
2066
2548
  if (!raw) return "UNKNOWN";
@@ -2095,12 +2577,12 @@ function parseNpmAuditJsonFromString(content) {
2095
2577
  return findings;
2096
2578
  }
2097
2579
  function parseNpmAuditJsonFile(filePath) {
2098
- const content = readFileSync5(filePath, "utf8");
2580
+ const content = readFileSync7(filePath, "utf8");
2099
2581
  return parseNpmAuditJsonFromString(content);
2100
2582
  }
2101
2583
 
2102
2584
  // src/scanner/adapters/yarn-audit.ts
2103
- import { readFileSync as readFileSync6 } from "fs";
2585
+ import { readFileSync as readFileSync8 } from "fs";
2104
2586
  var CVE_REGEX2 = /CVE-\d{4}-\d+/gi;
2105
2587
  function normalizeSeverity2(raw) {
2106
2588
  if (!raw) return "UNKNOWN";
@@ -2144,12 +2626,12 @@ function parseYarnAuditJsonFromString(content) {
2144
2626
  return findings;
2145
2627
  }
2146
2628
  function parseYarnAuditJsonFile(filePath) {
2147
- const content = readFileSync6(filePath, "utf8");
2629
+ const content = readFileSync8(filePath, "utf8");
2148
2630
  return parseYarnAuditJsonFromString(content);
2149
2631
  }
2150
2632
 
2151
2633
  // src/scanner/adapters/sarif.ts
2152
- import { readFileSync as readFileSync7 } from "fs";
2634
+ import { readFileSync as readFileSync9 } from "fs";
2153
2635
  var CVE_REGEX3 = /CVE-\d{4}-\d+/gi;
2154
2636
  function extractPackageName(result) {
2155
2637
  const pkg = result.properties?.["packageName"];
@@ -2181,7 +2663,7 @@ function parseSarifFromString(content) {
2181
2663
  return findings;
2182
2664
  }
2183
2665
  function parseSarifFile(filePath) {
2184
- const content = readFileSync7(filePath, "utf8");
2666
+ const content = readFileSync9(filePath, "utf8");
2185
2667
  return parseSarifFromString(content);
2186
2668
  }
2187
2669
 
@@ -2203,7 +2685,7 @@ function inferFormat(filePath) {
2203
2685
  const ext = extname(filePath).toLowerCase();
2204
2686
  if (ext === ".sarif") return "sarif";
2205
2687
  try {
2206
- const content = readFileSync8(filePath, "utf8");
2688
+ const content = readFileSync10(filePath, "utf8");
2207
2689
  const firstLine = content.split("\n").find((line) => line.trim().startsWith("{"));
2208
2690
  if (firstLine) {
2209
2691
  const parsed = JSON.parse(firstLine);
@@ -2220,8 +2702,8 @@ function uniqueCveIds(findings) {
2220
2702
  }
2221
2703
 
2222
2704
  // src/platform/evidence.ts
2223
- import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
2224
- import { join as join9 } from "path";
2705
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
2706
+ import { join as join11 } from "path";
2225
2707
  function createEvidenceLog(cwd, cveIds, context = {}) {
2226
2708
  return {
2227
2709
  runId: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
@@ -2251,31 +2733,31 @@ function finalizeEvidence(log) {
2251
2733
  return log;
2252
2734
  }
2253
2735
  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");
2736
+ const dir = join11(cwd, ".autoremediator", "evidence");
2737
+ mkdirSync2(dir, { recursive: true });
2738
+ const filePath = join11(dir, `${log.runId}.json`);
2739
+ writeFileSync4(filePath, JSON.stringify(log, null, 2) + "\n", "utf8");
2258
2740
  return filePath;
2259
2741
  }
2260
2742
 
2261
2743
  // 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";
2744
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
2745
+ import { join as join12 } from "path";
2264
2746
  var DEFAULT_INDEX = {
2265
2747
  schemaVersion: "1.0",
2266
2748
  entries: {}
2267
2749
  };
2268
2750
  function indexFilePath(cwd) {
2269
- return join10(cwd, ".autoremediator", "state", "idempotency.json");
2751
+ return join12(cwd, ".autoremediator", "state", "idempotency.json");
2270
2752
  }
2271
2753
  function entryKey(idempotencyKey, cveId) {
2272
2754
  return `${idempotencyKey}::${cveId.toUpperCase()}`;
2273
2755
  }
2274
2756
  function loadIndex(cwd) {
2275
2757
  const filePath = indexFilePath(cwd);
2276
- if (!existsSync5(filePath)) return DEFAULT_INDEX;
2758
+ if (!existsSync6(filePath)) return DEFAULT_INDEX;
2277
2759
  try {
2278
- const parsed = JSON.parse(readFileSync9(filePath, "utf8"));
2760
+ const parsed = JSON.parse(readFileSync11(filePath, "utf8"));
2279
2761
  if (parsed && parsed.schemaVersion === "1.0" && parsed.entries) {
2280
2762
  return parsed;
2281
2763
  }
@@ -2286,8 +2768,8 @@ function loadIndex(cwd) {
2286
2768
  }
2287
2769
  function saveIndex(cwd, index) {
2288
2770
  const filePath = indexFilePath(cwd);
2289
- mkdirSync2(join10(cwd, ".autoremediator", "state"), { recursive: true });
2290
- writeFileSync3(filePath, JSON.stringify(index, null, 2) + "\n", "utf8");
2771
+ mkdirSync3(join12(cwd, ".autoremediator", "state"), { recursive: true });
2772
+ writeFileSync5(filePath, JSON.stringify(index, null, 2) + "\n", "utf8");
2291
2773
  }
2292
2774
  function readIdempotentReport(cwd, idempotencyKey, cveId) {
2293
2775
  const index = loadIndex(cwd);
@@ -2307,6 +2789,102 @@ function storeIdempotentReport(cwd, idempotencyKey, cveId, report) {
2307
2789
  }
2308
2790
 
2309
2791
  // src/api.ts
2792
+ var PACKAGE_MANAGER_VALUES = ["npm", "pnpm", "yarn"];
2793
+ var LLM_PROVIDER_VALUES = ["openai", "anthropic", "local"];
2794
+ var PROVENANCE_SOURCE_VALUES = ["cli", "sdk", "mcp", "openapi", "unknown"];
2795
+ var OPTION_DESCRIPTIONS = {
2796
+ cveId: "CVE ID, e.g. CVE-2021-23337",
2797
+ inputPath: "Absolute path to the scanner output file",
2798
+ cwd: "Absolute path to the project root (default: process.cwd())",
2799
+ packageManager: "Package manager override (auto-detected by default)",
2800
+ dryRun: "If true, plan changes but write nothing",
2801
+ preview: "If true, enforce non-mutating preview mode",
2802
+ runTests: "Run package-manager test command after applying fix",
2803
+ llmProvider: "LLM provider override",
2804
+ patchesDir: "Directory to write .patch files (default: ./patches)",
2805
+ policy: "Optional path to .autoremediator policy file",
2806
+ requestId: "Request correlation ID",
2807
+ sessionId: "Session correlation ID",
2808
+ parentRunId: "Parent run correlation ID",
2809
+ idempotencyKey: "Idempotency key for replay-safe execution",
2810
+ resume: "Return cached result for matching idempotency key when available",
2811
+ actor: "Actor identity for evidence provenance",
2812
+ source: "Source system for provenance",
2813
+ format: "Scanner format (default: auto)",
2814
+ evidence: "Write evidence JSON to .autoremediator/evidence/ (default: true)",
2815
+ directDependenciesOnly: "Restrict remediation to direct dependencies only",
2816
+ preferVersionBump: "Reject override and patch remediation when version-bump-only policy is required"
2817
+ };
2818
+ function createConstraintSchemaProperties() {
2819
+ return {
2820
+ directDependenciesOnly: { type: "boolean", description: OPTION_DESCRIPTIONS.directDependenciesOnly },
2821
+ preferVersionBump: { type: "boolean", description: OPTION_DESCRIPTIONS.preferVersionBump }
2822
+ };
2823
+ }
2824
+ function createRemediateOptionSchemaProperties(options) {
2825
+ const includeDryRun = options?.includeDryRun ?? true;
2826
+ const includePreview = options?.includePreview ?? true;
2827
+ return {
2828
+ cwd: { type: "string", description: OPTION_DESCRIPTIONS.cwd },
2829
+ packageManager: { type: "string", enum: [...PACKAGE_MANAGER_VALUES], description: OPTION_DESCRIPTIONS.packageManager },
2830
+ ...includeDryRun ? { dryRun: { type: "boolean", description: OPTION_DESCRIPTIONS.dryRun } } : {},
2831
+ ...includePreview ? { preview: { type: "boolean", description: OPTION_DESCRIPTIONS.preview } } : {},
2832
+ runTests: { type: "boolean", description: OPTION_DESCRIPTIONS.runTests },
2833
+ llmProvider: { type: "string", enum: [...LLM_PROVIDER_VALUES], description: OPTION_DESCRIPTIONS.llmProvider },
2834
+ patchesDir: { type: "string", description: OPTION_DESCRIPTIONS.patchesDir },
2835
+ policy: { type: "string", description: OPTION_DESCRIPTIONS.policy },
2836
+ requestId: { type: "string", description: OPTION_DESCRIPTIONS.requestId },
2837
+ sessionId: { type: "string", description: OPTION_DESCRIPTIONS.sessionId },
2838
+ parentRunId: { type: "string", description: OPTION_DESCRIPTIONS.parentRunId },
2839
+ idempotencyKey: { type: "string", description: OPTION_DESCRIPTIONS.idempotencyKey },
2840
+ resume: { type: "boolean", description: OPTION_DESCRIPTIONS.resume },
2841
+ actor: { type: "string", description: OPTION_DESCRIPTIONS.actor },
2842
+ source: { type: "string", enum: [...PROVENANCE_SOURCE_VALUES], description: OPTION_DESCRIPTIONS.source },
2843
+ constraints: {
2844
+ type: "object",
2845
+ properties: createConstraintSchemaProperties()
2846
+ }
2847
+ };
2848
+ }
2849
+ function createScanOptionSchemaProperties() {
2850
+ return {
2851
+ ...createRemediateOptionSchemaProperties(),
2852
+ format: { type: "string", enum: ["npm-audit", "yarn-audit", "sarif", "auto"], description: OPTION_DESCRIPTIONS.format },
2853
+ evidence: { type: "boolean", description: OPTION_DESCRIPTIONS.evidence }
2854
+ };
2855
+ }
2856
+ function createScanReportSchemaProperties() {
2857
+ return {
2858
+ schemaVersion: { type: "string" },
2859
+ status: { type: "string", enum: ["ok", "partial", "failed"] },
2860
+ generatedAt: { type: "string" },
2861
+ cveIds: { type: "array", items: { type: "string" } },
2862
+ reports: { type: "array", items: { type: "object" } },
2863
+ successCount: { type: "number" },
2864
+ failedCount: { type: "number" },
2865
+ errors: { type: "array", items: { type: "object" } },
2866
+ evidenceFile: { type: "string" },
2867
+ patchCount: { type: "number" },
2868
+ patchValidationFailures: { type: "array", items: { type: "object" } },
2869
+ strategyCounts: {
2870
+ type: "object",
2871
+ additionalProperties: { type: "number" }
2872
+ },
2873
+ dependencyScopeCounts: {
2874
+ type: "object",
2875
+ additionalProperties: { type: "number" }
2876
+ },
2877
+ unresolvedByReason: {
2878
+ type: "object",
2879
+ additionalProperties: { type: "number" }
2880
+ },
2881
+ patchesDir: { type: "string" },
2882
+ correlation: { type: "object" },
2883
+ provenance: { type: "object" },
2884
+ constraints: { type: "object" },
2885
+ idempotencyKey: { type: "string" }
2886
+ };
2887
+ }
2310
2888
  function buildRequestId() {
2311
2889
  return `req-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2312
2890
  }
@@ -2340,15 +2918,17 @@ function enforceConstraints(report, constraints) {
2340
2918
  ...result,
2341
2919
  strategy: "none",
2342
2920
  applied: false,
2921
+ unresolvedReason: "constraint-blocked",
2343
2922
  message: `Constraint blocked remediation for indirect dependency "${result.packageName}".`
2344
2923
  };
2345
2924
  }
2346
- if (constraints.preferVersionBump && result.strategy === "patch-file") {
2925
+ if (constraints.preferVersionBump && result.strategy !== "version-bump" && result.strategy !== "none") {
2347
2926
  return {
2348
2927
  ...result,
2349
2928
  strategy: "none",
2350
2929
  applied: false,
2351
- message: `Constraint prefers version-bump and rejected patch-file remediation for "${result.packageName}".`
2930
+ unresolvedReason: "constraint-blocked",
2931
+ message: `Constraint prefers version-bump and rejected ${result.strategy} remediation for "${result.packageName}".`
2352
2932
  };
2353
2933
  }
2354
2934
  return result;
@@ -2359,6 +2939,47 @@ function enforceConstraints(report, constraints) {
2359
2939
  constraints
2360
2940
  };
2361
2941
  }
2942
+ function buildStrategyCounts(reports) {
2943
+ const counts = {};
2944
+ for (const report of reports) {
2945
+ for (const result of report.results) {
2946
+ counts[result.strategy] = (counts[result.strategy] ?? 0) + 1;
2947
+ }
2948
+ }
2949
+ return Object.keys(counts).length > 0 ? counts : void 0;
2950
+ }
2951
+ function toDependencyScope(installedType) {
2952
+ return installedType === "direct" ? "direct" : "transitive";
2953
+ }
2954
+ function buildDependencyScopeCounts(reports) {
2955
+ const counts = {};
2956
+ for (const report of reports) {
2957
+ const packageScopes = /* @__PURE__ */ new Map();
2958
+ for (const vulnerablePackage of report.vulnerablePackages) {
2959
+ const scope = toDependencyScope(vulnerablePackage.installed.type);
2960
+ const current = packageScopes.get(vulnerablePackage.installed.name);
2961
+ if (!current || current !== "direct") {
2962
+ packageScopes.set(vulnerablePackage.installed.name, scope);
2963
+ }
2964
+ }
2965
+ for (const result of report.results) {
2966
+ const scope = packageScopes.get(result.packageName);
2967
+ if (!scope) continue;
2968
+ counts[scope] = (counts[scope] ?? 0) + 1;
2969
+ }
2970
+ }
2971
+ return Object.keys(counts).length > 0 ? counts : void 0;
2972
+ }
2973
+ function buildUnresolvedReasonCounts(reports) {
2974
+ const counts = {};
2975
+ for (const report of reports) {
2976
+ for (const result of report.results) {
2977
+ if (!result.unresolvedReason) continue;
2978
+ counts[result.unresolvedReason] = (counts[result.unresolvedReason] ?? 0) + 1;
2979
+ }
2980
+ }
2981
+ return Object.keys(counts).length > 0 ? counts : void 0;
2982
+ }
2362
2983
  async function remediate(cveId, options = {}) {
2363
2984
  if (!/^CVE-\d{4}-\d+$/i.test(cveId)) {
2364
2985
  throw new Error(
@@ -2480,6 +3101,26 @@ async function remediateFromScan(inputPath, options = {}) {
2480
3101
  } else if (failedCount > 0 && successCount === 0) {
2481
3102
  status = "failed";
2482
3103
  }
3104
+ const strategyCounts = buildStrategyCounts(reports);
3105
+ const dependencyScopeCounts = buildDependencyScopeCounts(reports);
3106
+ const unresolvedByReason = buildUnresolvedReasonCounts(reports);
3107
+ let remediationCount = 0;
3108
+ for (const report of reports) {
3109
+ remediationCount += report.results.length;
3110
+ }
3111
+ evidence.summary = {
3112
+ status,
3113
+ cveCount: cveIds.length,
3114
+ remediationCount,
3115
+ successCount,
3116
+ failedCount,
3117
+ patchCount,
3118
+ patchValidationFailures: patchValidationFailures.length > 0 ? patchValidationFailures : void 0,
3119
+ strategyCounts,
3120
+ dependencyScopeCounts,
3121
+ unresolvedByReason,
3122
+ patchesDir: patchCount > 0 ? patchesDir : void 0
3123
+ };
2483
3124
  finalizeEvidence(evidence);
2484
3125
  const evidenceFile = options.evidence === false ? void 0 : writeEvidenceLog(cwd, evidence);
2485
3126
  return {
@@ -2494,6 +3135,9 @@ async function remediateFromScan(inputPath, options = {}) {
2494
3135
  evidenceFile,
2495
3136
  patchCount,
2496
3137
  patchValidationFailures: patchValidationFailures.length > 0 ? patchValidationFailures : void 0,
3138
+ strategyCounts,
3139
+ dependencyScopeCounts,
3140
+ unresolvedByReason,
2497
3141
  patchesDir: patchCount > 0 ? patchesDir : void 0,
2498
3142
  correlation,
2499
3143
  provenance,
@@ -2518,6 +3162,9 @@ function toCiSummary(report) {
2518
3162
  evidenceFile: report.evidenceFile,
2519
3163
  patchCount: report.patchCount || 0,
2520
3164
  patchValidationFailures: report.patchValidationFailures,
3165
+ strategyCounts: report.strategyCounts,
3166
+ dependencyScopeCounts: report.dependencyScopeCounts,
3167
+ unresolvedByReason: report.unresolvedByReason,
2521
3168
  patchesDir: report.patchesDir,
2522
3169
  correlation: report.correlation,
2523
3170
  provenance: report.provenance,
@@ -2528,13 +3175,83 @@ function toCiSummary(report) {
2528
3175
  function ciExitCode(summary) {
2529
3176
  return summary.failedCount > 0 ? 1 : 0;
2530
3177
  }
3178
+ function severityToSarifLevel(severity) {
3179
+ if (severity === "CRITICAL" || severity === "HIGH") return "error";
3180
+ if (severity === "MEDIUM") return "warning";
3181
+ if (severity === "LOW") return "note";
3182
+ return "warning";
3183
+ }
3184
+ function toSarifOutput(report) {
3185
+ const rules = [];
3186
+ const results = [];
3187
+ const seenRules = /* @__PURE__ */ new Set();
3188
+ for (const r of report.reports) {
3189
+ const severity = r.cveDetails?.severity ?? "UNKNOWN";
3190
+ const level = severityToSarifLevel(severity);
3191
+ const summary = r.cveDetails?.summary ?? r.cveId;
3192
+ if (!seenRules.has(r.cveId)) {
3193
+ seenRules.add(r.cveId);
3194
+ rules.push({
3195
+ id: r.cveId,
3196
+ name: "VulnerableDependency",
3197
+ shortDescription: { text: r.cveId },
3198
+ fullDescription: { text: summary },
3199
+ defaultConfiguration: { level },
3200
+ helpUri: `https://osv.dev/vulnerability/${r.cveId}`,
3201
+ properties: { severity }
3202
+ });
3203
+ }
3204
+ for (const vp of r.vulnerablePackages) {
3205
+ const fixText = vp.affected.firstPatchedVersion ? ` Fix: upgrade to ${vp.affected.firstPatchedVersion}.` : " No fixed version available.";
3206
+ results.push({
3207
+ ruleId: r.cveId,
3208
+ level,
3209
+ message: {
3210
+ text: `${vp.installed.name}@${vp.installed.version} is vulnerable to ${r.cveId}: ${summary}${fixText}`
3211
+ },
3212
+ locations: [
3213
+ {
3214
+ physicalLocation: {
3215
+ artifactLocation: { uri: "package.json", uriBaseId: "%SRCROOT%" }
3216
+ }
3217
+ }
3218
+ ]
3219
+ });
3220
+ }
3221
+ }
3222
+ return {
3223
+ version: "2.1.0",
3224
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Documents/CommitteeSpecifications/2.1.0/sarif-schema-2.1.0.json",
3225
+ runs: [
3226
+ {
3227
+ tool: {
3228
+ driver: {
3229
+ name: "autoremediator",
3230
+ informationUri: "https://github.com/Rawlings/autoremediator",
3231
+ rules
3232
+ }
3233
+ },
3234
+ results
3235
+ }
3236
+ ]
3237
+ };
3238
+ }
2531
3239
 
2532
3240
  export {
2533
3241
  runRemediationPipeline,
3242
+ PACKAGE_MANAGER_VALUES,
3243
+ LLM_PROVIDER_VALUES,
3244
+ PROVENANCE_SOURCE_VALUES,
3245
+ OPTION_DESCRIPTIONS,
3246
+ createConstraintSchemaProperties,
3247
+ createRemediateOptionSchemaProperties,
3248
+ createScanOptionSchemaProperties,
3249
+ createScanReportSchemaProperties,
2534
3250
  remediate,
2535
3251
  planRemediation,
2536
3252
  remediateFromScan,
2537
3253
  toCiSummary,
2538
- ciExitCode
3254
+ ciExitCode,
3255
+ toSarifOutput
2539
3256
  };
2540
- //# sourceMappingURL=chunk-GBOD3DV6.js.map
3257
+ //# sourceMappingURL=chunk-ZXPLOIB7.js.map