v3.3.0 GUI dashboard + reports + model expansion + root fix

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>
This commit is contained in:
CyberSecurityUP
2026-06-14 23:26:11 -03:00
parent 22a7302a35
commit a5badefc29
205 changed files with 809 additions and 199 deletions
+14 -2
View File
@@ -122,18 +122,30 @@ class RunResult:
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) -> RunResult:
"""Execute a backend against the composed prompt and stream logs to disk."""
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)
+65 -4
View File
@@ -30,12 +30,13 @@ class Provider:
base_url_env: Optional[str] = None # env var the backend reads for base URL
models: List[Model] = field(default_factory=list)
subscription: bool = False # uses a CLI subscription rather than an API key
kind: str = "api" # "cli" (native agentic CLI) | "api" (OpenAI-compatible)
PROVIDERS: Dict[str, Provider] = {
# --- Anthropic (latest Claude family; default) -------------------------
"anthropic": Provider(
key="anthropic", label="Anthropic Claude",
key="anthropic", label="Anthropic Claude", kind="cli",
env_keys=["ANTHROPIC_API_KEY"],
models=[
Model("claude-opus-4-8", "Claude Opus 4.8", 1_000_000, "Most capable; deep multi-step pentest reasoning"),
@@ -45,16 +46,17 @@ PROVIDERS: Dict[str, Provider] = {
),
# --- OpenAI ------------------------------------------------------------
"openai": Provider(
key="openai", label="OpenAI",
key="openai", label="OpenAI", kind="cli",
env_keys=["OPENAI_API_KEY"],
models=[
Model("gpt-5.1", "GPT-5.1", 400_000, "Strong general reasoning"),
Model("gpt-5.1-codex", "GPT-5.1 Codex", 400_000, "Codex CLI default"),
Model("o4", "o4", 200_000, "Deliberate reasoning for validation"),
],
),
# --- xAI Grok ----------------------------------------------------------
"xai": Provider(
key="xai", label="xAI Grok",
key="xai", label="xAI Grok", kind="cli",
env_keys=["XAI_API_KEY", "GROK_API_KEY"],
base_url="https://api.x.ai/v1", base_url_env="OPENAI_BASE_URL",
models=[
@@ -72,6 +74,56 @@ PROVIDERS: Dict[str, Provider] = {
Model("nvidia/llama-3.3-nemotron-super-49b-v1", "Nemotron Super 49B", 128_000, "NIM hosted reasoning"),
Model("deepseek-ai/deepseek-r1", "DeepSeek-R1 (NIM)", 128_000, "Strong reasoning via NIM"),
Model("qwen/qwen2.5-coder-32b-instruct", "Qwen2.5 Coder 32B (NIM)", 128_000, "Code/exploit oriented"),
Model("qwen/qwq-32b", "QwQ 32B (NIM)", 128_000, "Reasoning"),
Model("meta/llama-3.3-70b-instruct", "Llama 3.3 70B (NIM)", 128_000),
Model("mistralai/mistral-large-2-instruct", "Mistral Large 2 (NIM)", 128_000),
],
),
# --- DeepSeek (direct API) --------------------------------------------
"deepseek": Provider(
key="deepseek", label="DeepSeek", env_keys=["DEEPSEEK_API_KEY"],
base_url="https://api.deepseek.com/v1", base_url_env="OPENAI_BASE_URL",
models=[
Model("deepseek-reasoner", "DeepSeek-R1 (reasoner)", 64_000, "Deep reasoning"),
Model("deepseek-chat", "DeepSeek-V3 (chat)", 64_000),
],
),
# --- Mistral (direct API) ---------------------------------------------
"mistral": Provider(
key="mistral", label="Mistral", env_keys=["MISTRAL_API_KEY"],
base_url="https://api.mistral.ai/v1", base_url_env="OPENAI_BASE_URL",
models=[
Model("mistral-large-latest", "Mistral Large", 128_000),
Model("codestral-latest", "Codestral", 256_000, "Code/exploit oriented"),
],
),
# --- Alibaba Qwen (DashScope, OpenAI-compatible) ----------------------
"qwen": Provider(
key="qwen", label="Qwen (DashScope)", env_keys=["DASHSCOPE_API_KEY", "QWEN_API_KEY"],
base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1", base_url_env="OPENAI_BASE_URL",
models=[
Model("qwen-max", "Qwen Max", 32_000),
Model("qwen2.5-coder-32b-instruct", "Qwen2.5 Coder 32B", 128_000, "Code/exploit oriented"),
Model("qwq-plus", "QwQ Plus", 128_000, "Reasoning"),
],
),
# --- Groq (fast OpenAI-compatible) ------------------------------------
"groq": Provider(
key="groq", label="Groq", env_keys=["GROQ_API_KEY"],
base_url="https://api.groq.com/openai/v1", base_url_env="OPENAI_BASE_URL",
models=[
Model("llama-3.3-70b-versatile", "Llama 3.3 70B (Groq)", 128_000, "Very fast"),
Model("qwen-2.5-coder-32b", "Qwen2.5 Coder 32B (Groq)", 128_000),
],
),
# --- Together AI ------------------------------------------------------
"together": Provider(
key="together", label="Together AI", env_keys=["TOGETHER_API_KEY"],
base_url="https://api.together.xyz/v1", base_url_env="OPENAI_BASE_URL",
models=[
Model("Qwen/Qwen2.5-Coder-32B-Instruct", "Qwen2.5 Coder 32B", 128_000),
Model("deepseek-ai/DeepSeek-R1", "DeepSeek-R1", 128_000),
Model("meta-llama/Llama-3.3-70B-Instruct-Turbo", "Llama 3.3 70B Turbo", 128_000),
],
),
# --- Google Gemini -----------------------------------------------------
@@ -88,7 +140,14 @@ PROVIDERS: Dict[str, Provider] = {
key="openrouter", label="OpenRouter",
env_keys=["OPENROUTER_API_KEY"],
base_url="https://openrouter.ai/api/v1", base_url_env="OPENAI_BASE_URL",
models=[Model("anthropic/claude-opus-4-8", "Opus 4.8 (OpenRouter)", 1_000_000)],
models=[
Model("anthropic/claude-opus-4-8", "Opus 4.8 (OpenRouter)", 1_000_000),
Model("qwen/qwen-2.5-coder-32b-instruct", "Qwen2.5 Coder 32B", 128_000),
Model("deepseek/deepseek-r1", "DeepSeek-R1", 128_000),
Model("meta-llama/llama-3.3-70b-instruct", "Llama 3.3 70B", 128_000),
Model("mistralai/mistral-large", "Mistral Large", 128_000),
Model("x-ai/grok-4", "Grok 4", 256_000),
],
),
# --- Local Ollama ------------------------------------------------------
"ollama": Provider(
@@ -97,6 +156,8 @@ PROVIDERS: Dict[str, Provider] = {
base_url="http://localhost:11434/v1", base_url_env="OPENAI_BASE_URL",
models=[
Model("qwen2.5-coder:32b", "Qwen2.5 Coder 32B (local)", 32_000),
Model("qwq:32b", "QwQ 32B (local)", 32_000, "Reasoning"),
Model("deepseek-r1:32b", "DeepSeek-R1 32B (local)", 64_000),
Model("llama3.3:70b", "Llama 3.3 70B (local)", 128_000),
],
),
+31 -16
View File
@@ -12,7 +12,7 @@ import json
import os
from typing import Dict, List, Optional
from . import backends, mcp, models
from . import backends, mcp, models, report
from .agent_loader import AgentLibrary
from .config import RunConfig, PATHS, ensure_dirs
from .rl import RLEngine, outcomes_from_findings
@@ -70,11 +70,19 @@ and follow its methodology and (strict) anti-false-positive System Prompt.
6. `meta/reporter.md` → write `results/findings.json` AND `reports/report.md`.
7. `meta/rl_feedback.md` → write/merge `data/rl_state.json`.
## Evidence: screenshots (MANDATORY for confirmed findings)
For every confirmed finding, use Playwright MCP to capture a screenshot proving
the issue (e.g. the executed XSS alert/DOM, the exposed data, the error oracle).
Save it under `{cfg.resolved_workdir()}/shots/<finding-id>.png` and record that
relative path in the finding's `screenshot` field.
## Output contract (MANDATORY)
Write `results/findings.json` as a JSON array of objects:
{{"id","agent","title","severity","cvss","cwe","endpoint","payload","evidence","impact","remediation","confidence","validated"}}
{{"id","agent","title","severity","cvss","cwe","endpoint","payload","evidence","impact","remediation","confidence","validated","screenshot"}}
Only include findings with `validated: true`. If you find nothing, write `[]`.
Also write `results/agents_ran.json` as a JSON array of the agent names you executed.
Also write `results/agents_ran.json` as a JSON array of the agent names you executed,
and `results/activity.json` as an array of `{{"agent","status","note"}}` task records
so the dashboard can show what was executed.
Stay strictly in scope. Never run destructive/DoS payloads unless ROE permits.
Report ONLY proven, reproducible findings.
@@ -83,23 +91,19 @@ Report ONLY proven, reproducible findings.
def collect_results(workdir: str) -> Dict:
findings, ran = [], []
fpath = os.path.join(workdir, "findings.json")
rpath = os.path.join(workdir, "agents_ran.json")
collected = {"findings": [], "agents_ran": [], "activity": []}
files = {"findings.json": "findings", "agents_ran.json": "agents_ran",
"activity.json": "activity"}
# The backend may write under results/<slug>/ or results/ — check both.
for base in (workdir, PATHS["results"]):
for name, sink in (("findings.json", "findings"), ("agents_ran.json", "ran")):
for name, sink in files.items():
p = os.path.join(base, name)
if os.path.exists(p):
if not collected[sink] and os.path.exists(p):
try:
data = json.load(open(p, encoding="utf-8"))
if sink == "findings" and not findings:
findings = data
elif sink == "ran" and not ran:
ran = data
collected[sink] = json.load(open(p, encoding="utf-8"))
except Exception:
pass
return {"findings": findings, "agents_ran": ran}
return collected
def run_engagement(cfg: RunConfig, recon: Optional[dict] = None,
@@ -131,14 +135,24 @@ def run_engagement(cfg: RunConfig, recon: Optional[dict] = None,
progress(f"Launching {backend.label} ({cfg.model}) — autonomous={cfg.autonomous}")
res = backends.run(backend, prompt, workdir, model=cfg.model,
autonomous=cfg.autonomous, mcp_config=mcp_cfg, env=env,
timeout=cfg.timeout, dry_run=cfg.dry_run)
timeout=cfg.timeout, dry_run=cfg.dry_run,
on_start=lambda argv: progress("exec: " + " ".join(argv)))
progress(f"Backend exited rc={res.returncode}; log: {res.log_path}")
out = collect_results(workdir)
findings = out["findings"] or []
ran = out["agents_ran"] or []
activity = out["activity"] or []
progress(f"Collected {len(findings)} validated finding(s) from {len(ran)} agent(s)")
reports = {}
if not cfg.dry_run:
try:
reports = report.generate(cfg.target, findings, PATHS["reports"])
progress("Report generated: " + ", ".join(k for k in reports if not k.endswith("_error")))
except Exception as e:
progress(f"Report generation skipped: {e}")
if cfg.use_rl and not cfg.dry_run:
tech = ((recon or {}).get("tech", {}) or {}).get("framework", "") or None
outcomes = outcomes_from_findings(findings, ran, tech=tech)
@@ -147,4 +161,5 @@ def run_engagement(cfg: RunConfig, recon: Optional[dict] = None,
progress("RL state updated → data/rl_state.json")
return {"workdir": workdir, "returncode": res.returncode,
"findings": findings, "agents_ran": ran, "log": res.log_path}
"findings": findings, "agents_ran": ran, "activity": activity,
"reports": reports, "log": res.log_path}
+152
View File
@@ -0,0 +1,152 @@
"""
Report generation for NeuroSploit v3.3.0.
Produces a polished **HTML** report from a run's validated findings, plus a
**PDF** via the Typst engine when the `typst` binary is available (it is the
intended report engine; HTML is always emitted as a fallback/companion).
from neurosploit_agent.report import generate
paths = generate(target, findings, out_dir) # -> {"html":..., "pdf":..., "typ":...}
"""
import datetime
import html
import os
import shutil
import subprocess
from typing import Dict, List, Optional
SEV_ORDER = {"Critical": 0, "High": 1, "Medium": 2, "Low": 3, "Info": 4}
SEV_COLOR = {"Critical": "#c0392b", "High": "#e67e22", "Medium": "#f1c40f",
"Low": "#3498db", "Info": "#7f8c8d"}
def _sorted(findings: List[Dict]) -> List[Dict]:
return sorted(findings, key=lambda f: SEV_ORDER.get(f.get("severity", "Info"), 9))
def _counts(findings: List[Dict]) -> Dict[str, int]:
c = {}
for f in findings:
c[f.get("severity", "Info")] = c.get(f.get("severity", "Info"), 0) + 1
return c
def typst_available() -> bool:
return shutil.which("typst") is not None
# --------------------------------------------------------------------------- HTML
def render_html(target: str, findings: List[Dict], when: str) -> str:
counts = _counts(findings)
chips = "".join(
f'<span class="chip" style="background:{SEV_COLOR.get(s,"#777")}">{s}: {n}</span>'
for s, n in sorted(counts.items(), key=lambda kv: SEV_ORDER.get(kv[0], 9))
) or '<span class="chip" style="background:#27ae60">No validated findings</span>'
rows = []
for i, f in enumerate(_sorted(findings), 1):
sev = f.get("severity", "Info")
rows.append(f"""
<section class="finding">
<h3><span class="sev" style="background:{SEV_COLOR.get(sev,'#777')}">{html.escape(sev)}</span>
{i}. {html.escape(str(f.get('title','Untitled')))}</h3>
<table class="kv">
<tr><th>Agent</th><td>{html.escape(str(f.get('agent','')))}</td>
<th>CWE</th><td>{html.escape(str(f.get('cwe','')))}</td></tr>
<tr><th>CVSS</th><td>{html.escape(str(f.get('cvss','')))}</td>
<th>Confidence</th><td>{html.escape(str(f.get('confidence','')))}</td></tr>
<tr><th>Endpoint</th><td colspan="3">{html.escape(str(f.get('endpoint','')))}</td></tr>
</table>
<h4>Payload</h4><pre>{html.escape(str(f.get('payload','')))}</pre>
<h4>Evidence</h4><pre>{html.escape(str(f.get('evidence','')))}</pre>
<h4>Impact</h4><p>{html.escape(str(f.get('impact','')))}</p>
<h4>Remediation</h4><p>{html.escape(str(f.get('remediation','')))}</p>
</section>""")
body = "\n".join(rows) or "<p><em>No validated findings were produced for this engagement.</em></p>"
return f"""<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
<title>NeuroSploit Report — {html.escape(target)}</title>
<style>
body{{font:14px/1.6 -apple-system,Segoe UI,Roboto,sans-serif;color:#1a1a1a;max-width:860px;margin:40px auto;padding:0 24px}}
h1{{margin:0;font-size:26px}} .meta{{color:#666;margin:4px 0 18px}}
.chips{{margin:14px 0 28px}} .chip{{color:#fff;border-radius:999px;padding:4px 12px;margin-right:8px;font-size:13px;font-weight:600}}
.finding{{border:1px solid #e3e3e3;border-radius:12px;padding:18px 20px;margin:18px 0}}
.finding h3{{margin:0 0 12px;font-size:17px}} .sev{{color:#fff;border-radius:6px;padding:2px 8px;font-size:12px;margin-right:8px}}
table.kv{{border-collapse:collapse;width:100%;margin:8px 0}} .kv th{{text-align:left;color:#666;font-weight:600;width:90px;padding:3px 8px}}
.kv td{{padding:3px 8px}} pre{{background:#0f1117;color:#dfe6f3;padding:12px;border-radius:8px;overflow:auto;font-size:12.5px}}
h4{{margin:14px 0 4px;font-size:13px;text-transform:uppercase;letter-spacing:.5px;color:#8b5cf6}}
.brand{{color:#8b5cf6;font-weight:800}} footer{{color:#999;font-size:12px;margin-top:30px;border-top:1px solid #eee;padding-top:12px}}
</style></head><body>
<h1><span class="brand">NeuroSploit</span> Penetration Test Report</h1>
<div class="meta">Target: <b>{html.escape(target)}</b> · Generated {html.escape(when)} · v3.3.0 Autonomous MD-Agent Engine</div>
<div class="chips">{chips}</div>
<h2>Findings ({len(findings)})</h2>
{body}
<footer>Authorized testing only. All findings were independently validated and false-positive-filtered before inclusion.</footer>
</body></html>"""
# --------------------------------------------------------------------------- Typst
def render_typst(target: str, findings: List[Dict], when: str) -> str:
def s(v):
"""Safely embed arbitrary text as a Typst string literal (// is inert)."""
return '"' + str(v).replace("\\", "\\\\").replace('"', '\\"').replace("\n", " ") + '"'
counts = _counts(findings)
summary = ", ".join(f"{k}: {n}" for k, n in sorted(counts.items(), key=lambda kv: SEV_ORDER.get(kv[0], 9))) or "No validated findings"
parts = [f'''#set page(margin: 2cm, numbering: "1")
#set text(size: 10pt)
#let sevcolor = (Critical: rgb("#c0392b"), High: rgb("#e67e22"), Medium: rgb("#f1c40f"), Low: rgb("#3498db"), Info: rgb("#7f8c8d"))
#let sev(label) = box(fill: sevcolor.at(label, default: rgb("#7f8c8d")), inset: 3pt, radius: 3pt, text(fill: white, weight: "bold", label))
#align(center)[#text(20pt, weight: "bold")[#text(fill: rgb("#8b5cf6"))[NeuroSploit] Penetration Test Report]]
#align(center)[Target: #strong(target) #h(6pt) Generated {s(when)} #h(6pt) v3.3.0]
#line(length: 100%, stroke: 0.5pt + gray)
#strong[Summary:] {s(summary)}
#v(6pt)
== Findings ({len(findings)})
'''.replace("#strong(target)", f"#strong({s(target)})")]
for i, f in enumerate(_sorted(findings), 1):
parts.append(f'''
#block(breakable: false, stroke: 0.5pt + rgb("#dddddd"), radius: 6pt, inset: 10pt, width: 100%)[
#sev({s(f.get('severity','Info'))}) #h(4pt) #strong[{i}. ] #strong({s(f.get('title','Untitled'))})
#v(4pt)
Agent: {s(f.get('agent',''))} #h(6pt) CWE: {s(f.get('cwe',''))} #h(6pt) CVSS: {s(f.get('cvss',''))}
#v(2pt) Endpoint: #raw({s(f.get('endpoint',''))})
#v(4pt) #strong[Payload] #linebreak() #raw({s(f.get('payload',''))})
#v(2pt) #strong[Evidence] #linebreak() #raw({s(f.get('evidence',''))})
#v(2pt) #strong[Impact:] {s(f.get('impact',''))}
#v(2pt) #strong[Remediation:] {s(f.get('remediation',''))}
]''')
if not findings:
parts.append("\n#emph[No validated findings were produced for this engagement.]\n")
return "\n".join(parts)
# --------------------------------------------------------------------------- API
def generate(target: str, findings: List[Dict], out_dir: str,
when: Optional[str] = None) -> Dict[str, str]:
os.makedirs(out_dir, exist_ok=True)
when = when or datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
out: Dict[str, str] = {}
html_path = os.path.join(out_dir, "report.html")
open(html_path, "w").write(render_html(target, findings, when))
out["html"] = html_path
typ_path = os.path.join(out_dir, "report.typ")
open(typ_path, "w").write(render_typst(target, findings, when))
out["typ"] = typ_path
if typst_available():
pdf_path = os.path.join(out_dir, "report.pdf")
try:
r = subprocess.run(["typst", "compile", typ_path, pdf_path],
capture_output=True, text=True, timeout=120)
if r.returncode == 0 and os.path.exists(pdf_path):
out["pdf"] = pdf_path
else:
out["pdf_error"] = (r.stderr or "typst failed").strip()[:400]
except Exception as e:
out["pdf_error"] = str(e)
else:
out["pdf_error"] = "typst binary not found"
return out