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