252 lines
8.4 KiB
Python
252 lines
8.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
METATRON - tools.py
|
|
Recon tool runners — all output returned as strings to feed into the LLM.
|
|
Tools used: nmap, whois, whatweb, curl, dig, nikto
|
|
OS: Parrot OS (all these tools are pre-installed or easily available)
|
|
"""
|
|
|
|
import subprocess
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# BASE RUNNER
|
|
# ─────────────────────────────────────────────
|
|
|
|
def run_tool(command: list, timeout: int = 120) -> str:
|
|
"""
|
|
Execute a shell command, return combined stdout + stderr as string.
|
|
Never crashes the program — always returns something.
|
|
"""
|
|
try:
|
|
result = subprocess.run(
|
|
command,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout
|
|
)
|
|
output = result.stdout.strip()
|
|
errors = result.stderr.strip()
|
|
|
|
if output and errors:
|
|
return output + "\n[STDERR]\n" + errors
|
|
elif output:
|
|
return output
|
|
elif errors:
|
|
return errors
|
|
else:
|
|
return "[!] Tool returned no output."
|
|
|
|
except subprocess.TimeoutExpired:
|
|
return f"[!] Timed out after {timeout}s: {' '.join(command)}"
|
|
except FileNotFoundError:
|
|
return f"[!] Tool not found: {command[0]} — install it with: sudo apt install {command[0]}"
|
|
except Exception as e:
|
|
return f"[!] Unexpected error running {command[0]}: {e}"
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# INDIVIDUAL TOOLS
|
|
# ─────────────────────────────────────────────
|
|
|
|
def run_nmap(target: str) -> str:
|
|
"""
|
|
nmap -sV -sC -T4 --open
|
|
-sV : detect service versions
|
|
-sC : run default scripts (basic vuln checks)
|
|
-T4 : aggressive timing (faster)
|
|
--open : only show open ports
|
|
"""
|
|
print(f" [*] nmap -sV -sC -T4 --open {target}")
|
|
return run_tool(["nmap", "-sV", "-sC", "-T4", "--open", target], timeout=180)
|
|
|
|
|
|
def run_whois(target: str) -> str:
|
|
"""
|
|
whois — domain registration, registrar, IP ownership info
|
|
"""
|
|
print(f" [*] whois {target}")
|
|
return run_tool(["whois", target], timeout=30)
|
|
|
|
|
|
def run_whatweb(target: str) -> str:
|
|
"""
|
|
whatweb -a 3 — fingerprint web technologies, CMS, frameworks, headers
|
|
-a 3 : aggression level 3 (active but not destructive)
|
|
"""
|
|
print(f" [*] whatweb -a 3 {target}")
|
|
return run_tool(["whatweb", "-a", "3", target], timeout=60)
|
|
|
|
|
|
def run_curl_headers(target: str) -> str:
|
|
"""
|
|
curl -sI — fetch HTTP headers only
|
|
Reveals: server software, X-Powered-By, cookies, security headers (or lack of them)
|
|
"""
|
|
print(f" [*] curl -sI http://{target}")
|
|
output = run_tool([
|
|
"curl", "-sI",
|
|
"--max-time", "10",
|
|
"--location", # follow redirects
|
|
f"http://{target}"
|
|
], timeout=20)
|
|
|
|
# also try https
|
|
https_output = run_tool([
|
|
"curl", "-sI",
|
|
"--max-time", "10",
|
|
"--location",
|
|
"-k", # ignore cert errors
|
|
f"https://{target}"
|
|
], timeout=20)
|
|
|
|
return f"[HTTP Headers]\n{output}\n\n[HTTPS Headers]\n{https_output}"
|
|
|
|
|
|
def run_dig(target: str) -> str:
|
|
"""
|
|
dig — DNS records: A, MX, NS, TXT
|
|
Useful for subdomains, mail servers, SPF/DKIM info
|
|
"""
|
|
print(f" [*] dig {target} ANY")
|
|
a_record = run_tool(["dig", "+short", "A", target], timeout=15)
|
|
mx_record = run_tool(["dig", "+short", "MX", target], timeout=15)
|
|
ns_record = run_tool(["dig", "+short", "NS", target], timeout=15)
|
|
txt_record= run_tool(["dig", "+short", "TXT", target], timeout=15)
|
|
|
|
return (
|
|
f"[A Records]\n{a_record}\n\n"
|
|
f"[MX Records]\n{mx_record}\n\n"
|
|
f"[NS Records]\n{ns_record}\n\n"
|
|
f"[TXT Records]\n{txt_record}"
|
|
)
|
|
|
|
|
|
def run_nikto(target: str) -> str:
|
|
"""
|
|
nikto -h — web server vulnerability scanner
|
|
Checks for outdated software, dangerous files, misconfigurations
|
|
WARNING: noisy tool, only run with permission
|
|
"""
|
|
print(f" [*] nikto -h {target} (this may take a while...)")
|
|
return run_tool(["nikto", "-h", target, "-nointeractive"], timeout=300)
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# MAIN RECON PIPELINE
|
|
# ─────────────────────────────────────────────
|
|
|
|
TOOLS_MENU = {
|
|
"1": ("nmap", run_nmap),
|
|
"2": ("whois", run_whois),
|
|
"3": ("whatweb", run_whatweb),
|
|
"4": ("curl headers", run_curl_headers),
|
|
"5": ("dig DNS", run_dig),
|
|
"6": ("nikto", run_nikto),
|
|
}
|
|
|
|
|
|
def run_default_recon(target: str) -> dict:
|
|
"""
|
|
Run the standard recon pipeline (everything except nikto).
|
|
Returns a dict of {tool_name: output_string}.
|
|
Nikto is excluded by default — too slow/noisy for auto-run.
|
|
"""
|
|
print(f"\n[*] Starting recon on: {target}")
|
|
print("─" * 50)
|
|
|
|
results = {}
|
|
results["nmap"] = run_nmap(target)
|
|
results["whois"] = run_whois(target)
|
|
results["whatweb"] = run_whatweb(target)
|
|
results["curl_headers"] = run_curl_headers(target)
|
|
results["dig"] = run_dig(target)
|
|
|
|
print("─" * 50)
|
|
print("[+] Recon complete.\n")
|
|
return results
|
|
|
|
|
|
def run_single_tool(tool_key: str, target: str) -> str:
|
|
"""Run one tool by its menu key. Used by AI tool dispatch."""
|
|
if tool_key in TOOLS_MENU:
|
|
name, func = TOOLS_MENU[tool_key]
|
|
return func(target)
|
|
return f"[!] Unknown tool key: {tool_key}"
|
|
|
|
|
|
def format_recon_for_llm(results: dict) -> str:
|
|
"""
|
|
Flatten the recon results dict into one clean string
|
|
to paste into the LLM prompt.
|
|
"""
|
|
output = ""
|
|
for tool, data in results.items():
|
|
output += f"\n{'='*50}\n"
|
|
output += f"[ {tool.upper()} OUTPUT ]\n"
|
|
output += f"{'='*50}\n"
|
|
output += data.strip() + "\n"
|
|
return output
|
|
|
|
|
|
ALLOWED_TOOLS = {"nmap", "whois", "whatweb", "curl", "dig", "nikto"}
|
|
|
|
def run_tool_by_command(command_str: str) -> str:
|
|
parts = command_str.strip().split()
|
|
if not parts:
|
|
return "[!] Empty command."
|
|
|
|
# allowlist only — reject anything not in the list
|
|
tool = parts[0].lower().split("/")[-1] # handles /bin/nmap etc
|
|
if tool not in ALLOWED_TOOLS:
|
|
return f"[!] Tool '{parts[0]}' is not permitted. Allowed: {ALLOWED_TOOLS}"
|
|
|
|
return run_tool(parts)
|
|
|
|
# ─────────────────────────────────────────────
|
|
# INTERACTIVE TOOL SELECTOR (called from CLI)
|
|
# ─────────────────────────────────────────────
|
|
|
|
def interactive_tool_run(target: str) -> str:
|
|
"""
|
|
Let user manually pick which tools to run.
|
|
Returns combined output string.
|
|
"""
|
|
print("\n[ SELECT TOOLS TO RUN ]")
|
|
for key, (name, _) in TOOLS_MENU.items():
|
|
print(f" [{key}] {name}")
|
|
print(" [a] Run all (except nikto)")
|
|
print(" [n] Run all + nikto (slow)")
|
|
|
|
choice = input("\nChoice(s) e.g. 1 2 4 or a: ").strip().lower()
|
|
|
|
if choice == "a":
|
|
results = run_default_recon(target)
|
|
return format_recon_for_llm(results)
|
|
|
|
if choice == "n":
|
|
results = run_default_recon(target)
|
|
results["nikto"] = run_nikto(target)
|
|
return format_recon_for_llm(results)
|
|
|
|
combined = {}
|
|
for key in choice.split():
|
|
if key in TOOLS_MENU:
|
|
name, func = TOOLS_MENU[key]
|
|
print(f"\n[*] Running {name}...")
|
|
combined[name] = func(target)
|
|
else:
|
|
print(f"[!] Unknown option: {key}")
|
|
|
|
return format_recon_for_llm(combined)
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# QUICK TEST
|
|
# ─────────────────────────────────────────────
|
|
|
|
if __name__ == "__main__":
|
|
target = input("Enter test target (IP or domain): ").strip()
|
|
results = run_default_recon(target)
|
|
print(format_recon_for_llm(results))
|