// 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} 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}| 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} */ 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} */ 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 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"); }