#!/usr/bin/env python3 """METATRON — lightweight Ollama terminal harness. Talks to Ollama on nightmare (hardcoded). Launched by typing `metatron`: 1. model selection screen (fetches live models + sizes from nightmare) 2. chat REPL with streaming 3. system access: `!command` + native run_command tool 4. internet: `?query` (search) / `?fetch ` (read a page) + web_search tool Stdlib-only (urllib + subprocess). Hardcoded: Ollama URL, SearXNG URL, system prompt, tools. Only the model list is dynamic. """ import json import os import re import subprocess import sys import time import urllib.parse import urllib.request from html import unescape # Windows console is cp1252 — replace unencodable chars instead of crashing on # arbitrary Unicode the model emits (arrows, emoji, etc.). for _s in (sys.stdout, sys.stderr): try: _s.reconfigure(errors="replace") except Exception: pass OLLAMA = "http://10.30.20.29:11434" # nightmare SEARXNG = "http://10.30.20.35:6969" # self-hosted meta-search (CT 516) HERE = os.path.dirname(os.path.abspath(__file__)) LAST_MODEL = os.path.join(HERE, ".last_model") SYSTEM_PROMPT = ( "You are METATRON, a system-access AI harness on a Windows Commando VM with a " "large tool catalog organized by category (recon, web, exploit, post, crack, " "wireless, sniff, osint, forensics, utils). DISCOVER LAZILY: call list_categories " "first, then list_tools() to see a category's tools, then run_command to " "execute one. Never enumerate everything at once — drill in only as needed. Use " "web_search for internet facts. Be concise and direct." ) TOOLS = [{ "type": "function", "function": { "name": "run_command", "description": "Execute a system command (cmd/PowerShell) and return its output. " "Use for file ops, process management, recon, anything on the box.", "parameters": { "type": "object", "properties": { "command": {"type": "string", "description": "The command to run"}, }, "required": ["command"], }, }, }, { "type": "function", "function": { "name": "web_search", "description": "Search the internet and return the top results (title, URL, snippet). " "Use for current events, facts, anything you don't already know.", "parameters": { "type": "object", "properties": { "query": {"type": "string", "description": "The search query"}, }, "required": ["query"], }, }, }, { "type": "function", "function": { "name": "list_categories", "description": "List the available tool categories (recon, web, exploit, post, crack, " "wireless, sniff, osint, forensics, utils) with descriptions. Use this " "first to discover what tools exist.", "parameters": {"type": "object", "properties": {}}, }, }, { "type": "function", "function": { "name": "list_tools", "description": "List the tools within a category (name, description, usage). Use after " "list_categories to drill into a category.", "parameters": { "type": "object", "properties": { "category": {"type": "string", "description": "Category name (e.g. 'recon', 'post')"}, }, "required": ["category"], }, }, }] C = {"r": "\033[0m", "b": "\033[1m", "c": "\033[36m", "g": "\033[32m", "y": "\033[33m", "m": "\033[35m"} # ---- Commando tool catalog, organized by category (lazy discovery) ---- # The model sees list_categories -> list_tools -> run_command, never the whole tree. CATALOG = { "recon": {"desc": "Network recon & scanning", "tools": [ {"name": "nmap", "desc": "Port/service/OS scanner", "usage": "nmap -sV -sC -p- "}, {"name": "masscan", "desc": "Ultra-fast port scanner", "usage": "masscan -p1-65535 --rate=1000"}, {"name": "rustscan", "desc": "Fast port scanner feeding nmap", "usage": "rustscan -a "}, {"name": "netdiscover", "desc": "ARP-based LAN discovery", "usage": "netdiscover -r "}, {"name": "arp-scan", "desc": "ARP host discovery", "usage": "arp-scan -l"}, {"name": "amass", "desc": "Subdomain enumeration", "usage": "amass enum -d "}, {"name": "sublist3r", "desc": "Subdomain enumeration", "usage": "sublist3r -d "}, {"name": "theHarvester", "desc": "Email/domain OSINT", "usage": "theHarvester -d -b all"}, {"name": "dnsrecon", "desc": "DNS reconnaissance", "usage": "dnsrecon -d "}, {"name": "whatweb", "desc": "Web tech fingerprinting", "usage": "whatweb "}, ]}, "web": {"desc": "Web app testing", "tools": [ {"name": "nikto", "desc": "Web server vuln scanner", "usage": "nikto -h "}, {"name": "gobuster", "desc": "Directory/file brute force", "usage": "gobuster dir -u -w "}, {"name": "dirb", "desc": "Directory brute force", "usage": "dirb "}, {"name": "ffuf", "desc": "Fast web fuzzer", "usage": "ffuf -u /FUZZ -w "}, {"name": "wpscan", "desc": "WordPress scanner", "usage": "wpscan --url "}, {"name": "sqlmap", "desc": "SQL injection", "usage": "sqlmap -u --dbs"}, {"name": "commix", "desc": "Command injection", "usage": "commix --url "}, ]}, "exploit": {"desc": "Exploitation frameworks", "tools": [ {"name": "msfconsole", "desc": "Metasploit framework", "usage": "msfconsole"}, {"name": "searchsploit", "desc": "Exploit-DB search", "usage": "searchsploit "}, {"name": "msfvenom", "desc": "Payload generation", "usage": "msfvenom -p LHOST= LPORT= -f exe"}, ]}, "post": {"desc": "Post-exploitation & lateral movement", "tools": [ {"name": "impacket-secretsdump", "desc": "Remote credential dump", "usage": "impacket-secretsdump /:@"}, {"name": "impacket-psexec", "desc": "Remote shell (SMB)", "usage": "impacket-psexec /:@"}, {"name": "impacket-wmiexec", "desc": "Remote shell (WMI)", "usage": "impacket-wmiexec /:@"}, {"name": "impacket-GetNPUsers", "desc": "AS-REP roast (no creds)", "usage": "impacket-GetNPUsers / -usersfile -no-pass"}, {"name": "impacket-GetUserSPNs", "desc": "Kerberoast", "usage": "impacket-GetUserSPNs /:"}, {"name": "crackmapexec", "desc": "SMB/WinRM brute & exec (also nxc)", "usage": "crackmapexec smb "}, {"name": "evil-winrm", "desc": "WinRM shell", "usage": "evil-winrm -i -u -p "}, {"name": "mimikatz", "desc": "Credential dumping", "usage": "mimikatz"}, {"name": "bloodhound-python", "desc": "AD enumeration", "usage": "bloodhound-python -d -u -p -ns "}, {"name": "responder", "desc": "LLMNR/NBT-NS poisoning", "usage": "responder -I "}, ]}, "crack": {"desc": "Password cracking & brute force", "tools": [ {"name": "hashcat", "desc": "GPU password cracking", "usage": "hashcat -m "}, {"name": "john", "desc": "Password cracking", "usage": "john --wordlist= "}, {"name": "hydra", "desc": "Network login brute force", "usage": "hydra -l -P "}, {"name": "medusa", "desc": "Parallel brute force", "usage": "medusa -h -u -P -M "}, ]}, "wireless": {"desc": "Wireless attacks", "tools": [ {"name": "aircrack-ng", "desc": "WEP/WPA cracking", "usage": "aircrack-ng -w "}, {"name": "airmon-ng", "desc": "Monitor mode", "usage": "airmon-ng start "}, {"name": "bettercap", "desc": "MITM/recon framework", "usage": "bettercap"}, ]}, "sniff": {"desc": "Sniffing & MITM", "tools": [ {"name": "tshark", "desc": "CLI packet analysis", "usage": "tshark -i "}, {"name": "tcpdump", "desc": "Packet capture", "usage": "tcpdump -i -w out.pcap"}, {"name": "mitmproxy", "desc": "HTTP MITM proxy", "usage": "mitmproxy"}, {"name": "ettercap", "desc": "MITM attacks", "usage": "ettercap -T -M arp"}, ]}, "osint": {"desc": "Open-source intelligence", "tools": [ {"name": "theHarvester", "desc": "Email/domain harvesting", "usage": "theHarvester -d -b all"}, {"name": "sherlock", "desc": "Username search across sites", "usage": "sherlock "}, {"name": "holehe", "desc": "Check email against services", "usage": "holehe "}, {"name": "spiderfoot", "desc": "OSINT automation", "usage": "spiderfoot -s "}, ]}, "forensics": {"desc": "Forensics & analysis", "tools": [ {"name": "volatility", "desc": "Memory forensics", "usage": "volatility -f "}, {"name": "binwalk", "desc": "Firmware/embedded analysis", "usage": "binwalk "}, {"name": "exiftool", "desc": "Metadata extraction", "usage": "exiftool "}, {"name": "strings", "desc": "String extraction", "usage": "strings "}, ]}, "utils": {"desc": "Utilities & networking", "tools": [ {"name": "nc", "desc": "Netcat — raw TCP/UDP", "usage": "nc -lvnp "}, {"name": "socat", "desc": "Networking relay", "usage": "socat TCP-LISTEN:,reuseaddr,fork TCP::"}, {"name": "proxychains", "desc": "Proxy chaining", "usage": "proxychains "}, {"name": "curl", "desc": "HTTP client", "usage": "curl "}, {"name": "certutil", "desc": "Download/encode (Windows)", "usage": "certutil -urlcache -f "}, {"name": "powershell", "desc": "Scripting / one-liners", "usage": "powershell -c "}, ]}, } def list_categories(): lines = [f"{cat} — {info['desc']} ({len(info['tools'])} tools)" for cat, info in CATALOG.items()] return "\n".join(lines) def list_tools(category): info = CATALOG.get(category) if not info: return (f"unknown category '{category}'. " f"Categories: {', '.join(CATALOG.keys())}") return "\n".join( f"- {t['name']}: {t['desc']}\n {t['usage']}" for t in info["tools"]) def get(path): req = urllib.request.Request(OLLAMA + path) with urllib.request.urlopen(req, timeout=30) as r: return json.load(r) def post_stream(path, payload): data = json.dumps(payload).encode() req = urllib.request.Request(OLLAMA + path, data=data, headers={"Content-Type": "application/json"}) with urllib.request.urlopen(req, timeout=120) as r: for line in r: line = line.strip() if line: yield json.loads(line) def list_models(): d = get("/api/tags") out = [] for m in d.get("models", []): ps = (m.get("details") or {}).get("parameter_size", "") or "" out.append((m["name"], ps)) return out def load_last(): try: return open(LAST_MODEL).read().strip() except Exception: return None def save_last(model): try: open(LAST_MODEL, "w").write(model) except Exception: pass def pick_model(models): last = load_last() names = [n for n, _ in models] print(f"\n{C['b']}{C['m']} METATRON{C['r']} — select a model " f"{C['y']}(smaller = faster){C['r']}\n") for i, (name, ps) in enumerate(models, 1): mark = f" {C['m']}*{C['r']}" if name == last else "" print(f" {C['c']}[{i}]{C['r']} {name} {C['g']}{ps}{C['r']}{mark}") tail = f" {C['c']}[0]{C['r']} {C['y']}quit{C['r']}" if last and last in names: tail += f" {C['y']}(Enter = {last}){C['r']}" print(tail + "\n") while True: try: sel = input(f"{C['g']}model>{C['r']} ").strip() if sel == "" and last and last in names: return last if sel in ("0", "q", "quit", "exit"): sys.exit(0) idx = int(sel) - 1 if 0 <= idx < len(models): return names[idx] except ValueError: pass print(f"{C['y']} pick a number 1-{len(models)}{C['r']}") def warm_model(model): sys.stdout.write(f"{C['y']} warming {model} ...{C['r']}") sys.stdout.flush() payload = {"model": model, "messages": [{"role": "user", "content": "hi"}], "stream": False, "think": False, "keep_alive": "30m", "options": {"num_predict": 1}} try: data = json.dumps(payload).encode() req = urllib.request.Request(OLLAMA + "/api/chat", data=data, headers={"Content-Type": "application/json"}) urllib.request.urlopen(req, timeout=120) sys.stdout.write(f"\r{C['g']} ready.{C['r']} \n") except Exception as e: sys.stdout.write(f"\r{C['y']} warm failed ({e}){C['r']} \n") def supports_tools(model): try: data = json.dumps({"model": model}).encode() req = urllib.request.Request(OLLAMA + "/api/show", data=data, headers={"Content-Type": "application/json"}) d = json.load(urllib.request.urlopen(req, timeout=15)) return "tools" in (d.get("capabilities") or []) except Exception: return False def run_command(cmd): try: r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=120) out = (r.stdout or "") + (r.stderr or "") return out.strip() or "(no output)" except Exception as e: return f"error: {e}" def web_search(query): url = f"{SEARXNG}/search?q={urllib.parse.quote(query)}&format=json" try: d = json.load(urllib.request.urlopen(url, timeout=25)) except Exception as e: return f"search error: {e}" results = d.get("results", []) if not results: return "no results" lines = [] for r in results[:8]: lines.append(f"- {r.get('title', '')}\n {r.get('url', '')}\n" f" {(r.get('content') or '')[:220]}") return "\n".join(lines) def fetch_page(url): if not re.match(r"^https?://", url): url = "https://" + url try: req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) raw = urllib.request.urlopen(req, timeout=25).read().decode("utf-8", "ignore") except Exception as e: return f"fetch error: {e}" t = re.sub(r"", " ", raw, flags=re.S) t = re.sub(r"", " ", t, flags=re.S) t = re.sub(r"<[^>]+>", " ", t) t = unescape(t) t = re.sub(r"\s+", " ", t) return t.strip()[:2000] def truncate(text, max_lines=50): lines = text.split("\n") if len(lines) > max_lines: return "\n".join(lines[:max_lines]) + f"\n [... {len(lines) - max_lines} more lines]" return text def print_help(): print(f""" {C['b']}commands{C['r']} {C['c']}!{C['r']} run a system command {C['c']}?{C['r']} search the web (SearXNG) {C['c']}?fetch {C['r']} read a page's text {C['c']}/tools{C['r']} list tool categories {C['c']}/tools {C['r']} list tools in a category {C['c']}/save{C['r']} save this session to markdown {C['c']}/help{C['r']} this list {C['c']}exit{C['r']} quit """) def save_transcript(messages, model): fn = os.path.join(os.getcwd(), f"metatron-{time.strftime('%Y%m%d-%H%M%S')}.md") try: with open(fn, "w", encoding="utf-8") as f: f.write(f"# METATRON session — {model}\n\n") for m in messages: role, content = m.get("role"), m.get("content", "") if role == "user": f.write(f"**you:** {content}\n\n") elif role == "assistant" and content: f.write(f"**metatron:** {content}\n\n") elif role == "tool": f.write(f"```\n{content}\n```\n\n") return fn except Exception as e: return f"save failed: {e}" def chat(model): messages = [{"role": "system", "content": SYSTEM_PROMPT}] use_tools = supports_tools(model) print(f"\n{C['b']} METATRON {C['c']}:: {model}{C['r']} " f"{C['y']}(!cmd, ?search, ?fetch, /tools, /save, exit){C['r']}\n") while True: try: user = input(f"{C['g']}you>{C['r']} ").strip() except (EOFError, KeyboardInterrupt): break if not user: continue if user.lower() in ("exit", "quit", "/q", "/exit"): break if user in ("/help", "help", "?"): print_help() continue if user == "/save": fn = save_transcript(messages, model) print(f" {C['g']}saved: {fn}{C['r']}\n") continue if user == "/tools": print(f"{C['c']} [categories]{C['r']}\n {list_categories()}\n") continue if user.startswith("/tools "): cat = user[7:].strip() print(f"{C['c']} [tools: {cat}]{C['r']}\n {list_tools(cat)}\n") continue if user.startswith("!"): cmd = user[1:].strip() print(f"{C['y']} $ {cmd}{C['r']}") print(f" {truncate(run_command(cmd))}\n") continue if user.startswith("?"): q = user[1:].strip() if q.startswith("fetch "): url = q[6:].strip() print(f"{C['c']} [fetch] {url}{C['r']}") print(f" {fetch_page(url)}\n") elif re.match(r"^https?://|^www\.", q): print(f"{C['c']} [fetch] {q}{C['r']}") print(f" {fetch_page(q)}\n") else: print(f"{C['c']} [search] {q}{C['r']}") print(f" {web_search(q)}\n") continue messages.append({"role": "user", "content": user}) for _ in range(6): payload = {"model": model, "messages": messages, "stream": True, "think": False, "keep_alive": "30m"} if use_tools: payload["tools"] = TOOLS buf = "" tool_calls = [] got_content = False sys.stdout.write(" ...") sys.stdout.flush() try: for obj in post_stream("/api/chat", payload): msg = obj.get("message", {}) piece = msg.get("content") or "" if piece: if not got_content: sys.stdout.write("\r ") got_content = True buf += piece sys.stdout.write(piece) sys.stdout.flush() if obj.get("done") and msg.get("tool_calls"): tool_calls = msg["tool_calls"] except Exception as e: print(f"\n{C['y']} [ollama error: {e}]{C['r']}") break if not got_content: sys.stdout.write("\r") sys.stdout.flush() if tool_calls: messages.append({"role": "assistant", "content": buf, "tool_calls": tool_calls}) for tc in tool_calls: fn = tc.get("function", {}) name = fn.get("name") raw = fn.get("arguments") or {} if isinstance(raw, str): try: args = json.loads(raw) except Exception: args = {} else: args = raw if name == "run_command": cmd = args.get("command", "") print(f"\n{C['y']} $ {cmd}{C['r']}") out = run_command(cmd) print(f" {truncate(out)}") messages.append({"role": "tool", "content": out}) elif name == "web_search": q = args.get("query", "") print(f"\n{C['c']} [search] {q}{C['r']}") out = web_search(q) print(f" {out[:600]}") messages.append({"role": "tool", "content": out}) elif name == "list_categories": out = list_categories() print(f"\n{C['c']} [categories]{C['r']}") print(f" {out}") messages.append({"role": "tool", "content": out}) elif name == "list_tools": cat = args.get("category", "") out = list_tools(cat) print(f"\n{C['c']} [tools: {cat}]{C['r']}") print(f" {out[:900]}") messages.append({"role": "tool", "content": out}) continue else: messages.append({"role": "assistant", "content": buf}) break print("\n") def main(): try: models = list_models() except Exception as e: print(f"{C['y']} can't reach Ollama at {OLLAMA}: {e}{C['r']}") sys.exit(1) if not models: print(f"{C['y']} no models on nightmare{C['r']}") sys.exit(1) model = pick_model(models) save_last(model) warm_model(model) chat(model) if __name__ == "__main__": main()