diff --git a/README.md b/README.md index 76bd0c8..1bb798c 100755 --- a/README.md +++ b/README.md @@ -681,6 +681,22 @@ Critical is not a Critical. --- +## 🔌 Run it as an MCP server + +Drive NeuroSploit from Claude Code, Codex or Cursor as tools: + +```bash +neurosploit mcp # MCP server over stdio +claude mcp add neurosploit -- neurosploit mcp +``` + +Exposes `neurosploit_run`, `neurosploit_findings`, `neurosploit_report`, +`neurosploit_rebuild`, `neurosploit_internal`, `neurosploit_compliance`, +`neurosploit_list_runs`. Each shells out to the same binary, so scope, safety +and authorization are identical to the CLI. See TUTORIAL section 8. + +--- + ## 📊 How we compare A rough, honest capability benchmark against Strix, Shannon, Penligent and the diff --git a/TUTORIAL.md b/TUTORIAL.md index 4537f11..4704b2f 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -431,7 +431,63 @@ neurosploit tui http://testphp.vulnweb.com/ --subscription --model anthropic:cla --- -## 8. Web console +## 8. NeuroSploit as an MCP server + +Run NeuroSploit as a Model Context Protocol server and any MCP client (Claude +Code, Codex, Cursor, ...) can drive it as tools, from inside your normal agent +session. + +```bash +neurosploit mcp # speaks MCP over stdio +``` + +### Install in Claude Code + +```bash +claude mcp add neurosploit -- neurosploit mcp +``` + +Or add it by hand to `~/.claude.json` (or a project `.mcp.json`): + +```json +{ + "mcpServers": { + "neurosploit": { "command": "neurosploit", "args": ["mcp"] } + } +} +``` + +### Install in Codex / Cursor / generic MCP client + +Point the client at the command `neurosploit mcp` (stdio transport). For Codex, +add to its MCP config: + +```toml +[mcp_servers.neurosploit] +command = "neurosploit" +args = ["mcp"] +``` + +### Tools it exposes + +| Tool | What it does | +|---|---| +| `neurosploit_run` | Launch an engagement (target, mode, model, focus, scope-file, sandbox, typesafe) | +| `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 (no model calls) | +| `neurosploit_internal` | Internal / AD attack-graph analysis | +| `neurosploit_compliance` | Map a run onto PCI-DSS / HIPAA / SOC 2 | + +Each tool shells out to the same `neurosploit` binary, so authorization, scope +and safety are identical to the CLI. The MCP server needs `neurosploit` on +`PATH` (or use an absolute path in the config) and, for a run, whatever the +engagement needs (a model API key or `--subscription`). + +--- + +## 8b. Web console A browser UI for the same harness — one `node` process serves the SPA and drives the compiled `neurosploit` binary; nothing about the harness logic is reimplemented in the browser. diff --git a/neurosploit-rs/app/src/main.rs b/neurosploit-rs/app/src/main.rs index cc94ac2..b2f2fd0 100644 --- a/neurosploit-rs/app/src/main.rs +++ b/neurosploit-rs/app/src/main.rs @@ -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. diff --git a/neurosploit-rs/app/src/mcp.rs b/neurosploit-rs/app/src/mcp.rs new file mode 100644 index 0000000..35f93f5 --- /dev/null +++ b/neurosploit-rs/app/src/mcp.rs @@ -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, result: Value) -> Value { + json!({ "jsonrpc": "2.0", "id": id, "result": result }) +} +fn err(id: Option, 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, 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 = 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, 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 { + let base = std::env::current_dir().unwrap_or_default().join("runs"); + let mut names: Vec = 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, 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, 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 +} diff --git a/neurosploit-rs/crates/harness/src/pipeline.rs b/neurosploit-rs/crates/harness/src/pipeline.rs index fc46103..a26eb0e 100644 --- a/neurosploit-rs/crates/harness/src/pipeline.rs +++ b/neurosploit-rs/crates/harness/src/pipeline.rs @@ -300,6 +300,26 @@ fn tool_doctrine(mcp_on: bool) -> String { (`timeout 90 || 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 ` (Exploit-DB, offline in Kali); `searchsploit -m ` 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 --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", diff --git a/neurosploit-rs/crates/harness/src/sandbox.rs b/neurosploit-rs/crates/harness/src/sandbox.rs index 77c4a43..b41edb6 100644 --- a/neurosploit-rs/crates/harness/src/sandbox.rs +++ b/neurosploit-rs/crates/harness/src/sandbox.rs @@ -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(),