diff --git a/.gitignore b/.gitignore index eed9662..4d728f6 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,9 @@ results/ # v3.3.0 runtime RL state data/rl_state.json + +# Playwright demo artifacts +.playwright-mcp/ +neurosploit_gui_*.png +neurosploit_demo_*.png +logs/webgui.log diff --git a/README.md b/README.md index 2226fb6..25c27e7 100755 --- a/README.md +++ b/README.md @@ -114,6 +114,19 @@ data-driven builder, `scripts/build_agents.py`). It is picked up automatically. Outputs land in `results//findings.json` and `reports/`, and the RL state updates in `data/rl_state.json`. +### Minimalist web GUI + +A zero-dependency (Python stdlib only) web front-end exposes just the essential +options — target URL, backend, model, collaborator, and the RL / Playwright-MCP +toggles — and launches an engagement with a live progress console: + +```bash +python3 webgui/server.py # → http://127.0.0.1:8787 +``` + +No npm, no build step. It calls `neurosploit_agent` directly. (The previous heavy +React app remains under `frontend/` but is no longer the primary interface.) + ### Backends | Backend | Binary | Autonomy flag | Subscription | diff --git a/webgui/index.html b/webgui/index.html new file mode 100644 index 0000000..fa3face --- /dev/null +++ b/webgui/index.html @@ -0,0 +1,176 @@ + + + + + +NeuroSploit v3.3.0 + + + +
+
+ +

NeuroSploit v3.3.0

+
+
Autonomous MD-Agent pentest — enter a URL, pick a backend, run.
+ +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ agents + backends + Playwright proof-of-exec +
+ +
+
+
+ +
Authorized testing only · findings are validated & false-positive-filtered before reporting
+
+ + + + diff --git a/webgui/server.py b/webgui/server.py new file mode 100644 index 0000000..743af9a --- /dev/null +++ b/webgui/server.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +""" +NeuroSploit v3.3.0 — minimalist web GUI server. + +A tiny, dependency-free (Python stdlib only) web front-end for the autonomous +engine. It exposes just the essential options — target URL, backend, model, +collaborator, and the RL / Playwright-MCP toggles — and launches an engagement. + + python3 webgui/server.py # serves http://127.0.0.1:8787 + +No npm, no build step, no FastAPI. It talks to neurosploit_agent directly. +""" + +import json +import os +import sys +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, ROOT) + +from neurosploit_agent import backends, models # noqa: E402 +from neurosploit_agent.agent_loader import AgentLibrary # noqa: E402 +from neurosploit_agent.config import RunConfig # noqa: E402 +from neurosploit_agent.orchestrator import run_engagement # noqa: E402 + +HERE = os.path.dirname(os.path.abspath(__file__)) +_RUNS = {} # run_id -> {log:[], done:bool, result:dict} +_LOCK = threading.Lock() +_PROV_FOR_BACKEND = {"claude": "anthropic", "codex": "openai", "grok": "xai"} + + +def _info(): + lib = AgentLibrary() + det = backends.detect() + provs = {} + for p in models.PROVIDERS.values(): + provs[p.key] = {"label": p.label, + "models": [{"id": m.id, "label": m.label} for m in p.models]} + return { + "version": "3.3.0", + "agents": lib.counts(), + "backends": [{"key": b.key, "label": b.label, "version": b.version()} for b in det], + "providers": provs, + "backend_provider": _PROV_FOR_BACKEND, + } + + +def _start_run(params): + run_id = "run-%d" % (len(_RUNS) + 1) + with _LOCK: + _RUNS[run_id] = {"log": [], "done": False, "result": None} + + def progress(msg): + with _LOCK: + _RUNS[run_id]["log"].append(msg) + + def worker(): + try: + backend = params.get("backend") or (backends.detect()[0].key if backends.detect() else "claude") + provider = params.get("provider") or _PROV_FOR_BACKEND.get(backend, "anthropic") + mlist = models.list_models(provider) + model = params.get("model") or (mlist[0].id if mlist else "") + url = params["url"] + if not url.startswith(("http://", "https://")): + url = "https://" + url + cfg = RunConfig( + target=url, scope=params.get("scope") or url, backend=backend, + provider=provider, model=model, collaborator=params.get("collaborator", ""), + use_rl=bool(params.get("rl", True)), use_mcp=bool(params.get("mcp", True)), + dry_run=bool(params.get("dry_run", False)), + ) + res = run_engagement(cfg, progress=progress) + with _LOCK: + _RUNS[run_id]["result"] = { + "returncode": res["returncode"], "workdir": res["workdir"], + "findings": res["findings"], "agents_ran": res["agents_ran"], + } + except Exception as e: # surface errors to the UI + progress(f"ERROR: {e}") + with _LOCK: + _RUNS[run_id]["result"] = {"error": str(e)} + finally: + with _LOCK: + _RUNS[run_id]["done"] = True + + threading.Thread(target=worker, daemon=True).start() + return run_id + + +class Handler(BaseHTTPRequestHandler): + def _send(self, code, body, ctype="application/json"): + data = body if isinstance(body, bytes) else body.encode("utf-8") + self.send_response(code) + self.send_header("Content-Type", ctype) + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def log_message(self, *a): + pass + + def do_GET(self): + if self.path in ("/", "/index.html"): + self._send(200, open(os.path.join(HERE, "index.html"), "rb").read(), "text/html; charset=utf-8") + elif self.path == "/api/info": + self._send(200, json.dumps(_info())) + elif self.path.startswith("/api/status/"): + rid = self.path.rsplit("/", 1)[-1] + with _LOCK: + st = _RUNS.get(rid) + self._send(200 if st else 404, json.dumps(st or {"error": "unknown run"})) + else: + self._send(404, json.dumps({"error": "not found"})) + + def do_POST(self): + if self.path != "/api/run": + return self._send(404, json.dumps({"error": "not found"})) + n = int(self.headers.get("Content-Length", 0)) + try: + params = json.loads(self.rfile.read(n) or b"{}") + except Exception: + return self._send(400, json.dumps({"error": "bad json"})) + if not params.get("url"): + return self._send(400, json.dumps({"error": "url required"})) + rid = _start_run(params) + self._send(200, json.dumps({"run_id": rid})) + + +def main(): + host = os.getenv("NEUROSPLOIT_GUI_HOST", "127.0.0.1") + port = int(os.getenv("NEUROSPLOIT_GUI_PORT", "8787")) + print(f"NeuroSploit v3.3.0 GUI → http://{host}:{port}") + ThreadingHTTPServer((host, port), Handler).serve_forever() + + +if __name__ == "__main__": + main()