Skip to content

feat: dual FLM/Ollama backend, Docker MariaDB, external config, and benchmark suite#22

Open
Matcraft94 wants to merge 6 commits into
sooryathejas:mainfrom
Matcraft94:main
Open

feat: dual FLM/Ollama backend, Docker MariaDB, external config, and benchmark suite#22
Matcraft94 wants to merge 6 commits into
sooryathejas:mainfrom
Matcraft94:main

Conversation

@Matcraft94
Copy link
Copy Markdown

Summary

This PR adapts METATRON for modern local LLM runtimes and containerized infrastructure:

  • Dual LLM backend — adds llm_backends.py supporting FLM (OpenAI-compatible, ideal for AMD Ryzen AI / NPU) and Ollama via environment switch (METATRON_LLM_BACKEND)
  • Docker MariaDB — replaces native DB dependency with docker-compose.yml and automatic schema initialization
  • Docker tool fallback — missing pentest tools (nmap, whatweb, nikto, etc.) automatically run inside a Parrot OS container (docker-compose.tools.yml)
  • External configuration — moves credentials, backend URLs, and model names to .env; system prompt to config/system_prompt.txt
  • Robust LLM parsers — hybrid parser in llm.py handles both structured and markdown outputs, typo-tolerant severity/risk extraction
  • FLM benchmark suitebenchmark_models.py auto-starts/stops FLM per model, compares response time and parsing quality across 6 models
  • README overhaul — updated installation instructions for Arch/Parrot OS, model recommendation table with real benchmark data, troubleshooting section, and contributors

Test plan

  • METATRON_LLM_BACKEND=flm + flm serve qwen3.5:4b — full E2E scan against scanme.nmap.org
  • METATRON_LLM_BACKEND=ollama — backend connectivity and parsing validated
  • Docker MariaDB starts cleanly and persists data
  • Docker fallback for whatweb/nikto works when host tools are missing
  • PDF/HTML export generates valid reports
  • python benchmark_models.py completes for all 6 models

- Add llm_backends.py with unified FLM (OpenAI-compatible) and Ollama support
- Add docker-compose.yml for MariaDB and docker-compose.tools.yml for Parrot OS fallback
- Migrate db.py and export.py to environment-based credentials
- Add .env.example, config/system_prompt.txt, and config/initdb/01-schema.sql
- Update tools.py with automatic Docker fallback for missing pentest tools
- Refactor llm.py to use new backend and load system prompt from file
- Update metatron.py banner to show active backend and model
Copilot AI review requested due to automatic review settings April 16, 2026 21:40
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR modernizes METATRON’s local runtime by adding a switchable FLM/Ollama LLM layer, containerizing supporting infrastructure (MariaDB + pentest tools fallback), and moving runtime configuration into external files/env vars.

Changes:

  • Introduces a unified LLM backend interface (llm_backends.py) and updates llm.py to use it, including more tolerant parsing and an externalized system prompt.
  • Adds Docker Compose stacks for MariaDB and a “tools” container fallback, plus a start.sh launcher.
  • Adds an FLM benchmarking script + sample results and updates docs/config templates accordingly.

Reviewed changes

Copilot reviewed 17 out of 19 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
tools.py Adds local tool execution with Docker fallback via docker exec metatron-tools.
start.sh New environment launcher that starts Docker services, sets up venv/deps, and launches the CLI.
requirements.txt Adds openai and python-dotenv dependencies for the new backend/config approach.
metatron.py Updates DB connection failure guidance to include Docker startup instructions.
llm_backends.py New unified FLM (OpenAI-compatible) + Ollama backend selector via env var.
llm.py Switches to unified backend, loads system prompt from file, and expands parsers to handle more output formats.
export.py Moves DB connection details to env vars (external config).
docker-compose.yml New MariaDB container with init scripts and healthcheck.
docker-compose.tools.yml New Parrot OS tools container to run missing pentest tools.
db.py Moves DB connection details to env vars (external config).
config/system_prompt.txt New external system prompt file.
config/initdb/01-schema.sql New schema initialization SQL for Docker MariaDB.
benchmark_models.py New benchmark runner that starts/stops FLM per model and evaluates parsing/risk output.
benchmark_results.json Adds sample benchmark output data used in documentation.
README.md Updates setup/docs for FLM/Ollama, Docker DB/tools, .env config, and benchmark guidance.
Modelfile.test Adds a test Modelfile variant.
.env.example Adds env template for backend selection, model endpoints, DB credentials, and prompt path.
.dockerignore Adds dockerignore defaults (venv, caches, .env, etc.).
.gitignore Adds .gitnexus ignore entry.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread export.py
Comment on lines +29 to +33
DB_HOST = os.getenv("METATRON_DB_HOST", "localhost")
DB_USER = os.getenv("METATRON_DB_USER", "metatron")
DB_PASS = os.getenv("METATRON_DB_PASS", "metatron123")
DB_NAME = os.getenv("METATRON_DB_NAME", "metatron")

Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like db.py, this module reads DB settings from environment variables but does not load .env. If users follow the README/.env.example workflow, exports may still use the hard-coded defaults unless .env is sourced elsewhere. Consider calling dotenv.load_dotenv() at startup (or sharing a single config loader) so .env-driven settings apply consistently.

Copilot uses AI. Check for mistakes.
Comment thread db.py
Comment on lines +17 to 31
DB_HOST = os.getenv("METATRON_DB_HOST", "localhost")
DB_PORT = int(os.getenv("METATRON_DB_PORT", "3306"))
DB_USER = os.getenv("METATRON_DB_USER", "metatron")
DB_PASS = os.getenv("METATRON_DB_PASS", "metatron123")
DB_NAME = os.getenv("METATRON_DB_NAME", "metatron")

def get_connection():
"""Returns a MariaDB connection. No password (local setup)."""
"""Returns a MariaDB connection."""
return mysql.connector.connect(
host="localhost",
user="metatron",
password="123",
database="metatron"
host=DB_HOST,
port=DB_PORT,
user=DB_USER,
password=DB_PASS,
database=DB_NAME
)
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DB_* values are read from environment variables, but this module does not load .env and the provided start.sh also doesn’t source .env. As a result, users configuring credentials in .env may still connect with defaults. Consider calling dotenv.load_dotenv() at process startup (e.g., in metatron.py before importing db/export) or loading it in db.py itself.

Copilot uses AI. Check for mistakes.
Comment thread export.py
Comment on lines +29 to 41
DB_HOST = os.getenv("METATRON_DB_HOST", "localhost")
DB_USER = os.getenv("METATRON_DB_USER", "metatron")
DB_PASS = os.getenv("METATRON_DB_PASS", "metatron123")
DB_NAME = os.getenv("METATRON_DB_NAME", "metatron")


def get_connection():
return mysql.connector.connect(
host="localhost",
user="metatron",
password="123",
database="metatron"
host=DB_HOST,
user=DB_USER,
password=DB_PASS,
database=DB_NAME
)
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export.py reads DB connection info from env vars but does not support METATRON_DB_PORT (db.py does). If MariaDB is exposed on a non-3306 port (common with Docker), exports will fail to connect. Add DB_PORT handling here for consistency with db.py.

Copilot uses AI. Check for mistakes.
Comment thread docker-compose.yml
Comment on lines +7 to +10
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: metatron
MYSQL_USER: metatron
MYSQL_PASSWORD: metatron123
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MariaDB credentials are hard-coded in docker-compose.yml. Since the PR introduces external configuration via .env, consider using Compose env var substitution (e.g., ${MYSQL_PASSWORD}) and/or a local-only .env file to avoid committing real credentials and to keep config in one place.

Suggested change
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: metatron
MYSQL_USER: metatron
MYSQL_PASSWORD: metatron123
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}

Copilot uses AI. Check for mistakes.
Comment thread docker-compose.tools.yml
command: sleep infinity
cap_add:
- NET_RAW
network_mode: host
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docker-compose.tools.yml runs the tools container with host networking and NET_RAW capability. That’s a significant privilege escalation surface. If this is required for nmap, consider documenting the risk clearly and/or narrowing privileges (e.g., avoid host networking when possible, add only the minimum caps needed).

Suggested change
network_mode: host

Copilot uses AI. Check for mistakes.
Comment thread start.sh
Comment on lines +26 to +34
echo "[*] Waiting for MariaDB to be ready..."
for i in {1..30}; do
if docker exec metatron-mariadb healthcheck.sh --connect --innodb_initialized &> /dev/null; then
echo "[+] MariaDB is ready."
break
fi
sleep 1
done

Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “wait for MariaDB” loop doesn’t fail if the DB never becomes healthy; the script will continue and metatron.py will likely exit with a DB error. Consider exiting with a clear message after the loop (or using docker compose up --wait) so failures are surfaced deterministically.

Suggested change
echo "[*] Waiting for MariaDB to be ready..."
for i in {1..30}; do
if docker exec metatron-mariadb healthcheck.sh --connect --innodb_initialized &> /dev/null; then
echo "[+] MariaDB is ready."
break
fi
sleep 1
done
echo "[*] Waiting for MariaDB to be ready..."
mariadb_ready=false
for i in {1..30}; do
if docker exec metatron-mariadb healthcheck.sh --connect --innodb_initialized &> /dev/null; then
echo "[+] MariaDB is ready."
mariadb_ready=true
break
fi
sleep 1
done
if [ "$mariadb_ready" != true ]; then
echo "[!] MariaDB did not become healthy within 30 seconds."
exit 1
fi

Copilot uses AI. Check for mistakes.
Comment thread llm.py
Comment on lines +115 to +119
if re.match(r'VULN\s*:', part, re.IGNORECASE):
vuln["vuln_name"] = re.sub(r'(?i)^VULN\s*:', '', part).strip()
elif re.match(r'SEVER\w*?:', part, re.IGNORECASE):
vuln["severity"] = re.sub(r'(?i)^SEVER\w*?:', '', part).strip().lower()
elif re.match(r'PORT\s*:', part, re.IGNORECASE):
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity parsing currently accepts arbitrary strings (e.g., values like "medium/high" seen in benchmark output). Downstream (e.g., export severity color mapping) expects a finite set (critical/high/medium/low/info/unknown). Normalize parsed values to the closest supported level (e.g., split on non-letters and take the highest) to avoid inconsistent DB/reporting behavior.

Copilot uses AI. Check for mistakes.
Comment thread llm_backends.py

BACKEND = os.getenv("METATRON_LLM_BACKEND", "flm").lower()

FLM_BASE_URL = os.getenv("FLM_BASE_URL", "http://localhost:11434/v1")
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default FLM_BASE_URL points to port 11434 (Ollama’s default). If METATRON_LLM_BACKEND=flm and the user doesn’t set FLM_BASE_URL, requests will go to the wrong service/port. Align the default with the documented/example FLM port (e.g., 8000) or require FLM_BASE_URL explicitly when backend=flm.

Suggested change
FLM_BASE_URL = os.getenv("FLM_BASE_URL", "http://localhost:11434/v1")
FLM_BASE_URL = os.getenv("FLM_BASE_URL", "http://localhost:8000/v1")

Copilot uses AI. Check for mistakes.
Comment thread start.sh
Comment on lines +47 to +55
# ── Check FLM ────────────────────────────────
FLM_PORT=$(grep "^FLM_BASE_URL=" .env 2>/dev/null | grep -oP '(?<=:)[0-9]+' || echo "8000")
if ! ss -tlnp 2>/dev/null | grep -q ":$FLM_PORT "; then
echo "[!] FLM server not detected on port $FLM_PORT."
echo " Start it manually with:"
echo " flm serve qwen3.5:4b --port $FLM_PORT --ctx-len 16384 --pmode performance"
echo ""
read -p "Press Enter to continue anyway, or Ctrl+C to abort..."
else
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

start.sh reads values from .env (for FLM_PORT) but never sources it, and Python only sees env vars that are exported. If the intent is “external configuration via .env”, either set -a; source .env; set +a in this script or rely consistently on python-dotenv in the Python entrypoint so DB/LLM settings are actually applied.

Copilot uses AI. Check for mistakes.
Comment thread llm.py
Comment on lines 239 to +243
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()
if re.match(r'EXPLOIT\s*:', part, re.IGNORECASE):
exploit["exploit_name"] = re.sub(r'(?i)^EXPLOIT\s*:', '', part).strip()
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block now parses EXPLOIT fields case-insensitively, but exploits are still only entered when the header line matches the exact uppercase prefix (the line.startswith("EXPLOIT:") check a few lines above). To avoid silently missing exploits, make the header detection case/whitespace tolerant as well (e.g., a regex match similar to the field parsing).

Copilot uses AI. Check for mistakes.
@zakariazakia15-jpg
Copy link
Copy Markdown

zakariazakia15-jpg commented Apr 16, 2026 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants