commit 4a2225c0ded71f98643183b63f2eec3b0fa9a75e Author: wehub-resource-sync Date: Mon Jul 13 12:30:44 2026 +0800 chore: import upstream snapshot with attribution diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e18affd --- /dev/null +++ b/.gitignore @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d8b0a87 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/Modelfile b/Modelfile new file mode 100644 index 0000000..ff4a9a1 --- /dev/null +++ b/Modelfile @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..f77d2fa --- /dev/null +++ b/README.md @@ -0,0 +1,355 @@ +# METATRON +AI-powered penetration testing assistant using local LLM on linux (Parrot OS) +# ๐Ÿ”ฑ METATRON +### AI-Powered Penetration Testing Assistant + +

+ Metatron Banner +

+ +

+ + + + + +

+ +--- + +## ๐Ÿ“Œ 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 + +

+ Main Menu +
Main Menu +

+ +

+ Scan Running +
Recon tools running on target +

+ +

+ AI Analysis +
metatron-qwen analyzing scan results +

+ +

+ Results +
Vulnerabilities saved to database +

+

Export Menu
Export scan results as PDF and or HTML

+--- + +## ๐Ÿงฑ 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. diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..0d68525 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub ๆฅๆบ่ฏดๆ˜Ž + +- ๅŽŸๅง‹้กน็›ฎ๏ผš`sooryathejas/METATRON` +- ๅŽŸๅง‹ไป“ๅบ“๏ผšhttps://github.com/sooryathejas/METATRON +- ๅฏผๅ…ฅๆ–นๅผ๏ผšไธŠๆธธ้ป˜่ฎคๅˆ†ๆ”ฏ็š„ๆœ€ๆ–ฐๅฟซ็…ง +- ๅŽŸไฝœ่€…ใ€็‰ˆๆƒๅ’Œ่ฎธๅฏ่ฏไฟกๆฏไปฅๅŽŸๅง‹ไป“ๅบ“ๅŠๆœฌไป“ๅบ“ LICENSE ไธบๅ‡† +- ๆœฌๆ–‡ไปถไป…็”จไบŽ่ฎฐๅฝ•ๆฅๆบ๏ผŒไธไปฃ่กจ WeHub ๆ˜ฏๅŽŸ้กน็›ฎไฝœ่€… diff --git a/db.py b/db.py new file mode 100644 index 0000000..80ede37 --- /dev/null +++ b/db.py @@ -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}") diff --git a/export.py b/export.py new file mode 100644 index 0000000..f109b8d --- /dev/null +++ b/export.py @@ -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"{v[0]}" + f"{v[2]}
{v[6] or ''}" + f"" + f"{(v[3] or 'unknown').upper()}" + f"{v[4] or '-'}{v[5] or '-'}") + + fix_rows = "" + for f in data["fixes"]: + fix_rows += (f"{f[0]}vuln #{f[2]}" + f"{f[3] or '-'}" + f"{f[4] or 'ai'}") + + exp_rows = "" + for e in data["exploits"]: + exp_rows += (f"{e[0]}{e[2] or '-'}" + f"{e[3] or '-'}" + f"{str(e[4] or '-')[:80]}" + f"{e[5] or '-'}") + + ai_html = "".join(f"

{line}

" + for line in str(ai).split("\n") if line.strip()) + + html = f""" + + + +Metatron Report โ€” {tgt} + + + +
+ +
+

๐Ÿ”ฑ METATRON

+

AI Penetration Testing Report

+
+ +
+
+
Target
+
{tgt}
+
+
+
Session
+
SL# {sl}
+
+
+
Scan Date
+
{date}
+
+
+
Risk Level
+
{risk}
+
+
+ +
+

Vulnerabilities

+ {'' + vuln_rows + '
#VulnerabilitySeverityPortService
' if data["vulns"] else '

None recorded.

'} +
+ +
+

Fixes & Mitigations

+ {'' + fix_rows + '
#VulnFixSource
' if data["fixes"] else '

None recorded.

'} +
+ +
+

Exploits Attempted

+ {'' + exp_rows + '
#ExploitToolPayloadResult
' if data["exploits"] else '

None recorded.

'} +
+ +
+

AI Analysis Summary

+
+ {ai_html if ai_html else '

None recorded.

'} +
+
+ + + +
+ +""" + + 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) diff --git a/llm.py b/llm.py new file mode 100644 index 0000000..d4c8f95 --- /dev/null +++ b/llm.py @@ -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: | SEVERITY: | PORT: | SERVICE: +DESC: +FIX: + +Output format for exploits: +EXPLOIT: | TOOL: | PAYLOAD: +RESULT: +NOTES: + +End your analysis with: +RISK_LEVEL: +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'])}") diff --git a/metatron.py b/metatron.py new file mode 100644 index 0000000..6506c82 --- /dev/null +++ b/metatron.py @@ -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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7058cc0 --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/screenshots/ai_analysis.png b/screenshots/ai_analysis.png new file mode 100644 index 0000000..dd69ea3 Binary files /dev/null and b/screenshots/ai_analysis.png differ diff --git a/screenshots/banner.png b/screenshots/banner.png new file mode 100644 index 0000000..18989d1 Binary files /dev/null and b/screenshots/banner.png differ diff --git a/screenshots/export_menu.png b/screenshots/export_menu.png new file mode 100644 index 0000000..2a82db5 Binary files /dev/null and b/screenshots/export_menu.png differ diff --git a/screenshots/main_menu.png b/screenshots/main_menu.png new file mode 100644 index 0000000..4d1d53e Binary files /dev/null and b/screenshots/main_menu.png differ diff --git a/screenshots/results.png b/screenshots/results.png new file mode 100644 index 0000000..f0bcc7d Binary files /dev/null and b/screenshots/results.png differ diff --git a/screenshots/scan_running.png b/screenshots/scan_running.png new file mode 100644 index 0000000..3425c7f Binary files /dev/null and b/screenshots/scan_running.png differ diff --git a/search.py b/search.py new file mode 100644 index 0000000..c991398 --- /dev/null +++ b/search.py @@ -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)) diff --git a/tools.py b/tools.py new file mode 100644 index 0000000..5862b49 --- /dev/null +++ b/tools.py @@ -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))