pentesting 0.21.11 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -90,7 +90,21 @@ var DISPLAY_LIMITS = {
90
90
  /** Max endpoints to sample */
91
91
  ENDPOINT_SAMPLE: 5,
92
92
  /** Purpose truncation length */
93
- PURPOSE_TRUNCATE: 27
93
+ PURPOSE_TRUNCATE: 27,
94
+ /** Max characters for command description in background process */
95
+ COMMAND_DESCRIPTION_PREVIEW: 80,
96
+ /** Max characters for hash preview in tool output */
97
+ HASH_PREVIEW_LENGTH: 20,
98
+ /** Max characters for loot detail preview */
99
+ LOOT_DETAIL_PREVIEW: 30,
100
+ /** Max characters for link text preview in browser */
101
+ LINK_TEXT_PREVIEW: 100,
102
+ /** Prefix length for API key redaction in logs */
103
+ API_KEY_PREFIX: 8,
104
+ /** Prefix length for sensitive value redaction */
105
+ REDACT_PREFIX: 4,
106
+ /** Max characters for raw JSON error preview */
107
+ RAW_JSON_ERROR_PREVIEW: 500
94
108
  };
95
109
  var AGENT_LIMITS = {
96
110
  /** Maximum agent loop iterations — generous to allow complex tasks.
@@ -123,7 +137,17 @@ var AGENT_LIMITS = {
123
137
  /** Max chars of error text to include in web_search suggestion */
124
138
  ERROR_SEARCH_PREVIEW_SLICE: 50,
125
139
  /** How many consecutive identical blocks before warning + forcing alternative */
126
- MAX_BLOCKED_BEFORE_WARN: 2
140
+ MAX_BLOCKED_BEFORE_WARN: 2,
141
+ /** Maximum action log entries to retain in memory.
142
+ * WHY: actionLog grows unboundedly during long engagements.
143
+ * Older entries are pruned to prevent memory bloat and oversized session files.
144
+ */
145
+ MAX_ACTION_LOG: 200,
146
+ /** Maximum session files to keep on disk.
147
+ * WHY: Each save creates a timestamped .json in .pentesting/sessions/.
148
+ * Without rotation, disk usage grows unboundedly across engagements.
149
+ */
150
+ MAX_SESSION_FILES: 10
127
151
  };
128
152
 
129
153
  // src/shared/constants/patterns.ts
@@ -150,14 +174,16 @@ var EXIT_CODES = {
150
174
  /** Process killed by SIGINT (Ctrl+C) */
151
175
  SIGINT: 130,
152
176
  /** Process killed by SIGTERM */
153
- SIGTERM: 143
177
+ SIGTERM: 143,
178
+ /** Process killed by SIGKILL */
179
+ SIGKILL: 137
154
180
  };
155
181
 
156
182
  // src/shared/constants/agent.ts
157
183
  var ID_LENGTH = AGENT_LIMITS.ID_LENGTH;
158
184
  var ID_RADIX = AGENT_LIMITS.ID_RADIX;
159
185
  var APP_NAME = "Pentest AI";
160
- var APP_VERSION = "0.21.11";
186
+ var APP_VERSION = "0.23.0";
161
187
  var APP_DESCRIPTION = "Autonomous Penetration Testing AI Agent";
162
188
  var LLM_ROLES = {
163
189
  SYSTEM: "system",
@@ -406,7 +432,8 @@ var EVENT_TYPES = {
406
432
  RETRY: "retry",
407
433
  USAGE_UPDATE: "usage_update",
408
434
  INPUT_REQUEST: "input_request",
409
- LOG: "log"
435
+ LOG: "log",
436
+ FLAG_FOUND: "flag_found"
410
437
  };
411
438
  var UI_COMMANDS = {
412
439
  HELP: "help",
@@ -423,6 +450,7 @@ var UI_COMMANDS = {
423
450
  ASSETS_SHORT: "a",
424
451
  LOGS: "logs",
425
452
  LOGS_SHORT: "l",
453
+ CTF: "ctf",
426
454
  EXIT: "exit",
427
455
  QUIT: "quit",
428
456
  EXIT_SHORT: "q"
@@ -454,13 +482,13 @@ var DEFAULTS = {
454
482
  };
455
483
 
456
484
  // src/engine/process-manager.ts
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";
485
+ import { spawn as spawn3, execSync as execSync2 } from "child_process";
486
+ import { readFileSync as readFileSync2, existsSync as existsSync3, unlinkSync, writeFileSync as writeFileSync3, appendFileSync as appendFileSync2 } from "fs";
459
487
 
460
488
  // src/engine/tools-base.ts
461
- import { spawn } from "child_process";
462
- import { readFileSync, existsSync as existsSync2, writeFileSync } from "fs";
463
- import { join, dirname } from "path";
489
+ import { spawn as spawn2 } from "child_process";
490
+ import { readFileSync, existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
491
+ import { join as join2, dirname } from "path";
464
492
  import { tmpdir } from "os";
465
493
 
466
494
  // src/shared/utils/file-utils.ts
@@ -569,7 +597,7 @@ var ORPHAN_PROCESS_NAMES = [
569
597
  "python"
570
598
  ];
571
599
 
572
- // src/shared/utils/command-validator.ts
600
+ // src/shared/utils/command-security-lists.ts
573
601
  var ALLOWED_BINARIES = /* @__PURE__ */ new Set([
574
602
  // Network scanning
575
603
  "nmap",
@@ -797,6 +825,131 @@ var BLOCKED_BINARIES = /* @__PURE__ */ new Set([
797
825
  "nft",
798
826
  "crontab"
799
827
  ]);
828
+
829
+ // src/shared/utils/debug-logger.ts
830
+ import { appendFileSync, writeFileSync } from "fs";
831
+ import { join } from "path";
832
+
833
+ // src/shared/constants/paths.ts
834
+ import path from "path";
835
+ import { fileURLToPath } from "url";
836
+ import { homedir } from "os";
837
+ var __filename = fileURLToPath(import.meta.url);
838
+ var __dirname = path.dirname(__filename);
839
+ var PROJECT_ROOT = path.resolve(__dirname, "../../../");
840
+ var WORKSPACE_DIR_NAME = ".pentesting";
841
+ function getWorkspaceRoot() {
842
+ return path.join(homedir(), WORKSPACE_DIR_NAME);
843
+ }
844
+ var WORKSPACE = {
845
+ /** Root directory (resolved lazily via getWorkspaceRoot) */
846
+ get ROOT() {
847
+ return getWorkspaceRoot();
848
+ },
849
+ /** Per-session state snapshots */
850
+ get SESSIONS() {
851
+ return path.join(getWorkspaceRoot(), "sessions");
852
+ },
853
+ /** Debug logs */
854
+ get DEBUG() {
855
+ return path.join(getWorkspaceRoot(), "debug");
856
+ },
857
+ /** Generated reports */
858
+ get REPORTS() {
859
+ return path.join(getWorkspaceRoot(), "reports");
860
+ },
861
+ /** Downloaded loot, captured files */
862
+ get LOOT() {
863
+ return path.join(getWorkspaceRoot(), "loot");
864
+ },
865
+ /** Temporary files for active operations */
866
+ get TEMP() {
867
+ return path.join(getWorkspaceRoot(), "temp");
868
+ }
869
+ };
870
+ var PATHS = {
871
+ ROOT: PROJECT_ROOT,
872
+ SRC: path.join(PROJECT_ROOT, "src"),
873
+ DIST: path.join(PROJECT_ROOT, "dist")
874
+ };
875
+
876
+ // src/shared/utils/debug-logger.ts
877
+ var DebugLogger = class _DebugLogger {
878
+ static instance;
879
+ logPath;
880
+ initialized = false;
881
+ constructor(clearOnInit = false) {
882
+ const debugDir = WORKSPACE.DEBUG;
883
+ try {
884
+ ensureDirExists(debugDir);
885
+ this.logPath = join(debugDir, "debug.log");
886
+ if (clearOnInit) {
887
+ this.clear();
888
+ }
889
+ this.initialized = true;
890
+ this.log("general", "=== DEBUG LOGGER INITIALIZED ===");
891
+ this.log("general", `Log file: ${this.logPath}`);
892
+ } catch (e) {
893
+ console.error("[DebugLogger] Failed to initialize:", e);
894
+ this.logPath = "";
895
+ }
896
+ }
897
+ static getInstance(clearOnInit = false) {
898
+ if (!_DebugLogger.instance) {
899
+ _DebugLogger.instance = new _DebugLogger(clearOnInit);
900
+ }
901
+ return _DebugLogger.instance;
902
+ }
903
+ /** Reset the singleton instance (used by initDebugLogger) */
904
+ static resetInstance(clearOnInit = false) {
905
+ _DebugLogger.instance = new _DebugLogger(clearOnInit);
906
+ return _DebugLogger.instance;
907
+ }
908
+ log(category, message, data) {
909
+ if (!this.initialized || !this.logPath) return;
910
+ try {
911
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
912
+ let logLine = `[${timestamp}] [${category.toUpperCase()}] ${message}`;
913
+ if (data !== void 0) {
914
+ logLine += ` | ${JSON.stringify(data)}`;
915
+ }
916
+ logLine += "\n";
917
+ appendFileSync(this.logPath, logLine);
918
+ } catch (e) {
919
+ console.error("[DebugLogger] Write error:", e);
920
+ }
921
+ }
922
+ logRaw(category, label, raw) {
923
+ if (!this.initialized || !this.logPath) return;
924
+ try {
925
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
926
+ const logLine = `[${timestamp}] [${category.toUpperCase()}] ${label}:
927
+ ${raw}
928
+ ---
929
+ `;
930
+ appendFileSync(this.logPath, logLine);
931
+ } catch (e) {
932
+ console.error("[DebugLogger] Write error:", e);
933
+ }
934
+ }
935
+ clear() {
936
+ if (!this.logPath) return;
937
+ try {
938
+ writeFileSync(this.logPath, "");
939
+ } catch (e) {
940
+ console.error("[DebugLogger] Clear error:", e);
941
+ }
942
+ }
943
+ };
944
+ var logger = DebugLogger.getInstance(false);
945
+ function initDebugLogger() {
946
+ DebugLogger.resetInstance(true);
947
+ }
948
+ function debugLog(category, message, data) {
949
+ logger.log(category, message, data);
950
+ }
951
+
952
+ // src/shared/utils/command-validator.ts
800
953
  function validateCommand(command) {
801
954
  if (!command || typeof command !== "string") {
802
955
  return { safe: false, error: "Empty or invalid command" };
@@ -891,7 +1044,7 @@ function validateSingleCommand(command) {
891
1044
  };
892
1045
  }
893
1046
  if (!ALLOWED_BINARIES.has(binary)) {
894
- console.warn(`[Security] Unknown binary '${binary}' - use with caution`);
1047
+ debugLog("security", `Unknown binary '${binary}' - use with caution`);
895
1048
  }
896
1049
  }
897
1050
  const redirectResult = validateRedirects(command);
@@ -990,7 +1143,8 @@ function extractBinary(command) {
990
1143
  return binary.toLowerCase();
991
1144
  }
992
1145
 
993
- // src/engine/tools-base.ts
1146
+ // src/engine/tool-auto-installer.ts
1147
+ import { spawn } from "child_process";
994
1148
  var TOOL_PACKAGE_MAP = {
995
1149
  "nmap": { apt: "nmap", brew: "nmap" },
996
1150
  "masscan": { apt: "masscan", brew: "masscan" },
@@ -1044,15 +1198,7 @@ var TOOL_PACKAGE_MAP = {
1044
1198
  "evil-winrm": { apt: "evil-winrm", brew: "evil-winrm" },
1045
1199
  "netexec": { apt: "netexec", brew: "netexec" }
1046
1200
  };
1047
- var globalEventEmitter = null;
1048
- function setCommandEventEmitter(emitter) {
1049
- globalEventEmitter = emitter;
1050
- }
1051
1201
  var installedTools = /* @__PURE__ */ new Set();
1052
- var globalInputHandler = null;
1053
- function setInputHandler(handler) {
1054
- globalInputHandler = handler;
1055
- }
1056
1202
  async function detectPackageManager() {
1057
1203
  const managers = [
1058
1204
  { name: "apt", check: "which apt-get" },
@@ -1089,7 +1235,7 @@ function isCommandNotFound(stderr, stdout) {
1089
1235
  const toolName = toolMatch ? toolMatch[1] : null;
1090
1236
  return { missing: true, toolName };
1091
1237
  }
1092
- async function installTool(toolName) {
1238
+ async function installTool(toolName, eventEmitter, inputHandler) {
1093
1239
  const pkgInfo = TOOL_PACKAGE_MAP[toolName];
1094
1240
  if (!pkgInfo) {
1095
1241
  return {
@@ -1112,7 +1258,7 @@ async function installTool(toolName) {
1112
1258
  };
1113
1259
  }
1114
1260
  const packageName = pkgInfo[pkgManager] || pkgInfo.apt;
1115
- globalEventEmitter?.({
1261
+ eventEmitter?.({
1116
1262
  type: COMMAND_EVENT_TYPES.TOOL_INSTALL,
1117
1263
  message: `Installing missing tool: ${toolName}`,
1118
1264
  detail: `Using ${pkgManager} to install ${packageName}`
@@ -1132,10 +1278,10 @@ async function installTool(toolName) {
1132
1278
  let stdout = "";
1133
1279
  let stderr = "";
1134
1280
  const checkForSudoPrompt = async (data) => {
1135
- if (!globalInputHandler) return;
1281
+ if (!inputHandler) return;
1136
1282
  for (const pattern of INPUT_PROMPT_PATTERNS) {
1137
1283
  if (pattern.test(data)) {
1138
- const userInput = await globalInputHandler(data.trim());
1284
+ const userInput = await inputHandler(data.trim());
1139
1285
  if (userInput !== null && child.stdin.writable) {
1140
1286
  child.stdin.write(userInput + "\n");
1141
1287
  }
@@ -1155,14 +1301,14 @@ async function installTool(toolName) {
1155
1301
  });
1156
1302
  child.on("close", (code) => {
1157
1303
  if (code === 0) {
1158
- globalEventEmitter?.({
1304
+ eventEmitter?.({
1159
1305
  type: COMMAND_EVENT_TYPES.TOOL_INSTALLED,
1160
1306
  message: `Successfully installed: ${toolName}`,
1161
1307
  detail: `Package: ${packageName}`
1162
1308
  });
1163
1309
  resolve({ success: true, output: `Successfully installed ${toolName}` });
1164
1310
  } else {
1165
- globalEventEmitter?.({
1311
+ eventEmitter?.({
1166
1312
  type: COMMAND_EVENT_TYPES.TOOL_INSTALL_FAILED,
1167
1313
  message: `Failed to install: ${toolName}`,
1168
1314
  detail: stderr.slice(0, SYSTEM_LIMITS.MAX_ERROR_DETAIL_SLICE)
@@ -1178,6 +1324,16 @@ async function installTool(toolName) {
1178
1324
  });
1179
1325
  });
1180
1326
  }
1327
+
1328
+ // src/engine/tools-base.ts
1329
+ var globalEventEmitter = null;
1330
+ function setCommandEventEmitter(emitter) {
1331
+ globalEventEmitter = emitter;
1332
+ }
1333
+ var globalInputHandler = null;
1334
+ function setInputHandler(handler) {
1335
+ globalInputHandler = handler;
1336
+ }
1181
1337
  async function runCommand(command, args = [], options = {}) {
1182
1338
  const fullCommand = args.length > 0 ? `${command} ${args.join(" ")}` : command;
1183
1339
  const validation = validateCommand(fullCommand);
@@ -1206,7 +1362,7 @@ async function runCommand(command, args = [], options = {}) {
1206
1362
  message: `${STATUS_MARKERS.WARNING} Tool not found: ${toolName}`,
1207
1363
  detail: `Attempting to install...`
1208
1364
  });
1209
- const installResult = await installTool(toolName);
1365
+ const installResult = await installTool(toolName, globalEventEmitter, globalInputHandler);
1210
1366
  if (!installResult.success) {
1211
1367
  return {
1212
1368
  success: false,
@@ -1231,7 +1387,7 @@ async function executeCommandOnce(command, args = [], options = {}) {
1231
1387
  type: COMMAND_EVENT_TYPES.COMMAND_START,
1232
1388
  message: `Executing: ${command.slice(0, DISPLAY_LIMITS.COMMAND_PREVIEW)}${command.length > DISPLAY_LIMITS.COMMAND_PREVIEW ? "..." : ""}`
1233
1389
  });
1234
- const child = spawn("sh", ["-c", command], {
1390
+ const child = spawn2("sh", ["-c", command], {
1235
1391
  timeout,
1236
1392
  env: { ...process.env, ...options.env },
1237
1393
  cwd: options.cwd
@@ -1337,7 +1493,7 @@ async function writeFileContent(filePath, content) {
1337
1493
  try {
1338
1494
  const dir = dirname(filePath);
1339
1495
  ensureDirExists(dir);
1340
- writeFileSync(filePath, content, "utf-8");
1496
+ writeFileSync2(filePath, content, "utf-8");
1341
1497
  return {
1342
1498
  success: true,
1343
1499
  output: `Written to ${filePath}`
@@ -1352,7 +1508,7 @@ async function writeFileContent(filePath, content) {
1352
1508
  }
1353
1509
  }
1354
1510
  function createTempFile(suffix = "") {
1355
- return join(tmpdir(), generateTempFilename(suffix));
1511
+ return join2(tmpdir(), generateTempFilename(suffix));
1356
1512
  }
1357
1513
 
1358
1514
  // src/engine/process-detector.ts
@@ -1510,12 +1666,12 @@ function startBackgroundProcess(command, options = {}) {
1510
1666
  const { tags, port, role, isInteractive } = detectProcessRole(command);
1511
1667
  let wrappedCmd;
1512
1668
  if (isInteractive) {
1513
- writeFileSync2(stdinFile, "", "utf-8");
1669
+ writeFileSync3(stdinFile, "", "utf-8");
1514
1670
  wrappedCmd = `tail -f ${stdinFile} | ${command} > ${stdoutFile} 2> ${stderrFile}`;
1515
1671
  } else {
1516
1672
  wrappedCmd = `${command} > ${stdoutFile} 2> ${stderrFile}`;
1517
1673
  }
1518
- const child = spawn2("sh", ["-c", wrappedCmd], {
1674
+ const child = spawn3("sh", ["-c", wrappedCmd], {
1519
1675
  detached: true,
1520
1676
  stdio: "ignore",
1521
1677
  cwd: options.cwd,
@@ -1569,7 +1725,7 @@ async function sendToProcess(processId, input, waitMs = SYSTEM_LIMITS.DEFAULT_WA
1569
1725
  } catch {
1570
1726
  }
1571
1727
  try {
1572
- appendFileSync(proc.stdinFile, input + "\n", "utf-8");
1728
+ appendFileSync2(proc.stdinFile, input + "\n", "utf-8");
1573
1729
  } catch (error) {
1574
1730
  return { success: false, output: `Failed to send input: ${error}`, newOutput: "" };
1575
1731
  }
@@ -1977,6 +2133,16 @@ var StateSerializer = class {
1977
2133
  if (resourceInfo) {
1978
2134
  lines.push(resourceInfo);
1979
2135
  }
2136
+ if (state.isCtfMode()) {
2137
+ lines.push(`Mode: CTF \u{1F3F4}`);
2138
+ const flags = state.getFlags();
2139
+ if (flags.length > 0) {
2140
+ lines.push(`Flags Found (${flags.length}):`);
2141
+ for (const f of flags) {
2142
+ lines.push(` \u{1F3F4} ${f}`);
2143
+ }
2144
+ }
2145
+ }
1980
2146
  lines.push(`Phase: ${state.getPhase()}`);
1981
2147
  return lines.join("\n");
1982
2148
  }
@@ -1995,7 +2161,10 @@ var SharedState = class {
1995
2161
  actionLog: [],
1996
2162
  currentPhase: PHASES.RECON,
1997
2163
  missionSummary: "",
1998
- missionChecklist: []
2164
+ missionChecklist: [],
2165
+ ctfMode: true,
2166
+ // CTF mode ON by default
2167
+ flags: []
1999
2168
  };
2000
2169
  }
2001
2170
  // --- Mission & Persistent Context ---
@@ -2125,6 +2294,9 @@ var SharedState = class {
2125
2294
  timestamp: Date.now(),
2126
2295
  ...action
2127
2296
  });
2297
+ if (this.data.actionLog.length > AGENT_LIMITS.MAX_ACTION_LOG) {
2298
+ this.data.actionLog.splice(0, this.data.actionLog.length - AGENT_LIMITS.MAX_ACTION_LOG);
2299
+ }
2128
2300
  }
2129
2301
  getRecentActions(count = DISPLAY_LIMITS.COMPACT_LIST_ITEMS) {
2130
2302
  return this.data.actionLog.slice(-count);
@@ -2135,6 +2307,21 @@ var SharedState = class {
2135
2307
  getPhase() {
2136
2308
  return this.data.currentPhase;
2137
2309
  }
2310
+ // --- CTF Mode ---
2311
+ setCtfMode(enabled) {
2312
+ this.data.ctfMode = enabled;
2313
+ }
2314
+ isCtfMode() {
2315
+ return this.data.ctfMode;
2316
+ }
2317
+ addFlag(flag) {
2318
+ if (this.data.flags.includes(flag)) return false;
2319
+ this.data.flags.push(flag);
2320
+ return true;
2321
+ }
2322
+ getFlags() {
2323
+ return this.data.flags;
2324
+ }
2138
2325
  /**
2139
2326
  * @remarks
2140
2327
  * WHY: Delegating complex string builder to specialized serializer.
@@ -2534,7 +2721,7 @@ Used ports: ${usedPorts.join(", ")}
2534
2721
  }
2535
2722
  try {
2536
2723
  const proc = startBackgroundProcess(command, {
2537
- description: command.slice(0, 80),
2724
+ description: command.slice(0, DISPLAY_LIMITS.COMMAND_DESCRIPTION_PREVIEW),
2538
2725
  purpose: params.purpose
2539
2726
  });
2540
2727
  const portInfo = proc.listeningPort ? `
@@ -2907,7 +3094,7 @@ Types: credential, hash, token, ssh_key, api_key, file, session, ticket, certifi
2907
3094
  success: true,
2908
3095
  output: `Loot recorded: [${lootType}] from ${p.host}
2909
3096
  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.`)
3097
+ ` + (crackableTypes.includes(lootType) ? `This is crackable. Consider: hash_crack({ hashes: "${p.detail.slice(0, DISPLAY_LIMITS.LOOT_DETAIL_PREVIEW)}..." })` : `Consider credential reuse / lateral movement with this loot.`)
2911
3098
  };
2912
3099
  }
2913
3100
  },
@@ -2941,10 +3128,10 @@ Detail: ${p.detail}
2941
3128
  import { execFileSync } from "child_process";
2942
3129
 
2943
3130
  // 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);
3131
+ import path2 from "path";
3132
+ import { fileURLToPath as fileURLToPath2 } from "url";
3133
+ var __filename2 = fileURLToPath2(import.meta.url);
3134
+ var __dirname2 = path2.dirname(__filename2);
2948
3135
  var ENV_KEYS = {
2949
3136
  API_KEY: "PENTEST_API_KEY",
2950
3137
  BASE_URL: "PENTEST_BASE_URL",
@@ -2992,129 +3179,6 @@ var SEARCH_LIMIT = {
2992
3179
  TIMEOUT_MS: 1e4
2993
3180
  };
2994
3181
 
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";
3083
- appendFileSync2(this.logPath, logLine);
3084
- } catch (e) {
3085
- console.error("[DebugLogger] Write error:", e);
3086
- }
3087
- }
3088
- logRaw(category, label, raw) {
3089
- if (!this.initialized || !this.logPath) return;
3090
- try {
3091
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3092
- const logLine = `[${timestamp}] [${category.toUpperCase()}] ${label}:
3093
- ${raw}
3094
- ---
3095
- `;
3096
- appendFileSync2(this.logPath, logLine);
3097
- } catch (e) {
3098
- console.error("[DebugLogger] Write error:", e);
3099
- }
3100
- }
3101
- clear() {
3102
- if (!this.logPath) return;
3103
- try {
3104
- writeFileSync3(this.logPath, "");
3105
- } catch (e) {
3106
- console.error("[DebugLogger] Clear error:", e);
3107
- }
3108
- }
3109
- };
3110
- var logger = DebugLogger.getInstance(false);
3111
- function initDebugLogger() {
3112
- DebugLogger.resetInstance(true);
3113
- }
3114
- function debugLog(category, message, data) {
3115
- logger.log(category, message, data);
3116
- }
3117
-
3118
3182
  // src/engine/tools/web-browser.ts
3119
3183
  import { join as join5 } from "path";
3120
3184
  import { tmpdir as tmpdir3 } from "os";
@@ -3166,7 +3230,7 @@ var PLAYWRIGHT_SCRIPT = {
3166
3230
  };
3167
3231
 
3168
3232
  // src/engine/tools/web-browser-setup.ts
3169
- import { spawn as spawn3 } from "child_process";
3233
+ import { spawn as spawn4 } from "child_process";
3170
3234
  import { existsSync as existsSync4 } from "fs";
3171
3235
  import { join as join3, dirname as dirname2 } from "path";
3172
3236
  function getPlaywrightPath() {
@@ -3187,12 +3251,12 @@ function getPlaywrightPath() {
3187
3251
  async function checkPlaywright() {
3188
3252
  try {
3189
3253
  const result2 = await new Promise((resolve) => {
3190
- const child = spawn3(PLAYWRIGHT_CMD.NPM, [PLAYWRIGHT_CMD.PLAYWRIGHT, PLAYWRIGHT_ACTION.VERSION], { shell: true });
3254
+ const child = spawn4(PLAYWRIGHT_CMD.NPM, [PLAYWRIGHT_CMD.PLAYWRIGHT, PLAYWRIGHT_ACTION.VERSION], { shell: true });
3191
3255
  let stdout = "";
3192
3256
  child.stdout.on("data", (data) => stdout += data);
3193
3257
  child.on("close", (code) => {
3194
3258
  if (code === 0) {
3195
- const browserCheck = spawn3(PLAYWRIGHT_CMD.NPM, [PLAYWRIGHT_CMD.PLAYWRIGHT, PLAYWRIGHT_BROWSER.CHROMIUM, PLAYWRIGHT_ACTION.HELP], { shell: true });
3259
+ const browserCheck = spawn4(PLAYWRIGHT_CMD.NPM, [PLAYWRIGHT_CMD.PLAYWRIGHT, PLAYWRIGHT_BROWSER.CHROMIUM, PLAYWRIGHT_ACTION.HELP], { shell: true });
3196
3260
  browserCheck.on("close", (browserCode) => {
3197
3261
  resolve({ installed: true, browserInstalled: browserCode === 0 });
3198
3262
  });
@@ -3210,7 +3274,7 @@ async function checkPlaywright() {
3210
3274
  }
3211
3275
  async function installPlaywright() {
3212
3276
  return new Promise((resolve) => {
3213
- const child = spawn3(PLAYWRIGHT_CMD.NPM, [PLAYWRIGHT_CMD.PLAYWRIGHT, PLAYWRIGHT_ACTION.INSTALL, PLAYWRIGHT_BROWSER.CHROMIUM], {
3277
+ const child = spawn4(PLAYWRIGHT_CMD.NPM, [PLAYWRIGHT_CMD.PLAYWRIGHT, PLAYWRIGHT_ACTION.INSTALL, PLAYWRIGHT_BROWSER.CHROMIUM], {
3214
3278
  shell: true,
3215
3279
  stdio: ["ignore", "pipe", "pipe"]
3216
3280
  });
@@ -3232,7 +3296,7 @@ async function installPlaywright() {
3232
3296
  }
3233
3297
 
3234
3298
  // src/engine/tools/web-browser-script.ts
3235
- import { spawn as spawn4 } from "child_process";
3299
+ import { spawn as spawn5 } from "child_process";
3236
3300
  import { writeFileSync as writeFileSync4, unlinkSync as unlinkSync2 } from "fs";
3237
3301
  import { join as join4 } from "path";
3238
3302
  import { tmpdir as tmpdir2 } from "os";
@@ -3259,7 +3323,7 @@ function runPlaywrightScript(script, timeout, scriptPrefix) {
3259
3323
  join4(process.cwd(), "node_modules"),
3260
3324
  process.env.NODE_PATH || ""
3261
3325
  ].filter(Boolean).join(":");
3262
- const child = spawn4(PLAYWRIGHT_CMD.NODE, [scriptPath], {
3326
+ const child = spawn5(PLAYWRIGHT_CMD.NODE, [scriptPath], {
3263
3327
  timeout: timeout + PLAYWRIGHT_SCRIPT.SPAWN_TIMEOUT_BUFFER_MS,
3264
3328
  env: { ...process.env, NODE_PATH: nodePathDirs }
3265
3329
  });
@@ -3362,7 +3426,7 @@ const { chromium } = require(${safePlaywrightPath});
3362
3426
  result.links = await page.evaluate(() => {
3363
3427
  return Array.from(document.querySelectorAll('a[href]')).map(a => ({
3364
3428
  href: a.href,
3365
- text: a.textContent.trim().slice(0, 100)
3429
+ text: a.textContent.trim().slice(0, ${DISPLAY_LIMITS.LINK_TEXT_PREVIEW})
3366
3430
  })).slice(0, ${BROWSER_LIMITS.MAX_LINKS_EXTRACTION});
3367
3431
  });` : ""}
3368
3432
 
@@ -3411,11 +3475,11 @@ function formatBrowserOutput(data, options) {
3411
3475
  }
3412
3476
  if (data.links && options.extractLinks && data.links.length > 0) {
3413
3477
  lines.push(`## Links (${data.links.length} found)`);
3414
- data.links.slice(0, 20).forEach((link) => {
3478
+ data.links.slice(0, DISPLAY_LIMITS.LINKS_PREVIEW).forEach((link) => {
3415
3479
  lines.push(`- [${link.text || "no text"}](${link.href})`);
3416
3480
  });
3417
- if (data.links.length > 20) {
3418
- lines.push(`... and ${data.links.length - 20} more`);
3481
+ if (data.links.length > DISPLAY_LIMITS.LINKS_PREVIEW) {
3482
+ lines.push(`... and ${data.links.length - DISPLAY_LIMITS.LINKS_PREVIEW} more`);
3419
3483
  }
3420
3484
  lines.push("");
3421
3485
  }
@@ -3570,146 +3634,10 @@ async function webSearchWithBrowser(query, engine = "google") {
3570
3634
  });
3571
3635
  }
3572
3636
 
3573
- // src/engine/tools-mid.ts
3637
+ // src/engine/web-search-providers.ts
3574
3638
  function getErrorMessage(error) {
3575
3639
  return error instanceof Error ? error.message : String(error);
3576
3640
  }
3577
- async function parseNmap(xmlPath) {
3578
- try {
3579
- const fileResult = await readFileContent(xmlPath);
3580
- if (!fileResult.success) {
3581
- return fileResult;
3582
- }
3583
- const xmlContent = fileResult.output;
3584
- const results = {
3585
- targets: [],
3586
- summary: { totalTargets: 0, openPorts: 0, servicesFound: 0 }
3587
- };
3588
- const hostRegex = /<host[^>]*>[\s\S]*?<\/host>/g;
3589
- const hosts = xmlContent.match(hostRegex) || [];
3590
- for (const hostBlock of hosts) {
3591
- const ipMatch = hostBlock.match(/<address[^>]*addr="([^"]+)"/);
3592
- const ip = ipMatch ? ipMatch[1] : "";
3593
- const hostnameMatch = hostBlock.match(/<hostname[^>]*name="([^"]+)"/);
3594
- const hostname = hostnameMatch ? hostnameMatch[1] : void 0;
3595
- const ports = [];
3596
- const portRegex = /<port[^>]*protocol="([^"]*)"[^>]*portid="(\d+)">[\s\S]*?<\/port>/g;
3597
- let portMatch;
3598
- while ((portMatch = portRegex.exec(hostBlock)) !== null) {
3599
- const protocol = portMatch[1];
3600
- const port = parseInt(portMatch[2]);
3601
- const stateMatch = portMatch[0].match(/<state[^>]*state="([^"]+)"/);
3602
- const state = stateMatch ? stateMatch[1] : "";
3603
- const serviceMatch = portMatch[0].match(/<service[^>]*name="([^"]*)"(?:[^>]*version="([^"]*)")?/);
3604
- const service = serviceMatch ? serviceMatch[1] : void 0;
3605
- const version = serviceMatch && serviceMatch[2] ? serviceMatch[2] : void 0;
3606
- if (state === "open") {
3607
- ports.push({ port, protocol, state, service, version });
3608
- results.summary.openPorts++;
3609
- if (service) results.summary.servicesFound++;
3610
- }
3611
- }
3612
- if (ip) {
3613
- results.targets.push({ ip, hostname, ports });
3614
- results.summary.totalTargets++;
3615
- }
3616
- }
3617
- return {
3618
- success: true,
3619
- output: JSON.stringify(results, null, 2)
3620
- };
3621
- } catch (error) {
3622
- return {
3623
- success: false,
3624
- output: "",
3625
- error: getErrorMessage(error)
3626
- };
3627
- }
3628
- }
3629
- async function searchCVE(service, version) {
3630
- try {
3631
- return searchExploitDB(service, version);
3632
- } catch (error) {
3633
- return {
3634
- success: false,
3635
- output: "",
3636
- error: getErrorMessage(error)
3637
- };
3638
- }
3639
- }
3640
- async function searchExploitDB(service, version) {
3641
- try {
3642
- const query = version ? `${service} ${version}` : service;
3643
- try {
3644
- const output = execFileSync("searchsploit", [query, "--color", "never"], {
3645
- encoding: "utf-8",
3646
- stdio: ["ignore", "pipe", "pipe"],
3647
- timeout: SEARCH_LIMIT.TIMEOUT_MS
3648
- });
3649
- const lines = output.trim().split("\n").slice(0, SEARCH_LIMIT.MAX_OUTPUT_LINES);
3650
- return {
3651
- success: true,
3652
- output: lines.join("\n") || `No exploits found for ${query}`
3653
- };
3654
- } catch (e) {
3655
- const execError = e;
3656
- const stderr = String(execError.stderr || "");
3657
- const stdout = String(execError.stdout || "");
3658
- if (stderr.includes("No results")) {
3659
- return {
3660
- success: true,
3661
- output: `No exploits found for ${query}`
3662
- };
3663
- }
3664
- return {
3665
- success: true,
3666
- output: stdout || `No exploits found for ${query}`
3667
- };
3668
- }
3669
- } catch (error) {
3670
- return {
3671
- success: false,
3672
- output: "",
3673
- error: getErrorMessage(error)
3674
- };
3675
- }
3676
- }
3677
- async function webSearch(query, _engine) {
3678
- debugLog("search", "webSearch START", { query });
3679
- try {
3680
- const apiKey = getSearchApiKey();
3681
- const apiUrl = getSearchApiUrl();
3682
- debugLog("search", "Search API config", {
3683
- hasApiKey: !!apiKey,
3684
- apiUrl,
3685
- apiKeyPrefix: apiKey ? apiKey.slice(0, 8) + "..." : null
3686
- });
3687
- if (apiKey) {
3688
- if (apiUrl.includes(SEARCH_URL_PATTERN.GLM) || apiUrl.includes(SEARCH_URL_PATTERN.ZHIPU)) {
3689
- debugLog("search", "Using GLM search");
3690
- return await searchWithGLM(query, apiKey, apiUrl);
3691
- } else if (apiUrl.includes(SEARCH_URL_PATTERN.BRAVE)) {
3692
- debugLog("search", "Using Brave search");
3693
- return await searchWithBrave(query, apiKey, apiUrl);
3694
- } else if (apiUrl.includes(SEARCH_URL_PATTERN.SERPER)) {
3695
- debugLog("search", "Using Serper search");
3696
- return await searchWithSerper(query, apiKey, apiUrl);
3697
- } else {
3698
- debugLog("search", "Using generic search API");
3699
- return await searchWithGenericApi(query, apiKey, apiUrl);
3700
- }
3701
- }
3702
- debugLog("search", "SEARCH_API_KEY not set \u2014 falling back to Playwright Google search");
3703
- return await searchWithPlaywright(query);
3704
- } catch (error) {
3705
- debugLog("search", "webSearch ERROR", { error: getErrorMessage(error) });
3706
- return {
3707
- success: false,
3708
- output: "",
3709
- error: getErrorMessage(error)
3710
- };
3711
- }
3712
- }
3713
3641
  async function searchWithGLM(query, apiKey, apiUrl) {
3714
3642
  debugLog("search", "GLM request START", { apiUrl, query });
3715
3643
  const requestBody = {
@@ -3836,24 +3764,165 @@ async function searchWithGenericApi(query, apiKey, apiUrl) {
3836
3764
  async function searchWithPlaywright(query) {
3837
3765
  debugLog("search", "Playwright Google search START", { query });
3838
3766
  try {
3839
- const result2 = await webSearchWithBrowser(query, "google");
3840
- if (!result2.success) {
3767
+ const result2 = await webSearchWithBrowser(query, "google");
3768
+ if (!result2.success) {
3769
+ return {
3770
+ success: false,
3771
+ output: "",
3772
+ error: `Playwright search failed: ${result2.error}. Set SEARCH_API_KEY for reliable search (Brave API by default).`
3773
+ };
3774
+ }
3775
+ debugLog("search", "Playwright Google search COMPLETE", { outputLength: result2.output.length });
3776
+ return {
3777
+ success: true,
3778
+ output: result2.output || `No results found for: ${query}`
3779
+ };
3780
+ } catch (error) {
3781
+ return {
3782
+ success: false,
3783
+ output: "",
3784
+ error: `Playwright search error: ${getErrorMessage(error)}. Set SEARCH_API_KEY for reliable search (Brave API by default).`
3785
+ };
3786
+ }
3787
+ }
3788
+
3789
+ // src/engine/tools-mid.ts
3790
+ function getErrorMessage2(error) {
3791
+ return error instanceof Error ? error.message : String(error);
3792
+ }
3793
+ async function parseNmap(xmlPath) {
3794
+ try {
3795
+ const fileResult = await readFileContent(xmlPath);
3796
+ if (!fileResult.success) {
3797
+ return fileResult;
3798
+ }
3799
+ const xmlContent = fileResult.output;
3800
+ const results = {
3801
+ targets: [],
3802
+ summary: { totalTargets: 0, openPorts: 0, servicesFound: 0 }
3803
+ };
3804
+ const hostRegex = /<host[^>]*>[\s\S]*?<\/host>/g;
3805
+ const hosts = xmlContent.match(hostRegex) || [];
3806
+ for (const hostBlock of hosts) {
3807
+ const ipMatch = hostBlock.match(/<address[^>]*addr="([^"]+)"/);
3808
+ const ip = ipMatch ? ipMatch[1] : "";
3809
+ const hostnameMatch = hostBlock.match(/<hostname[^>]*name="([^"]+)"/);
3810
+ const hostname = hostnameMatch ? hostnameMatch[1] : void 0;
3811
+ const ports = [];
3812
+ const portRegex = /<port[^>]*protocol="([^"]*)"[^>]*portid="(\d+)">[\s\S]*?<\/port>/g;
3813
+ let portMatch;
3814
+ while ((portMatch = portRegex.exec(hostBlock)) !== null) {
3815
+ const protocol = portMatch[1];
3816
+ const port = parseInt(portMatch[2]);
3817
+ const stateMatch = portMatch[0].match(/<state[^>]*state="([^"]+)"/);
3818
+ const state = stateMatch ? stateMatch[1] : "";
3819
+ const serviceMatch = portMatch[0].match(/<service[^>]*name="([^"]*)"(?:[^>]*version="([^"]*)")?/);
3820
+ const service = serviceMatch ? serviceMatch[1] : void 0;
3821
+ const version = serviceMatch && serviceMatch[2] ? serviceMatch[2] : void 0;
3822
+ if (state === "open") {
3823
+ ports.push({ port, protocol, state, service, version });
3824
+ results.summary.openPorts++;
3825
+ if (service) results.summary.servicesFound++;
3826
+ }
3827
+ }
3828
+ if (ip) {
3829
+ results.targets.push({ ip, hostname, ports });
3830
+ results.summary.totalTargets++;
3831
+ }
3832
+ }
3833
+ return {
3834
+ success: true,
3835
+ output: JSON.stringify(results, null, 2)
3836
+ };
3837
+ } catch (error) {
3838
+ return {
3839
+ success: false,
3840
+ output: "",
3841
+ error: getErrorMessage2(error)
3842
+ };
3843
+ }
3844
+ }
3845
+ async function searchCVE(service, version) {
3846
+ try {
3847
+ return searchExploitDB(service, version);
3848
+ } catch (error) {
3849
+ return {
3850
+ success: false,
3851
+ output: "",
3852
+ error: getErrorMessage2(error)
3853
+ };
3854
+ }
3855
+ }
3856
+ async function searchExploitDB(service, version) {
3857
+ try {
3858
+ const query = version ? `${service} ${version}` : service;
3859
+ try {
3860
+ const output = execFileSync("searchsploit", [query, "--color", "never"], {
3861
+ encoding: "utf-8",
3862
+ stdio: ["ignore", "pipe", "pipe"],
3863
+ timeout: SEARCH_LIMIT.TIMEOUT_MS
3864
+ });
3865
+ const lines = output.trim().split("\n").slice(0, SEARCH_LIMIT.MAX_OUTPUT_LINES);
3841
3866
  return {
3842
- success: false,
3843
- output: "",
3844
- error: `Playwright search failed: ${result2.error}. Set SEARCH_API_KEY for reliable search (Brave API by default).`
3867
+ success: true,
3868
+ output: lines.join("\n") || `No exploits found for ${query}`
3869
+ };
3870
+ } catch (e) {
3871
+ const execError = e;
3872
+ const stderr = String(execError.stderr || "");
3873
+ const stdout = String(execError.stdout || "");
3874
+ if (stderr.includes("No results")) {
3875
+ return {
3876
+ success: true,
3877
+ output: `No exploits found for ${query}`
3878
+ };
3879
+ }
3880
+ return {
3881
+ success: true,
3882
+ output: stdout || `No exploits found for ${query}`
3845
3883
  };
3846
3884
  }
3847
- debugLog("search", "Playwright Google search COMPLETE", { outputLength: result2.output.length });
3885
+ } catch (error) {
3848
3886
  return {
3849
- success: true,
3850
- output: result2.output || `No results found for: ${query}`
3887
+ success: false,
3888
+ output: "",
3889
+ error: getErrorMessage2(error)
3851
3890
  };
3891
+ }
3892
+ }
3893
+ async function webSearch(query, _engine) {
3894
+ debugLog("search", "webSearch START", { query });
3895
+ try {
3896
+ const apiKey = getSearchApiKey();
3897
+ const apiUrl = getSearchApiUrl();
3898
+ debugLog("search", "Search API config", {
3899
+ hasApiKey: !!apiKey,
3900
+ apiUrl,
3901
+ apiKeyPrefix: apiKey ? apiKey.slice(0, DISPLAY_LIMITS.API_KEY_PREFIX) + "..." : null
3902
+ });
3903
+ if (apiKey) {
3904
+ if (apiUrl.includes(SEARCH_URL_PATTERN.GLM) || apiUrl.includes(SEARCH_URL_PATTERN.ZHIPU)) {
3905
+ debugLog("search", "Using GLM search");
3906
+ return await searchWithGLM(query, apiKey, apiUrl);
3907
+ } else if (apiUrl.includes(SEARCH_URL_PATTERN.BRAVE)) {
3908
+ debugLog("search", "Using Brave search");
3909
+ return await searchWithBrave(query, apiKey, apiUrl);
3910
+ } else if (apiUrl.includes(SEARCH_URL_PATTERN.SERPER)) {
3911
+ debugLog("search", "Using Serper search");
3912
+ return await searchWithSerper(query, apiKey, apiUrl);
3913
+ } else {
3914
+ debugLog("search", "Using generic search API");
3915
+ return await searchWithGenericApi(query, apiKey, apiUrl);
3916
+ }
3917
+ }
3918
+ debugLog("search", "SEARCH_API_KEY not set \u2014 falling back to Playwright Google search");
3919
+ return await searchWithPlaywright(query);
3852
3920
  } catch (error) {
3921
+ debugLog("search", "webSearch ERROR", { error: getErrorMessage2(error) });
3853
3922
  return {
3854
3923
  success: false,
3855
3924
  output: "",
3856
- error: `Playwright search error: ${getErrorMessage(error)}. Set SEARCH_API_KEY for reliable search (Brave API by default).`
3925
+ error: getErrorMessage2(error)
3857
3926
  };
3858
3927
  }
3859
3928
  }
@@ -4185,6 +4254,10 @@ Can extract forms and inputs for security testing.`,
4185
4254
  {
4186
4255
  name: TOOL_NAMES.GET_OWASP_KNOWLEDGE,
4187
4256
  description: `Get OWASP Top 10 vulnerability knowledge (2017, 2021, 2025 editions).
4257
+ This is BOOTSTRAP knowledge for initial direction. Use it to quickly
4258
+ orient your attack approach, then try attacks based on what you learn.
4259
+ If built-in knowledge isn't enough, use web_search for version-specific details.
4260
+
4188
4261
  Returns structured information about:
4189
4262
  - OWASP Top 10 category details (detection methods, test payloads, tools)
4190
4263
  - Common vulnerability patterns
@@ -4263,10 +4336,14 @@ Returns a step-by-step guide for:
4263
4336
  },
4264
4337
  {
4265
4338
  name: TOOL_NAMES.GET_CVE_INFO,
4266
- description: `Get information about known CVEs. Use this to:
4267
- - Look up vulnerability details
4268
- - Check if a service version has known vulnerabilities
4269
- - Get exploit information
4339
+ description: `Get information about known CVEs from local bootstrap database.
4340
+ This is a STARTING POINT \u2014 use it first for quick lookups on major CVEs.
4341
+ If the CVE isn't found locally or you need more detail, then use web_search.
4342
+
4343
+ Use this to:
4344
+ - Quick-check if a well-known CVE exists locally
4345
+ - Get initial exploit hints for common vulnerabilities
4346
+ - If not found or insufficient, use web_search for full details and PoCs
4270
4347
 
4271
4348
  Includes critical CVEs like Log4Shell, Spring4Shell, EternalBlue, XZ Backdoor, etc.
4272
4349
  If CVE not found locally, use web_search to find it online.`,
@@ -4634,7 +4711,7 @@ Common wordlists are automatically searched in /usr/share/wordlists (rockyou.txt
4634
4711
  const cmd = `hashcat -m ${format || HASHCAT_MODES.MD5} "${hashes}" "${wordlistPath}" --force`;
4635
4712
  if (background) {
4636
4713
  const proc = startBackgroundProcess(cmd, {
4637
- description: `Cracking hashes: ${hashes.slice(0, 20)}...`,
4714
+ description: `Cracking hashes: ${hashes.slice(0, DISPLAY_LIMITS.HASH_PREVIEW_LENGTH)}...`,
4638
4715
  purpose: `Attempting to crack ${format || "unknown"} hashes using ${wordlist}`
4639
4716
  });
4640
4717
  return {
@@ -4741,72 +4818,61 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
4741
4818
  "/usr/share/dirb/",
4742
4819
  "/opt/wordlists/"
4743
4820
  ];
4744
- const results = [];
4745
- results.push("# Available Wordlists on System\\n");
4821
+ const CATEGORY_KEYWORDS = {
4822
+ passwords: ["password", "rockyou"],
4823
+ usernames: ["user"],
4824
+ web: ["web", "content", "raft"],
4825
+ dns: ["dns", "subdomain"],
4826
+ fuzzing: ["fuzz", "injection"],
4827
+ api: ["api"]
4828
+ };
4829
+ const WORDLIST_EXTENSIONS = /* @__PURE__ */ new Set(["txt", "lst", "dict", "list", "wlist"]);
4830
+ const SKIP_DIRS = /* @__PURE__ */ new Set([".", "doc"]);
4831
+ const matchesCategory = (filePath) => {
4832
+ if (!category) return true;
4833
+ const pathLower = filePath.toLowerCase();
4834
+ const keywords = CATEGORY_KEYWORDS[category.toLowerCase()] || [category.toLowerCase()];
4835
+ return keywords.some((kw) => pathLower.includes(kw));
4836
+ };
4837
+ const matchesSearch = (filePath, fileName) => {
4838
+ if (!search) return true;
4839
+ const q = search.toLowerCase();
4840
+ return fileName.toLowerCase().includes(q) || filePath.toLowerCase().includes(q);
4841
+ };
4842
+ const results = ["# Available Wordlists on System\\n"];
4746
4843
  let totalCount = 0;
4747
- const scanDir = (dirPath, maxDepth = 3, currentDepth = 0) => {
4748
- if (currentDepth > maxDepth) return;
4749
- if (!existsSync7(dirPath)) return;
4844
+ const processFile = (fullPath, fileName) => {
4845
+ const ext = fileName.split(".").pop()?.toLowerCase();
4846
+ if (!WORDLIST_EXTENSIONS.has(ext || "")) return;
4847
+ const stats = statSync2(fullPath);
4848
+ if (stats.size < minSize) return;
4849
+ if (!matchesCategory(fullPath)) return;
4850
+ if (!matchesSearch(fullPath, fileName)) return;
4851
+ totalCount++;
4852
+ results.push(`### ${fullPath}`);
4853
+ results.push(`- Size: ${Math.round(stats.size / 1024)} KB (${stats.size.toLocaleString()} bytes)`);
4854
+ results.push("");
4855
+ };
4856
+ const scanDir = (dirPath, maxDepth = 3, depth = 0) => {
4857
+ if (depth > maxDepth || !existsSync7(dirPath)) return;
4858
+ let entries;
4750
4859
  try {
4751
- const entries = readdirSync3(dirPath, { withFileTypes: true });
4752
- for (const entry of entries) {
4753
- const fullPath = join10(dirPath, entry.name);
4754
- if (entry.isDirectory()) {
4755
- if (entry.name.startsWith(".")) continue;
4756
- if (entry.name === "doc") continue;
4757
- scanDir(fullPath, maxDepth, currentDepth + 1);
4758
- } else if (entry.isFile()) {
4759
- const ext = entry.name.split(".").pop()?.toLowerCase();
4760
- if (!["txt", "lst", "dict", "list", "wlist"].includes(ext || "")) {
4761
- continue;
4762
- }
4763
- try {
4764
- const stats = statSync2(fullPath);
4765
- const sizeBytes = stats.size;
4766
- if (sizeBytes < minSize) continue;
4767
- const relPath = fullPath;
4768
- const filename = entry.name;
4769
- const sizeKB = Math.round(sizeBytes / 1024);
4770
- if (category) {
4771
- const pathLower = relPath.toLowerCase();
4772
- let matchesCategory = false;
4773
- switch (category.toLowerCase()) {
4774
- case "passwords":
4775
- matchesCategory = pathLower.includes("password") || pathLower.includes("rockyou");
4776
- break;
4777
- case "usernames":
4778
- matchesCategory = pathLower.includes("user");
4779
- break;
4780
- case "web":
4781
- matchesCategory = pathLower.includes("web") || pathLower.includes("content") || pathLower.includes("raft");
4782
- break;
4783
- case "dns":
4784
- matchesCategory = pathLower.includes("dns") || pathLower.includes("subdomain");
4785
- break;
4786
- case "fuzzing":
4787
- matchesCategory = pathLower.includes("fuzz") || pathLower.includes("injection");
4788
- break;
4789
- case "api":
4790
- matchesCategory = pathLower.includes("api");
4791
- break;
4792
- default:
4793
- matchesCategory = pathLower.includes(category.toLowerCase());
4794
- }
4795
- if (!matchesCategory) continue;
4796
- }
4797
- if (search && !filename.toLowerCase().includes(search.toLowerCase()) && !relPath.toLowerCase().includes(search.toLowerCase())) {
4798
- continue;
4799
- }
4800
- totalCount++;
4801
- results.push(`### ${relPath}`);
4802
- results.push(`- Size: ${sizeKB} KB (${sizeBytes.toLocaleString()} bytes)`);
4803
- results.push("");
4804
- } catch {
4805
- continue;
4806
- }
4807
- }
4808
- }
4860
+ entries = readdirSync3(dirPath, { withFileTypes: true });
4809
4861
  } catch {
4862
+ return;
4863
+ }
4864
+ for (const entry of entries) {
4865
+ if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) continue;
4866
+ const fullPath = join10(dirPath, entry.name);
4867
+ if (entry.isDirectory()) {
4868
+ scanDir(fullPath, maxDepth, depth + 1);
4869
+ continue;
4870
+ }
4871
+ if (!entry.isFile()) continue;
4872
+ try {
4873
+ processFile(fullPath, entry.name);
4874
+ } catch {
4875
+ }
4810
4876
  }
4811
4877
  };
4812
4878
  for (const basePath of scanPaths) {
@@ -6062,237 +6128,6 @@ var CategorizedToolRegistry = class extends ToolRegistry {
6062
6128
  }
6063
6129
  };
6064
6130
 
6065
- // src/shared/constants/service-ports.ts
6066
- var SERVICE_PORTS = {
6067
- SSH: 22,
6068
- FTP: 21,
6069
- TELNET: 23,
6070
- SMTP: 25,
6071
- DNS: 53,
6072
- HTTP: 80,
6073
- POP3: 110,
6074
- IMAP: 143,
6075
- SMB_NETBIOS: 139,
6076
- SMB: 445,
6077
- HTTPS: 443,
6078
- MSSQL: 1433,
6079
- MYSQL: 3306,
6080
- RDP: 3389,
6081
- POSTGRESQL: 5432,
6082
- REDIS: 6379,
6083
- HTTP_ALT: 8080,
6084
- HTTPS_ALT: 8443,
6085
- MONGODB: 27017,
6086
- ELASTICSEARCH: 9200,
6087
- MEMCACHED: 11211,
6088
- NODE_DEFAULT: 3e3,
6089
- FLASK_DEFAULT: 5e3,
6090
- DJANGO_DEFAULT: 8e3
6091
- };
6092
- var CRITICAL_SERVICE_PORTS = [
6093
- SERVICE_PORTS.SSH,
6094
- SERVICE_PORTS.RDP,
6095
- SERVICE_PORTS.MYSQL,
6096
- SERVICE_PORTS.POSTGRESQL,
6097
- SERVICE_PORTS.REDIS,
6098
- SERVICE_PORTS.MONGODB
6099
- ];
6100
- var NO_AUTH_CRITICAL_PORTS = [
6101
- SERVICE_PORTS.REDIS,
6102
- SERVICE_PORTS.MONGODB,
6103
- SERVICE_PORTS.ELASTICSEARCH,
6104
- SERVICE_PORTS.MEMCACHED
6105
- ];
6106
- var WEB_SERVICE_PORTS = [
6107
- SERVICE_PORTS.HTTP,
6108
- SERVICE_PORTS.HTTPS,
6109
- SERVICE_PORTS.HTTP_ALT,
6110
- SERVICE_PORTS.HTTPS_ALT,
6111
- SERVICE_PORTS.NODE_DEFAULT,
6112
- SERVICE_PORTS.FLASK_DEFAULT,
6113
- SERVICE_PORTS.DJANGO_DEFAULT
6114
- ];
6115
- var PLAINTEXT_HTTP_PORTS = [
6116
- SERVICE_PORTS.HTTP,
6117
- SERVICE_PORTS.HTTP_ALT,
6118
- SERVICE_PORTS.NODE_DEFAULT
6119
- ];
6120
- var DATABASE_PORTS = [
6121
- SERVICE_PORTS.MYSQL,
6122
- SERVICE_PORTS.POSTGRESQL,
6123
- SERVICE_PORTS.MSSQL,
6124
- SERVICE_PORTS.MONGODB,
6125
- SERVICE_PORTS.REDIS
6126
- ];
6127
- var SMB_PORTS = [
6128
- SERVICE_PORTS.SMB,
6129
- SERVICE_PORTS.SMB_NETBIOS
6130
- ];
6131
-
6132
- // src/shared/utils/logger.ts
6133
- var COLORS = {
6134
- reset: "\x1B[0m",
6135
- dim: "\x1B[2m",
6136
- red: "\x1B[31m",
6137
- yellow: "\x1B[33m",
6138
- green: "\x1B[32m",
6139
- blue: "\x1B[34m",
6140
- magenta: "\x1B[35m",
6141
- cyan: "\x1B[36m"
6142
- };
6143
- var levelColors = {
6144
- [0 /* DEBUG */]: COLORS.dim,
6145
- [1 /* INFO */]: COLORS.green,
6146
- [2 /* WARN */]: COLORS.yellow,
6147
- [3 /* ERROR */]: COLORS.red,
6148
- [999 /* SILENT */]: COLORS.reset
6149
- };
6150
- var levelNames = {
6151
- [0 /* DEBUG */]: "DEBUG",
6152
- [1 /* INFO */]: "INFO",
6153
- [2 /* WARN */]: "WARN",
6154
- [3 /* ERROR */]: "ERROR",
6155
- [999 /* SILENT */]: "SILENT"
6156
- };
6157
- var Logger = class {
6158
- constructor(component, config = {}) {
6159
- this.component = component;
6160
- this.config = {
6161
- minLevel: config.minLevel ?? 1 /* INFO */,
6162
- includeTimestamp: config.includeTimestamp ?? true,
6163
- includeComponent: config.includeComponent ?? true,
6164
- colorOutput: config.colorOutput ?? true,
6165
- outputToFile: config.outputToFile ?? false,
6166
- logFilePath: config.logFilePath
6167
- };
6168
- }
6169
- config;
6170
- logBuffer = [];
6171
- maxBufferSize = 1e3;
6172
- /**
6173
- * Set minimum log level
6174
- */
6175
- setMinLevel(level) {
6176
- this.config.minLevel = level;
6177
- }
6178
- /**
6179
- * Log at a specific level
6180
- */
6181
- log(level, message, data) {
6182
- if (level < this.config.minLevel) {
6183
- return;
6184
- }
6185
- const entry = {
6186
- timestamp: Date.now(),
6187
- level,
6188
- component: this.component,
6189
- message,
6190
- data
6191
- };
6192
- this.logBuffer.push(entry);
6193
- if (this.logBuffer.length > this.maxBufferSize) {
6194
- this.logBuffer.shift();
6195
- }
6196
- const formatted = this.formatEntry(entry);
6197
- console.log(formatted);
6198
- }
6199
- /**
6200
- * Format a log entry for output
6201
- */
6202
- formatEntry(entry) {
6203
- const parts = [];
6204
- if (this.config.includeTimestamp) {
6205
- const date = new Date(entry.timestamp);
6206
- const ts = date.toISOString().split("T")[1].slice(0, -1);
6207
- parts.push(this.config.colorOutput ? COLORS.dim + ts + COLORS.reset : ts);
6208
- }
6209
- const levelName = levelNames[entry.level];
6210
- const levelColor = this.config.colorOutput ? levelColors[entry.level] : "";
6211
- parts.push(levelColor + `[${levelName}]` + (this.config.colorOutput ? COLORS.reset : ""));
6212
- if (this.config.includeComponent) {
6213
- const comp = this.config.colorOutput ? COLORS.cyan + entry.component + COLORS.reset : entry.component;
6214
- parts.push(`[${comp}]`);
6215
- }
6216
- parts.push(entry.message);
6217
- if (entry.data) {
6218
- const dataStr = Object.entries(entry.data).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(" ");
6219
- parts.push(this.config.colorOutput ? COLORS.dim + dataStr + COLORS.reset : dataStr);
6220
- }
6221
- return parts.join(" ");
6222
- }
6223
- /**
6224
- * Debug level logging
6225
- */
6226
- debug(message, data) {
6227
- this.log(0 /* DEBUG */, message, data);
6228
- }
6229
- /**
6230
- * Info level logging
6231
- */
6232
- info(message, data) {
6233
- this.log(1 /* INFO */, message, data);
6234
- }
6235
- /**
6236
- * Warning level logging
6237
- */
6238
- warn(message, data) {
6239
- this.log(2 /* WARN */, message, data);
6240
- }
6241
- /**
6242
- * Error level logging
6243
- */
6244
- error(message, data) {
6245
- this.log(3 /* ERROR */, message, data);
6246
- }
6247
- /**
6248
- * Get all log entries
6249
- */
6250
- getLogs() {
6251
- return [...this.logBuffer];
6252
- }
6253
- /**
6254
- * Get logs by level
6255
- */
6256
- getLogsByLevel(minLevel) {
6257
- return this.logBuffer.filter((entry) => entry.level >= minLevel);
6258
- }
6259
- /**
6260
- * Clear log buffer
6261
- */
6262
- clearLogs() {
6263
- this.logBuffer = [];
6264
- }
6265
- /**
6266
- * Export logs to string
6267
- */
6268
- exportLogs() {
6269
- return this.logBuffer.map((entry) => this.formatEntry(entry)).join("\n");
6270
- }
6271
- };
6272
- var agentLogger = new Logger("Agent", {
6273
- minLevel: 1 /* INFO */,
6274
- colorOutput: true
6275
- });
6276
-
6277
- // src/shared/constants/_shared/http.const.ts
6278
- var HTTP_STATUS = {
6279
- // 2xx Success
6280
- OK: 200,
6281
- CREATED: 201,
6282
- NO_CONTENT: 204,
6283
- // 4xx Client Errors
6284
- BAD_REQUEST: 400,
6285
- UNAUTHORIZED: 401,
6286
- FORBIDDEN: 403,
6287
- NOT_FOUND: 404,
6288
- RATE_LIMIT: 429,
6289
- // 5xx Server Errors
6290
- INTERNAL_ERROR: 500,
6291
- BAD_GATEWAY: 502,
6292
- SERVICE_UNAVAILABLE: 503,
6293
- GATEWAY_TIMEOUT: 504
6294
- };
6295
-
6296
6131
  // src/shared/constants/llm/api.ts
6297
6132
  var LLM_API = {
6298
6133
  /** Default Anthropic API base URL */
@@ -6364,7 +6199,26 @@ var LLM_ERROR_TYPES = {
6364
6199
  UNKNOWN: "unknown"
6365
6200
  };
6366
6201
 
6367
- // src/engine/llm.ts
6202
+ // src/shared/constants/_shared/http.const.ts
6203
+ var HTTP_STATUS = {
6204
+ // 2xx Success
6205
+ OK: 200,
6206
+ CREATED: 201,
6207
+ NO_CONTENT: 204,
6208
+ // 4xx Client Errors
6209
+ BAD_REQUEST: 400,
6210
+ UNAUTHORIZED: 401,
6211
+ FORBIDDEN: 403,
6212
+ NOT_FOUND: 404,
6213
+ RATE_LIMIT: 429,
6214
+ // 5xx Server Errors
6215
+ INTERNAL_ERROR: 500,
6216
+ BAD_GATEWAY: 502,
6217
+ SERVICE_UNAVAILABLE: 503,
6218
+ GATEWAY_TIMEOUT: 504
6219
+ };
6220
+
6221
+ // src/engine/llm-types.ts
6368
6222
  var LLMError = class extends Error {
6369
6223
  /** Structured error information */
6370
6224
  errorInfo;
@@ -6395,6 +6249,8 @@ function classifyError(error) {
6395
6249
  }
6396
6250
  return { type: LLM_ERROR_TYPES.UNKNOWN, message: errorMessage, statusCode, isRetryable: false, suggestedAction: "Analyze error" };
6397
6251
  }
6252
+
6253
+ // src/engine/llm.ts
6398
6254
  var LLMClient = class {
6399
6255
  apiKey;
6400
6256
  baseUrl;
@@ -6404,7 +6260,7 @@ var LLMClient = class {
6404
6260
  this.apiKey = getApiKey();
6405
6261
  this.baseUrl = getBaseUrl() || LLM_API.DEFAULT_BASE_URL;
6406
6262
  this.model = getModel();
6407
- debugLog("llm", "LLMClient constructed", { model: this.model, baseUrl: this.baseUrl, apiKeyPrefix: this.apiKey.slice(0, 8) + "..." });
6263
+ debugLog("llm", "LLMClient constructed", { model: this.model, baseUrl: this.baseUrl, apiKeyPrefix: this.apiKey.slice(0, DISPLAY_LIMITS.API_KEY_PREFIX) + "..." });
6408
6264
  }
6409
6265
  /**
6410
6266
  * Generate a non-streaming response
@@ -6584,7 +6440,7 @@ var LLMClient = class {
6584
6440
  toolCall.input = parseResult.data;
6585
6441
  debugLog("llm", `[${requestId}] Tool parsed OK`, { id, name: toolCall.name, input: toolCall.input });
6586
6442
  } else {
6587
- toolCall.input = { _parse_error: parseResult.error, _raw_json: toolCall._pendingJson.slice(0, 500) };
6443
+ toolCall.input = { _parse_error: parseResult.error, _raw_json: toolCall._pendingJson.slice(0, DISPLAY_LIMITS.RAW_JSON_ERROR_PREVIEW) };
6588
6444
  debugLog("llm", `[${requestId}] Tool parse FAILED`, { id, name: toolCall.name, error: parseResult.error, raw: toolCall._pendingJson });
6589
6445
  }
6590
6446
  delete toolCall._pendingJson;
@@ -6734,7 +6590,7 @@ var __filename3 = fileURLToPath4(import.meta.url);
6734
6590
  var __dirname4 = dirname4(__filename3);
6735
6591
 
6736
6592
  // src/engine/state-persistence.ts
6737
- import { writeFileSync as writeFileSync5, readFileSync as readFileSync3, existsSync as existsSync5, readdirSync, statSync } from "fs";
6593
+ import { writeFileSync as writeFileSync5, readFileSync as readFileSync3, existsSync as existsSync5, readdirSync, statSync, unlinkSync as unlinkSync3 } from "fs";
6738
6594
  import { join as join8, basename } from "path";
6739
6595
  function saveState(state) {
6740
6596
  const sessionsDir = WORKSPACE.SESSIONS;
@@ -6747,7 +6603,7 @@ function saveState(state) {
6747
6603
  findings: state.getFindings(),
6748
6604
  loot: state.getLoot(),
6749
6605
  todo: state.getTodo(),
6750
- actionLog: state.getRecentActions(9999),
6606
+ actionLog: state.getRecentActions(AGENT_LIMITS.MAX_ACTION_LOG),
6751
6607
  currentPhase: state.getPhase(),
6752
6608
  missionSummary: state.getMissionSummary(),
6753
6609
  missionChecklist: state.getMissionChecklist()
@@ -6757,8 +6613,23 @@ function saveState(state) {
6757
6613
  writeFileSync5(sessionFile, JSON.stringify(snapshot, null, 2), "utf-8");
6758
6614
  const latestFile = join8(sessionsDir, "latest.json");
6759
6615
  writeFileSync5(latestFile, JSON.stringify(snapshot, null, 2), "utf-8");
6616
+ pruneOldSessions(sessionsDir);
6760
6617
  return sessionFile;
6761
6618
  }
6619
+ function pruneOldSessions(sessionsDir) {
6620
+ try {
6621
+ const sessionFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(".json") && f !== "latest.json").map((f) => ({
6622
+ name: f,
6623
+ path: join8(sessionsDir, f),
6624
+ mtime: statSync(join8(sessionsDir, f)).mtimeMs
6625
+ })).sort((a, b) => b.mtime - a.mtime);
6626
+ const toDelete = sessionFiles.slice(AGENT_LIMITS.MAX_SESSION_FILES);
6627
+ for (const file of toDelete) {
6628
+ unlinkSync3(file.path);
6629
+ }
6630
+ } catch {
6631
+ }
6632
+ }
6762
6633
  function loadState(state) {
6763
6634
  const latestFile = join8(WORKSPACE.SESSIONS, "latest.json");
6764
6635
  if (!existsSync5(latestFile)) {
@@ -6768,7 +6639,7 @@ function loadState(state) {
6768
6639
  const raw = readFileSync3(latestFile, "utf-8");
6769
6640
  const snapshot = JSON.parse(raw);
6770
6641
  if (snapshot.version !== 1) {
6771
- console.warn(`[StatePersistence] Unknown snapshot version: ${snapshot.version}`);
6642
+ debugLog("general", `Unknown snapshot version: ${snapshot.version}`);
6772
6643
  return false;
6773
6644
  }
6774
6645
  if (snapshot.engagement) {
@@ -6786,7 +6657,9 @@ function loadState(state) {
6786
6657
  for (const item of snapshot.todo) {
6787
6658
  state.addTodo(item.content, item.priority);
6788
6659
  }
6789
- state.setPhase(snapshot.currentPhase);
6660
+ const validPhases = new Set(Object.values(PHASES));
6661
+ const restoredPhase = validPhases.has(snapshot.currentPhase) ? snapshot.currentPhase : PHASES.RECON;
6662
+ state.setPhase(restoredPhase);
6790
6663
  if (snapshot.missionSummary) {
6791
6664
  state.setMissionSummary(snapshot.missionSummary);
6792
6665
  }
@@ -6795,11 +6668,130 @@ function loadState(state) {
6795
6668
  }
6796
6669
  return true;
6797
6670
  } catch (err) {
6798
- console.warn(`[StatePersistence] Failed to load state: ${err}`);
6671
+ debugLog("general", `Failed to load state: ${err}`);
6799
6672
  return false;
6800
6673
  }
6801
6674
  }
6802
6675
 
6676
+ // src/shared/utils/ctf-knowledge.ts
6677
+ var FLAG_PATTERNS = {
6678
+ // Generic CTF flag formats
6679
+ generic: /flag\{[^}]+\}/gi,
6680
+ curly_upper: /FLAG\{[^}]+\}/g,
6681
+ // Platform-specific (major competitions)
6682
+ htb: /HTB\{[^}]+\}/g,
6683
+ thm: /THM\{[^}]+\}/g,
6684
+ picoCTF: /picoCTF\{[^}]+\}/g,
6685
+ defcon_ooo: /OOO\{[^}]+\}/g,
6686
+ // DEF CON CTF (Order of the Overflow)
6687
+ csaw: /CSAW\{[^}]+\}/g,
6688
+ // CSAW CTF
6689
+ google: /CTF\{[^}]+\}/g,
6690
+ // Google CTF
6691
+ dragon: /DrgnS\{[^}]+\}/g,
6692
+ // Dragon Sector
6693
+ hxp: /hxp\{[^}]+\}/g,
6694
+ // hxp CTF
6695
+ zer0pts: /zer0pts\{[^}]+\}/g,
6696
+ // zer0pts CTF
6697
+ ctfd_generic: /[A-Z]{2,10}\{[^}]+\}/g,
6698
+ // Generic CTFd format
6699
+ // Hash-style flags (HTB user.txt / root.txt — 32-char hex)
6700
+ hash_flag: /\b[a-f0-9]{32}\b/g,
6701
+ // Base64-encoded flag detection (common in steganography/forensics)
6702
+ base64_flag: /flag\{[A-Za-z0-9_+\-/=]+\}/gi
6703
+ };
6704
+ var MIN_FLAG_SCAN_LENGTH = 5;
6705
+ function detectFlags(output) {
6706
+ if (!output || output.length < MIN_FLAG_SCAN_LENGTH) return [];
6707
+ const found = /* @__PURE__ */ new Set();
6708
+ for (const pattern of Object.values(FLAG_PATTERNS)) {
6709
+ pattern.lastIndex = 0;
6710
+ const matches = output.match(pattern);
6711
+ if (matches) {
6712
+ for (const m of matches) found.add(m);
6713
+ }
6714
+ }
6715
+ return Array.from(found);
6716
+ }
6717
+
6718
+ // src/agents/tool-error-enrichment.ts
6719
+ function enrichToolErrorContext(ctx) {
6720
+ const { toolName, input, error, originalOutput, progress } = ctx;
6721
+ const lines = [];
6722
+ if (originalOutput) lines.push(originalOutput);
6723
+ lines.push("");
6724
+ lines.push(`[TOOL ERROR ANALYSIS]`);
6725
+ lines.push(`Tool: ${toolName}`);
6726
+ lines.push(`Error: ${error}`);
6727
+ const errorLower = error.toLowerCase();
6728
+ if (isBlockedCommandError(errorLower)) {
6729
+ trackBlockedPattern(progress, toolName, errorLower);
6730
+ appendBlockedCommandHints(lines, errorLower);
6731
+ } else if (isMissingParamError(errorLower)) {
6732
+ const providedParams = Object.keys(input).join(", ") || "none";
6733
+ lines.push(`Provided parameters: ${providedParams}`);
6734
+ lines.push(`Fix: Check the tool's required parameters and provide ALL required values.`);
6735
+ lines.push(`Action: Retry this tool call with the missing parameter(s) added.`);
6736
+ } else if (isNotFoundError(errorLower)) {
6737
+ lines.push(`Fix: The tool or command may not be installed.`);
6738
+ lines.push(`Actions: (1) Try an alternative tool, (2) Use web_search to find installation instructions, or (3) Use a different approach entirely.`);
6739
+ } else if (isPermissionError(errorLower)) {
6740
+ lines.push(`Fix: Insufficient permissions for this operation.`);
6741
+ lines.push(`Actions: (1) Try with sudo if appropriate, (2) Use a different approach, or (3) Check if the target path/resource is correct.`);
6742
+ } else if (isConnectionError(errorLower)) {
6743
+ lines.push(`Fix: Cannot reach the target service.`);
6744
+ lines.push(`Actions: (1) Verify the target host/port is correct, (2) Check if the service is running, (3) Try a different port or protocol.`);
6745
+ } else if (isInvalidInputError(errorLower)) {
6746
+ lines.push(`Fix: The input format is incorrect.`);
6747
+ lines.push(`Actions: (1) Check the input format and fix it, (2) Use web_search to find the correct syntax, or (3) Try a simpler input.`);
6748
+ } else {
6749
+ lines.push(`Actions: (1) Analyze the error and modify your approach, (2) Use web_search("${toolName} ${errorLower.slice(0, AGENT_LIMITS.ERROR_SEARCH_PREVIEW_SLICE)}") to research the error, (3) Try an alternative tool or method.`);
6750
+ }
6751
+ return lines.join("\n");
6752
+ }
6753
+ function isBlockedCommandError(e) {
6754
+ return e.includes("command blocked") || e.includes("injection pattern") || e.includes("not in the allowed") || e.includes("not in allowed paths");
6755
+ }
6756
+ function isMissingParamError(e) {
6757
+ return e.includes("missing") && e.includes("parameter") || e.includes("required") && e.includes("missing") || e.includes("is required");
6758
+ }
6759
+ function isNotFoundError(e) {
6760
+ return e.includes("not found") || e.includes("command not found") || e.includes("no such file");
6761
+ }
6762
+ function isPermissionError(e) {
6763
+ return e.includes("permission denied") || e.includes("access denied") || e.includes("eacces");
6764
+ }
6765
+ function isConnectionError(e) {
6766
+ return e.includes("connection refused") || e.includes("econnrefused") || e.includes("connection reset") || e.includes("timeout");
6767
+ }
6768
+ function isInvalidInputError(e) {
6769
+ return e.includes("invalid") || e.includes("malformed") || e.includes("syntax error");
6770
+ }
6771
+ function trackBlockedPattern(progress, toolName, errorLower) {
6772
+ if (!progress) return;
6773
+ const patternKey = `${toolName}:${errorLower.slice(0, AGENT_LIMITS.BLOCKED_PATTERN_KEY_SLICE)}`;
6774
+ const count = (progress.blockedCommandPatterns.get(patternKey) || 0) + 1;
6775
+ progress.blockedCommandPatterns.set(patternKey, count);
6776
+ progress.totalBlockedCommands++;
6777
+ }
6778
+ function appendBlockedCommandHints(lines, errorLower) {
6779
+ if (errorLower.includes("pipe target") || errorLower.includes("injection")) {
6780
+ lines.push(`Fix: Use the tool's built-in output options instead of shell pipes.`);
6781
+ lines.push(`Alternative approaches:`);
6782
+ lines.push(` 1. Save output to file: command > /tmp/output.txt, then use read_file("/tmp/output.txt")`);
6783
+ lines.push(` 2. Use tool-specific flags: nmap -oN /tmp/scan.txt, curl -o /tmp/page.html`);
6784
+ lines.push(` 3. Use browse_url for web content instead of curl | grep`);
6785
+ lines.push(` 4. If filtering output: run the command first, then read_file + parse results`);
6786
+ } else if (errorLower.includes("redirect")) {
6787
+ lines.push(`Fix: Redirect to /tmp/ or /root/ paths only.`);
6788
+ lines.push(`Example: command > /tmp/output.txt or command 2>&1 > /tmp/errors.txt`);
6789
+ } else {
6790
+ lines.push(`Fix: Simplify the command. Avoid chaining complex operations.`);
6791
+ lines.push(`Break into multiple steps: run_cmd \u2192 read_file \u2192 run_cmd.`);
6792
+ }
6793
+ }
6794
+
6803
6795
  // src/agents/core-agent.ts
6804
6796
  var CoreAgent = class _CoreAgent {
6805
6797
  llm;
@@ -6870,44 +6862,28 @@ var CoreAgent = class _CoreAgent {
6870
6862
  const phase = this.state.getPhase();
6871
6863
  const targets = this.state.getTargets().size;
6872
6864
  const findings = this.state.getFindings().length;
6873
- const phaseActions = {
6874
- [PHASES.RECON]: `RECON PHASE: Start scanning NOW.
6875
- \u2192 run_cmd({ command: "nmap -sV -sC -p- --min-rate=1000 <target>" })
6876
- \u2192 run_cmd({ command: "ffuf -u http://<target>/FUZZ -w /usr/share/wordlists/dirb/common.txt -mc all -fc 404" })`,
6877
- [PHASES.VULN_ANALYSIS]: `VULN ANALYSIS: You have ${targets} target(s). Search CVEs NOW.
6878
- \u2192 For every discovered service+version: web_search("{service} {version} exploit hacktricks")
6879
- \u2192 web_search("{service} {version} CVE PoC github")`,
6880
- [PHASES.EXPLOIT]: `EXPLOIT PHASE: You have ${findings} finding(s). ATTACK NOW.
6881
- \u2192 Pick the highest-severity finding and write an exploit script.
6882
- \u2192 web_search("{CVE} PoC github") \u2192 browse_url \u2192 write_file \u2192 run_cmd`,
6883
- [PHASES.POST_EXPLOIT]: `POST-EXPLOIT: Escalate privileges and move laterally.
6884
- \u2192 bg_process interact: "cat /etc/passwd && cat /etc/shadow && sudo -l"
6885
- \u2192 web_search("{OS} privilege escalation hacktricks")`,
6886
- [PHASES.PRIV_ESC]: `PRIVESC: Escalate NOW.
6887
- \u2192 run_cmd({ command: "sudo -l && find / -perm -4000 -type f 2>/dev/null" })
6888
- \u2192 web_search("{kernel_version} privilege escalation exploit")`,
6889
- [PHASES.LATERAL]: `LATERAL MOVEMENT: Spread to other hosts.
6890
- \u2192 Reuse discovered credentials on all known hosts
6891
- \u2192 run_cmd({ command: "for ip in $(cat /tmp/hosts); do sshpass -p '<cred>' ssh user@$ip id; done" })`,
6892
- [PHASES.WEB]: `WEB PHASE: Test injection points.
6893
- \u2192 get_web_attack_surface with target URL
6894
- \u2192 Test every parameter for SQLi, SSTI, XSS, command injection`
6865
+ const phaseDirection = {
6866
+ [PHASES.RECON]: `RECON: Scan targets. Enumerate services and versions.`,
6867
+ [PHASES.VULN_ANALYSIS]: `VULN ANALYSIS: ${targets} target(s) discovered. Search for CVEs and known exploits.`,
6868
+ [PHASES.EXPLOIT]: `EXPLOIT: ${findings} finding(s) available. Attack the highest-severity one.`,
6869
+ [PHASES.POST_EXPLOIT]: `POST-EXPLOIT: Escalate privileges. Search for escalation paths.`,
6870
+ [PHASES.PRIV_ESC]: `PRIVESC: Find and exploit privilege escalation vectors.`,
6871
+ [PHASES.LATERAL]: `LATERAL: Reuse discovered credentials on other hosts.`,
6872
+ [PHASES.WEB]: `WEB: Enumerate the attack surface. Test every input for injection.`
6895
6873
  };
6896
- const phaseHint = phaseActions[phase] || phaseActions[PHASES.RECON];
6874
+ const direction = phaseDirection[phase] || phaseDirection[PHASES.RECON];
6897
6875
  messages.push({
6898
6876
  role: LLM_ROLES.USER,
6899
- content: `[CTF ALERT] DEADLOCK: ${AGENT_LIMITS.MAX_CONSECUTIVE_IDLE} turns with ZERO tool calls. You are WASTING CTF TIME!
6877
+ content: `\u26A1 DEADLOCK: ${AGENT_LIMITS.MAX_CONSECUTIVE_IDLE} turns with ZERO tool calls.
6900
6878
  Phase: ${phase} | Targets: ${targets} | Findings: ${findings} | Tools executed: ${progress.totalToolsExecuted} (${progress.toolSuccesses}\u2713 ${progress.toolErrors}\u2717)
6901
6879
 
6902
- ${phaseHint}
6880
+ ${direction}
6903
6881
 
6904
- CTF SPEED RULES:
6905
- - \u26A1 EVERY turn MUST have tool calls \u2014 NO exceptions
6906
- - \u26A1 PARALLELIZE: Run 3-5 tools simultaneously when possible
6907
- - \u26A1 NO planning: "Let me..." is forbidden \u2014 JUST ACT
6908
- - \u26A1 web_search is your weapon: Use it immediately when stuck
6909
- - \u26A1 1 fail \u2260 stop: Try DIFFERENT approach immediately
6910
- - \u26A1 NEVER output text without a tool call`
6882
+ RULES:
6883
+ - Every turn MUST have tool calls
6884
+ - If stuck: search for techniques (web_search)
6885
+ - If failed: try a DIFFERENT approach
6886
+ - ACT NOW \u2014 do not plan or explain`
6911
6887
  });
6912
6888
  }
6913
6889
  } catch (error) {
@@ -7103,7 +7079,7 @@ Please decide how to handle this error and continue.`;
7103
7079
  toolName,
7104
7080
  success,
7105
7081
  output,
7106
- outputSummary: output.slice(0, 200),
7082
+ outputSummary: output.slice(0, DISPLAY_LIMITS.OUTPUT_SUMMARY),
7107
7083
  error,
7108
7084
  duration
7109
7085
  }
@@ -7214,7 +7190,7 @@ Please decide how to handle this error and continue.`;
7214
7190
  \u{1F4A1} TIP: If you need to see the full output, use a tool to read the file directly or run the command with | head, | tail, or | grep.`;
7215
7191
  }
7216
7192
  if (result2.error) {
7217
- outputText = this.enrichToolErrorContext({ toolName: call.name, input: call.input, error: result2.error, originalOutput: outputText, progress });
7193
+ outputText = this.enrichToolError({ toolName: call.name, input: call.input, error: result2.error, originalOutput: outputText, progress });
7218
7194
  if (progress) progress.toolErrors++;
7219
7195
  } else {
7220
7196
  if (progress) {
@@ -7223,78 +7199,21 @@ Please decide how to handle this error and continue.`;
7223
7199
  }
7224
7200
  }
7225
7201
  this.emitToolResult(call.name, result2.success, outputText, result2.error, Date.now() - toolStartTime);
7202
+ this.scanForFlags(outputText);
7226
7203
  return { toolCallId: call.id, output: outputText, error: result2.error };
7227
7204
  } catch (error) {
7228
7205
  const errorMsg = String(error);
7229
- const enrichedError = this.enrichToolErrorContext({ toolName: call.name, input: call.input, error: errorMsg, originalOutput: "", progress });
7206
+ const enrichedError = this.enrichToolError({ toolName: call.name, input: call.input, error: errorMsg, originalOutput: "", progress });
7230
7207
  if (progress) progress.toolErrors++;
7231
7208
  this.emitToolResult(call.name, false, enrichedError, errorMsg, Date.now() - toolStartTime);
7232
7209
  return { toolCallId: call.id, output: enrichedError, error: errorMsg };
7233
7210
  }
7234
7211
  }
7235
7212
  /**
7236
- * Enrich tool error messages with actionable context so the LLM can self-correct.
7237
- * Instead of just showing the raw error, we add hints about what went wrong
7238
- * and what the agent can do to fix it.
7213
+ * Enrich tool error delegates to extracted module (§3-1)
7239
7214
  */
7240
- enrichToolErrorContext(ctx) {
7241
- const { toolName, input, error, originalOutput, progress } = ctx;
7242
- const lines = [];
7243
- if (originalOutput) {
7244
- lines.push(originalOutput);
7245
- }
7246
- lines.push("");
7247
- lines.push(`[TOOL ERROR ANALYSIS]`);
7248
- lines.push(`Tool: ${toolName}`);
7249
- lines.push(`Error: ${error}`);
7250
- const errorLower = error.toLowerCase();
7251
- if (errorLower.includes("command blocked") || errorLower.includes("injection pattern") || errorLower.includes("not in the allowed") || errorLower.includes("not in allowed paths")) {
7252
- if (progress) {
7253
- const patternKey = `${toolName}:${errorLower.slice(0, AGENT_LIMITS.BLOCKED_PATTERN_KEY_SLICE)}`;
7254
- const count = (progress.blockedCommandPatterns.get(patternKey) || 0) + 1;
7255
- progress.blockedCommandPatterns.set(patternKey, count);
7256
- progress.totalBlockedCommands++;
7257
- if (count >= AGENT_LIMITS.MAX_BLOCKED_BEFORE_WARN) {
7258
- lines.push(`
7259
- \u26A0\uFE0F REPEAT BLOCK DETECTED (${count}x same pattern). STOP retrying this approach.`);
7260
- lines.push(`You have been blocked ${progress.totalBlockedCommands} times total this session.`);
7261
- }
7262
- }
7263
- if (errorLower.includes("pipe target") || errorLower.includes("injection")) {
7264
- lines.push(`Fix: Use the tool's built-in output options instead of shell pipes.`);
7265
- lines.push(`Alternative approaches:`);
7266
- lines.push(` 1. Save output to file: command > /tmp/output.txt, then use read_file("/tmp/output.txt")`);
7267
- lines.push(` 2. Use tool-specific flags: nmap -oN /tmp/scan.txt, curl -o /tmp/page.html`);
7268
- lines.push(` 3. Use browse_url for web content instead of curl | grep`);
7269
- lines.push(` 4. If filtering output: run the command first, then read_file + parse results`);
7270
- } else if (errorLower.includes("redirect")) {
7271
- lines.push(`Fix: Redirect to /tmp/ or /root/ paths only.`);
7272
- lines.push(`Example: command > /tmp/output.txt or command 2>&1 > /tmp/errors.txt`);
7273
- } else {
7274
- lines.push(`Fix: Simplify the command. Avoid chaining complex operations.`);
7275
- lines.push(`Break into multiple steps: run_cmd \u2192 read_file \u2192 run_cmd.`);
7276
- }
7277
- } else if (errorLower.includes("missing") && errorLower.includes("parameter") || errorLower.includes("required") && errorLower.includes("missing") || errorLower.includes("is required")) {
7278
- const providedParams = Object.keys(input).join(", ") || "none";
7279
- lines.push(`Provided parameters: ${providedParams}`);
7280
- lines.push(`Fix: Check the tool's required parameters and provide ALL required values.`);
7281
- lines.push(`Action: Retry this tool call with the missing parameter(s) added.`);
7282
- } else if (errorLower.includes("not found") || errorLower.includes("command not found") || errorLower.includes("no such file")) {
7283
- lines.push(`Fix: The tool or command may not be installed.`);
7284
- lines.push(`Actions: (1) Try an alternative tool, (2) Use web_search to find installation instructions, or (3) Use a different approach entirely.`);
7285
- } else if (errorLower.includes("permission denied") || errorLower.includes("access denied") || errorLower.includes("eacces")) {
7286
- lines.push(`Fix: Insufficient permissions for this operation.`);
7287
- lines.push(`Actions: (1) Try with sudo if appropriate, (2) Use a different approach, or (3) Check if the target path/resource is correct.`);
7288
- } else if (errorLower.includes("connection refused") || errorLower.includes("econnrefused") || errorLower.includes("connection reset") || errorLower.includes("timeout")) {
7289
- lines.push(`Fix: Cannot reach the target service.`);
7290
- lines.push(`Actions: (1) Verify the target host/port is correct, (2) Check if the service is running, (3) Try a different port or protocol.`);
7291
- } else if (errorLower.includes("invalid") || errorLower.includes("malformed") || errorLower.includes("syntax error")) {
7292
- lines.push(`Fix: The input format is incorrect.`);
7293
- lines.push(`Actions: (1) Check the input format and fix it, (2) Use web_search to find the correct syntax, or (3) Try a simpler input.`);
7294
- } else {
7295
- lines.push(`Actions: (1) Analyze the error and modify your approach, (2) Use web_search("${toolName} ${errorLower.slice(0, AGENT_LIMITS.ERROR_SEARCH_PREVIEW_SLICE)}") to research the error, (3) Try an alternative tool or method.`);
7296
- }
7297
- return lines.join("\n");
7215
+ enrichToolError(ctx) {
7216
+ return enrichToolErrorContext(ctx);
7298
7217
  }
7299
7218
  getToolSchemas() {
7300
7219
  if (!this.toolRegistry) {
@@ -7311,6 +7230,35 @@ Please decide how to handle this error and continue.`;
7311
7230
  }));
7312
7231
  }
7313
7232
  // ─────────────────────────────────────────────────────────────────
7233
+ // SUBSECTION: CTF Flag Detection
7234
+ // ─────────────────────────────────────────────────────────────────
7235
+ /**
7236
+ * Scan tool output for CTF flag patterns.
7237
+ * When a new flag is detected, store it in state and emit event.
7238
+ *
7239
+ * @remarks
7240
+ * WHY: Automatic flag detection eliminates manual flag hunting in CTF competitions.
7241
+ * Called after every tool execution — zero overhead when CTF mode is OFF.
7242
+ */
7243
+ scanForFlags(output) {
7244
+ if (!this.state.isCtfMode()) return;
7245
+ const flags = detectFlags(output);
7246
+ for (const flag of flags) {
7247
+ const isNew = this.state.addFlag(flag);
7248
+ if (isNew) {
7249
+ this.events.emit({
7250
+ type: EVENT_TYPES.FLAG_FOUND,
7251
+ timestamp: Date.now(),
7252
+ data: {
7253
+ flag,
7254
+ totalFlags: this.state.getFlags().length,
7255
+ phase: this.state.getPhase()
7256
+ }
7257
+ });
7258
+ }
7259
+ }
7260
+ }
7261
+ // ─────────────────────────────────────────────────────────────────
7314
7262
  // SUBSECTION: Abort Helpers
7315
7263
  // ─────────────────────────────────────────────────────────────────
7316
7264
  isAbortError(error) {
@@ -7327,13 +7275,14 @@ Please decide how to handle this error and continue.`;
7327
7275
  };
7328
7276
 
7329
7277
  // src/agents/prompt-builder.ts
7330
- import { readFileSync as readFileSync4, existsSync as existsSync6 } from "fs";
7278
+ import { readFileSync as readFileSync4, existsSync as existsSync6, readdirSync as readdirSync2 } from "fs";
7331
7279
  import { join as join9, dirname as dirname5 } from "path";
7332
7280
  import { fileURLToPath as fileURLToPath5 } from "url";
7333
7281
 
7334
7282
  // src/shared/constants/prompts.ts
7335
7283
  var PROMPT_PATHS = {
7336
7284
  BASE: "base.md",
7285
+ CTF_MODE: "ctf-mode.md",
7337
7286
  AGENT_FILES: {
7338
7287
  ORCHESTRATOR: "orchestrator.md",
7339
7288
  RECON: "recon.md",
@@ -7413,15 +7362,15 @@ var CORE_KNOWLEDGE_FILES = [
7413
7362
  // Detection avoidance (always relevant)
7414
7363
  ];
7415
7364
  var PHASE_TECHNIQUE_MAP = {
7416
- [PHASES.RECON]: ["network-svc", "shells"],
7417
- [PHASES.VULN_ANALYSIS]: ["injection", "network-svc", "file-attacks"],
7418
- [PHASES.EXPLOIT]: ["injection", "shells", "file-attacks", "network-svc"],
7419
- [PHASES.POST_EXPLOIT]: ["privesc", "lateral", "auth-access", "shells"],
7420
- [PHASES.PRIV_ESC]: ["privesc", "auth-access", "shells"],
7421
- [PHASES.LATERAL]: ["lateral", "ad-attack", "auth-access"],
7365
+ [PHASES.RECON]: ["network-svc", "shells", "crypto"],
7366
+ [PHASES.VULN_ANALYSIS]: ["injection", "network-svc", "file-attacks", "crypto", "reversing"],
7367
+ [PHASES.EXPLOIT]: ["injection", "shells", "file-attacks", "network-svc", "pwn", "container-escape", "reversing"],
7368
+ [PHASES.POST_EXPLOIT]: ["privesc", "lateral", "auth-access", "shells", "container-escape", "forensics"],
7369
+ [PHASES.PRIV_ESC]: ["privesc", "auth-access", "shells", "pwn", "container-escape"],
7370
+ [PHASES.LATERAL]: ["lateral", "ad-attack", "auth-access", "container-escape"],
7422
7371
  [PHASES.PERSISTENCE]: ["shells", "privesc"],
7423
- [PHASES.EXFIL]: ["lateral", "network-svc"],
7424
- [PHASES.WEB]: ["injection", "file-attacks", "auth-access"],
7372
+ [PHASES.EXFIL]: ["lateral", "network-svc", "forensics"],
7373
+ [PHASES.WEB]: ["injection", "file-attacks", "auth-access", "crypto"],
7425
7374
  [PHASES.REPORT]: []
7426
7375
  // Report phase needs no attack techniques
7427
7376
  };
@@ -7446,6 +7395,7 @@ var PromptBuilder = class {
7446
7395
  build(userInput, phase) {
7447
7396
  const fragments = [
7448
7397
  this.loadPromptFile(PROMPT_PATHS.BASE),
7398
+ this.loadCtfModePrompt(),
7449
7399
  this.loadPhasePrompt(phase),
7450
7400
  this.loadCoreKnowledge(phase),
7451
7401
  this.loadPhaseRelevantTechniques(phase),
@@ -7456,6 +7406,17 @@ var PromptBuilder = class {
7456
7406
  ];
7457
7407
  return fragments.filter((f) => !!f).join("\n\n");
7458
7408
  }
7409
+ /**
7410
+ * Load CTF mode prompt when CTF mode is active.
7411
+ * Adds ~3K tokens of CTF-specific strategy and flag hunting protocol.
7412
+ */
7413
+ loadCtfModePrompt() {
7414
+ if (!this.state.isCtfMode()) return "";
7415
+ const content = this.loadPromptFile(PROMPT_PATHS.CTF_MODE);
7416
+ return content ? `<ctf-mode active="true">
7417
+ ${content}
7418
+ </ctf-mode>` : "";
7419
+ }
7459
7420
  /**
7460
7421
  * Load a prompt file from src/agents/prompts/
7461
7422
  */
@@ -7496,19 +7457,22 @@ ${content}
7496
7457
  return fragments.join("\n\n");
7497
7458
  }
7498
7459
  /**
7499
- * Load only technique files relevant to the current phase.
7500
- * Phase→technique mapping defined in PHASE_TECHNIQUE_MAP.
7460
+ * Load technique files relevant to the current phase.
7461
+ *
7462
+ * Loading strategy (Philosophy §11 — zero-code extension):
7463
+ * 1. PHASE_TECHNIQUE_MAP defines priority techniques per phase (loaded first)
7464
+ * 2. Any .md file in techniques/ NOT in the map is auto-discovered and loaded
7465
+ * as general reference — NO code change needed to add new techniques.
7501
7466
  *
7502
- * Saves ~15-20K tokens vs loading all 8 technique files.
7503
- * If a technique is needed but not mapped, the agent can always
7504
- * use web_search to look up specific attack techniques on demand.
7467
+ * The map is an optimization (priority ordering), not a gate.
7468
+ * "마크다운 파일 하나를 폴더에 넣으면, PromptBuilder가 자동으로 발견하고 로드한다."
7505
7469
  */
7506
7470
  loadPhaseRelevantTechniques(phase) {
7507
7471
  if (!existsSync6(TECHNIQUES_DIR)) return "";
7508
- const relevantTechniques = PHASE_TECHNIQUE_MAP[phase] || [];
7509
- if (relevantTechniques.length === 0) return "";
7472
+ const priorityTechniques = PHASE_TECHNIQUE_MAP[phase] || [];
7473
+ const loadedSet = /* @__PURE__ */ new Set();
7510
7474
  const fragments = [];
7511
- for (const technique of relevantTechniques) {
7475
+ for (const technique of priorityTechniques) {
7512
7476
  const filePath = join9(TECHNIQUES_DIR, `${technique}.md`);
7513
7477
  try {
7514
7478
  if (!existsSync6(filePath)) continue;
@@ -7517,10 +7481,25 @@ ${content}
7517
7481
  fragments.push(`<technique-reference category="${technique}">
7518
7482
  ${content}
7519
7483
  </technique-reference>`);
7484
+ loadedSet.add(`${technique}.md`);
7520
7485
  }
7521
7486
  } catch {
7522
7487
  }
7523
7488
  }
7489
+ try {
7490
+ const allFiles = readdirSync2(TECHNIQUES_DIR).filter((f) => f.endsWith(".md") && f !== "README.md" && !loadedSet.has(f));
7491
+ for (const file of allFiles) {
7492
+ const filePath = join9(TECHNIQUES_DIR, file);
7493
+ const content = readFileSync4(filePath, PROMPT_CONFIG.ENCODING);
7494
+ if (content) {
7495
+ const category = file.replace(".md", "");
7496
+ fragments.push(`<technique-reference category="${category}">
7497
+ ${content}
7498
+ </technique-reference>`);
7499
+ }
7500
+ }
7501
+ } catch {
7502
+ }
7524
7503
  return fragments.join("\n\n");
7525
7504
  }
7526
7505
  getScopeFragment() {
@@ -7649,6 +7628,14 @@ var MainAgent = class extends CoreAgent {
7649
7628
  getEventEmitter() {
7650
7629
  return this.events;
7651
7630
  }
7631
+ toggleCtfMode() {
7632
+ const newState = !this.state.isCtfMode();
7633
+ this.state.setCtfMode(newState);
7634
+ return newState;
7635
+ }
7636
+ isCtfMode() {
7637
+ return this.state.isCtfMode();
7638
+ }
7652
7639
  setScope(allowed, exclusions = []) {
7653
7640
  this.state.setScope({
7654
7641
  allowedCidrs: allowed.filter((a) => a.includes("/")),
@@ -7741,30 +7728,6 @@ var formatInlineStatus = () => {
7741
7728
  // src/platform/tui/hooks/useAgentState.ts
7742
7729
  import { useState, useRef, useCallback } from "react";
7743
7730
 
7744
- // src/shared/constants/thought.ts
7745
- var THOUGHT_TYPE = {
7746
- THINKING: "thinking",
7747
- // LLM text streaming
7748
- REASONING: "reasoning",
7749
- // LLM extended thinking
7750
- PLANNING: "planning",
7751
- // Strategic planning
7752
- OBSERVATION: "observation",
7753
- // Observing results
7754
- HYPOTHESIS: "hypothesis",
7755
- // Forming hypothesis
7756
- REFLECTION: "reflection",
7757
- // Self-reflection
7758
- ACTION: "action",
7759
- // Taking action
7760
- RESULT: "result",
7761
- // Action result
7762
- STUCK: "stuck",
7763
- // Detected stuck state
7764
- BREAKTHROUGH: "breakthrough"
7765
- // Found breakthrough
7766
- };
7767
-
7768
7731
  // src/shared/constants/theme.ts
7769
7732
  var THEME = {
7770
7733
  // Backgrounds (deep dark with blue tint)
@@ -7912,18 +7875,6 @@ var ICONS = {
7912
7875
  pwned: "\u25C8"
7913
7876
  // Compromised
7914
7877
  };
7915
- var THOUGHT_LABELS = {
7916
- [THOUGHT_TYPE.THINKING]: "[think]",
7917
- [THOUGHT_TYPE.REASONING]: "[reason]",
7918
- [THOUGHT_TYPE.PLANNING]: "[plan]",
7919
- [THOUGHT_TYPE.OBSERVATION]: "[observe]",
7920
- [THOUGHT_TYPE.HYPOTHESIS]: "[hypothesis]",
7921
- [THOUGHT_TYPE.REFLECTION]: "[reflect]",
7922
- [THOUGHT_TYPE.ACTION]: "[action]",
7923
- [THOUGHT_TYPE.RESULT]: "[result]",
7924
- [THOUGHT_TYPE.STUCK]: "[stuck]",
7925
- [THOUGHT_TYPE.BREAKTHROUGH]: "[!]"
7926
- };
7927
7878
 
7928
7879
  // src/platform/tui/constants/display.ts
7929
7880
  var TUI_DISPLAY_LIMITS = {
@@ -7952,7 +7903,19 @@ var TUI_DISPLAY_LIMITS = {
7952
7903
  /** Timer update interval in ms */
7953
7904
  timerInterval: 1e3,
7954
7905
  /** Exit delay in ms */
7955
- exitDelay: 100
7906
+ exitDelay: 100,
7907
+ /** Purpose text max length before truncation */
7908
+ purposeMaxLength: 30,
7909
+ /** Purpose text truncated length */
7910
+ purposeTruncated: 27,
7911
+ /** Tool detail preview length in flow display */
7912
+ toolDetailPreview: 100,
7913
+ /** Observe detail preview length in flow display */
7914
+ observeDetailPreview: 80,
7915
+ /** Max flow nodes to display */
7916
+ maxFlowNodes: 50,
7917
+ /** Max stopped processes to show */
7918
+ maxStoppedProcesses: 3
7956
7919
  };
7957
7920
  var MESSAGE_STYLES = {
7958
7921
  colors: {
@@ -7983,6 +7946,7 @@ var HELP_TEXT = `
7983
7946
  /findings Show discovered vulnerabilities
7984
7947
  /assets List active background processes (listeners, shells)
7985
7948
  /logs <id> Show full logs for a specific asset
7949
+ /ctf Toggle CTF mode (default: ON)
7986
7950
  /auto Toggle auto-approve mode
7987
7951
  /clear Clear screen
7988
7952
  /exit Exit
@@ -7991,11 +7955,13 @@ var HELP_TEXT = `
7991
7955
  \u2022 Auto-install missing tools (apt/brew)
7992
7956
  \u2022 Transparent command execution
7993
7957
  \u2022 Interactive sudo password input
7958
+ \u2022 CTF mode: Auto flag detection & CTF-specific prompts
7994
7959
 
7995
7960
  \u2500\u2500 Examples \u2500\u2500
7996
7961
  /target 192.168.1.1
7997
7962
  /start "Full pentest on target"
7998
7963
  /findings
7964
+ /ctf (toggle CTF/standard mode)
7999
7965
  /auto (toggle approval mode)
8000
7966
  `;
8001
7967
 
@@ -8140,6 +8106,9 @@ var useAgentEvents = (agent, eventsRef, state) => {
8140
8106
  lastStepTokensRef.current = stepTokens;
8141
8107
  setCurrentTokens(tokenAccumRef.current + stepTokens);
8142
8108
  };
8109
+ const onFlagFound = (e) => {
8110
+ addMessage("system", `\u{1F3F4} FLAG FOUND: ${e.data.flag} (total: ${e.data.totalFlags})`);
8111
+ };
8143
8112
  setInputHandler((p) => {
8144
8113
  return new Promise((resolve) => {
8145
8114
  const isPassword = /password|passphrase/i.test(p);
@@ -8192,6 +8161,7 @@ var useAgentEvents = (agent, eventsRef, state) => {
8192
8161
  events.on(EVENT_TYPES.ERROR, onError);
8193
8162
  events.on(EVENT_TYPES.RETRY, onRetry);
8194
8163
  events.on(EVENT_TYPES.USAGE_UPDATE, onUsageUpdate);
8164
+ events.on(EVENT_TYPES.FLAG_FOUND, onFlagFound);
8195
8165
  events.on(EVENT_TYPES.STATE_CHANGE, updateStats);
8196
8166
  events.on(EVENT_TYPES.START, updateStats);
8197
8167
  updateStats();
@@ -8415,7 +8385,7 @@ function ProcessRow({ proc, compact }) {
8415
8385
  const duration = formatDuration2(proc.durationMs);
8416
8386
  const port = proc.listeningPort ? `:${proc.listeningPort}` : "";
8417
8387
  const purpose = proc.purpose || proc.description || "";
8418
- const truncatedPurpose = compact && purpose.length > 30 ? purpose.slice(0, 27) + "..." : purpose;
8388
+ const truncatedPurpose = compact && purpose.length > TUI_DISPLAY_LIMITS.purposeMaxLength ? purpose.slice(0, TUI_DISPLAY_LIMITS.purposeTruncated) + "..." : purpose;
8419
8389
  return /* @__PURE__ */ jsxs(Box, { children: [
8420
8390
  /* @__PURE__ */ jsx(StatusIndicator, { running: proc.running, exitCode: proc.exitCode }),
8421
8391
  /* @__PURE__ */ jsxs(Text, { color: THEME.text.muted, children: [
@@ -8472,10 +8442,10 @@ var InlineStatus = ({
8472
8442
  stopped.length,
8473
8443
  ")"
8474
8444
  ] }),
8475
- stopped.slice(0, 3).map((proc) => /* @__PURE__ */ jsx(ProcessRow, { proc, compact }, proc.id)),
8476
- stopped.length > 3 && /* @__PURE__ */ jsxs(Text, { color: THEME.text.muted, children: [
8445
+ stopped.slice(0, TUI_DISPLAY_LIMITS.maxStoppedProcesses).map((proc) => /* @__PURE__ */ jsx(ProcessRow, { proc, compact }, proc.id)),
8446
+ stopped.length > TUI_DISPLAY_LIMITS.maxStoppedProcesses && /* @__PURE__ */ jsxs(Text, { color: THEME.text.muted, children: [
8477
8447
  " ... and ",
8478
- stopped.length - 3,
8448
+ stopped.length - TUI_DISPLAY_LIMITS.maxStoppedProcesses,
8479
8449
  " more"
8480
8450
  ] })
8481
8451
  ] }),
@@ -8812,6 +8782,10 @@ var App = ({ autoApprove = false, target }) => {
8812
8782
  ${procData.stdout || "(no output)"}
8813
8783
  --- End Log ---`);
8814
8784
  break;
8785
+ case UI_COMMANDS.CTF:
8786
+ const ctfEnabled = agent.toggleCtfMode();
8787
+ addMessage("system", ctfEnabled ? "\u{1F3F4} CTF mode ON \u2014 flag detection active, CTF prompts loaded" : "\u{1F512} CTF mode OFF \u2014 standard pentest mode");
8788
+ break;
8815
8789
  case "auto":
8816
8790
  setAutoApproveMode((prev) => {
8817
8791
  const newVal = !prev;
@@ -8908,19 +8882,6 @@ ${procData.stdout || "(no output)"}
8908
8882
  };
8909
8883
  var app_default = App;
8910
8884
 
8911
- // src/shared/constants/_shared/signal.const.ts
8912
- var EXIT_CODE = {
8913
- SUCCESS: 0,
8914
- ERROR: 1,
8915
- // Unix convention: 128 + signal number
8916
- SIGINT: 130,
8917
- // 128 + 2
8918
- SIGTERM: 143,
8919
- // 128 + 15
8920
- SIGKILL: 137
8921
- // 128 + 9
8922
- };
8923
-
8924
8885
  // src/shared/constants/cli-defaults.const.ts
8925
8886
  var CLI_DEFAULT = {
8926
8887
  /** Default maximum steps for run command */
@@ -8991,8 +8952,8 @@ program.command("run <objective>").alias("r").description("Run a single objectiv
8991
8952
  const shutdown = async (exitCode = 0) => {
8992
8953
  process.exit(exitCode);
8993
8954
  };
8994
- process.on("SIGINT", () => shutdown(EXIT_CODE.SIGINT));
8995
- process.on("SIGTERM", () => shutdown(EXIT_CODE.SIGTERM));
8955
+ process.on("SIGINT", () => shutdown(EXIT_CODES.SIGINT));
8956
+ process.on("SIGTERM", () => shutdown(EXIT_CODES.SIGTERM));
8996
8957
  try {
8997
8958
  const result2 = await agent.execute(objective);
8998
8959
  console.log(chalk.hex(THEME.status.success)("\n[+] Assessment complete!\n"));
@@ -9024,8 +8985,8 @@ program.command("scan <target>").description("Quick scan a target").option("-s,
9024
8985
  const shutdown = async (exitCode = 0) => {
9025
8986
  process.exit(exitCode);
9026
8987
  };
9027
- process.on("SIGINT", () => shutdown(EXIT_CODE.SIGINT));
9028
- process.on("SIGTERM", () => shutdown(EXIT_CODE.SIGTERM));
8988
+ process.on("SIGINT", () => shutdown(EXIT_CODES.SIGINT));
8989
+ process.on("SIGTERM", () => shutdown(EXIT_CODES.SIGTERM));
9029
8990
  try {
9030
8991
  await agent.execute(`Perform a ${options.scanType} scan on ${target}${options.ports ? ` focusing on ports ${options.ports}` : ""}. Analyze the results and identify potential vulnerabilities.`);
9031
8992
  console.log(chalk.hex(THEME.status.success)("[+] Scan complete!"));