// Phantom — API server + GUI dashboard // Extracted from phantom.mjs for cleaner module structure import http from "http"; import fs from "fs"; import { resolve } from "path"; import { homedir } from "os"; import { execSync } from "child_process"; import { DASHBOARD_HTML } from "./dashboard.mjs"; import { log } from "./logger.mjs"; import * as osModule from "os"; // Mission directory for the new monitoring endpoints const MISSIONS_DIR = resolve(homedir(), ".config", "phantom", "missions"); /** Shared deps — passed in at setup time */ let hackerTools = null; let __r = null; let REPORTS_DIR = ""; // Tool categorization for the /api/tasks endpoint function categorizeTool(name) { if (name.startsWith("mission") || name.startsWith("hackerone")) return "mission"; if (name.startsWith("recon") || name === "subfinder" || name === "dnsx" || name === "httpx" || name === "sub_enum" || name === "wayback" || name === "amass") return "recon"; if (name.startsWith("net:") || name === "port_scan" || name === "ssl_check" || name === "whois" || name === "geoip" || name === "dns_lookup" || name === "reverse_dns" || name === "nmap" || name === "naabu") return "network"; if (name.startsWith("vuln:") || name === "cve_search" || name === "exploit_search" || name === "nuclei" || name === "nikto" || name === "sqlmap" || name === "sql_detect" || name === "xss_scan") return "vulnerability"; if (name.startsWith("web:") || name === "crawl" || name === "http_headers" || name === "web_fetch" || name === "web_links" || name === "web_snapshot" || name === "whatweb" || name === "wafw00f" || name === "robots_txt" || name === "dir_bruteforce" || name === "ffuf" || name === "gobuster") return "web"; if (name.startsWith("osint:") || name === "email_verify" || name === "email_breach" || name === "github_dork" || name === "shodan_search" || name === "vt_check" || name === "cert_expiry") return "osint"; if (name.startsWith("schedule") || name === "cron" || name === "batch" || name === "pipe" || name === "session" || name === "playbook") return "automation"; return "other"; } function formatUptime(seconds) { const d = Math.floor(seconds / 86400); const h = Math.floor((seconds % 86400) / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); const parts = []; if (d) parts.push(`${d}d`); if (h) parts.push(`${h}h`); if (m) parts.push(`${m}m`); if (s || parts.length === 0) parts.push(`${s}s`); return parts.join(" "); } export function initApiDeps(tools, runtime, reportsDir) { hackerTools = tools; __r = runtime; REPORTS_DIR = reportsDir; } // ── REST API Handler ────────────────────────────────────── // Agent reference for chat — set by dashboard/server startup let chatAgent = null; let chatAgentManager = null; let chatBus = null; export function setChatAgent(agent, manager, bus) { chatAgent = agent; chatAgentManager = manager; chatBus = bus || null; } const json = (res, data, code = 200) => { res.writeHead(code, { "Content-Type": "application/json" }); res.end(JSON.stringify(data)); }; const parseBody = (req) => new Promise((resolve) => { let b = ""; req.on("data", (c) => (b += c)); req.on("end", () => { try { resolve(JSON.parse(b)); } catch { resolve({}); } }); }); export async function handleApiRequest(req, res) { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type"); if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; } const url = new URL(req.url || "/", "http://" + (req.headers.host || "localhost")); try { // GET /api — list all if (url.pathname === "/api" || url.pathname === "/api/") { return json(res, { ok: true, tools: Object.keys(hackerTools).sort(), version: "0.2.0", docs: "/api/tools, /api/run, /api/info, /api/playbooks, /api/reports", }); } // GET /api/tools — list tool names if (url.pathname === "/api/tools") { return json(res, { ok: true, count: Object.keys(hackerTools).length, tools: Object.keys(hackerTools).sort(), }); } // GET /api/info — detailed tool info if (url.pathname === "/api/info") { const entries = Object.entries(hackerTools) .sort(([a], [b]) => (a < b ? -1 : 1)) .map(([name]) => ({ name })); return json(res, { ok: true, count: entries.length, tools: entries }); } // GET /api/tool/:name — tool metadata const toolMatch = url.pathname.match(/^\/api\/tool\/(.+)$/); if (toolMatch) { const name = decodeURIComponent(toolMatch[1]); if (!hackerTools[name]) return json(res, { ok: false, error: "Tool not found" }, 404); return json(res, { ok: true, name }); } // POST /api/run — execute tool if (url.pathname === "/api/run" && req.method === "POST") { const { tool, args } = await parseBody(req); if (!tool || !hackerTools[tool]) return json(res, { ok: false, error: `Tool "${tool}" not found` }, 404); const result = await hackerTools[tool](args || ""); return json(res, { ok: true, tool, args: args || "", result }); } // GET /api/run?tool=...&args=... — execute via GET if (url.pathname === "/api/run" && req.method === "GET") { const tool = url.searchParams.get("tool"); const args = url.searchParams.get("args") || ""; if (!tool || !hackerTools[tool]) return json(res, { ok: false, error: `Tool "${tool}" not found` }, 404); const result = await hackerTools[tool](args); return json(res, { ok: true, tool, args, result }); } // GET /api/playbooks — list playbooks if (url.pathname === "/api/playbooks") { const pbDir = resolve(homedir(), ".config", "phantom", "playbooks"); const names = []; if (fs.existsSync(pbDir)) { for (const f of fs.readdirSync(pbDir).filter((f) => f.endsWith(".json"))) { const pb = JSON.parse(fs.readFileSync(resolve(pbDir, f), "utf-8")); names.push({ name: pb.name, description: pb.description, steps: (pb.steps || []).length, vars: pb.variables, }); } } return json(res, { ok: true, playbooks: names }); } // POST /api/playbook/run — run a playbook if (url.pathname === "/api/playbook/run" && req.method === "POST") { const { name, vars } = await parseBody(req); if (!hackerTools.playbook_run) return json(res, { ok: false, error: "playbook_run tool not loaded" }, 404); const result = await hackerTools.playbook_run(vars ? name + "|" + vars : name); return json(res, { ok: true, name, result }); } // GET /api/playbook/run?name=...&vars=... — run via GET if (url.pathname === "/api/playbook/run" && req.method === "GET") { const name = url.searchParams.get("name"); const vars = url.searchParams.get("vars") || ""; if (!name) return json(res, { ok: false, error: "?name= required" }, 400); if (!hackerTools.playbook_run) return json(res, { ok: false, error: "playbook_run not loaded" }, 404); const result = await hackerTools.playbook_run(vars ? name + "|" + vars : name); return json(res, { ok: true, name, result }); } // GET /api/reports — list reports if (url.pathname === "/api/reports") { const reports = []; if (fs.existsSync(REPORTS_DIR)) { for (const f of fs.readdirSync(REPORTS_DIR).filter((f) => f.endsWith(".md") || f.endsWith(".txt"))) { const s = fs.readFileSync(resolve(REPORTS_DIR, f)).length; reports.push({ name: f, size: s < 1024 ? s + "B" : (s / 1024).toFixed(1) + "KB" }); } } return json(res, { ok: true, reports }); } // GET /api/report/:name — view a report if (url.pathname.startsWith("/api/report/")) { const name = decodeURIComponent(url.pathname.slice(12)); const fp = resolve(REPORTS_DIR, name); if (!fs.existsSync(fp)) return json(res, { ok: false, error: "Report not found" }, 404); return json(res, { ok: true, name, content: fs.readFileSync(fp, "utf-8") }); } // GET /api/health — health check if (url.pathname === "/api/health") { const mem = process.memoryUsage(); return json(res, { ok: true, status: "running", tools: Object.keys(hackerTools).length, chat: chatAgent ? `${chatAgent.name} (lvl ${chatAgent.evolutionLevel})` : false, pid: process.pid, uptime: process.uptime().toFixed(0) + "s", uptimeHuman: formatUptime(process.uptime()), memory: { rss: Math.round(mem.rss / 1024 / 1024) + "MB", heapUsed: Math.round(mem.heapUsed / 1024 / 1024) + "MB", heapTotal: Math.round(mem.heapTotal / 1024 / 1024) + "MB", external: Math.round(mem.external / 1024 / 1024) + "MB" }, platform: process.platform, nodeVersion: process.version, usbMounted: fs.existsSync("/root/usb/Phantom"), timestamp: new Date().toISOString() }); } // GET /api/missions — list all missions if (url.pathname === "/api/missions") { const missions = []; if (fs.existsSync(MISSIONS_DIR)) { for (const f of fs.readdirSync(MISSIONS_DIR).filter((f) => f.endsWith(".json"))) { const m = JSON.parse(fs.readFileSync(resolve(MISSIONS_DIR, f), "utf-8")); missions.push({ id: m.id, programName: m.programName, programHandle: m.programHandle, status: m.status, createdAt: m.createdAt, updatedAt: m.updatedAt, inScopeCount: m.scope?.inScope?.length || 0, objectives: m.objectives?.length || 0 }); } } missions.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); return json(res, { ok: true, missions }); } // GET /api/missions/:id — get single mission detail if (url.pathname.startsWith("/api/missions/") && url.pathname.length > 12) { const missionId = decodeURIComponent(url.pathname.slice(12)); const f = resolve(MISSIONS_DIR, `${missionId}.json`); if (!fs.existsSync(f)) return json(res, { ok: false, error: "Mission not found" }, 404); const mission = JSON.parse(fs.readFileSync(f, "utf-8")); return json(res, { ok: true, mission }); } // GET /api/schedules — list active schedules if (url.pathname === "/api/schedules") { const SCHEDULES_FILE = resolve(homedir(), ".config", "phantom", "schedules.json"); let schedules = []; if (fs.existsSync(SCHEDULES_FILE)) { try { schedules = JSON.parse(fs.readFileSync(SCHEDULES_FILE, "utf-8")); } catch {} } return json(res, { ok: true, schedules: schedules.map((s) => ({ id: s.id, tool: s.tool, target: s.target, interval: s.interval, nextAt: s.nextAt, nextAtHuman: new Date(s.nextAt).toLocaleString() })) }); } // GET /api/tasks — list all available tasks/tools with categories if (url.pathname === "/api/tasks") { const tasks = Object.keys(hackerTools).map((name) => ({ name, category: categorizeTool(name) })); return json(res, { ok: true, tasks }); } // GET /api/system — system info (disk, load, memory) if (url.pathname === "/api/system") { let diskInfo = ""; let loadAvg = ""; let netInfo = ""; try { diskInfo = execSync("df -h /root/usb", { encoding: "utf-8", timeout: 5000 }).trim(); } catch {} try { loadAvg = execSync("cat /proc/loadavg", { encoding: "utf-8", timeout: 5000 }).trim(); } catch {} try { netInfo = execSync("cat /proc/net/dev", { encoding: "utf-8", timeout: 5000 }).trim(); } catch {} // Get disk I/O stats let diskIO = ""; try { diskIO = execSync("cat /proc/diskstats", { encoding: "utf-8", timeout: 5000 }).trim(); } catch {} return json(res, { ok: true, disk: diskInfo, loadAvg: loadAvg, netDev: netInfo, diskIO: diskIO, cpus: osModule.cpus().length, totalMem: Math.round(osModule.totalmem() / 1024 / 1024 / 1024 * 10) / 10 + "GB", freeMem: Math.round(osModule.freemem() / 1024 / 1024 / 1024 * 10) / 10 + "GB", uptime: process.uptime(), platform: process.platform, nodeVersion: process.version, pid: process.pid }); } // POST /api/chat — send message to agent if (url.pathname === "/api/chat" && req.method === "POST") { if (!chatAgent) return json(res, { ok: false, error: "No chat agent running" }, 503); const { message } = await parseBody(req); if (!message) return json(res, { ok: false, error: "message required" }, 400); const response = await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error("Agent timeout")), 90000); const handler = ({ agent: a, text }) => { if (a && a.id === chatAgent.id) { clearTimeout(timeout); try { if (chatBus) chatBus.off("agent:msg", handler); } catch {} resolve(text); } }; if (chatBus) chatBus.on("agent:msg", handler); chatAgent.receive("user", message).catch(err => { clearTimeout(timeout); try { if (chatBus) chatBus.off("agent:msg", handler); } catch {} reject(err); }); }); return json(res, { ok: true, response, level: chatAgent.evolutionLevel }); } // POST /api/chat/reset — reset agent conversation if (url.pathname === "/api/chat/reset" && req.method === "POST") { if (!chatAgent) return json(res, { ok: false, error: "No chat agent" }, 503); chatAgent.memory = []; chatAgent.evolutionLevel = 1; return json(res, { ok: true }); } return json(res, { ok: false, error: "Not found" }, 404); } catch (e) { return json(res, { ok: false, error: e.message }, 500); } } export function startApiServer(port) { initApiDeps(hackerTools, __r, REPORTS_DIR); // keep refs const server = http.createServer((req, res) => handleApiRequest(req, res)); server.listen(port, () => { const clickable = `\x1b]8;;http://localhost:${port}\x07http://localhost:${port}\x1b]8;;\x07`; log.cli(` ◆ Phantom API: ${clickable} (--api)`); }); return server; } export function startGuiDashboard(port) { initApiDeps(hackerTools, __r, REPORTS_DIR); const html = DASHBOARD_HTML; const server = http.createServer((req, res) => { if (req.url?.startsWith("/api")) return handleApiRequest(req, res); res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(html); }); const url = `http://localhost:${port}`; server.on("error", e => { if (e.code === "EADDRINUSE") { log.warn(`Port ${port} in use — killing previous process...`); try { execSync(`lsof -ti:${port} | xargs kill 2>/dev/null`, { timeout: 3000 }); setTimeout(() => { server.close(); server.listen(port); }, 500); } catch { /* ignore kill failures */ } } }); server.listen(port, () => { const clickable = `\x1b]8;;${url}\x07${url}\x1b]8;;\x07`; log.cli(`\n \u25C8 Phantom Dashboard: ${clickable}`); log.cli(` \u26A1 Ctrl+Click or tap to open in browser\n`); try { const isTermux = process.env.PREFIX === "/data/data/com.termux/files/usr"; execSync(isTermux ? `termux-open-url ${url}` : `xdg-open ${url}`, { timeout: 2000, stdio: "ignore", }); } catch { /* browser may not be available */ } }); return server; }