pentesting 0.52.2 → 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.52.2";
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",
@@ -931,7 +925,6 @@ var DEFAULT_DIRECTIVE_FOCUS = PHASES.RECON;
931
925
  var ALLOWED_BINARIES = /* @__PURE__ */ new Set([
932
926
  // Network scanning
933
927
  "nmap",
934
- "masscan",
935
928
  "rustscan",
936
929
  // Web scanning
937
930
  "ffuf",
@@ -1557,7 +1550,6 @@ function extractBinary(command) {
1557
1550
  import { spawn } from "child_process";
1558
1551
  var TOOL_PACKAGE_MAP = {
1559
1552
  "nmap": { apt: "nmap", brew: "nmap" },
1560
- "masscan": { apt: "masscan", brew: "masscan" },
1561
1553
  "rustscan": { apt: "rustscan", brew: "rustscan" },
1562
1554
  "ffuf": { apt: "ffuf", brew: "ffuf" },
1563
1555
  "gobuster": { apt: "gobuster", brew: "gobuster" },
@@ -1732,6 +1724,161 @@ async function installTool(toolName, eventEmitter, inputHandler) {
1732
1724
  });
1733
1725
  }
1734
1726
 
1727
+ // src/shared/utils/config.ts
1728
+ var ENV_KEYS = {
1729
+ API_KEY: "PENTEST_API_KEY",
1730
+ BASE_URL: "PENTEST_BASE_URL",
1731
+ MODEL: "PENTEST_MODEL",
1732
+ SEARCH_API_KEY: "SEARCH_API_KEY",
1733
+ SEARCH_API_URL: "SEARCH_API_URL",
1734
+ THINKING: "PENTEST_THINKING",
1735
+ THINKING_BUDGET: "PENTEST_THINKING_BUDGET"
1736
+ };
1737
+ var DEFAULT_SEARCH_API_URL = "https://api.search.brave.com/res/v1/web/search";
1738
+ var DEFAULT_MODEL = "glm-4.7";
1739
+ function getApiKey() {
1740
+ return process.env[ENV_KEYS.API_KEY] || "";
1741
+ }
1742
+ function getBaseUrl() {
1743
+ return process.env[ENV_KEYS.BASE_URL] || void 0;
1744
+ }
1745
+ function getModel() {
1746
+ return process.env[ENV_KEYS.MODEL] || "";
1747
+ }
1748
+ function getSearchApiKey() {
1749
+ if (process.env[ENV_KEYS.SEARCH_API_KEY]) {
1750
+ return process.env[ENV_KEYS.SEARCH_API_KEY];
1751
+ }
1752
+ return process.env[ENV_KEYS.API_KEY];
1753
+ }
1754
+ function getSearchApiUrl() {
1755
+ return process.env[ENV_KEYS.SEARCH_API_URL] || DEFAULT_SEARCH_API_URL;
1756
+ }
1757
+ function isZaiProvider() {
1758
+ const baseUrl = getBaseUrl() || "";
1759
+ return baseUrl.includes("z.ai");
1760
+ }
1761
+ function isThinkingEnabled() {
1762
+ return process.env[ENV_KEYS.THINKING] === "true";
1763
+ }
1764
+ function getThinkingBudget() {
1765
+ const val = parseInt(process.env[ENV_KEYS.THINKING_BUDGET] || "", 10);
1766
+ return isNaN(val) ? 8e3 : Math.max(1024, val);
1767
+ }
1768
+ var TOR_PROXY = {
1769
+ /** SOCKS5 proxy host (Tor daemon listens here) */
1770
+ SOCKS_HOST: "127.0.0.1",
1771
+ /** SOCKS5 proxy port */
1772
+ SOCKS_PORT: 9050,
1773
+ /** Shell wrapper command for proxying CLI tools */
1774
+ WRAPPER_CMD: "proxychains4",
1775
+ /** Flags for the wrapper command (-q = quiet, no banners) */
1776
+ WRAPPER_FLAGS: "-q"
1777
+ };
1778
+ var torEnabled = false;
1779
+ function isTorEnabled() {
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 };
1816
+ }
1817
+ function wrapCommandForTor(command) {
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
+ }
1854
+ return `${TOR_PROXY.WRAPPER_CMD} ${TOR_PROXY.WRAPPER_FLAGS} ${command}`;
1855
+ }
1856
+ function getTorBrowserArgs() {
1857
+ if (!isTorEnabled()) return [];
1858
+ return [`--proxy-server=socks5://${TOR_PROXY.SOCKS_HOST}:${TOR_PROXY.SOCKS_PORT}`];
1859
+ }
1860
+ function validateRequiredConfig() {
1861
+ const errors = [];
1862
+ if (!getApiKey()) {
1863
+ errors.push(
1864
+ `[config] PENTEST_API_KEY is required.
1865
+ Export it before running:
1866
+ export PENTEST_API_KEY=your_api_key
1867
+ For z.ai: get your key at https://api.z.ai`
1868
+ );
1869
+ }
1870
+ const budgetRaw = process.env[ENV_KEYS.THINKING_BUDGET];
1871
+ if (budgetRaw !== void 0 && budgetRaw !== "") {
1872
+ const parsed = parseInt(budgetRaw, 10);
1873
+ if (isNaN(parsed) || parsed < 1024) {
1874
+ errors.push(
1875
+ `[config] PENTEST_THINKING_BUDGET must be an integer \u2265 1024 (got: "${budgetRaw}")`
1876
+ );
1877
+ }
1878
+ }
1879
+ return errors;
1880
+ }
1881
+
1735
1882
  // src/engine/tools-base.ts
1736
1883
  var globalEventEmitter = null;
1737
1884
  function setCommandEventEmitter(emitter) {
@@ -1757,6 +1904,16 @@ async function runCommand(command, args = [], options = {}) {
1757
1904
  error: `Command blocked: ${validation.error}`
1758
1905
  };
1759
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
+ }
1760
1917
  const timeout = options.timeout ?? TOOL_TIMEOUTS.DEFAULT_COMMAND;
1761
1918
  const maxRetries = options.maxRetries ?? AGENT_LIMITS.MAX_INSTALL_RETRIES;
1762
1919
  const toolName = command.split(/[\s/]/).pop() || command;
@@ -1794,15 +1951,22 @@ async function runCommand(command, args = [], options = {}) {
1794
1951
  }
1795
1952
  return lastResult;
1796
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
+ }
1797
1959
  async function executeCommandOnce(command, options = {}) {
1798
1960
  return new Promise((resolve) => {
1799
1961
  const timeout = options.timeout ?? TOOL_TIMEOUTS.DEFAULT_COMMAND;
1962
+ const safeCommand = injectCurlMaxTime(command, CURL_MAX_TIME_SEC);
1800
1963
  globalEventEmitter?.({
1801
1964
  type: COMMAND_EVENT_TYPES.COMMAND_START,
1802
- 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 ? "..." : ""}`
1803
1966
  });
1804
- const child = spawn2("sh", ["-c", command], {
1805
- timeout,
1967
+ const execCommand = wrapCommandForTor(safeCommand);
1968
+ const child = spawn2("sh", ["-c", execCommand], {
1969
+ detached: true,
1806
1970
  env: { ...process.env, ...options.env },
1807
1971
  cwd: options.cwd
1808
1972
  });
@@ -1810,6 +1974,20 @@ async function executeCommandOnce(command, options = {}) {
1810
1974
  let stderr = "";
1811
1975
  let inputHandled = false;
1812
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);
1813
1991
  const checkForInputPrompt = async (data) => {
1814
1992
  if (inputHandled || !globalInputHandler || processTerminated) return;
1815
1993
  for (const pattern of INPUT_PROMPT_PATTERNS) {
@@ -1843,7 +2021,21 @@ async function executeCommandOnce(command, options = {}) {
1843
2021
  checkForInputPrompt(text);
1844
2022
  });
1845
2023
  child.on("close", (code) => {
2024
+ clearTimeout(killTimer);
1846
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
+ }
1847
2039
  if (code === 0) {
1848
2040
  globalEventEmitter?.({
1849
2041
  type: COMMAND_EVENT_TYPES.COMMAND_SUCCESS,
@@ -1867,6 +2059,7 @@ async function executeCommandOnce(command, options = {}) {
1867
2059
  }
1868
2060
  });
1869
2061
  child.on("error", (err) => {
2062
+ clearTimeout(killTimer);
1870
2063
  processTerminated = true;
1871
2064
  globalEventEmitter?.({
1872
2065
  type: COMMAND_EVENT_TYPES.COMMAND_ERROR,
@@ -2118,12 +2311,23 @@ function startBackgroundProcess(command, options = {}) {
2118
2311
  const stderrFile = createTempFile(FILE_EXTENSIONS.STDERR);
2119
2312
  const stdinFile = createTempFile(FILE_EXTENSIONS.STDIN);
2120
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
+ }
2324
+ const torCommand = wrapCommandForTor(command);
2121
2325
  let wrappedCmd;
2122
2326
  if (isInteractive) {
2123
2327
  writeFileSync3(stdinFile, "", "utf-8");
2124
- wrappedCmd = `tail -f ${stdinFile} | ${command} > ${stdoutFile} 2> ${stderrFile}`;
2328
+ wrappedCmd = `tail -f ${stdinFile} | ${torCommand} > ${stdoutFile} 2> ${stderrFile}`;
2125
2329
  } else {
2126
- wrappedCmd = `${command} > ${stdoutFile} 2> ${stderrFile}`;
2330
+ wrappedCmd = `${torCommand} > ${stdoutFile} 2> ${stderrFile}`;
2127
2331
  }
2128
2332
  const child = spawn3("sh", ["-c", wrappedCmd], {
2129
2333
  detached: true,
@@ -2559,7 +2763,7 @@ var StateSerializer = class {
2559
2763
  lines.push(resourceInfo);
2560
2764
  }
2561
2765
  if (state.isCtfMode()) {
2562
- lines.push(`Mode: CTF \u{1F3F4}`);
2766
+ lines.push(`Flag Detection: ON \u{1F3F4}`);
2563
2767
  const flags = state.getFlags();
2564
2768
  if (flags.length > 0) {
2565
2769
  lines.push(`Flags Found (${flags.length}):`);
@@ -2568,6 +2772,16 @@ var StateSerializer = class {
2568
2772
  }
2569
2773
  }
2570
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
+ }
2571
2785
  lines.push(`Phase: ${state.getPhase()}`);
2572
2786
  return lines.join("\n");
2573
2787
  }
@@ -2730,10 +2944,6 @@ var AttackGraph = class {
2730
2944
  }
2731
2945
  this.failedPaths.push(`${fromId} \u2192 ${toId}${reason ? ` (${reason})` : ""}`);
2732
2946
  }
2733
- // Backward-compatible alias
2734
- markExploited(nodeId) {
2735
- this.markSucceeded(nodeId);
2736
- }
2737
2947
  // ─── Domain-Specific Registration ───────────────────────────
2738
2948
  /**
2739
2949
  * Record a host discovery.
@@ -2792,7 +3002,7 @@ var AttackGraph = class {
2792
3002
  hasExploit
2793
3003
  });
2794
3004
  for (const [id, node] of this.nodes) {
2795
- if (node.type === "service" && node.label.includes(target)) {
3005
+ if (node.type === NODE_TYPE.SERVICE && node.label.includes(target)) {
2796
3006
  this.addEdge(id, vulnId, "has_vulnerability", 0.8);
2797
3007
  }
2798
3008
  }
@@ -2834,7 +3044,7 @@ var AttackGraph = class {
2834
3044
  ...data
2835
3045
  });
2836
3046
  for (const [id, node] of this.nodes) {
2837
- if (node.type === "host" || node.type === "service") {
3047
+ if (node.type === NODE_TYPE.HOST || node.type === NODE_TYPE.SERVICE) {
2838
3048
  const hostIp = String(node.data.ip || node.data.host || "");
2839
3049
  const hostname = String(node.data.hostname || "");
2840
3050
  if (hostIp && detail.includes(hostIp) || hostname && detail.includes(hostname)) {
@@ -3254,23 +3464,6 @@ var WorkingMemory = class {
3254
3464
  (e) => e.category === "failure" && e.fingerprint != null && fingerprintsMatch(e.fingerprint, fp)
3255
3465
  );
3256
3466
  }
3257
- /**
3258
- * Get all previous attempts (success & failure) for a specific tool+target vector.
3259
- * Returns the full history so the LLM can see what parameter combinations were tried.
3260
- *
3261
- * TODO: Wire up in tools.ts to pass vector history to strategist for smarter retries.
3262
- */
3263
- getAttemptsForVector(tool, target) {
3264
- const lowerTool = tool.toLowerCase();
3265
- return this.entries.filter((e) => {
3266
- if (e.fingerprint) {
3267
- const matchTool = e.fingerprint.tool === lowerTool;
3268
- const matchTarget = !target || e.fingerprint.target.includes(target);
3269
- return matchTool && matchTarget;
3270
- }
3271
- return String(e.context.tool || "").toLowerCase() === lowerTool;
3272
- });
3273
- }
3274
3467
  /** Internal prune helper (used by both add() and recordFailure()) */
3275
3468
  pruneIfNeeded() {
3276
3469
  if (this.entries.length > this.maxEntries) {
@@ -3298,26 +3491,6 @@ var WorkingMemory = class {
3298
3491
  }
3299
3492
  return count;
3300
3493
  }
3301
- /**
3302
- * Get consecutive failures for a specific vector (tool+target).
3303
- * Returns count of sequential failures where all attempts used the same tool+target.
3304
- *
3305
- * TODO: Wire up in tools.ts for per-vector threshold checks alongside global consecutive count.
3306
- */
3307
- getConsecutiveVectorFailures(tool, target) {
3308
- const lowerTool = tool.toLowerCase();
3309
- let count = 0;
3310
- for (let i = this.entries.length - 1; i >= 0; i--) {
3311
- const e = this.entries[i];
3312
- if (e.category !== "failure") break;
3313
- if (e.fingerprint && e.fingerprint.tool === lowerTool && (!target || e.fingerprint.target.includes(target))) {
3314
- count++;
3315
- } else {
3316
- break;
3317
- }
3318
- }
3319
- return count;
3320
- }
3321
3494
  /**
3322
3495
  * Format for prompt injection.
3323
3496
  */
@@ -3757,7 +3930,7 @@ var SharedState = class {
3757
3930
  missionSummary: "",
3758
3931
  missionChecklist: [],
3759
3932
  ctfMode: true,
3760
- // CTF mode ON by default
3933
+ // Flag auto-detection ON by default
3761
3934
  flags: [],
3762
3935
  startedAt: now,
3763
3936
  // Auto-configure from PENTEST_DURATION env (seconds).
@@ -3933,7 +4106,7 @@ var SharedState = class {
3933
4106
  getPhase() {
3934
4107
  return this.data.currentPhase;
3935
4108
  }
3936
- // --- CTF Mode ---
4109
+ // --- Flag Detection ---
3937
4110
  setCtfMode(shouldEnable) {
3938
4111
  this.data.ctfMode = shouldEnable;
3939
4112
  }
@@ -4288,7 +4461,6 @@ var NOISE_CLASSIFICATION = {
4288
4461
  "dns_spoof",
4289
4462
  "traffic_intercept",
4290
4463
  "nmap",
4291
- "masscan",
4292
4464
  "nuclei",
4293
4465
  "nikto"
4294
4466
  ],
@@ -5156,10 +5328,27 @@ All ports freed. All children killed.`
5156
5328
  },
5157
5329
  {
5158
5330
  name: TOOL_NAMES.WRITE_FILE,
5159
- 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.`,
5160
5349
  parameters: {
5161
- path: { type: "string", description: "Absolute path to the file" },
5162
- 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" }
5163
5352
  },
5164
5353
  required: ["path", "content"],
5165
5354
  execute: async (params) => writeFileContent(params.path, params.content)
@@ -5594,69 +5783,6 @@ ${formatValidation(validation)}${rawOverride !== void 0 ? ` [confidence overridd
5594
5783
  // src/engine/tools/intel-utils.ts
5595
5784
  import { execFileSync } from "child_process";
5596
5785
 
5597
- // src/shared/utils/config.ts
5598
- var ENV_KEYS = {
5599
- API_KEY: "PENTEST_API_KEY",
5600
- BASE_URL: "PENTEST_BASE_URL",
5601
- MODEL: "PENTEST_MODEL",
5602
- SEARCH_API_KEY: "SEARCH_API_KEY",
5603
- SEARCH_API_URL: "SEARCH_API_URL",
5604
- THINKING: "PENTEST_THINKING",
5605
- THINKING_BUDGET: "PENTEST_THINKING_BUDGET"
5606
- };
5607
- var DEFAULT_SEARCH_API_URL = "https://api.search.brave.com/res/v1/web/search";
5608
- var DEFAULT_MODEL = "glm-4.7";
5609
- function getApiKey() {
5610
- return process.env[ENV_KEYS.API_KEY] || "";
5611
- }
5612
- function getBaseUrl() {
5613
- return process.env[ENV_KEYS.BASE_URL] || void 0;
5614
- }
5615
- function getModel() {
5616
- return process.env[ENV_KEYS.MODEL] || "";
5617
- }
5618
- function getSearchApiKey() {
5619
- if (process.env[ENV_KEYS.SEARCH_API_KEY]) {
5620
- return process.env[ENV_KEYS.SEARCH_API_KEY];
5621
- }
5622
- return process.env[ENV_KEYS.API_KEY];
5623
- }
5624
- function getSearchApiUrl() {
5625
- return process.env[ENV_KEYS.SEARCH_API_URL] || DEFAULT_SEARCH_API_URL;
5626
- }
5627
- function isZaiProvider() {
5628
- const baseUrl = getBaseUrl() || "";
5629
- return baseUrl.includes("z.ai");
5630
- }
5631
- function isThinkingEnabled() {
5632
- return process.env[ENV_KEYS.THINKING] === "true";
5633
- }
5634
- function getThinkingBudget() {
5635
- const val = parseInt(process.env[ENV_KEYS.THINKING_BUDGET] || "", 10);
5636
- return isNaN(val) ? 8e3 : Math.max(1024, val);
5637
- }
5638
- function validateRequiredConfig() {
5639
- const errors = [];
5640
- if (!getApiKey()) {
5641
- errors.push(
5642
- `[config] PENTEST_API_KEY is required.
5643
- Export it before running:
5644
- export PENTEST_API_KEY=your_api_key
5645
- For z.ai: get your key at https://api.z.ai`
5646
- );
5647
- }
5648
- const budgetRaw = process.env[ENV_KEYS.THINKING_BUDGET];
5649
- if (budgetRaw !== void 0 && budgetRaw !== "") {
5650
- const parsed = parseInt(budgetRaw, 10);
5651
- if (isNaN(parsed) || parsed < 1024) {
5652
- errors.push(
5653
- `[config] PENTEST_THINKING_BUDGET must be an integer \u2265 1024 (got: "${budgetRaw}")`
5654
- );
5655
- }
5656
- }
5657
- return errors;
5658
- }
5659
-
5660
5786
  // src/shared/constants/search-api.const.ts
5661
5787
  var SEARCH_URL_PATTERN = {
5662
5788
  GLM: "bigmodel.cn",
@@ -5880,7 +6006,7 @@ const { chromium } = require(${safePlaywrightPath});
5880
6006
  (async () => {
5881
6007
  const browser = await chromium.launch({
5882
6008
  headless: true,
5883
- args: ['${PLAYWRIGHT_ARG.NO_SANDBOX}', '${PLAYWRIGHT_ARG.DISABLE_SETUID_SANDBOX}']
6009
+ args: ['${PLAYWRIGHT_ARG.NO_SANDBOX}', '${PLAYWRIGHT_ARG.DISABLE_SETUID_SANDBOX}', ${JSON.stringify(getTorBrowserArgs()).slice(1, -1)}].filter(Boolean)
5884
6010
  });
5885
6011
 
5886
6012
  const context = await browser.newContext({
@@ -6066,7 +6192,10 @@ async function fillAndSubmitForm(url, formData, options = {}) {
6066
6192
  const { chromium } = require(${safePlaywrightPath});
6067
6193
 
6068
6194
  (async () => {
6069
- const browser = await chromium.launch({ headless: true });
6195
+ const browser = await chromium.launch({
6196
+ headless: true,
6197
+ args: ['--no-sandbox', '--disable-setuid-sandbox', ${JSON.stringify(getTorBrowserArgs()).slice(1, -1)}].filter(Boolean)
6198
+ });
6070
6199
  const page = await browser.newPage();
6071
6200
 
6072
6201
  try {
@@ -7520,16 +7649,20 @@ Common wordlists are automatically searched in /usr/share/wordlists (rockyou.txt
7520
7649
  if (wordlist === "rockyou") wordlistPath = WORDLISTS.ROCKYOU;
7521
7650
  const cmd = `hashcat -m ${format || HASHCAT_MODES.MD5} "${hashes}" "${wordlistPath}" --force`;
7522
7651
  if (background) {
7523
- const proc = startBackgroundProcess(cmd, {
7524
- description: `Cracking hashes: ${hashes.slice(0, DISPLAY_LIMITS.HASH_PREVIEW_LENGTH)}...`,
7525
- purpose: `Attempting to crack ${format || "unknown"} hashes using ${wordlist}`
7526
- });
7527
- return {
7528
- success: true,
7529
- 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}).
7530
7660
  Command: ${cmd}
7531
7661
  Check status with: bg_process({ action: "status", process_id: "${proc.id}" })`
7532
- };
7662
+ };
7663
+ } catch (err) {
7664
+ return { success: false, output: "", error: `Failed to start: ${err}` };
7665
+ }
7533
7666
  } else {
7534
7667
  return runCommand(cmd);
7535
7668
  }
@@ -10737,7 +10870,7 @@ var ToolExecutor = class _ToolExecutor {
10737
10870
  }
10738
10871
  }
10739
10872
  // ─────────────────────────────────────────────────────────────────
10740
- // SUBSECTION: CTF Flag Detection
10873
+ // SUBSECTION: Flag Detection
10741
10874
  // ─────────────────────────────────────────────────────────────────
10742
10875
  scanForFlags(output) {
10743
10876
  if (!this.state.isCtfMode()) return;
@@ -11075,12 +11208,12 @@ Phase: ${phase} | Targets: ${targets} | Findings: ${findings}
11075
11208
 
11076
11209
  ${direction}
11077
11210
 
11078
- ESCALATION:
11079
- 1. web_search for techniques
11080
- 2. Try alternative approaches
11081
- 3. Probe for unknown vulns
11082
- 4. Brute-force with wordlists
11083
- 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
11084
11217
 
11085
11218
  ACT NOW \u2014 EXECUTE.`;
11086
11219
  }
@@ -11187,7 +11320,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
11187
11320
  // src/shared/constants/prompts.ts
11188
11321
  var PROMPT_PATHS = {
11189
11322
  BASE: "base.md",
11190
- CTF_MODE: "ctf-mode.md",
11323
+ OFFENSIVE_PLAYBOOK: "offensive-playbook.md",
11191
11324
  AGENT_FILES: {
11192
11325
  ORCHESTRATOR: "orchestrator.md",
11193
11326
  RECON: "recon.md",
@@ -11665,9 +11798,9 @@ var PHASE_PROMPT_MAP = {
11665
11798
  };
11666
11799
  var CORE_KNOWLEDGE_FILES = [
11667
11800
  AGENT_FILES.STRATEGY,
11668
- // Attack prioritization, first-turn protocol, upgrade loop
11801
+ // Attack prioritization, first-turn protocol, upgrade loop (~2K tok)
11669
11802
  AGENT_FILES.ORCHESTRATOR,
11670
- // Phase transitions, multi-target management
11803
+ // Kill chain position, phase transitions, multi-target (~2K tok)
11671
11804
  AGENT_FILES.EVASION,
11672
11805
  // Detection avoidance (always relevant)
11673
11806
  AGENT_FILES.ZERO_DAY,
@@ -11707,9 +11840,10 @@ var PromptBuilder = class {
11707
11840
  * Build complete prompt for LLM iteration (async).
11708
11841
  *
11709
11842
  * Layers (phase-aware, enhanced with D-CIPHER meta-prompting):
11710
- * 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)
11711
11845
  * 2. Phase-specific prompt — current phase's full specialist knowledge (~2K tok)
11712
- * 3. Core methodology — strategy, orchestrator, evasion (always loaded, ~12K tok)
11846
+ * 3. Core methodology — strategy, orchestrator, evasion (always loaded, ~8K tok)
11713
11847
  * 4. Phase-relevant techniques — only attack techniques for current phase (~4-8K tok)
11714
11848
  * 5. Scope constraints
11715
11849
  * 6. Current state (targets, findings, loot, active processes)
@@ -11729,7 +11863,7 @@ var PromptBuilder = class {
11729
11863
  async build(userInput, phase) {
11730
11864
  const fragments = [
11731
11865
  this.loadPromptFile(PROMPT_PATHS.BASE),
11732
- this.loadCtfModePrompt(),
11866
+ this.loadOffensivePlaybook(),
11733
11867
  this.loadPhasePrompt(phase),
11734
11868
  this.loadCoreKnowledge(phase),
11735
11869
  this.loadPhaseRelevantTechniques(phase),
@@ -11762,15 +11896,14 @@ var PromptBuilder = class {
11762
11896
  return fragments.filter((f) => !!f).join("\n\n");
11763
11897
  }
11764
11898
  /**
11765
- * Load CTF mode prompt when CTF mode is active.
11766
- * 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.
11767
11901
  */
11768
- loadCtfModePrompt() {
11769
- if (!this.state.isCtfMode()) return "";
11770
- const content = this.loadPromptFile(PROMPT_PATHS.CTF_MODE);
11771
- return content ? `<ctf-mode active="true">
11902
+ loadOffensivePlaybook() {
11903
+ const content = this.loadPromptFile(PROMPT_PATHS.OFFENSIVE_PLAYBOOK);
11904
+ return content ? `<offensive-playbook>
11772
11905
  ${content}
11773
- </ctf-mode>` : "";
11906
+ </offensive-playbook>` : "";
11774
11907
  }
11775
11908
  /**
11776
11909
  * Load a prompt file from src/agents/prompts/
@@ -12993,7 +13126,8 @@ var COMMAND_DEFINITIONS = [
12993
13126
  { name: "paths", alias: "p", description: "Show ranked attack paths" },
12994
13127
  { name: "assets", alias: "a", description: "List background processes" },
12995
13128
  { name: "logs", alias: "l", args: "<id>", description: "Show logs for an asset" },
12996
- { name: "ctf", description: "Toggle CTF mode" },
13129
+ { name: "ctf", description: "Toggle auto flag detection" },
13130
+ { name: "tor", description: "Toggle Tor proxy routing" },
12997
13131
  { name: "auto", description: "Toggle auto-approve mode" },
12998
13132
  { name: "clear", alias: "c", description: "Reset session & clean workspace" },
12999
13133
  { name: "help", alias: "h", description: "Show detailed help" },
@@ -13018,7 +13152,8 @@ ${COMMAND_DEFINITIONS.map((cmd) => {
13018
13152
  \u2022 Auto-install missing tools (apt/brew)
13019
13153
  \u2022 Transparent command execution
13020
13154
  \u2022 Interactive sudo password input
13021
- \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)
13022
13157
  \u2022 Attack graph: Tracks discovered paths & prevents repeating failures
13023
13158
 
13024
13159
  \u2500\u2500 Tips \u2500\u2500
@@ -13044,6 +13179,7 @@ var useAgentState = () => {
13044
13179
  const retryCountRef = useRef(0);
13045
13180
  const tokenAccumRef = useRef(0);
13046
13181
  const lastStepTokensRef = useRef(0);
13182
+ const toolStartedAtRef = useRef(0);
13047
13183
  const addMessage = useCallback((type, content) => {
13048
13184
  const id = Math.random().toString(36).substring(7);
13049
13185
  setMessages((prev) => [...prev, { id, type, content, timestamp: /* @__PURE__ */ new Date() }]);
@@ -13106,7 +13242,8 @@ var useAgentState = () => {
13106
13242
  addMessage,
13107
13243
  resetCumulativeCounters,
13108
13244
  manageTimer,
13109
- clearAllTimers
13245
+ clearAllTimers,
13246
+ toolStartedAtRef
13110
13247
  };
13111
13248
  };
13112
13249
 
@@ -13125,19 +13262,22 @@ var useAgentEvents = (agent, eventsRef, state) => {
13125
13262
  retryCountRef,
13126
13263
  tokenAccumRef,
13127
13264
  lastStepTokensRef,
13128
- clearAllTimers
13265
+ clearAllTimers,
13266
+ toolStartedAtRef
13129
13267
  } = state;
13130
13268
  const reasoningBufferRef = useRef2("");
13131
13269
  useEffect(() => {
13132
13270
  const events = eventsRef.current;
13133
13271
  const onToolCall = (e) => {
13134
13272
  if (NOISE_CLASSIFICATION.LOW_VISIBILITY.includes(e.data.toolName)) return;
13273
+ toolStartedAtRef.current = Date.now();
13135
13274
  setCurrentStatus(`${e.data.toolName}\u2026`);
13136
13275
  const inputStr = formatToolInput(e.data.toolName, e.data.input);
13137
13276
  const label = inputStr ? `${toDisplayName(e.data.toolName)}(${inputStr})` : `${toDisplayName(e.data.toolName)}`;
13138
13277
  addMessage("tool", label);
13139
13278
  };
13140
13279
  const onToolResult = (e) => {
13280
+ toolStartedAtRef.current = 0;
13141
13281
  if (NOISE_CLASSIFICATION.LOW_VISIBILITY.includes(e.data.toolName)) {
13142
13282
  return;
13143
13283
  }
@@ -13296,6 +13436,7 @@ ${firstLine}`);
13296
13436
  tokenAccumRef,
13297
13437
  lastStepTokensRef,
13298
13438
  clearAllTimers,
13439
+ toolStartedAtRef,
13299
13440
  eventsRef
13300
13441
  ]);
13301
13442
  };
@@ -13735,7 +13876,7 @@ var MessageList = memo(({ messages }) => {
13735
13876
  });
13736
13877
 
13737
13878
  // src/platform/tui/components/StatusDisplay.tsx
13738
- import { memo as memo3 } from "react";
13879
+ import { memo as memo3, useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
13739
13880
  import { Box as Box3, Text as Text4 } from "ink";
13740
13881
 
13741
13882
  // src/platform/tui/components/MusicSpinner.tsx
@@ -13761,26 +13902,50 @@ var StatusDisplay = memo3(({
13761
13902
  retryState,
13762
13903
  isProcessing,
13763
13904
  currentStatus,
13764
- elapsedTime,
13765
13905
  currentTokens
13766
13906
  }) => {
13767
13907
  const truncateError = (err) => {
13768
13908
  return err.length > DISPLAY_LIMITS.RETRY_ERROR_PREVIEW ? err.substring(0, DISPLAY_LIMITS.RETRY_ERROR_TRUNCATED) + "..." : err;
13769
13909
  };
13770
- 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();
13771
13936
  const isThinkingStatus = currentStatus.startsWith("Reasoning");
13772
13937
  const statusLines = currentStatus ? currentStatus.split("\n").filter(Boolean) : [];
13773
13938
  const statusMain = statusLines[0] || "Processing...";
13774
13939
  if (retryState.status === "retrying") {
13775
13940
  return /* @__PURE__ */ jsxs3(Box3, { children: [
13776
- /* @__PURE__ */ jsx4(Text4, { color: THEME.yellow, children: /* @__PURE__ */ jsx4(MusicSpinner, { color: THEME.yellow }) }),
13777
- /* @__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: [
13778
13943
  " \u27F3 Retry #",
13779
13944
  retryState.attempt,
13780
13945
  "/",
13781
13946
  retryState.maxRetries
13782
13947
  ] }),
13783
- /* @__PURE__ */ jsxs3(Text4, { color: THEME.gray, children: [
13948
+ /* @__PURE__ */ jsxs3(Text4, { color: THEME.gray, wrap: "truncate", children: [
13784
13949
  " \u2014 ",
13785
13950
  retryState.countdown,
13786
13951
  "s \xB7 ",
@@ -13791,12 +13956,12 @@ var StatusDisplay = memo3(({
13791
13956
  if (isProcessing) {
13792
13957
  const previewText = isThinkingStatus && statusLines.length > 1 ? `${statusMain} \u2014 ${statusLines[statusLines.length - 1]}` : statusMain;
13793
13958
  return /* @__PURE__ */ jsxs3(Box3, { children: [
13794
- /* @__PURE__ */ jsx4(Text4, { color: isThinkingStatus ? THEME.cyan : THEME.primary, children: /* @__PURE__ */ jsx4(MusicSpinner, { color: isThinkingStatus ? THEME.cyan : THEME.primary }) }),
13795
- /* @__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: [
13796
13961
  " ",
13797
13962
  previewText
13798
13963
  ] }),
13799
- /* @__PURE__ */ jsxs3(Text4, { color: THEME.gray, children: [
13964
+ /* @__PURE__ */ jsxs3(Text4, { color: THEME.gray, wrap: "truncate", children: [
13800
13965
  " ",
13801
13966
  meta
13802
13967
  ] })
@@ -13806,7 +13971,7 @@ var StatusDisplay = memo3(({
13806
13971
  });
13807
13972
 
13808
13973
  // src/platform/tui/components/ChatInput.tsx
13809
- 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";
13810
13975
  import { Box as Box4, Text as Text5, useInput } from "ink";
13811
13976
  import TextInput from "ink-text-input";
13812
13977
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
@@ -13829,17 +13994,17 @@ var ChatInput = memo4(({
13829
13994
  return getMatchingCommands(partialCmd).slice(0, MAX_SUGGESTIONS);
13830
13995
  }, [isSlashMode, partialCmd, hasArgs]);
13831
13996
  const showPreview = isSlashMode && !hasArgs && suggestions.length > 0;
13832
- const suggestionsRef = useRef4(suggestions);
13997
+ const suggestionsRef = useRef5(suggestions);
13833
13998
  suggestionsRef.current = suggestions;
13834
- const isSlashModeRef = useRef4(isSlashMode);
13999
+ const isSlashModeRef = useRef5(isSlashMode);
13835
14000
  isSlashModeRef.current = isSlashMode;
13836
- const hasArgsRef = useRef4(hasArgs);
14001
+ const hasArgsRef = useRef5(hasArgs);
13837
14002
  hasArgsRef.current = hasArgs;
13838
- const inputRequestRef = useRef4(inputRequest);
14003
+ const inputRequestRef = useRef5(inputRequest);
13839
14004
  inputRequestRef.current = inputRequest;
13840
- const onChangeRef = useRef4(onChange);
14005
+ const onChangeRef = useRef5(onChange);
13841
14006
  onChangeRef.current = onChange;
13842
- const [inputKey, setInputKey] = useState4(0);
14007
+ const [inputKey, setInputKey] = useState5(0);
13843
14008
  useInput(useCallback3((_input, key) => {
13844
14009
  if (inputRequestRef.current.status === "active") return;
13845
14010
  if (key.tab && isSlashModeRef.current && !hasArgsRef.current && suggestionsRef.current.length > 0) {
@@ -13944,6 +14109,7 @@ var Footer = memo5(({ phase, targets, findings, todo, elapsedTime, isProcessing
13944
14109
  paddingX: 1,
13945
14110
  marginTop: 0,
13946
14111
  justifyContent: "space-between",
14112
+ overflow: "hidden",
13947
14113
  children: [
13948
14114
  /* @__PURE__ */ jsxs5(Box5, { gap: 2, children: [
13949
14115
  /* @__PURE__ */ jsxs5(Text6, { color: THEME.gray, children: [
@@ -13979,9 +14145,9 @@ var App = ({ autoApprove = false, target }) => {
13979
14145
  const { exit } = useApp();
13980
14146
  const { stdout } = useStdout();
13981
14147
  const terminalWidth = stdout?.columns ?? 80;
13982
- const [input, setInput] = useState5("");
13983
- const [secretInput, setSecretInput] = useState5("");
13984
- const [autoApproveMode, setAutoApproveMode] = useState5(autoApprove);
14148
+ const [input, setInput] = useState6("");
14149
+ const [secretInput, setSecretInput] = useState6("");
14150
+ const [autoApproveMode, setAutoApproveMode] = useState6(autoApprove);
13985
14151
  const {
13986
14152
  agent,
13987
14153
  messages,
@@ -14000,11 +14166,11 @@ var App = ({ autoApprove = false, target }) => {
14000
14166
  addMessage,
14001
14167
  refreshStats
14002
14168
  } = useAgent(autoApproveMode, target);
14003
- const isProcessingRef = useRef5(isProcessing);
14169
+ const isProcessingRef = useRef6(isProcessing);
14004
14170
  isProcessingRef.current = isProcessing;
14005
- const autoApproveModeRef = useRef5(autoApproveMode);
14171
+ const autoApproveModeRef = useRef6(autoApproveMode);
14006
14172
  autoApproveModeRef.current = autoApproveMode;
14007
- const inputRequestRef = useRef5(inputRequest);
14173
+ const inputRequestRef = useRef6(inputRequest);
14008
14174
  inputRequestRef.current = inputRequest;
14009
14175
  const handleExit = useCallback4(() => {
14010
14176
  const ir = inputRequestRef.current;
@@ -14144,7 +14310,12 @@ ${procData.stdout || "(no output)"}
14144
14310
  break;
14145
14311
  case UI_COMMANDS.CTF:
14146
14312
  const ctfEnabled = agent.toggleCtfMode();
14147
- 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");
14148
14319
  break;
14149
14320
  case UI_COMMANDS.GRAPH:
14150
14321
  case UI_COMMANDS.GRAPH_SHORT:
@@ -14196,8 +14367,8 @@ ${procData.stdout || "(no output)"}
14196
14367
  setInputRequest({ status: "inactive" });
14197
14368
  setSecretInput("");
14198
14369
  }, [addMessage, setInputRequest]);
14199
- const ctrlCTimerRef = useRef5(null);
14200
- const ctrlCPressedRef = useRef5(false);
14370
+ const ctrlCTimerRef = useRef6(null);
14371
+ const ctrlCPressedRef = useRef6(false);
14201
14372
  const handleCtrlC = useCallback4(() => {
14202
14373
  if (ctrlCPressedRef.current) {
14203
14374
  if (ctrlCTimerRef.current) clearTimeout(ctrlCTimerRef.current);
@@ -14212,7 +14383,7 @@ ${procData.stdout || "(no output)"}
14212
14383
  ctrlCTimerRef.current = null;
14213
14384
  }, 3e3);
14214
14385
  }, [handleExit, addMessage, abort]);
14215
- useEffect4(() => {
14386
+ useEffect5(() => {
14216
14387
  return () => {
14217
14388
  if (ctrlCTimerRef.current) clearTimeout(ctrlCTimerRef.current);
14218
14389
  };
@@ -14224,7 +14395,7 @@ ${procData.stdout || "(no output)"}
14224
14395
  }
14225
14396
  if (key.ctrl && ch === "c") handleCtrlC();
14226
14397
  }, [cancelInputRequest, abort, handleCtrlC]));
14227
- useEffect4(() => {
14398
+ useEffect5(() => {
14228
14399
  const onSignal = () => handleCtrlC();
14229
14400
  process.on("SIGINT", onSignal);
14230
14401
  process.on("SIGTERM", onSignal);
@@ -14233,16 +14404,21 @@ ${procData.stdout || "(no output)"}
14233
14404
  process.off("SIGTERM", onSignal);
14234
14405
  };
14235
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;
14236
14413
  return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", paddingX: 1, width: terminalWidth, children: [
14237
14414
  /* @__PURE__ */ jsx7(Box6, { flexDirection: "column", children: /* @__PURE__ */ jsx7(MessageList, { messages }) }),
14238
- /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", height: 6, children: [
14415
+ /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", height: bottomHeight, children: [
14239
14416
  /* @__PURE__ */ jsx7(
14240
14417
  StatusDisplay,
14241
14418
  {
14242
14419
  retryState,
14243
14420
  isProcessing,
14244
14421
  currentStatus,
14245
- elapsedTime,
14246
14422
  currentTokens
14247
14423
  }
14248
14424
  ),
@@ -14301,6 +14477,9 @@ var CLI_SCAN_TYPES = Object.freeze([
14301
14477
  import gradient from "gradient-string";
14302
14478
  import { jsx as jsx8 } from "react/jsx-runtime";
14303
14479
  initDebugLogger();
14480
+ if (process.env.PENTEST_TOR === "true") {
14481
+ setTorEnabled(true);
14482
+ }
14304
14483
  var _configErrors = validateRequiredConfig();
14305
14484
  if (_configErrors.length > 0) {
14306
14485
  _configErrors.forEach((e) => console.error(chalk.hex(HEX.red)(e)));