File size: 9,593 Bytes
20f108c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
// Phantom β€” shared runtime state
// Mutable exports set by phantom.mjs, read by lib modules.
// ESM live bindings ensure imports see the latest values.

import { execFileSync } from "child_process";
import { resolve } from "path";

// Lazy import for evolve β€” avoid circular dep at module load time
let _evolve = null;
async function getEvolve() {
  if (!_evolve) _evolve = await import("./evolve.mjs");
  return _evolve;
}

export const __r = {
  llmInstance: null,
  _config: {},
  setProvider: null,
  PROVIDERS: null,
  PHANTOM_LLM_PROVIDER: "openai",
  ENV: null,
};

// ── Async shell exec helper ──────────────────────────────
// Replaces execSync throughout tools.mjs so the event loop stays
// responsive during long-running commands (stdin keeps working).
export function execAsync(cmd, opts = {}) {
  const timeout = opts.timeout ?? 30000;
  const maxBuffer = opts.maxBuffer ?? 1024 * 1024;
  return new Promise((resolve, reject) => {
    // Dynamic import to avoid top-level require
    import("child_process").then(({ exec }) => {
      exec(cmd, { encoding: "utf-8", timeout, maxBuffer }, (err, stdout, stderr) => {
        if (err) {
          err.stderr = (stderr || "").substring(0, 500);
          return reject(err);
        }
        resolve(stdout);
      });
    }).catch(reject);
  });
}

// ── External tool helper ─────────────────────────────────
// DRY wrapper for all external-binary tools.
// Handles dynamic import caching, which-check, error formatting.
// Returns trimmed stdout lines array. Throws on error.
const _cp = /* lazily cached */ null;
function _ensureChildProcess() {
  // execFileSync is already statically imported; this is a no-op
  // but kept for symmetry with tools that imported dynamically
  return { execFileSync };
}

/**
 * Run an external binary and return its trimmed stdout lines.
 *
 * @param {string} tool   Binary name (e.g. "nmap", "subfinder")
 * @param {string[]} args CLI arguments
 * @param {object}  [opts]
 * @param {number}  [opts.timeout=60000]
 * @param {number}  [opts.maxBuffer=1024*1024]
 * @param {string}  [opts.input]         stdin content
 * @param {string}  [opts.installGuide]  shown if binary missing
 * @returns {string[]} non-empty stdout lines
 */
export function runExternal(tool, args, opts = {}) {
  const { execFileSync } = _ensureChildProcess();
  const timeout = opts.timeout ?? 60000;
  const maxBuffer = opts.maxBuffer ?? 1024 * 1024;
  const input = opts.input;
  const guide = opts.installGuide || `go install ...${tool}@latest`;

  // which check β€” prefer cached env.tools, fallback to which
  const cached = __r.ENV?.tools?.[tool];
  if (cached === false) {
    throw new Error(`[${tool}] NOT INSTALLED β€” ${guide}`);
  }
  if (cached !== true) {
    try { execFileSync("which", [tool], { encoding: "utf-8", timeout: 5000 }); }
    catch {
      throw new Error(`[${tool}] NOT INSTALLED β€” ${guide}`);
    }
  }

  const execOpts = { encoding: "utf-8", timeout, maxBuffer };
  if (input !== undefined) execOpts.input = input;

  const out = execFileSync(tool, args, execOpts);
  return out.trim().split("\n").filter(Boolean);
}

/**
 * Format external tool output for display.
 * @param {string} toolName
 * @param {string} target
 * @param {string[]} lines
 * @param {number} [maxLines=50]
 * @returns {string} formatted output
 */
export function formatExternal(toolName, target, lines, maxLines = 50) {
  if (!lines.length) return `[${toolName}] No results for ${target}`;
  const result = [`πŸ”Ž ${toolName}: ${target}`, `Results: ${lines.length}`];
  result.push(...lines.slice(0, maxLines).map(l => `  ${l}`));
  if (lines.length > maxLines) result.push(`  ... and ${lines.length - maxLines} more`);
  return result.join("\n");
}

// ── Tool execution wrapper ───────────────────────────────
// Handles JSON output, tool chaining/piping, error wrapping.

/**
 * Run a tool from the hackerTools registry with optional flags.
 *
 * @param {object} tools   - hackerTools object
 * @param {string} toolName
 * @param {string} input
 * @param {object} [opts]
 * @param {boolean} [opts.json=false]  Return JSON string instead of formatted text
 * @returns {Promise<string>}  Formatted text or JSON string
 */
export async function runTool(tools, toolName, input, opts = {}) {
  const fn = tools[toolName];
  if (!fn) {
    const msg = `[${toolName}] Unknown tool. Use --list to see available tools.`;
    return opts.json ? JSON.stringify({ ok: false, error: msg, tool: toolName }) : msg;
  }
  // Built-in --help for any tool
  const trimmed = (input || "").trim().toLowerCase();
  if (trimmed === "--help" || trimmed === "-h") {
    const help = `[${toolName}] Usage: @${toolName}|<args>
  Run "${toolName}" with empty input for built-in usage.
  Available tools: ${Object.keys(tools).length}`;
    return opts.json ? JSON.stringify({ ok: true, tool: toolName, help: true, data: help }) : help;
  }
  // Show spinner for slow tools (non-CLI mode β€” REPL handles its own feedback)
  const timer = setTimeout(() => process.stdout.write(`[*] ${toolName} running...\r`), 2000);
  try {
    const result = await fn(input);
    clearTimeout(timer);
    if (opts.json) {
      return JSON.stringify({ ok: true, tool: toolName, input, data: result, length: result.length });
    }
    return result;
  } catch (e) {
    clearTimeout(timer);
    // Auto-heal: try to fix the error and retry once
    try {
      const evolve = await getEvolve();
      const fix = await evolve.analyzeError(toolName, e.message);
      if (fix && fix.fixed) {
        // Retry the tool after successful fix
        const retryResult = await fn(input);
        if (opts.json) {
          return JSON.stringify({ ok: true, tool: toolName, input, data: retryResult, length: retryResult.length, auto_healed: true, fix: fix.message });
        }
        return `[auto-heal] ${fix.message}\n${retryResult}`;
      }
    } catch {}
    const msg = `[${toolName} Error] ${e.message}`;
    return opts.json ? JSON.stringify({ ok: false, error: e.message, tool: toolName, input }) : msg;
  }
}

/**
 * Parse and execute a piped tool chain.
 * Format: tool1|args | tool2 | tool3|more_args
 *
 * @param {object} tools  - hackerTools object
 * @param {string} chain  - e.g. "subfinder|example.com | httpx | nuclei|https://x.com"
 * @param {object} [opts]
 * @returns {Promise<string>}
 */
export async function runPipe(tools, chain, opts = {}) {
  // Chain separator = " | " (space-pipe-space)
  // Tool/args separator = "|" (no spaces needed)
  // This avoids conflict between "shell|echo hi" and "subfinder|x.com | httpx"
  const segments = chain.split(/\s+\|\s+/).filter(Boolean);
  if (segments.length === 0) return opts.json ? JSON.stringify({ ok: false, error: "Empty pipe chain" }) : "[pipe] Empty chain";
  if (segments.length === 1) {
    // Single tool, no piping
    const [toolName, ...args] = segments[0].split("|");
    const input = args.join("|").trim();
    return runTool(tools, toolName, input, opts);
  }

  // Multi-segment pipe: output of each tool becomes input of the next
  let pipeInput = "";
  let lastResult = "";

  for (let i = 0; i < segments.length; i++) {
    const seg = segments[i].trim();
    const [toolName, ...args] = seg.split("|");
    let input = args.join("|").trim();

    // If there's piped input from previous tool, prepend it
    // Format: previous output lines become part of the argument
    if (pipeInput && input) {
      // Both piped input and explicit args β€” append piped data
      input = input + " " + pipeInput;
    } else if (pipeInput && !input) {
      input = pipeInput;
    }

    // Last segment gets the opts as-is; intermediate segments are not JSON
    const segOpts = (i === segments.length - 1) ? opts : {};
    const result = await runTool(tools, toolName, input, segOpts);

    if (i === segments.length - 1) {
      lastResult = result;
    } else {
      // Intermediate output: take the last line or the whole thing
      const lines = result.split("\n").filter(Boolean);
      // Try to find the most relevant data lines (skip header/count)
      const dataLines = lines.filter(l => !l.startsWith("πŸ”Ž") && !l.startsWith("Results:") && !l.startsWith("[") && l.trim());
      pipeInput = dataLines.length > 0 ? dataLines.join("\n") : lines.slice(-10).join("\n");
    }
  }

  return lastResult;
}

/**
 * Run a scheduled scan: executes a tool on each scope target.
 * @param {object}  tools     - hackerTools object
 * @param {string}  toolName  - tool to run
 * @param {string}  [extra]   - extra options
 * @param {number}  [concurrency=1]
 * @returns {Promise<string>}
 */
export async function runScheduledScan(tools, toolName, extra = "", concurrency = 1) {
  // Load scope
  const scopeResult = await tools.scope("export");
  const targets = scopeResult.split("\n").filter(l => l.trim() && !l.includes("(empty") && !l.includes("Commands"));
  if (targets.length === 0) return "[schedule] No scope targets defined. Use scope add <target> first.";

  const fn = tools[toolName];
  if (!fn) return `[schedule] Unknown tool: ${toolName}`;

  const results = [];
  for (const target of targets) {
    try {
      const r = await fn(`${target} ${extra}`.trim());
      results.push(`=== ${target} ===\n${r}`);
    } catch (e) {
      results.push(`=== ${target} ===\n[${toolName} Error] ${e.message}`);
    }
  }
  return results.join("\n\n");
}