pentesting 0.53.0 → 0.54.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
@@ -12,7 +12,7 @@ import { Command } from "commander";
12
12
  import chalk from "chalk";
13
13
 
14
14
  // src/platform/tui/app.tsx
15
- import { useState as useState5, useCallback as useCallback4, useEffect as useEffect4, useRef as useRef5 } from "react";
15
+ import { useState as useState6, useCallback as useCallback4, useEffect as useEffect5, useRef as useRef6 } from "react";
16
16
  import { Box as Box6, useInput as useInput2, useApp, useStdout } from "ink";
17
17
 
18
18
  // src/platform/tui/hooks/useAgent.ts
@@ -33,6 +33,7 @@ var TOOL_TIMEOUTS = {
33
33
  /** Full web application scan */
34
34
  WEB_SCAN: 3e5
35
35
  };
36
+ var CURL_MAX_TIME_SEC = 600;
36
37
 
37
38
  // src/shared/constants/limits.ts
38
39
  var DISPLAY_LIMITS = {
@@ -188,8 +189,6 @@ var MEMORY_LIMITS = {
188
189
  TECHNIQUE_FAILURE_DECAY: 30,
189
190
  /** Auto-prune threshold: techniques below this confidence are discarded */
190
191
  TECHNIQUE_PRUNE_THRESHOLD: 10,
191
- /** @deprecated Superseded by fingerprint-based matching in extractFingerprint(). Kept for reference. */
192
- COMMAND_MATCH_WORDS: 3,
193
192
  /** Maximum unverified techniques to show in prompt */
194
193
  PROMPT_UNVERIFIED_TECHNIQUES: 10,
195
194
  /**
@@ -197,13 +196,7 @@ var MEMORY_LIMITS = {
197
196
  * WHY: journal.ts had this as a module-local constant (50).
198
197
  * Centralizing enables consistent rotation across journal JSON + turn MD files.
199
198
  */
200
- MAX_TURN_ENTRIES: 50,
201
- /**
202
- * Maximum raw output files kept in .pentesting/outputs/.
203
- * WHY: journal.ts had this as a module-local constant (30).
204
- * Output files are large raw dumps; the journal entries retain their summaries.
205
- */
206
- MAX_OUTPUT_FILES: 30
199
+ MAX_TURN_ENTRIES: 50
207
200
  };
208
201
 
209
202
  // src/shared/constants/patterns.ts
@@ -349,7 +342,7 @@ var ORPHAN_PROCESS_NAMES = [
349
342
 
350
343
  // src/shared/constants/agent.ts
351
344
  var APP_NAME = "Pentest AI";
352
- var APP_VERSION = "0.53.0";
345
+ var APP_VERSION = "0.54.0";
353
346
  var APP_DESCRIPTION = "Autonomous Penetration Testing AI Agent";
354
347
  var LLM_ROLES = {
355
348
  SYSTEM: "system",
@@ -789,6 +782,7 @@ var UI_COMMANDS = {
789
782
  LOGS: "logs",
790
783
  LOGS_SHORT: "l",
791
784
  CTF: "ctf",
785
+ TOR: "tor",
792
786
  AUTO: "auto",
793
787
  GRAPH: "graph",
794
788
  GRAPH_SHORT: "g",
@@ -1738,8 +1732,7 @@ var ENV_KEYS = {
1738
1732
  SEARCH_API_KEY: "SEARCH_API_KEY",
1739
1733
  SEARCH_API_URL: "SEARCH_API_URL",
1740
1734
  THINKING: "PENTEST_THINKING",
1741
- THINKING_BUDGET: "PENTEST_THINKING_BUDGET",
1742
- TOR: "PENTEST_TOR"
1735
+ THINKING_BUDGET: "PENTEST_THINKING_BUDGET"
1743
1736
  };
1744
1737
  var DEFAULT_SEARCH_API_URL = "https://api.search.brave.com/res/v1/web/search";
1745
1738
  var DEFAULT_MODEL = "glm-4.7";
@@ -1782,11 +1775,82 @@ var TOR_PROXY = {
1782
1775
  /** Flags for the wrapper command (-q = quiet, no banners) */
1783
1776
  WRAPPER_FLAGS: "-q"
1784
1777
  };
1778
+ var torEnabled = false;
1785
1779
  function isTorEnabled() {
1786
- return process.env[ENV_KEYS.TOR] === "true";
1780
+ return torEnabled;
1781
+ }
1782
+ function setTorEnabled(enabled) {
1783
+ torEnabled = enabled;
1784
+ }
1785
+ function checkTorLeakRisk(command) {
1786
+ if (!isTorEnabled()) return { safe: true };
1787
+ if (/\bping\b/.test(command)) {
1788
+ return {
1789
+ safe: false,
1790
+ reason: "ping uses ICMP \u2014 bypasses Tor, real IP exposed to target",
1791
+ suggestion: "Use TCP probe instead: curl --socks5-hostname 127.0.0.1:9050 -s --connect-timeout 5 http://<target>:<port>"
1792
+ };
1793
+ }
1794
+ if (/\btraceroute\b|\btracepath\b|\bmtr\b/.test(command)) {
1795
+ return {
1796
+ safe: false,
1797
+ reason: "traceroute/tracepath/mtr uses ICMP/UDP \u2014 bypasses Tor, real IP exposed",
1798
+ suggestion: "Skip traceroute when Tor is enabled (reveals all hops including your IP)"
1799
+ };
1800
+ }
1801
+ if (/\bdig\b/.test(command) || /\bnslookup\b/.test(command) || /(?:^|[;&|\s])host(?:\s|$)/.test(command)) {
1802
+ return {
1803
+ safe: false,
1804
+ reason: "dig/host/nslookup use UDP \u2014 DNS query bypasses Tor, real IP visible to DNS server",
1805
+ suggestion: `Use DNS-over-HTTPS: curl --socks5-hostname 127.0.0.1:9050 "https://dns.google/resolve?name=<host>&type=A"`
1806
+ };
1807
+ }
1808
+ if (/\bnmap\b/.test(command) && /\s-sU\b/.test(command)) {
1809
+ return {
1810
+ safe: false,
1811
+ reason: "nmap -sU (UDP scan) bypasses Tor \u2014 UDP is not routed through SOCKS5",
1812
+ suggestion: "Skip UDP scan with Tor ON. Use TCP scan: proxychains4 -q nmap -sT -Pn"
1813
+ };
1814
+ }
1815
+ return { safe: true };
1787
1816
  }
1788
1817
  function wrapCommandForTor(command) {
1789
1818
  if (!isTorEnabled()) return command;
1819
+ const SOCKS = `${TOR_PROXY.SOCKS_HOST}:${TOR_PROXY.SOCKS_PORT}`;
1820
+ if (/\bcurl\b/.test(command)) {
1821
+ if (/--socks5|--proxy\b|-x\s/.test(command)) return command;
1822
+ return command.replace(/\bcurl\b/, `curl --socks5-hostname ${SOCKS}`);
1823
+ }
1824
+ if (/\bwget\b/.test(command)) {
1825
+ if (/--execute.*proxy|http_proxy|https_proxy/i.test(command)) return command;
1826
+ return command.replace(
1827
+ /\bwget\b/,
1828
+ `wget -e use_proxy=yes -e http_proxy=socks5h://${SOCKS} -e https_proxy=socks5h://${SOCKS}`
1829
+ );
1830
+ }
1831
+ if (/\bsqlmap\b/.test(command)) {
1832
+ if (/--tor\b|--proxy\b/.test(command)) return command;
1833
+ return command.replace(
1834
+ /\bsqlmap\b/,
1835
+ `sqlmap --tor --tor-type=SOCKS5 --tor-port=${TOR_PROXY.SOCKS_PORT}`
1836
+ );
1837
+ }
1838
+ if (/\bgobuster\b/.test(command)) {
1839
+ if (/--proxy\b/.test(command)) return command;
1840
+ return command.replace(/\bgobuster\b/, `gobuster --proxy socks5://${SOCKS}`);
1841
+ }
1842
+ if (/\bffuf\b/.test(command)) {
1843
+ if (/-x\s/.test(command)) return command;
1844
+ return command.replace(/\bffuf\b/, `ffuf -x socks5://${SOCKS}`);
1845
+ }
1846
+ if (/\bnmap\b/.test(command)) {
1847
+ let nmapCmd = command;
1848
+ nmapCmd = nmapCmd.replace(/\s-s[SA]\b/g, " -sT");
1849
+ if (!/\s-Pn\b/.test(nmapCmd)) {
1850
+ nmapCmd = nmapCmd.replace(/\bnmap\b/, "nmap -Pn");
1851
+ }
1852
+ return `${TOR_PROXY.WRAPPER_CMD} ${TOR_PROXY.WRAPPER_FLAGS} ${nmapCmd}`;
1853
+ }
1790
1854
  return `${TOR_PROXY.WRAPPER_CMD} ${TOR_PROXY.WRAPPER_FLAGS} ${command}`;
1791
1855
  }
1792
1856
  function getTorBrowserArgs() {
@@ -1840,6 +1904,16 @@ async function runCommand(command, args = [], options = {}) {
1840
1904
  error: `Command blocked: ${validation.error}`
1841
1905
  };
1842
1906
  }
1907
+ const torLeak = checkTorLeakRisk(fullCommand);
1908
+ if (!torLeak.safe) {
1909
+ return {
1910
+ success: false,
1911
+ output: "",
1912
+ error: `\u{1F6D1} TOR IP LEAK BLOCKED
1913
+ Reason: ${torLeak.reason}
1914
+ Suggestion: ${torLeak.suggestion}`
1915
+ };
1916
+ }
1843
1917
  const timeout = options.timeout ?? TOOL_TIMEOUTS.DEFAULT_COMMAND;
1844
1918
  const maxRetries = options.maxRetries ?? AGENT_LIMITS.MAX_INSTALL_RETRIES;
1845
1919
  const toolName = command.split(/[\s/]/).pop() || command;
@@ -1877,16 +1951,22 @@ async function runCommand(command, args = [], options = {}) {
1877
1951
  }
1878
1952
  return lastResult;
1879
1953
  }
1954
+ function injectCurlMaxTime(command, timeoutSec) {
1955
+ if (!/\bcurl\b/.test(command)) return command;
1956
+ if (/--max-time\b|-m\s+\d/.test(command)) return command;
1957
+ return command.replace(/\bcurl\b/, `curl --max-time ${timeoutSec}`);
1958
+ }
1880
1959
  async function executeCommandOnce(command, options = {}) {
1881
1960
  return new Promise((resolve) => {
1882
1961
  const timeout = options.timeout ?? TOOL_TIMEOUTS.DEFAULT_COMMAND;
1962
+ const safeCommand = injectCurlMaxTime(command, CURL_MAX_TIME_SEC);
1883
1963
  globalEventEmitter?.({
1884
1964
  type: COMMAND_EVENT_TYPES.COMMAND_START,
1885
- message: `Executing: ${command.slice(0, DISPLAY_LIMITS.COMMAND_PREVIEW)}${command.length > DISPLAY_LIMITS.COMMAND_PREVIEW ? "..." : ""}`
1965
+ message: `Executing: ${safeCommand.slice(0, DISPLAY_LIMITS.COMMAND_PREVIEW)}${safeCommand.length > DISPLAY_LIMITS.COMMAND_PREVIEW ? "..." : ""}`
1886
1966
  });
1887
- const execCommand = wrapCommandForTor(command);
1967
+ const execCommand = wrapCommandForTor(safeCommand);
1888
1968
  const child = spawn2("sh", ["-c", execCommand], {
1889
- timeout,
1969
+ detached: true,
1890
1970
  env: { ...process.env, ...options.env },
1891
1971
  cwd: options.cwd
1892
1972
  });
@@ -1894,6 +1974,20 @@ async function executeCommandOnce(command, options = {}) {
1894
1974
  let stderr = "";
1895
1975
  let inputHandled = false;
1896
1976
  let processTerminated = false;
1977
+ let timedOut = false;
1978
+ const killTimer = setTimeout(() => {
1979
+ if (processTerminated) return;
1980
+ timedOut = true;
1981
+ processTerminated = true;
1982
+ try {
1983
+ process.kill(-child.pid, "SIGKILL");
1984
+ } catch {
1985
+ try {
1986
+ child.kill("SIGKILL");
1987
+ } catch {
1988
+ }
1989
+ }
1990
+ }, timeout);
1897
1991
  const checkForInputPrompt = async (data) => {
1898
1992
  if (inputHandled || !globalInputHandler || processTerminated) return;
1899
1993
  for (const pattern of INPUT_PROMPT_PATTERNS) {
@@ -1927,7 +2021,21 @@ async function executeCommandOnce(command, options = {}) {
1927
2021
  checkForInputPrompt(text);
1928
2022
  });
1929
2023
  child.on("close", (code) => {
2024
+ clearTimeout(killTimer);
1930
2025
  processTerminated = true;
2026
+ if (timedOut) {
2027
+ globalEventEmitter?.({
2028
+ type: COMMAND_EVENT_TYPES.COMMAND_FAILED,
2029
+ message: `Command timed out after ${Math.round(timeout / 1e3)}s`
2030
+ });
2031
+ resolve({
2032
+ success: false,
2033
+ output: stdout.trim(),
2034
+ error: `Command timed out after ${Math.round(timeout / 1e3)}s. Output so far:
2035
+ ${(stdout + stderr).trim().slice(-500)}`
2036
+ });
2037
+ return;
2038
+ }
1931
2039
  if (code === 0) {
1932
2040
  globalEventEmitter?.({
1933
2041
  type: COMMAND_EVENT_TYPES.COMMAND_SUCCESS,
@@ -1951,6 +2059,7 @@ async function executeCommandOnce(command, options = {}) {
1951
2059
  }
1952
2060
  });
1953
2061
  child.on("error", (err) => {
2062
+ clearTimeout(killTimer);
1954
2063
  processTerminated = true;
1955
2064
  globalEventEmitter?.({
1956
2065
  type: COMMAND_EVENT_TYPES.COMMAND_ERROR,
@@ -2202,6 +2311,16 @@ function startBackgroundProcess(command, options = {}) {
2202
2311
  const stderrFile = createTempFile(FILE_EXTENSIONS.STDERR);
2203
2312
  const stdinFile = createTempFile(FILE_EXTENSIONS.STDIN);
2204
2313
  const { tags, port, role, isInteractive } = detectProcessRole(command);
2314
+ const torLeak = checkTorLeakRisk(command);
2315
+ if (!torLeak.safe) {
2316
+ for (const f of [stdoutFile, stderrFile, stdinFile]) {
2317
+ try {
2318
+ unlinkSync2(f);
2319
+ } catch {
2320
+ }
2321
+ }
2322
+ throw new Error(`\u{1F6D1} TOR IP LEAK BLOCKED \u2014 ${torLeak.reason}. ${torLeak.suggestion}`);
2323
+ }
2205
2324
  const torCommand = wrapCommandForTor(command);
2206
2325
  let wrappedCmd;
2207
2326
  if (isInteractive) {
@@ -2644,7 +2763,7 @@ var StateSerializer = class {
2644
2763
  lines.push(resourceInfo);
2645
2764
  }
2646
2765
  if (state.isCtfMode()) {
2647
- lines.push(`Mode: CTF \u{1F3F4}`);
2766
+ lines.push(`Flag Detection: ON \u{1F3F4}`);
2648
2767
  const flags = state.getFlags();
2649
2768
  if (flags.length > 0) {
2650
2769
  lines.push(`Flags Found (${flags.length}):`);
@@ -2653,6 +2772,16 @@ var StateSerializer = class {
2653
2772
  }
2654
2773
  }
2655
2774
  }
2775
+ if (isTorEnabled()) {
2776
+ lines.push(
2777
+ `Tor Proxy: ON \u{1F9C5}
2778
+ Standard tools auto-proxied (curl, wget, nmap, sqlmap, gobuster, ffuf, hydra, etc.)
2779
+ Custom scripts: route ALL target connections through SOCKS5 127.0.0.1:9050.
2780
+ BLOCKED (leak real IP): ping, traceroute, dig, nslookup, nmap -sU`
2781
+ );
2782
+ } else {
2783
+ lines.push(`Tor Proxy: OFF \u2014 direct connections.`);
2784
+ }
2656
2785
  lines.push(`Phase: ${state.getPhase()}`);
2657
2786
  return lines.join("\n");
2658
2787
  }
@@ -2815,10 +2944,6 @@ var AttackGraph = class {
2815
2944
  }
2816
2945
  this.failedPaths.push(`${fromId} \u2192 ${toId}${reason ? ` (${reason})` : ""}`);
2817
2946
  }
2818
- // Backward-compatible alias
2819
- markExploited(nodeId) {
2820
- this.markSucceeded(nodeId);
2821
- }
2822
2947
  // ─── Domain-Specific Registration ───────────────────────────
2823
2948
  /**
2824
2949
  * Record a host discovery.
@@ -2877,7 +3002,7 @@ var AttackGraph = class {
2877
3002
  hasExploit
2878
3003
  });
2879
3004
  for (const [id, node] of this.nodes) {
2880
- if (node.type === "service" && node.label.includes(target)) {
3005
+ if (node.type === NODE_TYPE.SERVICE && node.label.includes(target)) {
2881
3006
  this.addEdge(id, vulnId, "has_vulnerability", 0.8);
2882
3007
  }
2883
3008
  }
@@ -2919,7 +3044,7 @@ var AttackGraph = class {
2919
3044
  ...data
2920
3045
  });
2921
3046
  for (const [id, node] of this.nodes) {
2922
- if (node.type === "host" || node.type === "service") {
3047
+ if (node.type === NODE_TYPE.HOST || node.type === NODE_TYPE.SERVICE) {
2923
3048
  const hostIp = String(node.data.ip || node.data.host || "");
2924
3049
  const hostname = String(node.data.hostname || "");
2925
3050
  if (hostIp && detail.includes(hostIp) || hostname && detail.includes(hostname)) {
@@ -3339,23 +3464,6 @@ var WorkingMemory = class {
3339
3464
  (e) => e.category === "failure" && e.fingerprint != null && fingerprintsMatch(e.fingerprint, fp)
3340
3465
  );
3341
3466
  }
3342
- /**
3343
- * Get all previous attempts (success & failure) for a specific tool+target vector.
3344
- * Returns the full history so the LLM can see what parameter combinations were tried.
3345
- *
3346
- * TODO: Wire up in tools.ts to pass vector history to strategist for smarter retries.
3347
- */
3348
- getAttemptsForVector(tool, target) {
3349
- const lowerTool = tool.toLowerCase();
3350
- return this.entries.filter((e) => {
3351
- if (e.fingerprint) {
3352
- const matchTool = e.fingerprint.tool === lowerTool;
3353
- const matchTarget = !target || e.fingerprint.target.includes(target);
3354
- return matchTool && matchTarget;
3355
- }
3356
- return String(e.context.tool || "").toLowerCase() === lowerTool;
3357
- });
3358
- }
3359
3467
  /** Internal prune helper (used by both add() and recordFailure()) */
3360
3468
  pruneIfNeeded() {
3361
3469
  if (this.entries.length > this.maxEntries) {
@@ -3383,26 +3491,6 @@ var WorkingMemory = class {
3383
3491
  }
3384
3492
  return count;
3385
3493
  }
3386
- /**
3387
- * Get consecutive failures for a specific vector (tool+target).
3388
- * Returns count of sequential failures where all attempts used the same tool+target.
3389
- *
3390
- * TODO: Wire up in tools.ts for per-vector threshold checks alongside global consecutive count.
3391
- */
3392
- getConsecutiveVectorFailures(tool, target) {
3393
- const lowerTool = tool.toLowerCase();
3394
- let count = 0;
3395
- for (let i = this.entries.length - 1; i >= 0; i--) {
3396
- const e = this.entries[i];
3397
- if (e.category !== "failure") break;
3398
- if (e.fingerprint && e.fingerprint.tool === lowerTool && (!target || e.fingerprint.target.includes(target))) {
3399
- count++;
3400
- } else {
3401
- break;
3402
- }
3403
- }
3404
- return count;
3405
- }
3406
3494
  /**
3407
3495
  * Format for prompt injection.
3408
3496
  */
@@ -3842,7 +3930,7 @@ var SharedState = class {
3842
3930
  missionSummary: "",
3843
3931
  missionChecklist: [],
3844
3932
  ctfMode: true,
3845
- // CTF mode ON by default
3933
+ // Flag auto-detection ON by default
3846
3934
  flags: [],
3847
3935
  startedAt: now,
3848
3936
  // Auto-configure from PENTEST_DURATION env (seconds).
@@ -4018,7 +4106,7 @@ var SharedState = class {
4018
4106
  getPhase() {
4019
4107
  return this.data.currentPhase;
4020
4108
  }
4021
- // --- CTF Mode ---
4109
+ // --- Flag Detection ---
4022
4110
  setCtfMode(shouldEnable) {
4023
4111
  this.data.ctfMode = shouldEnable;
4024
4112
  }
@@ -5240,10 +5328,27 @@ All ports freed. All children killed.`
5240
5328
  },
5241
5329
  {
5242
5330
  name: TOOL_NAMES.WRITE_FILE,
5243
- description: "Write content to file (creates parent directories if needed)",
5331
+ description: `Write content to a file. Creates parent directories automatically.
5332
+
5333
+ Primary use: Write custom exploit scripts, fuzzers, scanners, payloads.
5334
+
5335
+ Workflow:
5336
+ 1. write_file({ path: "/tmp/exploit.py", content: "..." })
5337
+ 2. run_cmd({ command: "python3 /tmp/exploit.py" })
5338
+ 3. read_file to review output
5339
+
5340
+ \u{1F9C5} TOR ON \u2014 MANDATORY: Any script that opens a network connection to the target
5341
+ MUST route through Tor SOCKS5 (127.0.0.1:9050). No exceptions.
5342
+ You are responsible for using the correct proxy method for the language you choose.
5343
+ Proxy patterns are in <current-state>. Ignoring this leaks your real IP.
5344
+
5345
+ \u26A1 TOR OFF: Write scripts normally. Direct connections are fine.
5346
+
5347
+ Standard tools (curl/wget/nmap/sqlmap/hydra etc.) via run_cmd are
5348
+ auto-proxied by the system \u2014 no extra work needed for those.`,
5244
5349
  parameters: {
5245
- path: { type: "string", description: "Absolute path to the file" },
5246
- content: { type: "string", description: "File content" }
5350
+ path: { type: "string", description: "Absolute file path (e.g., /tmp/exploit.py)" },
5351
+ content: { type: "string", description: "File content to write" }
5247
5352
  },
5248
5353
  required: ["path", "content"],
5249
5354
  execute: async (params) => writeFileContent(params.path, params.content)
@@ -7544,16 +7649,20 @@ Common wordlists are automatically searched in /usr/share/wordlists (rockyou.txt
7544
7649
  if (wordlist === "rockyou") wordlistPath = WORDLISTS.ROCKYOU;
7545
7650
  const cmd = `hashcat -m ${format || HASHCAT_MODES.MD5} "${hashes}" "${wordlistPath}" --force`;
7546
7651
  if (background) {
7547
- const proc = startBackgroundProcess(cmd, {
7548
- description: `Cracking hashes: ${hashes.slice(0, DISPLAY_LIMITS.HASH_PREVIEW_LENGTH)}...`,
7549
- purpose: `Attempting to crack ${format || "unknown"} hashes using ${wordlist}`
7550
- });
7551
- return {
7552
- success: true,
7553
- output: `Hash cracking started in background (ID: ${proc.id}).
7652
+ try {
7653
+ const proc = startBackgroundProcess(cmd, {
7654
+ description: `Cracking hashes: ${hashes.slice(0, DISPLAY_LIMITS.HASH_PREVIEW_LENGTH)}...`,
7655
+ purpose: `Attempting to crack ${format || "unknown"} hashes using ${wordlist}`
7656
+ });
7657
+ return {
7658
+ success: true,
7659
+ output: `Hash cracking started in background (ID: ${proc.id}).
7554
7660
  Command: ${cmd}
7555
7661
  Check status with: bg_process({ action: "status", process_id: "${proc.id}" })`
7556
- };
7662
+ };
7663
+ } catch (err) {
7664
+ return { success: false, output: "", error: `Failed to start: ${err}` };
7665
+ }
7557
7666
  } else {
7558
7667
  return runCommand(cmd);
7559
7668
  }
@@ -10761,7 +10870,7 @@ var ToolExecutor = class _ToolExecutor {
10761
10870
  }
10762
10871
  }
10763
10872
  // ─────────────────────────────────────────────────────────────────
10764
- // SUBSECTION: CTF Flag Detection
10873
+ // SUBSECTION: Flag Detection
10765
10874
  // ─────────────────────────────────────────────────────────────────
10766
10875
  scanForFlags(output) {
10767
10876
  if (!this.state.isCtfMode()) return;
@@ -11099,12 +11208,12 @@ Phase: ${phase} | Targets: ${targets} | Findings: ${findings}
11099
11208
 
11100
11209
  ${direction}
11101
11210
 
11102
- ESCALATION:
11103
- 1. web_search for techniques
11104
- 2. Try alternative approaches
11105
- 3. Probe for unknown vulns
11106
- 4. Brute-force with wordlists
11107
- 5. ask_user for hints
11211
+ PICK ANY \u2014 do whatever fits best (no order, all are valid):
11212
+ \u2022 Brute-force with wordlists (hydra/hashcat/ffuf + rockyou/seclists)
11213
+ \u2022 web_search for techniques
11214
+ \u2022 Try a completely different approach
11215
+ \u2022 Probe for unknown vulns
11216
+ \u2022 ask_user for hints
11108
11217
 
11109
11218
  ACT NOW \u2014 EXECUTE.`;
11110
11219
  }
@@ -11211,7 +11320,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
11211
11320
  // src/shared/constants/prompts.ts
11212
11321
  var PROMPT_PATHS = {
11213
11322
  BASE: "base.md",
11214
- CTF_MODE: "ctf-mode.md",
11323
+ OFFENSIVE_PLAYBOOK: "offensive-playbook.md",
11215
11324
  AGENT_FILES: {
11216
11325
  ORCHESTRATOR: "orchestrator.md",
11217
11326
  RECON: "recon.md",
@@ -11689,9 +11798,9 @@ var PHASE_PROMPT_MAP = {
11689
11798
  };
11690
11799
  var CORE_KNOWLEDGE_FILES = [
11691
11800
  AGENT_FILES.STRATEGY,
11692
- // Attack prioritization, first-turn protocol, upgrade loop
11801
+ // Attack prioritization, first-turn protocol, upgrade loop (~2K tok)
11693
11802
  AGENT_FILES.ORCHESTRATOR,
11694
- // Phase transitions, multi-target management
11803
+ // Kill chain position, phase transitions, multi-target (~2K tok)
11695
11804
  AGENT_FILES.EVASION,
11696
11805
  // Detection avoidance (always relevant)
11697
11806
  AGENT_FILES.ZERO_DAY,
@@ -11731,9 +11840,10 @@ var PromptBuilder = class {
11731
11840
  * Build complete prompt for LLM iteration (async).
11732
11841
  *
11733
11842
  * Layers (phase-aware, enhanced with D-CIPHER meta-prompting):
11734
- * 1. base.md — Core identity, rules, autonomy, shell management (~7.6K tok)
11843
+ * 1. base.md — Core identity, rules, OODA protocol, anti-hallucination (~2.7K tok)
11844
+ * 1b. Offensive playbook — attack methodology, time management, aggression rules (~2K tok)
11735
11845
  * 2. Phase-specific prompt — current phase's full specialist knowledge (~2K tok)
11736
- * 3. Core methodology — strategy, orchestrator, evasion (always loaded, ~12K tok)
11846
+ * 3. Core methodology — strategy, orchestrator, evasion (always loaded, ~8K tok)
11737
11847
  * 4. Phase-relevant techniques — only attack techniques for current phase (~4-8K tok)
11738
11848
  * 5. Scope constraints
11739
11849
  * 6. Current state (targets, findings, loot, active processes)
@@ -11753,7 +11863,7 @@ var PromptBuilder = class {
11753
11863
  async build(userInput, phase) {
11754
11864
  const fragments = [
11755
11865
  this.loadPromptFile(PROMPT_PATHS.BASE),
11756
- this.loadCtfModePrompt(),
11866
+ this.loadOffensivePlaybook(),
11757
11867
  this.loadPhasePrompt(phase),
11758
11868
  this.loadCoreKnowledge(phase),
11759
11869
  this.loadPhaseRelevantTechniques(phase),
@@ -11786,15 +11896,14 @@ var PromptBuilder = class {
11786
11896
  return fragments.filter((f) => !!f).join("\n\n");
11787
11897
  }
11788
11898
  /**
11789
- * Load CTF mode prompt when CTF mode is active.
11790
- * Adds ~3K tokens of CTF-specific strategy and flag hunting protocol.
11899
+ * Load offensive playbook attack methodology always active.
11900
+ * Adds ~3K tokens of time management, technique quick-starts, aggression rules.
11791
11901
  */
11792
- loadCtfModePrompt() {
11793
- if (!this.state.isCtfMode()) return "";
11794
- const content = this.loadPromptFile(PROMPT_PATHS.CTF_MODE);
11795
- return content ? `<ctf-mode active="true">
11902
+ loadOffensivePlaybook() {
11903
+ const content = this.loadPromptFile(PROMPT_PATHS.OFFENSIVE_PLAYBOOK);
11904
+ return content ? `<offensive-playbook>
11796
11905
  ${content}
11797
- </ctf-mode>` : "";
11906
+ </offensive-playbook>` : "";
11798
11907
  }
11799
11908
  /**
11800
11909
  * Load a prompt file from src/agents/prompts/
@@ -13017,7 +13126,8 @@ var COMMAND_DEFINITIONS = [
13017
13126
  { name: "paths", alias: "p", description: "Show ranked attack paths" },
13018
13127
  { name: "assets", alias: "a", description: "List background processes" },
13019
13128
  { name: "logs", alias: "l", args: "<id>", description: "Show logs for an asset" },
13020
- { name: "ctf", description: "Toggle CTF mode" },
13129
+ { name: "ctf", description: "Toggle auto flag detection" },
13130
+ { name: "tor", description: "Toggle Tor proxy routing" },
13021
13131
  { name: "auto", description: "Toggle auto-approve mode" },
13022
13132
  { name: "clear", alias: "c", description: "Reset session & clean workspace" },
13023
13133
  { name: "help", alias: "h", description: "Show detailed help" },
@@ -13042,7 +13152,8 @@ ${COMMAND_DEFINITIONS.map((cmd) => {
13042
13152
  \u2022 Auto-install missing tools (apt/brew)
13043
13153
  \u2022 Transparent command execution
13044
13154
  \u2022 Interactive sudo password input
13045
- \u2022 CTF mode: Auto flag detection & CTF-specific prompts
13155
+ \u2022 Flag detection: Auto-detect flags/proofs in tool output (/ctf to toggle)
13156
+ \u2022 Tor proxy: Route all traffic through Tor SOCKS proxy (/tor to toggle, default OFF)
13046
13157
  \u2022 Attack graph: Tracks discovered paths & prevents repeating failures
13047
13158
 
13048
13159
  \u2500\u2500 Tips \u2500\u2500
@@ -13068,6 +13179,7 @@ var useAgentState = () => {
13068
13179
  const retryCountRef = useRef(0);
13069
13180
  const tokenAccumRef = useRef(0);
13070
13181
  const lastStepTokensRef = useRef(0);
13182
+ const toolStartedAtRef = useRef(0);
13071
13183
  const addMessage = useCallback((type, content) => {
13072
13184
  const id = Math.random().toString(36).substring(7);
13073
13185
  setMessages((prev) => [...prev, { id, type, content, timestamp: /* @__PURE__ */ new Date() }]);
@@ -13130,7 +13242,8 @@ var useAgentState = () => {
13130
13242
  addMessage,
13131
13243
  resetCumulativeCounters,
13132
13244
  manageTimer,
13133
- clearAllTimers
13245
+ clearAllTimers,
13246
+ toolStartedAtRef
13134
13247
  };
13135
13248
  };
13136
13249
 
@@ -13149,19 +13262,22 @@ var useAgentEvents = (agent, eventsRef, state) => {
13149
13262
  retryCountRef,
13150
13263
  tokenAccumRef,
13151
13264
  lastStepTokensRef,
13152
- clearAllTimers
13265
+ clearAllTimers,
13266
+ toolStartedAtRef
13153
13267
  } = state;
13154
13268
  const reasoningBufferRef = useRef2("");
13155
13269
  useEffect(() => {
13156
13270
  const events = eventsRef.current;
13157
13271
  const onToolCall = (e) => {
13158
13272
  if (NOISE_CLASSIFICATION.LOW_VISIBILITY.includes(e.data.toolName)) return;
13273
+ toolStartedAtRef.current = Date.now();
13159
13274
  setCurrentStatus(`${e.data.toolName}\u2026`);
13160
13275
  const inputStr = formatToolInput(e.data.toolName, e.data.input);
13161
13276
  const label = inputStr ? `${toDisplayName(e.data.toolName)}(${inputStr})` : `${toDisplayName(e.data.toolName)}`;
13162
13277
  addMessage("tool", label);
13163
13278
  };
13164
13279
  const onToolResult = (e) => {
13280
+ toolStartedAtRef.current = 0;
13165
13281
  if (NOISE_CLASSIFICATION.LOW_VISIBILITY.includes(e.data.toolName)) {
13166
13282
  return;
13167
13283
  }
@@ -13320,6 +13436,7 @@ ${firstLine}`);
13320
13436
  tokenAccumRef,
13321
13437
  lastStepTokensRef,
13322
13438
  clearAllTimers,
13439
+ toolStartedAtRef,
13323
13440
  eventsRef
13324
13441
  ]);
13325
13442
  };
@@ -13759,7 +13876,7 @@ var MessageList = memo(({ messages }) => {
13759
13876
  });
13760
13877
 
13761
13878
  // src/platform/tui/components/StatusDisplay.tsx
13762
- import { memo as memo3 } from "react";
13879
+ import { memo as memo3, useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
13763
13880
  import { Box as Box3, Text as Text4 } from "ink";
13764
13881
 
13765
13882
  // src/platform/tui/components/MusicSpinner.tsx
@@ -13785,26 +13902,50 @@ var StatusDisplay = memo3(({
13785
13902
  retryState,
13786
13903
  isProcessing,
13787
13904
  currentStatus,
13788
- elapsedTime,
13789
13905
  currentTokens
13790
13906
  }) => {
13791
13907
  const truncateError = (err) => {
13792
13908
  return err.length > DISPLAY_LIMITS.RETRY_ERROR_PREVIEW ? err.substring(0, DISPLAY_LIMITS.RETRY_ERROR_TRUNCATED) + "..." : err;
13793
13909
  };
13794
- const meta = formatMeta(elapsedTime * 1e3, currentTokens);
13910
+ const [statusElapsed, setStatusElapsed] = useState4(0);
13911
+ const statusTimerRef = useRef4(null);
13912
+ const statusStartRef = useRef4(Date.now());
13913
+ useEffect4(() => {
13914
+ if (statusTimerRef.current) clearInterval(statusTimerRef.current);
13915
+ if (isProcessing && currentStatus) {
13916
+ statusStartRef.current = Date.now();
13917
+ setStatusElapsed(0);
13918
+ statusTimerRef.current = setInterval(() => {
13919
+ setStatusElapsed(Math.floor((Date.now() - statusStartRef.current) / 1e3));
13920
+ }, 1e3);
13921
+ } else {
13922
+ setStatusElapsed(0);
13923
+ statusTimerRef.current = null;
13924
+ }
13925
+ return () => {
13926
+ if (statusTimerRef.current) clearInterval(statusTimerRef.current);
13927
+ };
13928
+ }, [currentStatus, isProcessing]);
13929
+ const buildMeta = () => {
13930
+ const parts = [];
13931
+ if (statusElapsed > 0) parts.push(formatDuration2(statusElapsed * 1e3));
13932
+ if (currentTokens > 0) parts.push(`\u2191 ${formatTokens(currentTokens)} tokens`);
13933
+ return parts.length > 0 ? `(${parts.join(" \xB7 ")})` : "";
13934
+ };
13935
+ const meta = buildMeta();
13795
13936
  const isThinkingStatus = currentStatus.startsWith("Reasoning");
13796
13937
  const statusLines = currentStatus ? currentStatus.split("\n").filter(Boolean) : [];
13797
13938
  const statusMain = statusLines[0] || "Processing...";
13798
13939
  if (retryState.status === "retrying") {
13799
13940
  return /* @__PURE__ */ jsxs3(Box3, { children: [
13800
- /* @__PURE__ */ jsx4(Text4, { color: THEME.yellow, children: /* @__PURE__ */ jsx4(MusicSpinner, { color: THEME.yellow }) }),
13801
- /* @__PURE__ */ jsxs3(Text4, { color: THEME.yellow, bold: true, children: [
13941
+ /* @__PURE__ */ jsx4(Text4, { color: THEME.yellow, wrap: "truncate", children: /* @__PURE__ */ jsx4(MusicSpinner, { color: THEME.yellow }) }),
13942
+ /* @__PURE__ */ jsxs3(Text4, { color: THEME.yellow, bold: true, wrap: "truncate", children: [
13802
13943
  " \u27F3 Retry #",
13803
13944
  retryState.attempt,
13804
13945
  "/",
13805
13946
  retryState.maxRetries
13806
13947
  ] }),
13807
- /* @__PURE__ */ jsxs3(Text4, { color: THEME.gray, children: [
13948
+ /* @__PURE__ */ jsxs3(Text4, { color: THEME.gray, wrap: "truncate", children: [
13808
13949
  " \u2014 ",
13809
13950
  retryState.countdown,
13810
13951
  "s \xB7 ",
@@ -13815,12 +13956,12 @@ var StatusDisplay = memo3(({
13815
13956
  if (isProcessing) {
13816
13957
  const previewText = isThinkingStatus && statusLines.length > 1 ? `${statusMain} \u2014 ${statusLines[statusLines.length - 1]}` : statusMain;
13817
13958
  return /* @__PURE__ */ jsxs3(Box3, { children: [
13818
- /* @__PURE__ */ jsx4(Text4, { color: isThinkingStatus ? THEME.cyan : THEME.primary, children: /* @__PURE__ */ jsx4(MusicSpinner, { color: isThinkingStatus ? THEME.cyan : THEME.primary }) }),
13819
- /* @__PURE__ */ jsxs3(Text4, { color: isThinkingStatus ? THEME.cyan : THEME.primary, bold: true, children: [
13959
+ /* @__PURE__ */ jsx4(Text4, { color: isThinkingStatus ? THEME.cyan : THEME.primary, wrap: "truncate", children: /* @__PURE__ */ jsx4(MusicSpinner, { color: isThinkingStatus ? THEME.cyan : THEME.primary }) }),
13960
+ /* @__PURE__ */ jsxs3(Text4, { color: isThinkingStatus ? THEME.cyan : THEME.primary, bold: true, wrap: "truncate", children: [
13820
13961
  " ",
13821
13962
  previewText
13822
13963
  ] }),
13823
- /* @__PURE__ */ jsxs3(Text4, { color: THEME.gray, children: [
13964
+ /* @__PURE__ */ jsxs3(Text4, { color: THEME.gray, wrap: "truncate", children: [
13824
13965
  " ",
13825
13966
  meta
13826
13967
  ] })
@@ -13830,7 +13971,7 @@ var StatusDisplay = memo3(({
13830
13971
  });
13831
13972
 
13832
13973
  // src/platform/tui/components/ChatInput.tsx
13833
- import { useMemo, useCallback as useCallback3, useRef as useRef4, memo as memo4, useState as useState4 } from "react";
13974
+ import { useMemo, useCallback as useCallback3, useRef as useRef5, memo as memo4, useState as useState5 } from "react";
13834
13975
  import { Box as Box4, Text as Text5, useInput } from "ink";
13835
13976
  import TextInput from "ink-text-input";
13836
13977
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
@@ -13853,17 +13994,17 @@ var ChatInput = memo4(({
13853
13994
  return getMatchingCommands(partialCmd).slice(0, MAX_SUGGESTIONS);
13854
13995
  }, [isSlashMode, partialCmd, hasArgs]);
13855
13996
  const showPreview = isSlashMode && !hasArgs && suggestions.length > 0;
13856
- const suggestionsRef = useRef4(suggestions);
13997
+ const suggestionsRef = useRef5(suggestions);
13857
13998
  suggestionsRef.current = suggestions;
13858
- const isSlashModeRef = useRef4(isSlashMode);
13999
+ const isSlashModeRef = useRef5(isSlashMode);
13859
14000
  isSlashModeRef.current = isSlashMode;
13860
- const hasArgsRef = useRef4(hasArgs);
14001
+ const hasArgsRef = useRef5(hasArgs);
13861
14002
  hasArgsRef.current = hasArgs;
13862
- const inputRequestRef = useRef4(inputRequest);
14003
+ const inputRequestRef = useRef5(inputRequest);
13863
14004
  inputRequestRef.current = inputRequest;
13864
- const onChangeRef = useRef4(onChange);
14005
+ const onChangeRef = useRef5(onChange);
13865
14006
  onChangeRef.current = onChange;
13866
- const [inputKey, setInputKey] = useState4(0);
14007
+ const [inputKey, setInputKey] = useState5(0);
13867
14008
  useInput(useCallback3((_input, key) => {
13868
14009
  if (inputRequestRef.current.status === "active") return;
13869
14010
  if (key.tab && isSlashModeRef.current && !hasArgsRef.current && suggestionsRef.current.length > 0) {
@@ -13968,6 +14109,7 @@ var Footer = memo5(({ phase, targets, findings, todo, elapsedTime, isProcessing
13968
14109
  paddingX: 1,
13969
14110
  marginTop: 0,
13970
14111
  justifyContent: "space-between",
14112
+ overflow: "hidden",
13971
14113
  children: [
13972
14114
  /* @__PURE__ */ jsxs5(Box5, { gap: 2, children: [
13973
14115
  /* @__PURE__ */ jsxs5(Text6, { color: THEME.gray, children: [
@@ -14003,9 +14145,9 @@ var App = ({ autoApprove = false, target }) => {
14003
14145
  const { exit } = useApp();
14004
14146
  const { stdout } = useStdout();
14005
14147
  const terminalWidth = stdout?.columns ?? 80;
14006
- const [input, setInput] = useState5("");
14007
- const [secretInput, setSecretInput] = useState5("");
14008
- const [autoApproveMode, setAutoApproveMode] = useState5(autoApprove);
14148
+ const [input, setInput] = useState6("");
14149
+ const [secretInput, setSecretInput] = useState6("");
14150
+ const [autoApproveMode, setAutoApproveMode] = useState6(autoApprove);
14009
14151
  const {
14010
14152
  agent,
14011
14153
  messages,
@@ -14024,11 +14166,11 @@ var App = ({ autoApprove = false, target }) => {
14024
14166
  addMessage,
14025
14167
  refreshStats
14026
14168
  } = useAgent(autoApproveMode, target);
14027
- const isProcessingRef = useRef5(isProcessing);
14169
+ const isProcessingRef = useRef6(isProcessing);
14028
14170
  isProcessingRef.current = isProcessing;
14029
- const autoApproveModeRef = useRef5(autoApproveMode);
14171
+ const autoApproveModeRef = useRef6(autoApproveMode);
14030
14172
  autoApproveModeRef.current = autoApproveMode;
14031
- const inputRequestRef = useRef5(inputRequest);
14173
+ const inputRequestRef = useRef6(inputRequest);
14032
14174
  inputRequestRef.current = inputRequest;
14033
14175
  const handleExit = useCallback4(() => {
14034
14176
  const ir = inputRequestRef.current;
@@ -14168,7 +14310,12 @@ ${procData.stdout || "(no output)"}
14168
14310
  break;
14169
14311
  case UI_COMMANDS.CTF:
14170
14312
  const ctfEnabled = agent.toggleCtfMode();
14171
- addMessage("system", ctfEnabled ? "[CTF] Mode ON - flag detection active" : "[CTF] Mode OFF - standard pentest");
14313
+ addMessage("system", ctfEnabled ? "\u{1F3F4} Flag auto-detection ON" : "\u{1F3F4} Flag auto-detection OFF");
14314
+ break;
14315
+ case UI_COMMANDS.TOR:
14316
+ const newTorState = !isTorEnabled();
14317
+ setTorEnabled(newTorState);
14318
+ addMessage("system", newTorState ? "\u{1F9C5} Tor proxy ON \u2014 target traffic routed through SOCKS5 (proxychains4 / native flags)" : "\u{1F9C5} Tor proxy OFF \u2014 direct connections");
14172
14319
  break;
14173
14320
  case UI_COMMANDS.GRAPH:
14174
14321
  case UI_COMMANDS.GRAPH_SHORT:
@@ -14220,8 +14367,8 @@ ${procData.stdout || "(no output)"}
14220
14367
  setInputRequest({ status: "inactive" });
14221
14368
  setSecretInput("");
14222
14369
  }, [addMessage, setInputRequest]);
14223
- const ctrlCTimerRef = useRef5(null);
14224
- const ctrlCPressedRef = useRef5(false);
14370
+ const ctrlCTimerRef = useRef6(null);
14371
+ const ctrlCPressedRef = useRef6(false);
14225
14372
  const handleCtrlC = useCallback4(() => {
14226
14373
  if (ctrlCPressedRef.current) {
14227
14374
  if (ctrlCTimerRef.current) clearTimeout(ctrlCTimerRef.current);
@@ -14236,7 +14383,7 @@ ${procData.stdout || "(no output)"}
14236
14383
  ctrlCTimerRef.current = null;
14237
14384
  }, 3e3);
14238
14385
  }, [handleExit, addMessage, abort]);
14239
- useEffect4(() => {
14386
+ useEffect5(() => {
14240
14387
  return () => {
14241
14388
  if (ctrlCTimerRef.current) clearTimeout(ctrlCTimerRef.current);
14242
14389
  };
@@ -14248,7 +14395,7 @@ ${procData.stdout || "(no output)"}
14248
14395
  }
14249
14396
  if (key.ctrl && ch === "c") handleCtrlC();
14250
14397
  }, [cancelInputRequest, abort, handleCtrlC]));
14251
- useEffect4(() => {
14398
+ useEffect5(() => {
14252
14399
  const onSignal = () => handleCtrlC();
14253
14400
  process.on("SIGINT", onSignal);
14254
14401
  process.on("SIGTERM", onSignal);
@@ -14257,16 +14404,21 @@ ${procData.stdout || "(no output)"}
14257
14404
  process.off("SIGTERM", onSignal);
14258
14405
  };
14259
14406
  }, [handleCtrlC]);
14407
+ const isSlashMode = input.startsWith("/");
14408
+ const partialCmd = isSlashMode ? input.slice(1).split(" ")[0] : "";
14409
+ const hasArgs = isSlashMode && input.includes(" ");
14410
+ const suggestionCount = isSlashMode && !hasArgs && inputRequest.status !== "active" ? Math.min(getMatchingCommands(partialCmd).length, MAX_SUGGESTIONS) : 0;
14411
+ const previewHeight = suggestionCount > 0 ? suggestionCount + 2 : 0;
14412
+ const bottomHeight = 6 + previewHeight;
14260
14413
  return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", paddingX: 1, width: terminalWidth, children: [
14261
14414
  /* @__PURE__ */ jsx7(Box6, { flexDirection: "column", children: /* @__PURE__ */ jsx7(MessageList, { messages }) }),
14262
- /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", height: 6, children: [
14415
+ /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", height: bottomHeight, children: [
14263
14416
  /* @__PURE__ */ jsx7(
14264
14417
  StatusDisplay,
14265
14418
  {
14266
14419
  retryState,
14267
14420
  isProcessing,
14268
14421
  currentStatus,
14269
- elapsedTime,
14270
14422
  currentTokens
14271
14423
  }
14272
14424
  ),
@@ -14325,6 +14477,9 @@ var CLI_SCAN_TYPES = Object.freeze([
14325
14477
  import gradient from "gradient-string";
14326
14478
  import { jsx as jsx8 } from "react/jsx-runtime";
14327
14479
  initDebugLogger();
14480
+ if (process.env.PENTEST_TOR === "true") {
14481
+ setTorEnabled(true);
14482
+ }
14328
14483
  var _configErrors = validateRequiredConfig();
14329
14484
  if (_configErrors.length > 0) {
14330
14485
  _configErrors.forEach((e) => console.error(chalk.hex(HEX.red)(e)));