chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:30:44 +08:00
commit 4a2225c0de
18 changed files with 2643 additions and 0 deletions
+216
View File
@@ -0,0 +1,216 @@
reports/
exports/
venv/
__pycache__/
*.pyc# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
#poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
#pdm.lock
#pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
#pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
reports/
exports/
venv/
__pycache__/
*.pyc
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 sooryathejas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+5
View File
@@ -0,0 +1,5 @@
FROM huihui_ai/qwen3.5-abliterated:9b
PARAMETER num_ctx 16384
PARAMETER temperature 0.7
PARAMETER top_k 10
PARAMETER top_p 0.9
+355
View File
@@ -0,0 +1,355 @@
# METATRON
AI-powered penetration testing assistant using local LLM on linux (Parrot OS)
# 🔱 METATRON
### AI-Powered Penetration Testing Assistant
<p align="center">
<img src="screenshots/banner.png" alt="Metatron Banner" width="800"/>
</p>
<p align="center">
<img src="https://img.shields.io/badge/Python-3.x-blue?style=for-the-badge&logo=python"/>
<img src="https://img.shields.io/badge/OS-Parrot%20Linux-green?style=for-the-badge&logo=linux"/>
<img src="https://img.shields.io/badge/AI-metatron--qwen-red?style=for-the-badge"/>
<img src="https://img.shields.io/badge/DB-MariaDB-orange?style=for-the-badge&logo=mariadb"/>
<img src="https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge"/>
</p>
---
## 📌 What is Metatron?
**Metatron** is a CLI-based AI penetration testing assistant that runs entirely on your local machine — no cloud, no API keys, no subscriptions.
You give it a target IP or domain. It runs real recon tools (nmap, whois, whatweb, curl, dig, nikto), feeds all results to a locally running AI model, and the AI analyzes the target, identifies vulnerabilities, suggests exploits, and recommends fixes. Everything gets saved to a MariaDB database with full scan history.
---
## ✨ Features
- 🤖 **Local AI Analysis** — powered by `metatron-qwen` via Ollama, runs 100% offline
- 🔍 **Automated Recon** — nmap, whois, whatweb, curl headers, dig DNS, nikto
- 🌐 **Web Search** — DuckDuckGo search + CVE lookup (no API key needed)
- 🗄️ **MariaDB Backend** — full scan history with 5 linked tables
- ✏️ **Edit / Delete** — modify any saved result directly from the CLI
- 🔁 **Agentic Loop** — AI can request more tool runs mid-analysis
- 🚫 **No API Keys** — everything is free and local
-📤 Export Reports
Metatron allows you to export scan results into clean, shareable report formats by selecting '2.view history'->select slno and export
📄 PDF — professional vulnerability reports
🌐 HTML — browser-viewable reports
---
## 🖥️ Screenshots
<p align="center">
<img src="screenshots/main_menu.png" alt="Main Menu" width="700"/>
<br><i>Main Menu</i>
</p>
<p align="center">
<img src="screenshots/scan_running.png" alt="Scan Running" width="700"/>
<br><i>Recon tools running on target</i>
</p>
<p align="center">
<img src="screenshots/ai_analysis.png" alt="AI Analysis" width="700"/>
<br><i>metatron-qwen analyzing scan results</i>
</p>
<p align="center">
<img src="screenshots/results.png" alt="Results" width="700"/>
<br><i>Vulnerabilities saved to database</i>
</p>
<p align="center"> <img src="screenshots/export_menu.png" alt="Export Menu" width="700"/> <br><i>Export scan results as PDF and or HTML</i> </p>
---
## 🧱 Tech Stack
| Component | Technology |
|------------|-------------------------------------|
| Language | Python 3 |
| AI Model | metatron-qwen (fine-tuned Qwen 3.5) |
| Base Model | huihui_ai/qwen3.5-abliterated:9b |
| LLM Runner | Ollama |
| Database | MariaDB |
| OS | Parrot OS (Debian-based) |
| Search | DuckDuckGo (free, no key) |
---
## ⚙️ Installation
### 1. Clone the repository
```bash
git clone https://github.com/sooryathejas/METATRON.git
cd METATRON
```
### 2. Create and activate virtual environment
```bash
python3 -m venv venv
source venv/bin/activate
```
### 3. Install Python dependencies
```bash
pip install -r requirements.txt
```
### 4. Install system tools
```bash
sudo apt install nmap whois whatweb curl dnsutils nikto
```
---
## 🤖 AI Model Setup
### Step 1 — Install Ollama
```bash
curl -fsSL https://ollama.com/install.sh | sh
```
### Step 2 — Download the base model
```bash
ollama pull huihui_ai/qwen3.5-abliterated:9b
```
> ⚠️ This model requires at least 8.4 GB of RAM. If your system has less, use the 4b variant:
> ```bash
> ollama pull huihui_ai/qwen3.5-abliterated:4b
> ```
> Then edit `Modelfile` and change the FROM line to the 4b model.
### Step 3 — Build the custom metatron-qwen model
The repo includes a `Modelfile` that fine-tunes the base model with pentest-specific parameters:
```bash
ollama create metatron-qwen -f Modelfile
```
This creates your local `metatron-qwen` model with:
- 16,384 token context window
- Temperature: 0.7
- Top-k: 10
- Top-p: 0.9
### Step 4 — Verify the model exists
```bash
ollama list
```
You should see `metatron-qwen` in the list.
---
## 🗄️ Database Setup
### Step 1 — Make sure MariaDB is running
```bash
sudo systemctl start mariadb
sudo systemctl enable mariadb
```
### Step 2 — Create the database and user
```bash
mysql -u root
```
```sql
CREATE DATABASE metatron;
CREATE USER 'metatron'@'localhost' IDENTIFIED BY '123';
GRANT ALL PRIVILEGES ON metatron.* TO 'metatron'@'localhost';
FLUSH PRIVILEGES;
EXIT;
```
### Step 3 — Create the tables
```bash
mysql -u metatron -p123 metatron
```
```sql
CREATE TABLE history (
sl_no INT AUTO_INCREMENT PRIMARY KEY,
target VARCHAR(255) NOT NULL,
scan_date DATETIME NOT NULL,
status VARCHAR(50) DEFAULT 'active'
);
CREATE TABLE vulnerabilities (
id INT AUTO_INCREMENT PRIMARY KEY,
sl_no INT,
vuln_name TEXT,
severity VARCHAR(50),
port VARCHAR(20),
service VARCHAR(100),
description TEXT,
FOREIGN KEY (sl_no) REFERENCES history(sl_no)
);
CREATE TABLE fixes (
id INT AUTO_INCREMENT PRIMARY KEY,
sl_no INT,
vuln_id INT,
fix_text TEXT,
source VARCHAR(50),
FOREIGN KEY (sl_no) REFERENCES history(sl_no),
FOREIGN KEY (vuln_id) REFERENCES vulnerabilities(id)
);
CREATE TABLE exploits_attempted (
id INT AUTO_INCREMENT PRIMARY KEY,
sl_no INT,
exploit_name TEXT,
tool_used TEXT,
payload LONGTEXT,
result TEXT,
notes TEXT,
FOREIGN KEY (sl_no) REFERENCES history(sl_no)
);
CREATE TABLE summary (
id INT AUTO_INCREMENT PRIMARY KEY,
sl_no INT,
raw_scan LONGTEXT,
ai_analysis LONGTEXT,
risk_level VARCHAR(50),
generated_at DATETIME,
FOREIGN KEY (sl_no) REFERENCES history(sl_no)
);
```
---
## 🚀 Usage
Metatron needs **two terminal tabs** to run.
### Terminal 1 — Load the AI model
```bash
ollama run metatron-qwen
```
Wait until you see the `>>>` prompt. This means the model is loaded into memory and ready. You can leave this terminal running in the background.
### Terminal 2 — Launch Metatron
```bash
cd ~/METATRON
source venv/bin/activate
python metatron.py
```
---
### Walkthrough
**1. Main menu appears:**
```
[1] New Scan
[2] View History
[3] Exit
```
**2. Select [1] New Scan → enter your target:**
```
[?] Enter target IP or domain: 192.168.1.1
```
or
```
[?] Enter target IP or domain: example.com
```
**3. Select recon tools to run:**
```
[1] nmap
[2] whois
[3] whatweb
[4] curl headers
[5] dig DNS
[6] nikto
[a] Run all (except nikto)
[n] Run all + nikto (slow)
```
**4. Metatron runs the tools, feeds results to the AI, and prints the analysis.**
**5. Everything is saved to MariaDB automatically.**
**6. After the scan you can edit or delete any result.**
---
## 📁 Project Structure
```
METATRON/
├── metatron.py ← main CLI entry point
├── db.py ← MariaDB connection and all CRUD operations
├── tools.py ← recon tool runners (nmap, whois, etc.)
├── llm.py ← Ollama interface and AI tool dispatch loop
├── search.py ← DuckDuckGo web search and CVE lookup
├── Modelfile ← custom model config for metatron-qwen
├── requirements.txt ← Python dependencies
├── .gitignore ← excludes venv, pycache, db files
├── LICENSE ← MIT License
├── README.md ← this file
└── screenshots/ ← terminal screenshots for documentation
```
---
## 🗃️ Database Schema
All 5 tables are linked by `sl_no` (session number) from the `history` table:
```
history ← one row per scan session (sl_no is the spine)
├── vulnerabilities ← vulns found, linked by sl_no
│ │
│ └── fixes ← fixes per vuln, linked by vuln_id + sl_no
├── exploits_attempted ← exploits tried, linked by sl_no
└── summary ← full AI analysis dump, linked by sl_no
```
---
## ⚠️ Disclaimer
This tool is intended for **educational purposes and authorized penetration testing only**.
- Only use Metatron on systems you own or have **explicit written permission** to test.
- Unauthorized scanning or exploitation of systems is **illegal**.
- The author is not responsible for any misuse of this tool.
---
## 👤 Author
**Soorya Thejas**
- GitHub: [@sooryathejas](https://github.com/sooryathejas)
---
## 📄 License
This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`sooryathejas/METATRON`
- 原始仓库:https://github.com/sooryathejas/METATRON
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+351
View File
@@ -0,0 +1,351 @@
#!/usr/bin/env python3
"""
METATRON - db.py
MariaDB connection + all read/write/edit/delete operations
Database: metatron
"""
import mysql.connector
from datetime import datetime
# ─────────────────────────────────────────────
# CONNECTION
# ─────────────────────────────────────────────
def get_connection():
"""Returns a MariaDB connection. No password (local setup)."""
return mysql.connector.connect(
host="localhost",
user="metatron",
password="123",
database="metatron"
)
# ─────────────────────────────────────────────
# WRITE FUNCTIONS
# ─────────────────────────────────────────────
def create_session(target: str) -> int:
"""Insert new row into history. Returns sl_no."""
conn = get_connection()
c = conn.cursor()
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
c.execute(
"INSERT INTO history (target, scan_date, status) VALUES (%s, %s, %s)",
(target, now, "active")
)
conn.commit()
sl_no = c.lastrowid
conn.close()
return sl_no
def save_vulnerability(sl_no: int, vuln_name: str, severity: str,
port: str, service: str, description: str) -> int:
"""Insert a vulnerability. Returns its id."""
conn = get_connection()
c = conn.cursor()
c.execute("""
INSERT INTO vulnerabilities (sl_no, vuln_name, severity, port, service, description)
VALUES (%s, %s, %s, %s, %s, %s)
""", (sl_no, vuln_name, severity, port, service, description))
conn.commit()
vuln_id = c.lastrowid
conn.close()
return vuln_id
def save_fix(sl_no: int, vuln_id: int, fix_text: str, source: str = "ai"):
"""Insert a fix linked to a vulnerability."""
conn = get_connection()
c = conn.cursor()
c.execute("""
INSERT INTO fixes (sl_no, vuln_id, fix_text, source)
VALUES (%s, %s, %s, %s)
""", (sl_no, vuln_id, fix_text, source))
conn.commit()
conn.close()
def save_exploit(sl_no, exploit_name, tool_used, payload, result, notes):
conn = get_connection()
c = conn.cursor()
c.execute("""
INSERT INTO exploits_attempted
(sl_no, exploit_name, tool_used, payload, result, notes)
VALUES (%s, %s, %s, %s, %s, %s)
""", (
sl_no,
str(exploit_name or "")[:1000],
str(tool_used or "")[:500],
str(payload or ""),
str(result or "")[:2000],
str(notes or "")
))
conn.commit()
conn.close()
def save_summary(sl_no: int, raw_scan: str, ai_analysis: str, risk_level: str):
"""Insert the full session summary."""
conn = get_connection()
c = conn.cursor()
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
c.execute("""
INSERT INTO summary (sl_no, raw_scan, ai_analysis, risk_level, generated_at)
VALUES (%s, %s, %s, %s, %s)
""", (sl_no, raw_scan, ai_analysis, risk_level, now))
conn.commit()
conn.close()
# ─────────────────────────────────────────────
# READ FUNCTIONS
# ─────────────────────────────────────────────
def get_all_history():
"""Return all rows from history ordered by newest first."""
conn = get_connection()
c = conn.cursor()
c.execute("SELECT sl_no, target, scan_date, status FROM history ORDER BY sl_no DESC")
rows = c.fetchall()
conn.close()
return rows
def get_session(sl_no: int) -> dict:
"""Return everything linked to a sl_no across all tables."""
conn = get_connection()
c = conn.cursor()
c.execute("SELECT * FROM history WHERE sl_no = %s", (sl_no,))
history = c.fetchone()
c.execute("SELECT * FROM vulnerabilities WHERE sl_no = %s", (sl_no,))
vulns = c.fetchall()
c.execute("SELECT * FROM fixes WHERE sl_no = %s", (sl_no,))
fixes = c.fetchall()
c.execute("SELECT * FROM exploits_attempted WHERE sl_no = %s", (sl_no,))
exploits = c.fetchall()
c.execute("SELECT * FROM summary WHERE sl_no = %s", (sl_no,))
summary = c.fetchone()
conn.close()
return {
"history": history,
"vulns": vulns,
"fixes": fixes,
"exploits": exploits,
"summary": summary
}
def get_vulnerabilities(sl_no: int):
conn = get_connection()
c = conn.cursor()
c.execute("SELECT * FROM vulnerabilities WHERE sl_no = %s", (sl_no,))
rows = c.fetchall()
conn.close()
return rows
def get_fixes(sl_no: int):
conn = get_connection()
c = conn.cursor()
c.execute("SELECT * FROM fixes WHERE sl_no = %s", (sl_no,))
rows = c.fetchall()
conn.close()
return rows
def get_exploits(sl_no: int):
conn = get_connection()
c = conn.cursor()
c.execute("SELECT * FROM exploits_attempted WHERE sl_no = %s", (sl_no,))
rows = c.fetchall()
conn.close()
return rows
# ─────────────────────────────────────────────
# EDIT FUNCTIONS
# ─────────────────────────────────────────────
def edit_vulnerability(vuln_id: int, field: str, value: str):
"""Edit a single field in vulnerabilities by id."""
allowed = {"vuln_name", "severity", "port", "service", "description"}
if field not in allowed:
print(f"[!] Invalid field: {field}. Allowed: {allowed}")
return
conn = get_connection()
c = conn.cursor()
c.execute(
f"UPDATE vulnerabilities SET {field} = %s WHERE id = %s",
(value, vuln_id)
)
conn.commit()
conn.close()
print(f"[+] vulnerabilities.{field} updated for id={vuln_id}")
def edit_fix(fix_id: int, fix_text: str):
"""Edit the fix_text of a fix by id."""
conn = get_connection()
c = conn.cursor()
c.execute("UPDATE fixes SET fix_text = %s WHERE id = %s", (fix_text, fix_id))
conn.commit()
conn.close()
print(f"[+] fix id={fix_id} updated.")
def edit_exploit(exploit_id: int, field: str, value: str):
"""Edit a single field in exploits_attempted by id."""
allowed = {"exploit_name", "tool_used", "payload", "result", "notes"}
if field not in allowed:
print(f"[!] Invalid field: {field}. Allowed: {allowed}")
return
conn = get_connection()
c = conn.cursor()
c.execute(
f"UPDATE exploits_attempted SET {field} = %s WHERE id = %s",
(value, exploit_id)
)
conn.commit()
conn.close()
print(f"[+] exploits_attempted.{field} updated for id={exploit_id}")
def edit_summary_risk(sl_no: int, risk_level: str):
"""Update the risk level on a summary."""
conn = get_connection()
c = conn.cursor()
c.execute("UPDATE summary SET risk_level = %s WHERE sl_no = %s", (risk_level, sl_no))
conn.commit()
conn.close()
print(f"[+] Summary risk_level updated for SL#{sl_no}")
# ─────────────────────────────────────────────
# DELETE FUNCTIONS
# ─────────────────────────────────────────────
def delete_vulnerability(vuln_id: int):
"""Delete a single vulnerability and its linked fixes."""
conn = get_connection()
c = conn.cursor()
c.execute("DELETE FROM fixes WHERE vuln_id = %s", (vuln_id,))
c.execute("DELETE FROM vulnerabilities WHERE id = %s", (vuln_id,))
conn.commit()
conn.close()
print(f"[+] Vulnerability id={vuln_id} and its fixes deleted.")
def delete_exploit(exploit_id: int):
"""Delete a single exploit attempt."""
conn = get_connection()
c = conn.cursor()
c.execute("DELETE FROM exploits_attempted WHERE id = %s", (exploit_id,))
conn.commit()
conn.close()
print(f"[+] Exploit id={exploit_id} deleted.")
def delete_fix(fix_id: int):
"""Delete a single fix."""
conn = get_connection()
c = conn.cursor()
c.execute("DELETE FROM fixes WHERE id = %s", (fix_id,))
conn.commit()
conn.close()
print(f"[+] Fix id={fix_id} deleted.")
def delete_full_session(sl_no: int):
"""
Wipe everything linked to a sl_no across all 5 tables.
Order matters — delete children before parent (FK constraints).
"""
conn = get_connection()
c = conn.cursor()
c.execute("DELETE FROM fixes WHERE sl_no = %s", (sl_no,))
c.execute("DELETE FROM exploits_attempted WHERE sl_no = %s", (sl_no,))
c.execute("DELETE FROM vulnerabilities WHERE sl_no = %s", (sl_no,))
c.execute("DELETE FROM summary WHERE sl_no = %s", (sl_no,))
c.execute("DELETE FROM history WHERE sl_no = %s", (sl_no,))
conn.commit()
conn.close()
print(f"[+] Full session SL#{sl_no} deleted from all tables.")
# ─────────────────────────────────────────────
# DISPLAY HELPERS
# ─────────────────────────────────────────────
def print_history(rows):
print("\n" + ""*65)
print(f"{'SL#':<6} {'TARGET':<28} {'DATE':<22} {'STATUS'}")
print(""*65)
for row in rows:
print(f"{row[0]:<6} {row[1]:<28} {str(row[2]):<22} {row[3]}")
print()
def print_session(data: dict):
h = data["history"]
print(f"\n{''*60}")
print(f" SL# {h[0]} | Target: {h[1]} | {h[2]} | {h[3]}")
print(f"{''*60}")
print("\n[ VULNERABILITIES ]")
if data["vulns"]:
for v in data["vulns"]:
print(f" id={v[0]} | {v[2]} | Severity: {v[3]} | Port: {v[4]} | Service: {v[5]}")
print(f" {v[6]}")
else:
print(" None recorded.")
print("\n[ FIXES ]")
if data["fixes"]:
for f in data["fixes"]:
print(f" id={f[0]} | vuln_id={f[2]} | [{f[4]}] {f[3]}")
else:
print(" None recorded.")
print("\n[ EXPLOITS ATTEMPTED ]")
if data["exploits"]:
for e in data["exploits"]:
print(f" id={e[0]} | {e[2]} | Tool: {e[3]} | Result: {e[5]}")
print(f" Payload: {e[4]}")
print(f" Notes: {e[6]}")
else:
print(" None recorded.")
print("\n[ SUMMARY ]")
if data["summary"]:
s = data["summary"]
print(f" Risk Level : {s[4]}")
print(f" Generated : {s[5]}")
print(f"\n AI Analysis:\n {s[3][:500]}{'...' if len(str(s[3])) > 500 else ''}")
else:
print(" None recorded.")
print()
# ─────────────────────────────────────────────
# QUICK CONNECTION TEST
# ─────────────────────────────────────────────
if __name__ == "__main__":
try:
conn = get_connection()
print("[+] MariaDB connection successful.")
print("[+] Database: metatron")
conn.close()
except Exception as e:
print(f"[!] Connection failed: {e}")
+421
View File
@@ -0,0 +1,421 @@
#!/usr/bin/env python3
import os
import datetime
import mysql.connector
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable
from reportlab.lib.enums import TA_CENTER
SEVERITY_COLORS = {
"critical": "#c0392b",
"high": "#e67e22",
"medium": "#f1c40f",
"low": "#27ae60",
"unknown": "#7f8c8d",
}
RISK_COLORS = {
"CRITICAL": "#c0392b",
"HIGH": "#e67e22",
"MEDIUM": "#f1c40f",
"LOW": "#27ae60",
"UNKNOWN": "#7f8c8d",
}
def get_connection():
return mysql.connector.connect(
host="localhost",
user="metatron",
password="123",
database="metatron"
)
def fetch_session(sl_no: int) -> dict:
conn = get_connection()
c = conn.cursor()
c.execute("SELECT * FROM history WHERE sl_no = %s", (sl_no,))
history = c.fetchone()
c.execute("SELECT * FROM vulnerabilities WHERE sl_no = %s", (sl_no,))
vulns = c.fetchall()
c.execute("SELECT * FROM fixes WHERE sl_no = %s", (sl_no,))
fixes = c.fetchall()
c.execute("SELECT * FROM exploits_attempted WHERE sl_no = %s", (sl_no,))
exploits = c.fetchall()
c.execute("SELECT * FROM summary WHERE sl_no = %s", (sl_no,))
summary = c.fetchone()
conn.close()
return {"history": history, "vulns": vulns, "fixes": fixes,
"exploits": exploits, "summary": summary}
def fetch_all_history():
conn = get_connection()
c = conn.cursor()
c.execute("SELECT sl_no, target, scan_date, status FROM history ORDER BY sl_no DESC")
rows = c.fetchall()
conn.close()
return rows
def export_pdf(data: dict, output_dir: str) -> str:
h = data["history"]
sl = h[0]
tgt = h[1]
date = str(h[2])
risk = data["summary"][4] if data["summary"] else "UNKNOWN"
ai = data["summary"][3] if data["summary"] else ""
os.makedirs(output_dir, exist_ok=True)
safe = tgt.replace("https://","").replace("http://","").replace("/","_").replace(".","_")
filename = os.path.join(output_dir, f"metatron_SL{sl}_{safe}.pdf")
doc = SimpleDocTemplate(filename, pagesize=A4,
topMargin=15*mm, bottomMargin=15*mm,
leftMargin=15*mm, rightMargin=15*mm)
title_style = ParagraphStyle("t", fontSize=22, fontName="Helvetica-Bold",
textColor=colors.HexColor("#c0392b"), spaceAfter=4)
sub_style = ParagraphStyle("s", fontSize=10, fontName="Helvetica",
textColor=colors.HexColor("#555555"), spaceAfter=2)
h1_style = ParagraphStyle("h1", fontSize=13, fontName="Helvetica-Bold",
textColor=colors.HexColor("#2c3e50"),
spaceBefore=10, spaceAfter=4)
body_style = ParagraphStyle("b", fontSize=9, fontName="Helvetica",
textColor=colors.black, leading=13)
code_style = ParagraphStyle("c", fontSize=7.5, fontName="Courier",
textColor=colors.HexColor("#2c3e50"),
backColor=colors.HexColor("#f4f4f4"),
leading=11, leftIndent=6, rightIndent=6,
spaceBefore=2, spaceAfter=2)
footer_style = ParagraphStyle("f", fontSize=7,
textColor=colors.HexColor("#aaaaaa"),
alignment=TA_CENTER)
story = []
story.append(Paragraph("METATRON", title_style))
story.append(Paragraph("AI Penetration Testing Report", sub_style))
story.append(HRFlowable(width="100%", thickness=1.5,
color=colors.HexColor("#c0392b"), spaceAfter=8))
risk_color = colors.HexColor(RISK_COLORS.get(risk.upper(), "#7f8c8d"))
meta = [["Target", tgt], ["Scan Date", date],
["Session", f"SL# {sl}"], ["Risk Level", risk],
["Generated", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")]]
mt = Table(meta, colWidths=[35*mm, 130*mm])
mt.setStyle(TableStyle([
("FONTNAME", (0,0), (-1,-1), "Helvetica"),
("FONTSIZE", (0,0), (-1,-1), 9),
("FONTNAME", (0,0), (0,-1), "Helvetica-Bold"),
("TEXTCOLOR", (0,0), (0,-1), colors.HexColor("#2c3e50")),
("TEXTCOLOR", (1,3), (1,3), risk_color),
("FONTNAME", (1,3), (1,3), "Helvetica-Bold"),
("ROWBACKGROUNDS", (0,0), (-1,-1), [colors.HexColor("#f9f9f9"), colors.white]),
("GRID", (0,0), (-1,-1), 0.3, colors.HexColor("#dddddd")),
("PADDING", (0,0), (-1,-1), 5),
]))
story.append(mt)
story.append(Spacer(1, 10))
story.append(Paragraph("Vulnerabilities", h1_style))
story.append(HRFlowable(width="100%", thickness=0.5,
color=colors.HexColor("#dddddd"), spaceAfter=6))
if data["vulns"]:
vd = [["#", "Vulnerability", "Severity", "Port", "Service"]]
for v in data["vulns"]:
vd.append([str(v[0]), str(v[2] or "-"),
str(v[3] or "-").upper(), str(v[4] or "-"), str(v[5] or "-")])
vt = Table(vd, colWidths=[10*mm, 72*mm, 24*mm, 18*mm, 28*mm], repeatRows=1)
vts = [
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), 8),
("BACKGROUND", (0,0), (-1,0), colors.HexColor("#2c3e50")),
("TEXTCOLOR", (0,0), (-1,0), colors.white),
("GRID", (0,0), (-1,-1), 0.3, colors.HexColor("#dddddd")),
("PADDING", (0,0), (-1,-1), 5),
("ROWBACKGROUNDS", (0,1), (-1,-1), [colors.HexColor("#f9f9f9"), colors.white]),
]
for i, v in enumerate(data["vulns"], 1):
sc = colors.HexColor(SEVERITY_COLORS.get((v[3] or "unknown").lower(), "#7f8c8d"))
vts.append(("TEXTCOLOR", (2,i), (2,i), sc))
vts.append(("FONTNAME", (2,i), (2,i), "Helvetica-Bold"))
vt.setStyle(TableStyle(vts))
story.append(vt)
story.append(Spacer(1, 6))
story.append(Paragraph("Vulnerability Details", h1_style))
story.append(HRFlowable(width="100%", thickness=0.5,
color=colors.HexColor("#dddddd"), spaceAfter=6))
for v in data["vulns"]:
sc = colors.HexColor(SEVERITY_COLORS.get((v[3] or "unknown").lower(), "#7f8c8d"))
lbl = ParagraphStyle("vl", fontSize=9, fontName="Helvetica-Bold", textColor=sc)
story.append(Paragraph(f"[{(v[3] or 'UNKNOWN').upper()}] {v[2]}", lbl))
if v[6]:
story.append(Paragraph(str(v[6]), body_style))
story.append(Spacer(1, 4))
else:
story.append(Paragraph("No vulnerabilities recorded.", body_style))
story.append(Spacer(1, 6))
story.append(Paragraph("Fixes & Mitigations", h1_style))
story.append(HRFlowable(width="100%", thickness=0.5,
color=colors.HexColor("#dddddd"), spaceAfter=6))
if data["fixes"]:
for f in data["fixes"]:
story.append(Paragraph(f"Fix for vuln id={f[2]}:", body_style))
story.append(Paragraph(str(f[3] or "-"), code_style))
story.append(Spacer(1, 3))
else:
story.append(Paragraph("No fixes recorded.", body_style))
story.append(Spacer(1, 6))
story.append(Paragraph("Exploits Attempted", h1_style))
story.append(HRFlowable(width="100%", thickness=0.5,
color=colors.HexColor("#dddddd"), spaceAfter=6))
if data["exploits"]:
ed = [["#", "Exploit", "Tool", "Result"]]
for e in data["exploits"]:
ed.append([str(e[0]), str(e[2] or "-")[:60],
str(e[3] or "-")[:30], str(e[5] or "-")[:30]])
et = Table(ed, colWidths=[10*mm, 80*mm, 40*mm, 28*mm])
et.setStyle(TableStyle([
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTSIZE", (0,0), (-1,-1), 8),
("BACKGROUND", (0,0), (-1,0), colors.HexColor("#2c3e50")),
("TEXTCOLOR", (0,0), (-1,0), colors.white),
("GRID", (0,0), (-1,-1), 0.3, colors.HexColor("#dddddd")),
("PADDING", (0,0), (-1,-1), 5),
("ROWBACKGROUNDS", (0,1), (-1,-1), [colors.HexColor("#f9f9f9"), colors.white]),
]))
story.append(et)
else:
story.append(Paragraph("No exploits recorded.", body_style))
story.append(Spacer(1, 6))
story.append(Paragraph("AI Analysis Summary", h1_style))
story.append(HRFlowable(width="100%", thickness=0.5,
color=colors.HexColor("#dddddd"), spaceAfter=6))
if ai:
for line in str(ai).split("\n"):
line = line.strip()
if line:
story.append(Paragraph(line, body_style))
story.append(Spacer(1, 2))
else:
story.append(Paragraph("No AI analysis recorded.", body_style))
story.append(Spacer(1, 10))
story.append(HRFlowable(width="100%", thickness=0.5,
color=colors.HexColor("#dddddd"), spaceAfter=4))
story.append(Paragraph(
"Generated by METATRON — AI Penetration Testing Assistant | "
"github.com/sooryathejas/METATRON | For authorized use only.",
footer_style))
doc.build(story)
return filename
def export_html(data: dict, output_dir: str) -> str:
h = data["history"]
sl = h[0]
tgt = h[1]
date = str(h[2])
risk = data["summary"][4] if data["summary"] else "UNKNOWN"
ai = data["summary"][3] if data["summary"] else ""
rc = RISK_COLORS.get(risk.upper(), "#7f8c8d")
os.makedirs(output_dir, exist_ok=True)
safe = tgt.replace("https://","").replace("http://","").replace("/","_").replace(".","_")
filename = os.path.join(output_dir, f"metatron_SL{sl}_{safe}.html")
vuln_rows = ""
for v in data["vulns"]:
sc = SEVERITY_COLORS.get((v[3] or "unknown").lower(), "#7f8c8d")
vuln_rows += (f"<tr><td>{v[0]}</td>"
f"<td><strong>{v[2]}</strong><br><small>{v[6] or ''}</small></td>"
f"<td><span style='color:{sc};font-weight:bold'>"
f"{(v[3] or 'unknown').upper()}</span></td>"
f"<td>{v[4] or '-'}</td><td>{v[5] or '-'}</td></tr>")
fix_rows = ""
for f in data["fixes"]:
fix_rows += (f"<tr><td>{f[0]}</td><td>vuln #{f[2]}</td>"
f"<td><code>{f[3] or '-'}</code></td>"
f"<td>{f[4] or 'ai'}</td></tr>")
exp_rows = ""
for e in data["exploits"]:
exp_rows += (f"<tr><td>{e[0]}</td><td>{e[2] or '-'}</td>"
f"<td>{e[3] or '-'}</td>"
f"<td><code>{str(e[4] or '-')[:80]}</code></td>"
f"<td>{e[5] or '-'}</td></tr>")
ai_html = "".join(f"<p>{line}</p>"
for line in str(ai).split("\n") if line.strip())
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Metatron Report — {tgt}</title>
<style>
*{{box-sizing:border-box;margin:0;padding:0}}
body{{font-family:'Segoe UI',sans-serif;background:#0d0d0d;color:#e0e0e0;padding:30px}}
.container{{max-width:960px;margin:auto}}
.header{{border-left:5px solid #c0392b;padding-left:16px;margin-bottom:30px}}
.header h1{{font-size:2.2em;color:#c0392b}}
.header p{{color:#888;font-size:.95em}}
.meta-grid{{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:30px}}
.meta-card{{background:#1a1a1a;border:1px solid #333;border-radius:6px;padding:14px}}
.meta-card .label{{font-size:.75em;color:#888;text-transform:uppercase;margin-bottom:4px}}
.meta-card .value{{font-size:1.1em;font-weight:bold}}
.risk{{color:{rc}}}
section{{margin-bottom:30px}}
section h2{{font-size:1.2em;color:#c0392b;border-bottom:1px solid #333;
padding-bottom:8px;margin-bottom:14px}}
table{{width:100%;border-collapse:collapse;font-size:.88em}}
th{{background:#1e1e1e;color:#aaa;text-align:left;padding:10px;
font-size:.8em;text-transform:uppercase;border-bottom:2px solid #333}}
td{{padding:10px;border-bottom:1px solid #222;vertical-align:top}}
tr:hover td{{background:#1a1a1a}}
code{{background:#1e1e1e;padding:2px 6px;border-radius:3px;
font-family:monospace;font-size:.85em;color:#e74c3c}}
.ai-box{{background:#111;border:1px solid #333;border-radius:6px;
padding:16px;font-size:.9em;line-height:1.7;color:#ccc}}
.ai-box p{{margin-bottom:8px}}
.footer{{text-align:center;color:#444;font-size:.78em;
margin-top:40px;border-top:1px solid #222;padding-top:16px}}
a{{color:#555}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔱 METATRON</h1>
<p>AI Penetration Testing Report</p>
</div>
<div class="meta-grid">
<div class="meta-card">
<div class="label">Target</div>
<div class="value">{tgt}</div>
</div>
<div class="meta-card">
<div class="label">Session</div>
<div class="value">SL# {sl}</div>
</div>
<div class="meta-card">
<div class="label">Scan Date</div>
<div class="value">{date}</div>
</div>
<div class="meta-card">
<div class="label">Risk Level</div>
<div class="value risk">{risk}</div>
</div>
</div>
<section>
<h2>Vulnerabilities</h2>
{'<table><thead><tr><th>#</th><th>Vulnerability</th><th>Severity</th><th>Port</th><th>Service</th></tr></thead><tbody>' + vuln_rows + '</tbody></table>' if data["vulns"] else '<p style="color:#888">None recorded.</p>'}
</section>
<section>
<h2>Fixes &amp; Mitigations</h2>
{'<table><thead><tr><th>#</th><th>Vuln</th><th>Fix</th><th>Source</th></tr></thead><tbody>' + fix_rows + '</tbody></table>' if data["fixes"] else '<p style="color:#888">None recorded.</p>'}
</section>
<section>
<h2>Exploits Attempted</h2>
{'<table><thead><tr><th>#</th><th>Exploit</th><th>Tool</th><th>Payload</th><th>Result</th></tr></thead><tbody>' + exp_rows + '</tbody></table>' if data["exploits"] else '<p style="color:#888">None recorded.</p>'}
</section>
<section>
<h2>AI Analysis Summary</h2>
<div class="ai-box">
{ai_html if ai_html else '<p style="color:#888">None recorded.</p>'}
</div>
</section>
<div class="footer">
Generated by METATRON &mdash;
<a href="https://github.com/sooryathejas/METATRON">github.com/sooryathejas/METATRON</a>
&mdash; For authorized use only.
</div>
</div>
</body>
</html>"""
with open(filename, "w") as f:
f.write(html)
return filename
def export_menu(data: dict):
if not data["history"]:
print("[!] No session data to export.")
return
h = data["history"]
sl = h[0]
tgt = h[1]
print(f"\n\033[33m{''*20} EXPORT SL#{sl}{tgt} {''*20}\033[0m")
print(" [1] PDF report")
print(" [2] HTML report")
print(" [3] Both")
print(" [4] Back")
print(f"\033[90m{''*60}\033[0m")
choice = input("\033[36mExport format: \033[0m").strip()
output_dir = os.path.expanduser("~/METATRON/reports")
os.makedirs(output_dir, exist_ok=True)
if choice == "1":
p = export_pdf(data, output_dir)
print(f"\033[92m[+] PDF saved: {p}\033[0m")
elif choice == "2":
p = export_html(data, output_dir)
print(f"\033[92m[+] HTML saved: {p}\033[0m")
elif choice == "3":
p1 = export_pdf(data, output_dir)
p2 = export_html(data, output_dir)
print(f"\033[92m[+] PDF : {p1}\033[0m")
print(f"\033[92m[+] HTML : {p2}\033[0m")
elif choice == "4":
return
else:
print("\033[93m[!] Invalid choice.\033[0m")
if __name__ == "__main__":
print("\n\033[91m METATRON — Standalone Report Exporter\033[0m")
print("\033[90m ─────────────────────────────────────\033[0m\n")
rows = fetch_all_history()
if not rows:
print("[!] No sessions found in database.")
exit()
print(f"{'SL#':<6} {'TARGET':<28} {'DATE':<22} {'STATUS'}")
print("" * 65)
for row in rows:
print(f"{row[0]:<6} {row[1]:<28} {str(row[2]):<22} {row[3]}")
print()
sl_input = input("\033[36mEnter SL# to export: \033[0m").strip()
if not sl_input.isdigit():
print("[!] Invalid SL#.")
exit()
data = fetch_session(int(sl_input))
if not data["history"]:
print(f"[!] SL# {sl_input} not found.")
exit()
export_menu(data)
+387
View File
@@ -0,0 +1,387 @@
#!/usr/bin/env python3
"""
METATRON - llm.py
Ollama interface for metatron-qwen model.
Builds prompts, handles AI responses, runs tool dispatch loop.
Model: metatron-qwen (fine-tuned from huihui_ai/qwen3.5-abliterated:9b)
"""
import re
import requests
import json
from tools import run_tool_by_command, run_nmap, run_curl_headers
from search import handle_search_dispatch
OLLAMA_URL = "http://localhost:11434/api/chat"
MODEL_NAME = "metatron-qwen"
MAX_TOKENS = 8192
MAX_TOOL_LOOPS = 9 # max times AI can call tools per session
OLLAMA_TIMEOUT = 600
# ─────────────────────────────────────────────
# SYSTEM PROMPT
# ─────────────────────────────────────────────
SYSTEM_PROMPT = """You are METATRON, an elite AI penetration testing assistant running on Parrot OS.
You are precise, technical, and direct. No fluff.
You have access to real tools. To use them, write tags in your response:
[TOOL: nmap -sV 192.168.1.1] → runs nmap or any CLI tool
[SEARCH: CVE-2021-44228 exploit] → searches the web via DuckDuckGo
Rules:
- Always analyze scan data thoroughly before suggesting exploits
- List vulnerabilities with: name, severity (critical/high/medium/low), port, service
- For each vulnerability, suggest a concrete fix
- If you need more information, use [SEARCH:] or [TOOL:]
- Format vulnerabilities clearly so they can be saved to a database
- Be specific about CVE IDs when you know them
- Always give a final risk rating: CRITICAL / HIGH / MEDIUM / LOW
Output format for vulnerabilities (use this exactly):
VULN: <name> | SEVERITY: <level> | PORT: <port> | SERVICE: <service>
DESC: <description>
FIX: <fix recommendation>
Output format for exploits:
EXPLOIT: <name> | TOOL: <tool> | PAYLOAD: <payload or description>
RESULT: <expected result>
NOTES: <any notes>
End your analysis with:
RISK_LEVEL: <CRITICAL|HIGH|MEDIUM|LOW>
SUMMARY: <2-3 sentence overall summary>
IMPORTANT: Never use markdown bold (**text**) or
headers (## text). Plain text only. No exceptions.
IMPORTANT RULES FOR ACCURACY:
- nmap filtered or no-response means INCONCLUSIVE not vulnerable
- Never assert a server version without seeing it in scan output
- Never infer CVEs from guessed versions
- curl timeouts and HTTP_CODE=000 mean the host is unreachable not exploitable
- ab and stress tools are not Slowloris unless confirmed
- Only assign CRITICAL if there is direct evidence of exploitability
- If evidence is weak mark severity as LOW with note: unconfirmed"""
# ─────────────────────────────────────────────
# OLLAMA API CALL
# ─────────────────────────────────────────────
def ask_ollama(messages: list) -> str:
try:
payload = {
"model": MODEL_NAME,
"messages": messages,
"stream": False,
"options": {
"num_predict": MAX_TOKENS,
"temperature": 0.7,
"top_p": 0.9,
}
}
print(f"\n[*] Sending to {MODEL_NAME}...")
resp = requests.post(OLLAMA_URL, json=payload, timeout=OLLAMA_TIMEOUT)
resp.raise_for_status()
data = resp.json()
response = data.get("message", {}).get("content", "").strip()
if not response:
return "[!] Model returned empty response."
return response
except requests.exceptions.ConnectionError:
return "[!] Cannot connect to Ollama. Is it running? Try: ollama serve"
except requests.exceptions.Timeout:
return "[!] Ollama timed out. Model may be loading, try again."
except requests.exceptions.HTTPError as e:
return f"[!] Ollama HTTP error: {e}"
except Exception as e:
return f"[!] Unexpected error: {e}"
# ─────────────────────────────────────────────
# TOOL DISPATCH
# ─────────────────────────────────────────────
def extract_tool_calls(response: str) -> list:
"""
Extract all [TOOL: ...] and [SEARCH: ...] tags from AI response.
Returns list of tuples: [("TOOL", "nmap -sV x.x.x.x"), ("SEARCH", "CVE...")]
"""
calls = []
tool_matches = re.findall(r'\[TOOL:\s*(.+?)\]', response)
search_matches = re.findall(r'\[SEARCH:\s*(.+?)\]', response)
for m in tool_matches:
calls.append(("TOOL", m.strip()))
for m in search_matches:
calls.append(("SEARCH", m.strip()))
return calls
def summarize_tool_output(raw_output: str) -> str:
"""
Compress raw tool output into security-relevant bullet points
before injecting into the LLM context.
Keeps context size manageable across rounds.
"""
if len(raw_output) < 500:
return raw_output
try:
payload = {
"model": MODEL_NAME,
"messages": [
{"role": "system", "content": "You are a security data compressor. Extract only security-relevant facts. Return maximum 15 bullet points. Plain text only. No markdown."},
{"role": "user", "content": f"Compress this tool output:\n{raw_output[:6000]}"} ],
"stream": False,
"options": {
"num_predict": 512,
"temperature": 0.2,
"top_p": 0.9,
}
}
resp = requests.post(OLLAMA_URL, json=payload, timeout=120)
resp.raise_for_status()
summary = resp.json().get("message", {}).get("content", "").strip()
return summary if summary else raw_output
except Exception:
return raw_output
def run_tool_calls(calls: list) -> str:
"""
Execute all tool/search calls and return combined results string.
"""
if not calls:
return ""
results = ""
for call_type, call_content in calls:
print(f"\n [DISPATCH] {call_type}: {call_content}")
if call_type == "TOOL":
output = run_tool_by_command(call_content)
elif call_type == "SEARCH":
output = handle_search_dispatch(call_content)
else:
output = f"[!] Unknown call type: {call_type}"
compressed = summarize_tool_output(output.strip())
results += f"\n[{call_type} RESULT: {call_content}]\n"
results += "" * 40 + "\n"
results += compressed + "\n"
return results
# ─────────────────────────────────────────────
# PARSER — extract structured data from AI output
# ─────────────────────────────────────────────
def _clean(line: str) -> str:
return re.sub(r'\*+', '', line).strip()
def parse_vulnerabilities(response: str) -> list:
"""
Parse VULN: lines from AI response into dicts.
Returns list of vulnerability dicts ready for db.save_vulnerability()
"""
vulns = []
lines = response.splitlines()
i = 0
while i < len(lines):
line = _clean(lines[i])
if line.startswith("VULN:"):
vuln = {
"vuln_name": "",
"severity": "medium",
"port": "",
"service": "",
"description": "",
"fix": ""
}
# parse header line: VULN: name | SEVERITY: x | PORT: x | SERVICE: x
parts = line.split("|")
for part in parts:
part = part.strip()
if part.startswith("VULN:"):
vuln["vuln_name"] = part.replace("VULN:", "").strip()
elif part.startswith("SEVERITY:"):
vuln["severity"] = part.replace("SEVERITY:", "").strip().lower()
elif part.startswith("PORT:"):
vuln["port"] = part.replace("PORT:", "").strip()
elif part.startswith("SERVICE:"):
vuln["service"] = part.replace("SERVICE:", "").strip()
# look ahead for DESC: and FIX: lines
j = i + 1
while j < len(lines) and j <= i + 5:
next_line = _clean(lines[j])
if next_line.startswith(("VULN:", "EXPLOIT:", "RISK_LEVEL:", "SUMMARY:")):
break
if next_line.startswith("DESC:"):
vuln["description"] = next_line.replace("DESC:", "").strip()
elif next_line.startswith("FIX:"):
vuln["fix"] = next_line.replace("FIX:", "").strip()
j += 1
if vuln["vuln_name"]:
vulns.append(vuln)
i += 1
return vulns
def parse_exploits(response: str) -> list:
"""
Parse EXPLOIT: lines from AI response into dicts.
Returns list of exploit dicts ready for db.save_exploit()
"""
exploits = []
lines = response.splitlines()
i = 0
while i < len(lines):
line = _clean(lines[i])
if line.startswith("EXPLOIT:"):
exploit = {
"exploit_name": "",
"tool_used": "",
"payload": "",
"result": "unknown",
"notes": ""
}
parts = line.split("|")
for part in parts:
part = part.strip()
if part.startswith("EXPLOIT:"):
exploit["exploit_name"] = part.replace("EXPLOIT:", "").strip()
elif part.startswith("TOOL:"):
exploit["tool_used"] = part.replace("TOOL:", "").strip()
elif part.startswith("PAYLOAD:"):
exploit["payload"] = part.replace("PAYLOAD:", "").strip()
j = i + 1
while j < len(lines) and j <= i + 4:
next_line = _clean(lines[j])
if next_line.startswith(("VULN:", "EXPLOIT:", "RISK_LEVEL:", "SUMMARY:")):
break
if next_line.startswith("RESULT:"):
exploit["result"] = next_line.replace("RESULT:", "").strip()
elif next_line.startswith("NOTES:"):
exploit["notes"] = next_line.replace("NOTES:", "").strip()
j += 1
if exploit["exploit_name"]:
exploits.append(exploit)
i += 1
return exploits
def parse_risk_level(response: str) -> str:
"""Extract RISK_LEVEL from AI response."""
match = re.search(r'RISK_LEVEL:\s*(CRITICAL|HIGH|MEDIUM|LOW)', response, re.IGNORECASE)
return match.group(1).upper() if match else "UNKNOWN"
def parse_summary(response: str) -> str:
match = re.search(r'SUMMARY:\s*(.+)', response, re.IGNORECASE)
return match.group(1).strip() if match else ""
# ─────────────────────────────────────────────
# MAIN ANALYSIS FUNCTION
# ─────────────────────────────────────────────
def analyse_target(target: str, raw_scan: str) -> dict:
messages = [
{
"role": "system",
"content": SYSTEM_PROMPT
},
{
"role": "user",
"content": f"""TARGET: {target}
RECON DATA:
{raw_scan}
Analyze this target completely. Use [TOOL:] or [SEARCH:] if you need more information.
List all vulnerabilities, fixes, and suggest exploits where applicable."""
}
]
final_response = ""
for loop in range(MAX_TOOL_LOOPS):
response = ask_ollama(messages)
print(f"\n{''*60}")
print(f"[METATRON - Round {loop + 1}]")
print(f"{''*60}")
print(response)
final_response = response
tool_calls = extract_tool_calls(response)
if not tool_calls:
print("\n[*] No tool calls. Analysis complete.")
break
tool_results = run_tool_calls(tool_calls)
# add assistant response and tool results as new messages
messages.append({
"role": "assistant",
"content": response
})
messages.append({
"role": "user",
"content": f"""[TOOL RESULTS]
{tool_results}
Continue your analysis with this new information.
If analysis is complete, give the final RISK_LEVEL and SUMMARY."""
})
vulnerabilities = parse_vulnerabilities(final_response)
exploits = parse_exploits(final_response)
risk_level = parse_risk_level(final_response)
summary = parse_summary(final_response)
print(f"\n[+] Parsed: {len(vulnerabilities)} vulns, {len(exploits)} exploits | Risk: {risk_level}")
return {
"full_response": final_response,
"vulnerabilities": vulnerabilities,
"exploits": exploits,
"risk_level": risk_level,
"summary": summary,
"raw_scan": raw_scan
}
# ─────────────────────────────────────────────
# QUICK TEST
# ─────────────────────────────────────────────
if __name__ == "__main__":
print("[ llm.py test — direct AI query ]\n")
# test if ollama is reachable
try:
r = requests.get("http://localhost:11434", timeout=5)
print("[+] Ollama is running.")
except Exception:
print("[!] Ollama not reachable. Run: ollama serve")
exit(1)
target = input("Test target: ").strip()
test_scan = f"Test recon for {target} — nmap and whois data would appear here."
result = analyse_target(target, test_scan)
print(f"\nRisk Level : {result['risk_level']}")
print(f"Summary : {result['summary']}")
print(f"Vulns found: {len(result['vulnerabilities'])}")
print(f"Exploits : {len(result['exploits'])}")
+430
View File
@@ -0,0 +1,430 @@
#!/usr/bin/env python3
"""
METATRON - metatron.py
Main CLI entry point. Wires db.py + tools.py + search.py + llm.py together.
Run with: python metatron.py
"""
from export import export_menu
import os
import sys
from db import (
get_connection,
create_session,
save_vulnerability,
save_fix,
save_exploit,
save_summary,
get_all_history,
get_session,
get_vulnerabilities,
get_fixes,
get_exploits,
edit_vulnerability,
edit_fix,
edit_exploit,
edit_summary_risk,
delete_vulnerability,
delete_exploit,
delete_fix,
delete_full_session,
print_history,
print_session
)
from tools import interactive_tool_run, format_recon_for_llm, run_default_recon
from llm import analyse_target
# ─────────────────────────────────────────────
# BANNER
# ─────────────────────────────────────────────
def banner():
os.system("clear")
print("""
\033[91m
███╗ ███╗███████╗████████╗ █████╗ ████████╗██████╗ ██████╗ ███╗ ██╗
████╗ ████║██╔════╝╚══██╔══╝██╔══██╗╚══██╔══╝██╔══██╗██╔═══██╗████╗ ██║
██╔████╔██║█████╗ ██║ ███████║ ██║ ██████╔╝██║ ██║██╔██╗ ██║
██║╚██╔╝██║██╔══╝ ██║ ██╔══██║ ██║ ██╔══██╗██║ ██║██║╚██╗██║
██║ ╚═╝ ██║███████╗ ██║ ██║ ██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║
╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝
\033[0m
\033[90mAI Penetration Testing Assistant | Model: metatron-qwen | Parrot OS\033[0m
\033[90m─────────────────────────────────────────────────────────────────────\033[0m
""")
# ─────────────────────────────────────────────
# HELPERS
# ─────────────────────────────────────────────
def divider(label=""):
if label:
print(f"\n\033[33m{''*20} {label} {''*20}\033[0m")
else:
print(f"\033[90m{''*60}\033[0m")
def prompt(text):
return input(f"\033[36m{text}\033[0m").strip()
def success(text):
print(f"\033[92m[+] {text}\033[0m")
def warn(text):
print(f"\033[93m[!] {text}\033[0m")
def error(text):
print(f"\033[91m[✗] {text}\033[0m")
def info(text):
print(f"\033[94m[*] {text}\033[0m")
def confirm(question: str) -> bool:
ans = prompt(f"{question} [y/N]: ").lower()
return ans == "y"
# ─────────────────────────────────────────────
# NEW SCAN
# ─────────────────────────────────────────────
def new_scan():
divider("NEW SCAN")
target = prompt("[?] Enter target IP or domain: ")
if not target:
warn("No target entered.")
return
# check if target was scanned before
history = get_all_history()
past = [row for row in history if row[1] == target]
if past:
warn(f"Target '{target}' has been scanned before ({len(past)} time(s)).")
if not confirm("Continue with a new scan?"):
return
# create session in history table first
sl_no = create_session(target)
success(f"Session created — SL# {sl_no}")
# run recon tools
divider("RECON")
info("Choose recon tools to run:")
raw_scan = interactive_tool_run(target)
if not raw_scan.strip():
warn("No scan data collected. Aborting.")
delete_full_session(sl_no)
return
# send to AI
divider("AI ANALYSIS")
result = analyse_target(target, raw_scan)
# ── save everything to DB ──────────────────
divider("SAVING TO DATABASE")
# save vulnerabilities and their fixes
for vuln in result["vulnerabilities"]:
vuln_id = save_vulnerability(
sl_no,
vuln["vuln_name"],
vuln["severity"],
vuln["port"],
vuln["service"],
vuln["description"]
)
if vuln.get("fix"):
save_fix(sl_no, vuln_id, vuln["fix"], source="ai")
success(f"Saved vuln: {vuln['vuln_name']} [{vuln['severity']}]")
# save exploits
for exp in result["exploits"]:
save_exploit(
sl_no,
exp["exploit_name"],
exp["tool_used"],
exp["payload"],
exp["result"],
exp["notes"]
)
success(f"Saved exploit: {exp['exploit_name']}")
# save summary
save_summary(
sl_no,
result["raw_scan"],
result["full_response"],
result["risk_level"]
)
success(f"All data saved. SL# {sl_no} | Risk: {result['risk_level']}")
divider()
# show results and offer edit/delete
data = get_session(sl_no)
print_session(data)
if confirm("Edit or delete anything in this session?"):
edit_delete_menu(sl_no)
# ─────────────────────────────────────────────
# VIEW HISTORY
# ─────────────────────────────────────────────
def view_history():
divider("SCAN HISTORY")
rows = get_all_history()
if not rows:
warn("No scans in database yet.")
return
print_history(rows)
sl_no_str = prompt("Enter SL# to view details (or press Enter to go back): ")
if not sl_no_str:
return
try:
sl_no = int(sl_no_str)
except ValueError:
error("Invalid SL#.")
return
data = get_session(sl_no)
if not data["history"]:
error(f"SL# {sl_no} not found.")
return
print_session(data)
if confirm("Export this session?"):
export_menu(data)
if confirm("Edit or delete anything in this session?"):
edit_delete_menu(sl_no)
# ─────────────────────────────────────────────
# EDIT / DELETE MENU
# ─────────────────────────────────────────────
def edit_delete_menu(sl_no: int):
while True:
divider(f"EDIT / DELETE — SL# {sl_no}")
print(" [1] Edit a vulnerability")
print(" [2] Edit a fix")
print(" [3] Edit an exploit")
print(" [4] Edit risk level")
print(" [5] Delete a vulnerability")
print(" [6] Delete a fix")
print(" [7] Delete an exploit")
print(" [8] Delete FULL session (all tables)")
print(" [9] Back")
divider()
choice = prompt("Choice: ")
# ── EDIT VULNERABILITY ─────────────────
if choice == "1":
vulns = get_vulnerabilities(sl_no)
if not vulns:
warn("No vulnerabilities recorded for this session.")
continue
print("\n[ VULNERABILITIES ]")
for v in vulns:
print(f" id={v[0]} | {v[2]} | {v[3]} | port {v[4]} | {v[5]}")
vid = prompt("Enter vulnerability id to edit: ")
if not vid.isdigit():
error("Invalid id.")
continue
print(" Fields: vuln_name / severity / port / service / description")
field = prompt("Field to edit: ").strip()
value = prompt(f"New value for '{field}': ")
edit_vulnerability(int(vid), field, value)
# ── EDIT FIX ──────────────────────────
elif choice == "2":
fixes = get_fixes(sl_no)
if not fixes:
warn("No fixes recorded for this session.")
continue
print("\n[ FIXES ]")
for f in fixes:
print(f" id={f[0]} | vuln_id={f[2]} | {f[3][:80]}")
fid = prompt("Enter fix id to edit: ")
if not fid.isdigit():
error("Invalid id.")
continue
new_text = prompt("New fix text: ")
edit_fix(int(fid), new_text)
# ── EDIT EXPLOIT ──────────────────────
elif choice == "3":
exploits = get_exploits(sl_no)
if not exploits:
warn("No exploits recorded for this session.")
continue
print("\n[ EXPLOITS ]")
for e in exploits:
print(f" id={e[0]} | {e[2]} | tool: {e[3]} | result: {e[5]}")
eid = prompt("Enter exploit id to edit: ")
if not eid.isdigit():
error("Invalid id.")
continue
print(" Fields: exploit_name / tool_used / payload / result / notes")
field = prompt("Field to edit: ").strip()
value = prompt(f"New value for '{field}': ")
edit_exploit(int(eid), field, value)
# ── EDIT RISK LEVEL ───────────────────
elif choice == "4":
print(" Options: CRITICAL / HIGH / MEDIUM / LOW")
risk = prompt("New risk level: ").upper()
if risk not in ("CRITICAL", "HIGH", "MEDIUM", "LOW"):
error("Invalid risk level.")
continue
edit_summary_risk(sl_no, risk)
# ── DELETE VULNERABILITY ──────────────
elif choice == "5":
vulns = get_vulnerabilities(sl_no)
if not vulns:
warn("No vulnerabilities to delete.")
continue
print("\n[ VULNERABILITIES ]")
for v in vulns:
print(f" id={v[0]} | {v[2]} | {v[3]}")
vid = prompt("Enter vulnerability id to delete: ")
if not vid.isdigit():
error("Invalid id.")
continue
if confirm(f"Delete vulnerability id={vid} and its linked fixes?"):
delete_vulnerability(int(vid))
# ── DELETE FIX ────────────────────────
elif choice == "6":
fixes = get_fixes(sl_no)
if not fixes:
warn("No fixes to delete.")
continue
print("\n[ FIXES ]")
for f in fixes:
print(f" id={f[0]} | vuln_id={f[2]} | {f[3][:80]}")
fid = prompt("Enter fix id to delete: ")
if not fid.isdigit():
error("Invalid id.")
continue
if confirm(f"Delete fix id={fid}?"):
delete_fix(int(fid))
# ── DELETE EXPLOIT ────────────────────
elif choice == "7":
exploits = get_exploits(sl_no)
if not exploits:
warn("No exploits to delete.")
continue
print("\n[ EXPLOITS ]")
for e in exploits:
print(f" id={e[0]} | {e[2]} | result: {e[5]}")
eid = prompt("Enter exploit id to delete: ")
if not eid.isdigit():
error("Invalid id.")
continue
if confirm(f"Delete exploit id={eid}?"):
delete_exploit(int(eid))
# ── DELETE FULL SESSION ───────────────
elif choice == "8":
if confirm(f"\n\033[91mPermanently delete ENTIRE session SL# {sl_no} from all tables?\033[0m"):
delete_full_session(sl_no)
success(f"Session SL# {sl_no} wiped.")
return # go back to main menu
# ── BACK ──────────────────────────────
elif choice == "9":
break
else:
warn("Invalid choice.")
# ─────────────────────────────────────────────
# DB CONNECTION CHECK
# ─────────────────────────────────────────────
def check_db():
try:
conn = get_connection()
conn.close()
return True
except Exception as e:
error(f"MariaDB connection failed: {e}")
error("Make sure MariaDB is running: sudo systemctl start mariadb")
return False
# ─────────────────────────────────────────────
# MAIN MENU
# ─────────────────────────────────────────────
def main_menu():
while True:
banner()
print(" \033[92m[1]\033[0m New Scan")
print(" \033[92m[2]\033[0m View History")
print(" \033[92m[3]\033[0m Exit")
divider()
choice = prompt("metatron> ")
if choice == "1":
new_scan()
input("\n\033[90mPress Enter to continue...\033[0m")
elif choice == "2":
view_history()
input("\n\033[90mPress Enter to continue...\033[0m")
elif choice == "3":
print("\n\033[91m[*] Shutting down Metatron. Stay legal.\033[0m\n")
sys.exit(0)
else:
warn("Invalid choice.")
# ─────────────────────────────────────────────
# ENTRY POINT
# ─────────────────────────────────────────────
if __name__ == "__main__":
if not check_db():
sys.exit(1)
main_menu()
+24
View File
@@ -0,0 +1,24 @@
anyio==4.13.0
beautifulsoup4==4.14.3
certifi==2026.2.25
charset-normalizer==3.4.6
click==8.3.1
ddgs==9.12.1
duckduckgo_search==8.1.1
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
idna==3.11
lxml==6.0.2
markdown-it-py==4.0.0
mdurl==0.1.2
mysql-connector-python==9.6.0
pillow==12.2.0
primp==1.2.1
Pygments==2.20.0
reportlab==4.4.10
requests==2.33.1
rich==14.3.3
soupsieve==2.8.3
typing_extensions==4.15.0
urllib3==2.6.3
Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env python3
"""
METATRON - search.py
Free web search via DuckDuckGo — no API key needed.
Also fetches and extracts plain text from URLs.
Used by LLM tool dispatch when AI writes [SEARCH: query]
"""
import requests
from bs4 import BeautifulSoup
from ddgs import DDGS # pip install duckduckgo-search
# ─────────────────────────────────────────────
# DDG SEARCH
# ─────────────────────────────────────────────
def web_search(query: str, max_results: int = 5) -> str:
"""
Search DuckDuckGo and return formatted results.
No API key. No rate limit issues for reasonable usage.
Returns a string ready to paste into LLM prompt.
"""
print(f" [*] Searching: {query}")
try:
with DDGS() as ddgs:
results = list(ddgs.text(query, max_results=max_results))
if not results:
return "[!] No search results found."
output = f"[WEB SEARCH RESULTS FOR: {query}]\n"
output += "" * 50 + "\n"
for i, r in enumerate(results, 1):
output += f"\n[{i}] {r['title']}\n"
output += f" URL : {r['href']}\n"
output += f" Snippet : {r['body']}\n"
return output
except Exception as e:
return f"[!] Search failed: {e}"
# ─────────────────────────────────────────────
# CVE SPECIFIC SEARCH
# ─────────────────────────────────────────────
def search_cve(cve_id: str) -> str:
"""
Search for a specific CVE.
Queries DDG then also hits cve.mitre.org directly.
"""
print(f" [*] Looking up {cve_id}...")
# DDG search first
ddg_results = web_search(f"{cve_id} vulnerability exploit details", max_results=3)
# Direct MITRE fetch
mitre_url = f"https://cve.mitre.org/cgi-bin/cvename.cgi?name={cve_id}"
mitre_data = fetch_page(mitre_url, max_chars=2000)
return f"{ddg_results}\n\n[MITRE CVE LOOKUP: {cve_id}]\n{mitre_data}"
def search_exploit(service: str, version: str) -> str:
"""
Search for known exploits for a service + version combo.
e.g. search_exploit("apache", "2.4.49")
"""
query = f"{service} {version} exploit CVE vulnerability 2023 2024"
return web_search(query, max_results=5)
def search_fix(vuln_name: str) -> str:
"""
Search for mitigation/fix for a vulnerability.
"""
query = f"how to fix {vuln_name} security mitigation patch"
return web_search(query, max_results=3)
# ─────────────────────────────────────────────
# PAGE FETCHER
# ─────────────────────────────────────────────
def fetch_page(url: str, max_chars: int = 3000) -> str:
"""
Fetch a URL and return extracted plain text.
Strips all HTML tags. Truncated to max_chars for LLM context.
"""
try:
headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) Gecko/20100101 Firefox/120.0"}
resp = requests.get(url, headers=headers, timeout=15)
resp.raise_for_status()
soup = BeautifulSoup(resp.text, "html.parser")
# remove nav/footer/script noise
for tag in soup(["script", "style", "nav", "footer", "header", "aside"]):
tag.decompose()
text = soup.get_text(separator="\n", strip=True)
# collapse blank lines
lines = [l for l in text.splitlines() if l.strip()]
clean = "\n".join(lines)
if len(clean) > max_chars:
clean = clean[:max_chars] + f"\n... [truncated at {max_chars} chars]"
return clean
except requests.exceptions.ConnectionError:
return "[!] Could not connect to URL — check network."
except requests.exceptions.Timeout:
return "[!] Page fetch timed out."
except requests.exceptions.HTTPError as e:
return f"[!] HTTP error: {e}"
except Exception as e:
return f"[!] Fetch failed: {e}"
# ─────────────────────────────────────────────
# TOOL DISPATCH HANDLER
# ─────────────────────────────────────────────
def handle_search_dispatch(query: str) -> str:
"""
Called by llm.py when AI writes [SEARCH: something].
Smartly routes to CVE lookup, exploit search, or general search.
"""
query = query.strip()
# CVE pattern — CVE-YYYY-NNNNN
import re
cve_pattern = re.compile(r'CVE-\d{4}-\d{4,7}', re.IGNORECASE)
cve_match = cve_pattern.search(query)
if cve_match:
return search_cve(cve_match.group())
# exploit keywords
if any(word in query.lower() for word in ["exploit", "poc", "payload", "rce", "lfi", "sqli"]):
return web_search(query + " exploit poc github", max_results=5)
# fix/patch keywords
if any(word in query.lower() for word in ["fix", "patch", "mitigate", "harden", "secure"]):
return search_fix(query)
# default general search
return web_search(query, max_results=5)
# ─────────────────────────────────────────────
# QUICK TEST
# ─────────────────────────────────────────────
if __name__ == "__main__":
print("[ search.py test ]\n")
print("[1] General search")
print("[2] CVE lookup")
print("[3] Fetch a URL")
choice = input("Choice: ").strip()
if choice == "1":
q = input("Query: ").strip()
print(web_search(q))
elif choice == "2":
cve = input("CVE ID (e.g. CVE-2021-44228): ").strip()
print(search_cve(cve))
elif choice == "3":
url = input("URL: ").strip()
print(fetch_page(url))
+251
View File
@@ -0,0 +1,251 @@
#!/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))