@wrongstack/tools 0.291.0 → 0.291.1

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.
@@ -11,7 +11,14 @@
11
11
  * - Shell -c wrapped: bash -c "kill -9 12345" (any shell path — see P2 #10)
12
12
  * - Full path kills: /bin/kill -9 12345
13
13
  * - Name-based kills: pkill, killall, pgrep
14
- * - Windows equivalents: taskkill
14
+ * - Windows equivalents: taskkill, tskill
15
+ * - PowerShell Stop-Process / kill alias: Stop-Process -Name node, kill -Id 12345
16
+ * - WMIC process termination: wmic process where "name='node.exe'" delete
17
+ * - Script-based kill (script is named kill*.sh, kill*.ps1, kill*.bat)
18
+ *
19
+ * Security contract: every "Handles" bullet must map to both a detector
20
+ * AND a block path in isKillRelatedCommand + parseKillCommand + isKillProtected.
21
+ * Script-based kills are blocked conservatively (can't inspect script content).
15
22
  *
16
23
  * Known bypasses (NOT handled — this is a static regex parser, not a shell):
17
24
  * Static analysis of shell strings is inherently defeatable by obfuscation.
@@ -25,6 +32,8 @@
25
32
  * - String concatenation / quote-splitting: `ki''ll -9 12345`, `k"i"ll 12345`
26
33
  * - Aliases and functions: `alias x=kill; x -9 12345`
27
34
  * - eval / source: `eval "ki""ll -9 12345"`
35
+ * - node -e eval: `node -e "process.kill(12345)"` (handled by exec-kill-guard.ts)
36
+ * - Scripts not named kill/terminate/stop*: `runkill.sh`, `/tmp/cleanup.bat`
28
37
  *
29
38
  * Mitigation: rely on the permission policy (confirm/deny gate) and YOLO
30
39
  * destructive detection as the primary controls; this guard is a best-effort
@@ -1 +1 @@
1
- {"version":3,"file":"bash-kill-guard.d.ts","sourceRoot":"","sources":["../src/bash-kill-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAOH,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAwED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CA6FpE;AAqBD;;GAEG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAqCzE;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CA2CxF;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAK1E"}
1
+ {"version":3,"file":"bash-kill-guard.d.ts","sourceRoot":"","sources":["../src/bash-kill-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAqBH,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA+ED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAkPpE;AAqBD;;GAEG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CA0CzE;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAqDxF;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAK1E"}
package/dist/bash.js CHANGED
@@ -1199,17 +1199,18 @@ function getPersistentProcessRegistry() {
1199
1199
 
1200
1200
  // src/bash-kill-guard.ts
1201
1201
  var isWin = os3.platform() === "win32";
1202
+ var SCRIPT_KILL_RE = /^(?:\.\\|\.\/)?(?:kill|terminate|stop)\S*\.(?:ps1|bat|cmd|sh)(?:\s|$)/i;
1203
+ var SCRIPT_KILL_RE_POSIX = /^(?:\.\/)?(?:kill|terminate|stop)\S*\.sh(?:\s|$)/i;
1204
+ var SCRIPT_KILL_FALLBACK_RE = /^\S*(?:kill|terminate|stop)\S*\.(?:ps1|bat|cmd|sh)\b/i;
1202
1205
  function extractKillCommand(command) {
1203
1206
  const normalized = command.replace(/\s+/g, " ").trim();
1204
- const shellCMatch = normalized.match(
1205
- /^(?:\S+(?:\s+\S+)?)?\s+-c\s+['"](.+?)['"]$/
1206
- );
1207
- if (shellCMatch?.[1]) {
1208
- const inner = shellCMatch[1].trim();
1207
+ const shellCMatch = normalized.match(/^.+?\s+-c\s+(['"])([\s\S]+)\1$/);
1208
+ if (shellCMatch?.[2]) {
1209
+ const inner = shellCMatch[2].trim();
1209
1210
  return isKillRelatedCommand(inner) ? inner : null;
1210
1211
  }
1211
1212
  const shellCUnquoted = normalized.match(
1212
- /^(?:\S+(?:\s+\S+)?)?\s+-c\s+(kill(?:\s+-[a-zA-Z]+)?(?:\s+\d+)+)$/
1213
+ /^.+?\s+-c\s+(kill(?:\s+-s\s+[a-zA-Z0-9]+|\s+-[a-zA-Z0-9]+)?\s+\d+)$/
1213
1214
  );
1214
1215
  if (shellCUnquoted?.[1]) {
1215
1216
  return shellCUnquoted[1];
@@ -1221,22 +1222,47 @@ function isKillRelatedCommand(cmd) {
1221
1222
  if (isWin) {
1222
1223
  if (/^taskkill\s/i.test(normalized)) return true;
1223
1224
  if (/^tskill\s/i.test(normalized)) return true;
1225
+ if (/^(stop-process|kill|stop)\s/i.test(normalized)) return true;
1226
+ if (/^wmic\s+process\s/i.test(normalized) && /\bdelete\b/i.test(normalized)) return true;
1227
+ if (SCRIPT_KILL_RE.test(normalized)) return true;
1224
1228
  return false;
1225
1229
  }
1226
1230
  if (/^kill(\s|$)/.test(normalized)) return true;
1227
1231
  if (/^(pkill|killall|pgrep|skill)\s/.test(normalized)) return true;
1228
1232
  if (/^\/proc\/\d+\/(?:kill|fd)/.test(normalized)) return true;
1233
+ if (SCRIPT_KILL_RE_POSIX.test(normalized)) return true;
1229
1234
  return false;
1230
1235
  }
1231
1236
  function parseKillCommand(command) {
1232
1237
  const normalized = command.replace(/\s+/g, " ").trim();
1233
1238
  if (isWin) {
1234
- const taskkillMatch = normalized.match(/^taskkill\s+(?:\/[a-zA-Z]+\s+)*\/PID\s+(\d+)/i);
1235
- if (taskkillMatch?.[1]) {
1236
- const pidStr = taskkillMatch[1];
1239
+ const hasTaskkillForce = /(?:^|\s)\/F(?=\s|$)/i.test(normalized);
1240
+ const isSimpleTaskkill = /^taskkill\s+/i.test(normalized) && !/[|&<>]/.test(normalized);
1241
+ const taskkillPidMatch = isSimpleTaskkill ? normalized.match(/(?:^|\s)\/PID\s+(\d+)(?=\s|$)/i) : null;
1242
+ if (taskkillPidMatch?.[1]) {
1243
+ return {
1244
+ pid: parseInt(taskkillPidMatch[1], 10),
1245
+ signal: hasTaskkillForce ? "FORCE" : "TERM",
1246
+ isGroupKill: false,
1247
+ isAllKill: false,
1248
+ originalCommand: command
1249
+ };
1250
+ }
1251
+ const taskkillImMatch = isSimpleTaskkill ? normalized.match(/(?:^|\s)\/IM\s+([^\s/]+)(?=\s|$)/i) : null;
1252
+ if (taskkillImMatch?.[1]) {
1253
+ return {
1254
+ name: taskkillImMatch[1],
1255
+ signal: hasTaskkillForce ? "FORCE" : "TERM",
1256
+ isGroupKill: false,
1257
+ isAllKill: false,
1258
+ originalCommand: command
1259
+ };
1260
+ }
1261
+ const taskkillFiMatch = isSimpleTaskkill ? normalized.match(/(?:^|\s)\/FI\s+"IMAGENAME\s+eq\s+([^"]+)"(?=\s|$)/i) : null;
1262
+ if (taskkillFiMatch?.[1]) {
1237
1263
  return {
1238
- pid: parseInt(pidStr, 10),
1239
- signal: normalized.includes("/F") ? "FORCE" : "TERM",
1264
+ name: taskkillFiMatch[1],
1265
+ signal: hasTaskkillForce ? "FORCE" : "TERM",
1240
1266
  isGroupKill: false,
1241
1267
  isAllKill: false,
1242
1268
  originalCommand: command
@@ -1244,18 +1270,109 @@ function parseKillCommand(command) {
1244
1270
  }
1245
1271
  const tskillMatch = normalized.match(/^tskill\s+(\d+)/i);
1246
1272
  if (tskillMatch?.[1]) {
1247
- const pidStr = tskillMatch[1];
1248
1273
  return {
1249
- pid: parseInt(pidStr, 10),
1274
+ pid: parseInt(tskillMatch[1], 10),
1250
1275
  signal: "TERM",
1251
1276
  isGroupKill: false,
1252
1277
  isAllKill: false,
1253
1278
  originalCommand: command
1254
1279
  };
1255
1280
  }
1281
+ const isStopProcIdCommand = /^(?:stop-process|kill)(?:\s+-(?:id|pid)\s+\d+|\s+-[a-zA-Z]+)+$/i.test(normalized);
1282
+ const stopProcIdMatch = normalized.match(/(?:^|\s)-(?:id|pid)\s+(\d+)(?=\s|$)/i);
1283
+ if (isStopProcIdCommand && stopProcIdMatch?.[1]) {
1284
+ return {
1285
+ pid: parseInt(stopProcIdMatch[1], 10),
1286
+ signal: "FORCE",
1287
+ isGroupKill: false,
1288
+ isAllKill: false,
1289
+ originalCommand: command
1290
+ };
1291
+ }
1292
+ const killSignalOptionMatch = normalized.match(/^kill\s+-s\s+([a-zA-Z0-9]+)\s+(\d+)$/i);
1293
+ if (killSignalOptionMatch?.[1] && killSignalOptionMatch[2]) {
1294
+ return {
1295
+ pid: parseInt(killSignalOptionMatch[2], 10),
1296
+ signal: killSignalOptionMatch[1].toUpperCase(),
1297
+ isGroupKill: false,
1298
+ isAllKill: false,
1299
+ originalCommand: command
1300
+ };
1301
+ }
1302
+ const killPosixMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z0-9]+)\s+)?(\d+)$/);
1303
+ if (killPosixMatch?.[2]) {
1304
+ const sig = killPosixMatch[1] ? killPosixMatch[1].slice(1).toUpperCase() : "TERM";
1305
+ return {
1306
+ pid: parseInt(killPosixMatch[2], 10),
1307
+ signal: sig,
1308
+ isGroupKill: false,
1309
+ isAllKill: false,
1310
+ originalCommand: command
1311
+ };
1312
+ }
1313
+ const stopProcNameMatch = normalized.match(
1314
+ /^(?:stop-process|kill)\s+-(?:name|n)\s+(?:['"]([a-zA-Z0-9_.-]+)['"]|([a-zA-Z0-9_.-]+))(?:\s|$)/i
1315
+ );
1316
+ const stopProcName = stopProcNameMatch?.[1] ?? stopProcNameMatch?.[2];
1317
+ if (stopProcName) {
1318
+ return {
1319
+ name: stopProcName,
1320
+ signal: "FORCE",
1321
+ isGroupKill: false,
1322
+ isAllKill: false,
1323
+ originalCommand: command
1324
+ };
1325
+ }
1326
+ const stopProcStandalone = normalized.match(
1327
+ /^(?:stop-process|kill)\s+['"]?([a-zA-Z][a-zA-Z0-9_.-]+)['"]?$/i
1328
+ );
1329
+ if (stopProcStandalone?.[1]) {
1330
+ return {
1331
+ name: stopProcStandalone[1],
1332
+ signal: "FORCE",
1333
+ isGroupKill: false,
1334
+ isAllKill: false,
1335
+ originalCommand: command
1336
+ };
1337
+ }
1338
+ const wmicMatch = normalized.match(
1339
+ /^wmic\s+process\s+where\s+['"]?(?:name\s*=\s*['"]?)([a-zA-Z0-9_.-]+)/i
1340
+ );
1341
+ if (wmicMatch?.[1]) {
1342
+ return {
1343
+ name: wmicMatch[1],
1344
+ signal: "FORCE",
1345
+ isGroupKill: false,
1346
+ isAllKill: false,
1347
+ originalCommand: command
1348
+ };
1349
+ }
1350
+ const killScriptMatch = normalized.match(SCRIPT_KILL_RE);
1351
+ if (killScriptMatch) {
1352
+ return {
1353
+ name: "kill-script",
1354
+ // sentinel — isKillProtected always blocks "kill-script"
1355
+ signal: "FORCE",
1356
+ isGroupKill: false,
1357
+ isAllKill: false,
1358
+ originalCommand: command
1359
+ };
1360
+ }
1256
1361
  return null;
1257
1362
  }
1258
- const simpleMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z]+)\s+)?(\d+|-?\d+)$/);
1363
+ const signalOptionMatch = normalized.match(/^kill\s+-s\s+([a-zA-Z0-9]+)\s+(\d+|-?\d+)$/i);
1364
+ if (signalOptionMatch?.[1] && signalOptionMatch[2]) {
1365
+ const pidOrGroup = signalOptionMatch[2];
1366
+ const isGroupKill = pidOrGroup.startsWith("-");
1367
+ return {
1368
+ pid: parseInt(isGroupKill ? pidOrGroup.slice(1) : pidOrGroup, 10),
1369
+ signal: signalOptionMatch[1].toUpperCase(),
1370
+ isGroupKill,
1371
+ isAllKill: false,
1372
+ originalCommand: command
1373
+ };
1374
+ }
1375
+ const simpleMatch = normalized.match(/^kill\s+(?:(-[a-zA-Z0-9]+)\s+)?(\d+|-?\d+)$/);
1259
1376
  if (simpleMatch) {
1260
1377
  const signal = simpleMatch[1] ?? "-TERM";
1261
1378
  const pidOrGroup = simpleMatch[2];
@@ -1315,6 +1432,9 @@ async function getProtectedEntries() {
1315
1432
  }
1316
1433
  async function isKillProtected(kill) {
1317
1434
  const registry = getPersistentProcessRegistry();
1435
+ if (kill.name === "kill-script") {
1436
+ return true;
1437
+ }
1318
1438
  if (kill.name) {
1319
1439
  const entries = await getProtectedEntries();
1320
1440
  const killNameLower = kill.name.toLowerCase();
@@ -1354,6 +1474,12 @@ async function checkAndBlockKillCommand(command) {
1354
1474
  reason: `Blocked: complex kill pipeline detected \u2014 "${killCmd.slice(0, 50)}..."`
1355
1475
  };
1356
1476
  }
1477
+ if (SCRIPT_KILL_FALLBACK_RE.test(killCmd)) {
1478
+ return {
1479
+ blocked: true,
1480
+ reason: `Blocked: script-based kill detected \u2014 "${killCmd.slice(0, 80)}" may target protected WrongStack processes (cannot inspect script body).`
1481
+ };
1482
+ }
1357
1483
  return { blocked: false };
1358
1484
  }
1359
1485
  if (await isKillProtected(parsed)) {