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.
- package/README.md +11 -11
- package/dist/chunk-7XSZTGU7.js +16 -0
- package/dist/chunk-7XSZTGU7.js.map +1 -0
- package/dist/{chunk-GBOD3DV6.js → chunk-ZXPLOIB7.js} +842 -125
- package/dist/chunk-ZXPLOIB7.js.map +1 -0
- package/dist/cli.js +41 -6
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +99 -2
- package/dist/index.js +21 -3
- package/dist/mcp/server.d.ts +2 -240
- package/dist/mcp/server.js +14 -69
- package/dist/mcp/server.js.map +1 -1
- package/dist/openapi/server.d.ts +15 -232
- package/dist/openapi/server.js +16 -90
- package/dist/openapi/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-GBOD3DV6.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/remediation/pipeline.ts
|
|
2
2
|
import { generateText as generateText2 } from "ai";
|
|
3
|
-
import { existsSync as
|
|
4
|
-
import { join as
|
|
5
|
-
import
|
|
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
|
|
839
|
+
async function resolveSafeUpgradeVersion(packageName, installedVersion, firstPatchedVersion, vulnerableRange) {
|
|
840
840
|
const versions = await fetchPackageVersions(packageName);
|
|
841
|
-
if (!versions.length)
|
|
842
|
-
|
|
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)
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
1150
|
-
|
|
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
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
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 =
|
|
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 =
|
|
1169
|
-
await
|
|
1428
|
+
const tarballPath = join7(tempBaseDir, "package.tgz");
|
|
1429
|
+
await execa4("curl", ["-L", "-o", tarballPath, npmUrl]);
|
|
1170
1430
|
await mkdir2(extractDir, { recursive: true });
|
|
1171
|
-
await
|
|
1431
|
+
await execa4("tar", ["-xzf", tarballPath, "-C", extractDir]);
|
|
1172
1432
|
const extractedContents = await readdir(extractDir);
|
|
1173
|
-
const packageRootDir = extractedContents.includes("package") ?
|
|
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 =
|
|
1180
|
-
const relPath =
|
|
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
|
|
1244
|
-
import { z as
|
|
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 =
|
|
1512
|
+
var generatePatchTool = tool8({
|
|
1253
1513
|
description: "Generate a unified diff patch for a CVE using LLM analysis of vulnerable source code.",
|
|
1254
|
-
parameters:
|
|
1255
|
-
packageName:
|
|
1256
|
-
vulnerableVersion:
|
|
1257
|
-
cveId:
|
|
1258
|
-
cveSummary:
|
|
1259
|
-
sourceFiles:
|
|
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:
|
|
1263
|
-
dryRun:
|
|
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
|
|
1451
|
-
import { z as
|
|
1452
|
-
import { existsSync as
|
|
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
|
|
1456
|
-
import { execa as
|
|
1457
|
-
|
|
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:
|
|
1460
|
-
packageName:
|
|
1461
|
-
vulnerableVersion:
|
|
1462
|
-
patchContent:
|
|
1463
|
-
patches:
|
|
1464
|
-
|
|
1465
|
-
filePath:
|
|
1466
|
-
unifiedDiff:
|
|
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:
|
|
1470
|
-
cwd:
|
|
1471
|
-
packageManager:
|
|
1472
|
-
validateWithTests:
|
|
1473
|
-
dryRun:
|
|
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 =
|
|
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
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
1677
|
-
const tempPatchFile =
|
|
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
|
|
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
|
|
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 (
|
|
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("/") &&
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
2340
|
+
if (!semver5.valid(installed.version)) continue;
|
|
1942
2341
|
let isVulnerable = false;
|
|
1943
2342
|
try {
|
|
1944
|
-
isVulnerable =
|
|
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
|
-
|
|
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
|
-
|
|
2419
|
+
toVersion: safeUpgrade2.safeVersion,
|
|
1965
2420
|
dryRun,
|
|
1966
|
-
|
|
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
|
|
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 =
|
|
2031
|
-
if (!
|
|
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
|
|
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 =
|
|
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
|
|
2542
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
2061
2543
|
|
|
2062
2544
|
// src/scanner/adapters/npm-audit.ts
|
|
2063
|
-
import { readFileSync as
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
2224
|
-
import { join as
|
|
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 =
|
|
2255
|
-
|
|
2256
|
-
const filePath =
|
|
2257
|
-
|
|
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
|
|
2263
|
-
import { join as
|
|
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
|
|
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 (!
|
|
2758
|
+
if (!existsSync6(filePath)) return DEFAULT_INDEX;
|
|
2277
2759
|
try {
|
|
2278
|
-
const parsed = JSON.parse(
|
|
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
|
-
|
|
2290
|
-
|
|
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
|
|
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
|
-
|
|
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-
|
|
3257
|
+
//# sourceMappingURL=chunk-ZXPLOIB7.js.map
|