feat(mcp,tooling): NeuroSploit as an MCP server; tool-discovery + CVE-PoC + headless doctrine

MCP server (app/src/mcp.rs): `neurosploit mcp` speaks Model Context Protocol
over stdio (JSON-RPC 2.0), exposing run / list_runs / findings / report /
rebuild / internal / compliance as tools. Each shells out to the same binary,
so scope, safety and authorization match the CLI. Install with
`claude mcp add neurosploit -- neurosploit mcp`. Handshake, tools/list and a
live call verified. TUTORIAL section 8 + README document setup for Claude Code,
Codex and Cursor.

Tooling doctrine expanded so the agent researches and provisions the BEST tool
for the context instead of being limited to a fixed list:
- context toolboxes (AD: netexec/impacket/bloodhound-python/certipy/kerbrute/
  responder/evil-winrm; web recon; cloud; exploitation frameworks incl.
  metasploit/msfvenom; cracking) — provision on demand.
- CVE -> PoC sourcing as a core capability: on a fingerprinted version
  (WordPress/plugin/CMS/OS package/service) go to searchsploit, Exploit-DB,
  GitHub, PacketStorm/Vulners, wpscan; clone/fetch, compile (gcc/go/cargo) and
  run the PoC non-destructively, vetted and time-boxed.
- headless-only rule for GUI tools: mobsf (REST/Docker), ghidra analyzeHeadless,
  jadx/apktool/frida, radare2 — never require an X display.

383 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
CyberSecurityUPandClaude Opus 5 committed 2026-09-20 20:55:49 -03:00
1 parent 56b2c80ae4
commit a4afd784c7
6 files changed
+312 -2

No files matched your search

+5
View File
@@ -5,6 +5,7 @@ mod repl;
mod tui;
use clap::{Parser, Subcommand};
mod mcp;
use harness::{agents, models::ModelRef, pool::ModelPool, types::RunConfig, RunOutput};
use std::path::{Path, PathBuf};
@@ -187,6 +188,9 @@ enum Cmd {
#[arg(short, long)]
verbose: bool,
},
/// Run NeuroSploit as an MCP server (stdio) so Claude Code, Codex, Cursor
/// and other MCP clients can drive it as a set of tools.
Mcp,
/// Rebuild a finished run's report artifacts (md · json · html · pdf) from
/// its findings, without re-running the engagement.
Rebuild {
@@ -741,6 +745,7 @@ async fn main() -> anyhow::Result<()> {
}
}
}
Cmd::Mcp => { mcp::serve()?; }
Cmd::Rebuild { run } => {
// Accept either a path or a bare run id, resolved against the same
// runs root the engagement wrote to.
+213
View File
@@ -0,0 +1,213 @@
//! NeuroSploit as an MCP server.
//!
//! Speaks the Model Context Protocol over stdio (JSON-RPC 2.0, line-delimited),
//! so Claude Code, Codex, Cursor and any MCP client can drive NeuroSploit as a
//! set of tools. It does not re-implement the harness: each tool shells out to
//! the same `neurosploit` binary the operator already uses, so behaviour,
//! authorization and safety are identical to the CLI.
//!
//! Exposed tools:
//! - `neurosploit_run` launch an engagement (returns the run id + summary)
//! - `neurosploit_list_runs` list finished runs
//! - `neurosploit_findings` read a run's findings.json
//! - `neurosploit_report` read a run's markdown report
//! - `neurosploit_rebuild` rebuild a run's report from its findings
//! - `neurosploit_internal` internal/AD attack-graph analysis
//! - `neurosploit_compliance` map a run onto PCI-DSS / HIPAA / SOC 2
//!
//! Kept dependency-free: JSON-RPC framing is hand-rolled over stdin/stdout with
//! serde_json, and tools run via std::process::Command.
use serde_json::{json, Value};
use std::io::{BufRead, Write};
const PROTOCOL_VERSION: &str = "2024-11-05";
/// Run the MCP server loop until stdin closes.
pub fn serve() -> anyhow::Result<()> {
let stdin = std::io::stdin();
let mut out = std::io::stdout();
let exe = std::env::current_exe().unwrap_or_else(|_| "neurosploit".into());
for line in stdin.lock().lines() {
let line = match line {
Ok(l) if !l.trim().is_empty() => l,
Ok(_) => continue,
Err(_) => break,
};
let req: Value = match serde_json::from_str(&line) {
Ok(v) => v,
Err(_) => continue, // ignore malformed frames
};
let id = req.get("id").cloned();
let method = req.get("method").and_then(|m| m.as_str()).unwrap_or("");
// Notifications (no id) get no response.
let response = match method {
"initialize" => Some(ok(id, json!({
"protocolVersion": PROTOCOL_VERSION,
"capabilities": { "tools": {} },
"serverInfo": { "name": "neurosploit", "version": env!("CARGO_PKG_VERSION") }
}))),
"tools/list" => Some(ok(id, json!({ "tools": tool_list() }))),
"tools/call" => Some(handle_call(id, &req, &exe)),
"ping" => Some(ok(id, json!({}))),
m if m.starts_with("notifications/") => None,
_ if id.is_some() => Some(err(id, -32601, "method not found")),
_ => None,
};
if let Some(resp) = response {
let s = serde_json::to_string(&resp)?;
writeln!(out, "{s}")?;
out.flush()?;
}
}
Ok(())
}
fn ok(id: Option<Value>, result: Value) -> Value {
json!({ "jsonrpc": "2.0", "id": id, "result": result })
}
fn err(id: Option<Value>, code: i64, msg: &str) -> Value {
json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": msg } })
}
/// The tool catalogue advertised to the client.
fn tool_list() -> Value {
json!([
{
"name": "neurosploit_run",
"description": "Launch a NeuroSploit engagement against an authorized target. Black-box by default. Returns the run id, the finding summary, and the report path. Only test targets you are authorized to test.",
"inputSchema": {
"type": "object",
"properties": {
"target": { "type": "string", "description": "URL or host, e.g. https://app.example.com" },
"mode": { "type": "string", "enum": ["run","whitebox","greybox","host"], "description": "Engagement mode (default run)" },
"model": { "type": "string", "description": "provider:model, e.g. anthropic:claude-opus-4-8" },
"subscription": { "type": "boolean", "description": "Use the local CLI login instead of an API key" },
"focus": { "type": "string", "description": "What to prioritise" },
"scope_file": { "type": "string", "description": "Path to a scope YAML (hard boundary)" },
"sandbox": { "type": "boolean", "description": "Run tool commands in the Kali container" },
"typesafe": { "type": "string", "enum": ["on","off","auto"] },
"max_agents": { "type": "integer" }
},
"required": ["target"]
}
},
{ "name": "neurosploit_list_runs", "description": "List finished NeuroSploit runs (ids and targets).", "inputSchema": { "type": "object", "properties": {} } },
{ "name": "neurosploit_findings", "description": "Read a finished run's findings as JSON.", "inputSchema": { "type": "object", "properties": { "run": { "type": "string", "description": "Run id or path" } }, "required": ["run"] } },
{ "name": "neurosploit_report", "description": "Read a finished run's Markdown report.", "inputSchema": { "type": "object", "properties": { "run": { "type": "string" } }, "required": ["run"] } },
{ "name": "neurosploit_rebuild", "description": "Rebuild a run's report artifacts from its findings (no model calls).", "inputSchema": { "type": "object", "properties": { "run": { "type": "string" } }, "required": ["run"] } },
{ "name": "neurosploit_internal", "description": "Internal-network / Active Directory attack-graph analysis: paths to crown jewels and the choke point to fix first.", "inputSchema": { "type": "object", "properties": { "graph": { "type": "string", "description": "Path to a graph JSON" }, "scaffold": { "type": "string", "description": "Domain to scaffold, e.g. corp.local" }, "from": { "type": "string", "description": "Foothold node id" } } } },
{ "name": "neurosploit_compliance", "description": "Map a finished run's findings onto PCI-DSS, HIPAA or SOC 2 controls.", "inputSchema": { "type": "object", "properties": { "run": { "type": "string" }, "framework": { "type": "string", "enum": ["pci-dss","hipaa","soc2"] } }, "required": ["run"] } }
])
}
/// Dispatch a `tools/call`.
fn handle_call(id: Option<Value>, req: &Value, exe: &std::path::Path) -> Value {
let params = req.get("params").cloned().unwrap_or(json!({}));
let name = params.get("name").and_then(|n| n.as_str()).unwrap_or("");
let a = params.get("arguments").cloned().unwrap_or(json!({}));
let s = |k: &str| a.get(k).and_then(|v| v.as_str()).map(|x| x.to_string());
let b = |k: &str| a.get(k).and_then(|v| v.as_bool()).unwrap_or(false);
let mut argv: Vec<String> = Vec::new();
match name {
"neurosploit_run" => {
let Some(target) = s("target") else { return tool_err(id, "target is required") };
argv.push(s("mode").unwrap_or_else(|| "run".into()));
argv.push(target);
if let Some(m) = s("model") { argv.push("--model".into()); argv.push(m); }
if b("subscription") { argv.push("--subscription".into()); }
if let Some(f) = s("focus") { argv.push("--focus".into()); argv.push(f); }
if let Some(sf) = s("scope_file") { argv.push("--scope-file".into()); argv.push(sf); }
if b("sandbox") { argv.push("--sandbox".into()); }
if let Some(ts) = s("typesafe") { argv.push("--typesafe".into()); argv.push(ts); }
if let Some(n) = a.get("max_agents").and_then(|v| v.as_i64()) { argv.push("--max-agents".into()); argv.push(n.to_string()); }
argv.push("-v".into());
}
"neurosploit_list_runs" => { argv.push("runs".into()); }
"neurosploit_findings" => {
let Some(run) = s("run") else { return tool_err(id, "run is required") };
return read_run_file(id, &run, "findings.json");
}
"neurosploit_report" => {
let Some(run) = s("run") else { return tool_err(id, "run is required") };
return read_run_file(id, &run, "report.md");
}
"neurosploit_rebuild" => { let Some(run) = s("run") else { return tool_err(id, "run is required") }; argv.push("rebuild".into()); argv.push(run); }
"neurosploit_internal" => {
argv.push("internal".into());
if let Some(g) = s("graph") { argv.push("--graph".into()); argv.push(g); }
if let Some(sc) = s("scaffold") { argv.push("--scaffold".into()); argv.push(sc); }
if let Some(fr) = s("from") { argv.push("--from".into()); argv.push(fr); }
}
"neurosploit_compliance" => {
let Some(run) = s("run") else { return tool_err(id, "run is required") };
argv.push("compliance".into()); argv.push(run);
if let Some(fw) = s("framework") { argv.push("--framework".into()); argv.push(fw); }
}
_ => return tool_err(id, &format!("unknown tool: {name}")),
}
// `runs` subcommand doesn't exist as a bare CLI verb; list from the runs dir.
if name == "neurosploit_list_runs" {
return list_runs(id);
}
let out = std::process::Command::new(exe).args(&argv).output();
match out {
Ok(o) => {
let mut text = String::from_utf8_lossy(&o.stdout).to_string();
let stderr = String::from_utf8_lossy(&o.stderr);
if !stderr.trim().is_empty() {
text.push_str("\n--- stderr ---\n");
text.push_str(&stderr);
}
tool_text(id, &strip_ansi(&text))
}
Err(e) => tool_err(id, &format!("failed to run neurosploit: {e}")),
}
}
fn read_run_file(id: Option<Value>, run: &str, file: &str) -> Value {
let base = std::env::current_dir().unwrap_or_default();
let dir = if std::path::Path::new(run).is_dir() { std::path::PathBuf::from(run) } else { base.join("runs").join(run) };
match std::fs::read_to_string(dir.join(file)) {
Ok(s) => tool_text(id, &s),
Err(e) => tool_err(id, &format!("cannot read {}/{file}: {e}", dir.display())),
}
}
fn list_runs(id: Option<Value>) -> Value {
let base = std::env::current_dir().unwrap_or_default().join("runs");
let mut names: Vec<String> = std::fs::read_dir(&base)
.map(|rd| rd.filter_map(|e| e.ok()).filter(|e| e.path().is_dir()).map(|e| e.file_name().to_string_lossy().to_string()).collect())
.unwrap_or_default();
names.sort();
names.reverse();
tool_text(id, &if names.is_empty() { "no runs found".into() } else { names.join("\n") })
}
fn tool_text(id: Option<Value>, text: &str) -> Value {
// Cap the payload so a huge report doesn't blow the client's context.
let capped: String = text.chars().take(60_000).collect();
ok(id, json!({ "content": [ { "type": "text", "text": capped } ], "isError": false }))
}
fn tool_err(id: Option<Value>, msg: &str) -> Value {
ok(id, json!({ "content": [ { "type": "text", "text": msg } ], "isError": true }))
}
fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\u{1b}' {
if chars.peek() == Some(&'[') { chars.next(); while let Some(&n) = chars.peek() { chars.next(); if n.is_ascii_alphabetic() { break; } } }
continue;
}
out.push(c);
}
out
}
@@ -300,6 +300,26 @@ fn tool_doctrine(mcp_on: bool) -> String {
(`timeout 90 <install> || echo skip`) and try each at most once — if it fails, isn't packaged, has no network \
or hangs, SKIP it and fall back to curl/nc/dig/python3. A missing or un-downloadable tool is NEVER a reason \
to stall: move on with what you have.\n\
- PICK THE BEST TOOL, DON'T SETTLE FOR THIS LIST: the tools above are a starting menu, not a limit. You are expected to reason about the CONTEXT and reach for (or install) the strongest tool for THAT job, the way a real operator does — if a better/more specific tool exists, research it, provision it (see TOOL DOWNLOAD), and use it. Prefer battle-tested tooling over hand-rolled scripts when it fits.
- CONTEXT TOOLBOXES (provision what the detected surface calls for; time-box each install, skip on failure):
· Active Directory / Windows / SMB / LDAP / Kerberos: `netexec`(nxc)/`crackmapexec`, the `impacket` suite (secretsdump, GetUserSPNs, GetNPUsers, ntlmrelayx, psexec/wmiexec/smbexec), `bloodhound-python`/`bloodhound-ce` + neo4j for the AD graph, `kerbrute`, `certipy` (AD CS/ESC), `ldapdomaindump`, `enum4linux-ng`, `responder`, `evil-winrm`, `Coercer`/`PetitPotam`, `adidnsdump`. Map credential→identity→permission→machine and feed the internal attack graph.
· Web / API deep recon: `httpx`, `katana`, `gau`, `waybackurls`, `subfinder`/`amass` (only in-scope), `arjun` (param mining), `nuclei` (targeted), `feroxbuster`/`ffuf`, `jwt_tool`, `graphw00f`/`clairvoyance` (GraphQL), `trufflehog`/`gitleaks` on exposed repos.
· Cloud: `pacu`, `scoutsuite`, `prowler`, `trivy`, the provider CLIs (`aws`/`gcloud`/`az`), `cloud_enum`.
· Exploitation frameworks: `metasploit`/`msfconsole` and `msfvenom` for payloads/PoCs, `sqlmap` for deep SQLi, `commix` for command injection — use surgically, non-destructively, never a blind auto-exploit.
· Secrets/creds & cracking (offline, on captured material only): `hashcat`/`john`, `trufflehog`.
· Mobile (APK/IPA): `mobsf` (run HEADLESS via its Docker image / REST API, not the GUI), `apktool`, `jadx`, \
`apkleaks`, `objection`/`frida`; `nuclei` on discovered endpoints.\n\
· Reverse engineering / binaries: `ghidra` HEADLESS (`analyzeHeadless`), `radare2`/`rizin`, `binwalk`, \
`checksec`, `gdb`; decompile and triage without any GUI.\n\
· Any GUI tool runs HEADLESS: prefer a CLI / REST / `analyzeHeadless` / `--no-gui` mode or the Docker \
image; never require an X display. If a tool is GUI-only, drive its API or skip it.\n\
- HEAVY TOOLBOX: many of these ship in `kali-linux-large` or install via apt/pipx/go — when running in the Kali sandbox they are one `apt install`/`pipx install` away; provision on demand for the context at hand.
- CVE -> PoC SOURCING (core capability, not a last resort): the moment you fingerprint a concrete version (WordPress core/plugin/theme, a CMS, framework/library, an OS package, a service banner), find and run a real proof-of-concept for its known CVEs:
· `searchsploit <product> <version>` (Exploit-DB, offline in Kali); `searchsploit -m <id>` copies a PoC locally, `-x` reads it, `-u` updates the DB.
· Search Exploit-DB, GitHub (`github.com/search?q=CVE-XXXX-YYYY`), PacketStorm, Vulners and the GHSA/NVD advisory for a working PoC; `git clone` the specific repo (pinned) or fetch the single script.
· WordPress: `wpscan --url <t> --enumerate vp,vt,u` to pin vulnerable plugins/themes, then pull the PoC.
· BUILD when needed: compile C/Go/Rust PoCs (`gcc`/`make`/`go build`/`cargo build`) or install Python/Ruby deps in the sandbox; READ the exploit first, adapt hardcoded targets/ports to THIS engagement, run it non-destructively (benign marker/`id`/OOB callback as proof, never a destructive payload).
· Vet everything: reputable source, pinned version, read before running, time-box each fetch/build; if no PoC exists or it will not build, fall back to a manual attempt or report the version as a lead. This is how you exploit CVEs that have a public PoC.
- {browser}\n\
- {ua}{proxy}{pocs}\
Use only what is installed; degrade gracefully. Never block on a single tool install. Never run destructive or DoS actions.\n\n",
+1 -1
View File
@@ -82,7 +82,7 @@ pub struct SandboxConfig {
impl Default for SandboxConfig {
fn default() -> Self {
SandboxConfig {
image: "kalilinux/kali-rolling".into(),
image: "kalilinux/kali-rolling".into(), // override with --sandbox kalilinux/kali-linux-large for the full toolbox
name: "neurosploit-kali".into(),
workdir: None,
env: Vec::new(),