mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-06-30 16:25:48 +02:00
a5badefc29
Engine:
- Fix: inject IS_SANDBOX=1 so Claude Code's --dangerously-skip-permissions
works under root (real backend runs were exiting rc=1 immediately)
- models: expand to 40 models / 13 providers, tagged CLI vs API
(NVIDIA NIM, DeepSeek, Mistral, Qwen/DashScope, Groq, Together, OpenRouter,
Ollama, Gemini) — Qwen/DeepSeek/Llama usable via API
- backends: on_start callback surfaces the exact argv ("what runs behind it")
- orchestrator: require a Playwright screenshot per confirmed finding; collect
results/activity.json; auto-generate reports after a run
- report.py: HTML always + PDF via Typst engine (.typ source emitted too)
Web dashboard (webgui/, stdlib only — no npm/build):
- Sidebar dashboard (PentAGI-style): Run / Agents / Insights / Reports / Settings
- Multi-target runs; live execution console + per-task activity; finding cards
with screenshots; backend+provider+model pickers (CLI & API)
- Agents tab: browse 213 + add new .md agents from the UI
- Insights: interactive RL-weight + severity charts
- Reports: download/preview PDF + HTML
- Settings/API: execution mode, per-provider API keys, orchestrator, verbosity
- Endpoints: /api/agents (GET/POST), /api/rl, /api/config, /api/reports,
/reports/* + /shots/* static serving
Cleanup: retire replaced web stack (frontend React, FastAPI backend, core
orchestration, old test) to legacy/. Active engine + GUI are fully standalone.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
160 lines
5.5 KiB
Python
160 lines
5.5 KiB
Python
"""
|
|
Agentic CLI backends for NeuroSploit v3.3.0.
|
|
|
|
NeuroSploit does not embed its own agent loop — it delegates autonomous
|
|
execution to whichever agentic coding CLI is installed locally:
|
|
|
|
* Claude Code (`claude`) — also the path for a Claude *subscription*
|
|
* Codex CLI (`codex`)
|
|
* Grok CLI (`grok`)
|
|
|
|
Each backend is driven headlessly: we pass the composed master prompt, a working
|
|
directory (with `.mcp.json` for Playwright), and provider env, and let the CLI
|
|
run the test autonomously to completion. The engine then reads the artifacts the
|
|
run wrote to `results/`.
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from dataclasses import dataclass, field
|
|
from typing import Dict, List, Optional
|
|
|
|
|
|
@dataclass
|
|
class Backend:
|
|
key: str
|
|
label: str
|
|
binary: str
|
|
# builds argv given (prompt_file, workdir, model). Prompt is passed via file
|
|
# to avoid arg-length limits and shell-escaping issues.
|
|
def available(self) -> bool:
|
|
return shutil.which(self.binary) is not None
|
|
|
|
def version(self) -> str:
|
|
try:
|
|
out = subprocess.run([self.binary, "--version"], capture_output=True,
|
|
text=True, timeout=15)
|
|
return (out.stdout or out.stderr).strip().splitlines()[0] if (out.stdout or out.stderr) else "?"
|
|
except Exception:
|
|
return "?"
|
|
|
|
def build_argv(self, prompt_file: str, workdir: str, model: str,
|
|
autonomous: bool, mcp_config: Optional[str]) -> List[str]:
|
|
raise NotImplementedError
|
|
|
|
|
|
@dataclass
|
|
class ClaudeBackend(Backend):
|
|
def build_argv(self, prompt_file, workdir, model, autonomous, mcp_config):
|
|
# Headless "print" mode reads the prompt from stdin (caller pipes the file).
|
|
argv = [self.binary, "-p", "--output-format", "stream-json", "--verbose"]
|
|
if model:
|
|
argv += ["--model", model]
|
|
if mcp_config:
|
|
argv += ["--mcp-config", mcp_config]
|
|
if autonomous:
|
|
# Full autonomy for an authorized engagement in an isolated workdir.
|
|
argv += ["--dangerously-skip-permissions"]
|
|
return argv
|
|
|
|
stdin_prompt: bool = True
|
|
|
|
|
|
@dataclass
|
|
class CodexBackend(Backend):
|
|
def build_argv(self, prompt_file, workdir, model, autonomous, mcp_config):
|
|
# `codex exec` runs non-interactively to completion.
|
|
argv = [self.binary, "exec", "--cd", workdir]
|
|
if model:
|
|
argv += ["--model", model]
|
|
if autonomous:
|
|
argv += ["--dangerously-bypass-approvals-and-sandbox"]
|
|
if mcp_config:
|
|
argv += ["--config", f"mcp_config_file={mcp_config}"]
|
|
argv += ["-"] # read prompt from stdin
|
|
return argv
|
|
|
|
stdin_prompt: bool = True
|
|
|
|
|
|
@dataclass
|
|
class GrokBackend(Backend):
|
|
def build_argv(self, prompt_file, workdir, model, autonomous, mcp_config):
|
|
# grok-cli headless/print form.
|
|
argv = [self.binary, "--prompt-file", prompt_file, "--workdir", workdir]
|
|
if model:
|
|
argv += ["--model", model]
|
|
if mcp_config:
|
|
argv += ["--mcp-config", mcp_config]
|
|
if autonomous:
|
|
argv += ["--yolo"]
|
|
return argv
|
|
|
|
stdin_prompt: bool = False
|
|
|
|
|
|
REGISTRY: Dict[str, Backend] = {
|
|
"claude": ClaudeBackend("claude", "Claude Code", "claude"),
|
|
"codex": CodexBackend("codex", "Codex CLI", "codex"),
|
|
"grok": GrokBackend("grok", "Grok CLI", "grok"),
|
|
}
|
|
|
|
|
|
def detect() -> List[Backend]:
|
|
"""Return installed backends, in preference order."""
|
|
order = ["claude", "codex", "grok"]
|
|
return [REGISTRY[k] for k in order if REGISTRY[k].available()]
|
|
|
|
|
|
def get(key: str) -> Optional[Backend]:
|
|
return REGISTRY.get(key)
|
|
|
|
|
|
@dataclass
|
|
class RunResult:
|
|
backend: str
|
|
returncode: int
|
|
log_path: str
|
|
workdir: str
|
|
|
|
|
|
def run(backend: Backend, prompt: str, workdir: str, model: str = "",
|
|
autonomous: bool = True, mcp_config: Optional[str] = None,
|
|
env: Optional[Dict[str, str]] = None, timeout: int = 7200,
|
|
dry_run: bool = False, on_start=None) -> RunResult:
|
|
"""Execute a backend against the composed prompt and stream logs to disk.
|
|
|
|
on_start(argv): optional callback invoked with the exact command line, so
|
|
callers/UI can show precisely what is being executed behind the scenes.
|
|
"""
|
|
os.makedirs(workdir, exist_ok=True)
|
|
prompt_file = os.path.join(workdir, "master_prompt.md")
|
|
open(prompt_file, "w", encoding="utf-8").write(prompt)
|
|
log_path = os.path.join(workdir, "backend.log")
|
|
|
|
argv = backend.build_argv(prompt_file, workdir, model, autonomous, mcp_config)
|
|
if on_start:
|
|
on_start(argv)
|
|
full_env = os.environ.copy()
|
|
if env:
|
|
full_env.update(env)
|
|
|
|
# Claude Code refuses --dangerously-skip-permissions when running as root
|
|
# unless IS_SANDBOX=1 is set. The engine already isolates each run in its own
|
|
# workdir, so opt into the sandbox flag rather than failing rc=1 under root.
|
|
if autonomous and backend.key == "claude" and hasattr(os, "geteuid") and os.geteuid() == 0:
|
|
full_env.setdefault("IS_SANDBOX", "1")
|
|
|
|
if dry_run:
|
|
open(log_path, "w").write("DRY RUN\n" + " ".join(argv) + "\n")
|
|
return RunResult(backend.key, 0, log_path, workdir)
|
|
|
|
stdin_data = prompt if getattr(backend, "stdin_prompt", False) else None
|
|
with open(log_path, "w", encoding="utf-8") as logf:
|
|
proc = subprocess.run(
|
|
argv, input=stdin_data, stdout=logf, stderr=subprocess.STDOUT,
|
|
cwd=workdir, env=full_env, text=True, timeout=timeout,
|
|
)
|
|
return RunResult(backend.key, proc.returncode, log_path, workdir)
|