diff --git a/neurosploit-rs/Cargo.lock b/neurosploit-rs/Cargo.lock index b6a4dac..9ac02f8 100644 --- a/neurosploit-rs/Cargo.lock +++ b/neurosploit-rs/Cargo.lock @@ -940,7 +940,7 @@ dependencies = [ [[package]] name = "neurosploit" -version = "4.3.1" +version = "4.3.2" dependencies = [ "anyhow", "clap", @@ -957,7 +957,7 @@ dependencies = [ [[package]] name = "neurosploit-harness" -version = "4.3.1" +version = "4.3.2" dependencies = [ "anyhow", "base64", diff --git a/neurosploit-rs/Cargo.toml b/neurosploit-rs/Cargo.toml index f710cb0..11ed297 100644 --- a/neurosploit-rs/Cargo.toml +++ b/neurosploit-rs/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/harness", "app"] resolver = "2" [workspace.package] -version = "4.3.1" +version = "4.3.2" edition = "2021" license = "MIT" repository = "https://github.com/JoasASantos/NeuroSploit" diff --git a/neurosploit-rs/app/src/main.rs b/neurosploit-rs/app/src/main.rs index dd42433..edf2e8c 100644 --- a/neurosploit-rs/app/src/main.rs +++ b/neurosploit-rs/app/src/main.rs @@ -13,8 +13,8 @@ use std::path::{Path, PathBuf}; #[command( name = "neurosploit", version, - about = "NeuroSploit v4.3.1 — multi-model autonomous pentest harness", - long_about = "NeuroSploit v4.3.1 — a Rust multi-model harness that drives a pool of LLMs \ + about = "NeuroSploit v4.3.2 — multi-model autonomous pentest harness", + long_about = "NeuroSploit v4.3.2 — a Rust multi-model harness that drives a pool of LLMs \ (API key or local subscription: Claude/Codex/Gemini/Grok/OpenCode/Hermes) to autonomously test a target. \ After recon it INTELLIGENTLY selects only the agents matching the discovered surface, runs \ them in parallel, then validates every finding by cross-model voting before reporting.\n\n\ @@ -2252,9 +2252,10 @@ fn apply_quick(cfg: &mut RunConfig) { /// sees how many endpoints were seeded. fn apply_collection(cfg: &mut RunConfig, path: Option<&str>) -> anyhow::Result<()> { let Some(path) = path.filter(|p| !p.trim().is_empty()) else { return Ok(()) }; - let eps = harness::collection::parse_file(path) + let eps = harness::collection::parse_path(path) .map_err(|e| anyhow::anyhow!("--collection {path}: {e}"))?; - println!(" \x1b[2mcollection: imported {} endpoint(s) from {} — tested as known surface\x1b[0m", eps.len(), path); + let kind = if std::path::Path::new(path).is_dir() { "directory" } else { "file" }; + println!(" \x1b[2mcollection: imported {} endpoint(s) from {kind} {} — tested as known surface\x1b[0m", eps.len(), path); cfg.api_collection = Some(path.to_string()); Ok(()) } diff --git a/neurosploit-rs/app/src/mcp.rs b/neurosploit-rs/app/src/mcp.rs index 70c8d5b..3562230 100644 --- a/neurosploit-rs/app/src/mcp.rs +++ b/neurosploit-rs/app/src/mcp.rs @@ -1,19 +1,36 @@ //! 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. +//! so Claude Code, Codex, Cursor, Windsurf 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 +//! - neurosploit_run launch a black-box engagement +//! - neurosploit_whitebox white-box source review +//! - neurosploit_greybox grey-box (source + running app) +//! - neurosploit_host infra/host scan (Linux/Windows/AD) +//! - neurosploit_aitest AI/LLM red-teaming (OWASP LLM Top 10) +//! - neurosploit_mobile mobile binary analysis (APK/IPA) +//! - neurosploit_container OCI container scan + SBOM +//! - neurosploit_skills skill/plugin/n8n audit +//! - neurosploit_pr GitHub PR security review +//! - neurosploit_internal internal/AD attack-graph analysis +//! - 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 report from findings +//! - neurosploit_sarif emit SARIF 2.1.0 +//! - neurosploit_compliance map findings onto PCI-DSS/HIPAA/SOC2 +//! - neurosploit_graph attack graph (web + internal) +//! - neurosploit_audit verify audit trail +//! - neurosploit_poc re-validate PoCs +//! - neurosploit_traffic export HTTP traffic +//! - neurosploit_assurance assurance bundle +//! - neurosploit_agents list the agent library +//! - neurosploit_models list providers and models +//! - neurosploit_import_collection validate/preview a collection (file or dir) //! //! Kept dependency-free: JSON-RPC framing is hand-rolled over stdin/stdout with //! serde_json, and tools run via std::process::Command. @@ -37,12 +54,11 @@ pub fn serve() -> anyhow::Result<()> { }; let req: Value = match serde_json::from_str(&line) { Ok(v) => v, - Err(_) => continue, // ignore malformed frames + Err(_) => continue, }; 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, @@ -73,63 +89,372 @@ 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. +// --------------------------------------------------------------------------- +// Tool catalogue +// --------------------------------------------------------------------------- + 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.", + "description": "Launch a NeuroSploit black-box engagement against an authorized target. Returns the run id, finding summary, and 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" }, + "target": { "type": "string", "description": "URL or host, e.g. https://app.example.com" }, + "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" } + "focus": { "type": "string", "description": "What to prioritise" }, + "objective": { "type": "string", "description": "Engagement goal / context" }, + "scope_file": { "type": "string", "description": "Path to a scope YAML (hard boundary)" }, + "collection": { "type": "string", "description": "API collection file or directory (Postman/OpenAPI/HAR, JSON)" }, + "sandbox": { "type": "boolean", "description": "Run tool commands in the Kali container" }, + "typesafe": { "type": "string", "enum": ["on","off","auto"] }, + "max_agents": { "type": "integer" }, + "quick": { "type": "boolean", "description": "Economy preset: light recon, fewer agents, lower cost" } }, "required": ["target"] } }, + { + "name": "neurosploit_whitebox", + "description": "White-box: analyse a repository's source code for vulnerabilities. Accepts a local path, GitHub URL, or owner/repo shorthand.", + "inputSchema": { + "type": "object", + "properties": { + "path": { "type": "string", "description": "Local path, GitHub URL, or owner/repo" }, + "model": { "type": "string" }, + "subscription": { "type": "boolean" }, + "focus": { "type": "string" }, + "research": { "type": "boolean", "description": "Hunt novel CVE-reportable bugs" }, + "max_agents": { "type": "integer" }, + "quick": { "type": "boolean" } + }, + "required": ["path"] + } + }, + { + "name": "neurosploit_greybox", + "description": "Greybox: review source AND exploit the running app together.", + "inputSchema": { + "type": "object", + "properties": { + "repo": { "type": "string", "description": "Source repo (local path, GitHub URL, or owner/repo)" }, + "url": { "type": "string", "description": "URL of the running application" }, + "model": { "type": "string" }, + "subscription": { "type": "boolean" }, + "creds": { "type": "string", "description": "Credentials YAML path" }, + "focus": { "type": "string" }, + "collection": { "type": "string", "description": "API collection file or directory" }, + "research": { "type": "boolean" }, + "max_agents": { "type": "integer" }, + "quick": { "type": "boolean" } + }, + "required": ["repo", "url"] + } + }, + { + "name": "neurosploit_host", + "description": "Infra/host: scan an IP/host and run Linux/Windows/AD agents. SSH/Windows credentials come from --creds.", + "inputSchema": { + "type": "object", + "properties": { + "target": { "type": "string", "description": "IP or hostname" }, + "model": { "type": "string" }, + "subscription": { "type": "boolean" }, + "creds": { "type": "string", "description": "Credentials YAML (ssh/windows/ad blocks)" }, + "focus": { "type": "string" }, + "max_agents": { "type": "integer" } + }, + "required": ["target"] + } + }, + { + "name": "neurosploit_aitest", + "description": "AI/LLM red-team: test a live AI agent / LLM app / MCP endpoint (OWASP LLM Top 10 + MCP risks).", + "inputSchema": { + "type": "object", + "properties": { + "url": { "type": "string", "description": "URL of the AI agent / LLM endpoint" }, + "model": { "type": "string" }, + "subscription": { "type": "boolean" }, + "auth": { "type": "string", "description": "Auth header for the AI endpoint" }, + "focus": { "type": "string" }, + "max_agents": { "type": "integer" } + }, + "required": ["url"] + } + }, + { + "name": "neurosploit_mobile", + "description": "Mobile/binary: analyse a local artifact (APK/IPA/binary) with mobile RE agents (Ghidra, MobSF, Frida, apktool/jadx).", + "inputSchema": { + "type": "object", + "properties": { + "path": { "type": "string", "description": "Path to the artifact (.apk/.ipa/binary)" }, + "model": { "type": "string" }, + "subscription": { "type": "boolean" }, + "focus": { "type": "string" }, + "max_agents": { "type": "integer" } + }, + "required": ["path"] + } + }, + { + "name": "neurosploit_container", + "description": "Scan an OCI container image (repo:tag / tar / Dockerfile) for vulnerable packages, secrets, misconfig and emit an SBOM.", + "inputSchema": { + "type": "object", + "properties": { + "image": { "type": "string", "description": "Image reference, local tar, or Dockerfile path" }, + "model": { "type": "string" }, + "subscription": { "type": "boolean" }, + "focus": { "type": "string" } + }, + "required": ["image"] + } + }, + { + "name": "neurosploit_skills", + "description": "Audit AI skills/plugins or exported n8n workflows (white-box .md/.json file or folder).", + "inputSchema": { + "type": "object", + "properties": { + "path": { "type": "string", "description": "Path to a skill/plugin/n8n file or folder" }, + "model": { "type": "string" }, + "subscription": { "type": "boolean" } + }, + "required": ["path"] + } + }, + { + "name": "neurosploit_pr", + "description": "Review a GitHub Pull Request's code for security vulnerabilities. Optionally comments back on the PR.", + "inputSchema": { + "type": "object", + "properties": { + "repo": { "type": "string", "description": "owner/repo or GitHub URL" }, + "number": { "type": "integer", "description": "Pull request number" }, + "model": { "type": "string" }, + "subscription": { "type": "boolean" }, + "comment": { "type": "boolean", "description": "Post a summary comment back on the PR" }, + "fail_on": { "type": "string", "enum": ["critical","high","medium","low"], "description": "Block PR at this severity or worse" }, + "jira": { "type": "boolean", "description": "Open a Jira card per finding" } + }, + "required": ["repo", "number"] + } + }, + { + "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_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_sarif", "description": "Emit SARIF 2.1.0 for a finished run (report.sarif) so CI code-scanning can ingest the findings.", "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_sarif", "description": "Emit SARIF 2.1.0 for a finished run so CI code-scanning can ingest the findings.", "inputSchema": { "type": "object", "properties": { "run": { "type": "string" } }, "required": ["run"] } }, { "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"] } }, - { "name": "neurosploit_container", "description": "Scan an OCI container image (repo:tag / tar / Dockerfile) for vulnerable packages, secrets, misconfig and emit an SBOM.", "inputSchema": { "type": "object", "properties": { "image": { "type": "string" }, "model": { "type": "string" }, "subscription": { "type": "boolean" } }, "required": ["image"] } } + { + "name": "neurosploit_graph", + "description": "Attack graph for a finished run: attack chains with severity, next moves, Cypher export. With --push merges into Neo4j.", + "inputSchema": { + "type": "object", + "properties": { + "run": { "type": "string", "description": "Run id or path" }, + "internal": { "type": "string", "description": "Internal-network graph JSON to merge" }, + "push": { "type": "boolean", "description": "Push to Neo4j" }, + "top": { "type": "integer", "description": "How many chains to print (default 5)" } + }, + "required": ["run"] + } + }, + { + "name": "neurosploit_audit", + "description": "Verify a finished run's audit trail — the hash chain and, with --anchor, the signed anchors.", + "inputSchema": { + "type": "object", + "properties": { + "run": { "type": "string" }, + "anchor": { "type": "boolean", "description": "Also verify anchors (P4)" } + }, + "required": ["run"] + } + }, + { + "name": "neurosploit_poc", + "description": "Re-validate a finished run's PoCs: re-run each finding's proof and report which still reproduce.", + "inputSchema": { + "type": "object", + "properties": { + "run": { "type": "string" }, + "repeats": { "type": "integer", "description": "Re-runs per finding (default 2)" }, + "apply": { "type": "boolean", "description": "Write demoted findings back" }, + "only": { "type": "string", "description": "Re-test only this finding id" } + }, + "required": ["run"] + } + }, + { + "name": "neurosploit_traffic", + "description": "Export a run's archived HTTP traffic as a .http file for external inspection.", + "inputSchema": { "type": "object", "properties": { "run": { "type": "string" } }, "required": ["run"] } + }, + { + "name": "neurosploit_assurance", + "description": "Assemble or verify a run's assurance bundle (P1-P5 manifest).", + "inputSchema": { + "type": "object", + "properties": { + "run": { "type": "string" }, + "verify": { "type": "boolean", "description": "Verify instead of assembling" } + }, + "required": ["run"] + } + }, + { "name": "neurosploit_agents", "description": "List the NeuroSploit agent library (names, categories, counts).", "inputSchema": { "type": "object", "properties": {} } }, + { "name": "neurosploit_models", "description": "List available providers and models.", "inputSchema": { "type": "object", "properties": {} } }, + { + "name": "neurosploit_import_collection", + "description": "Validate and preview an API collection (Postman/OpenAPI/Swagger/HAR). Accepts a single JSON file or a directory of .json files. Returns the parsed endpoint list.", + "inputSchema": { + "type": "object", + "properties": { + "path": { "type": "string", "description": "Path to a collection file or a directory of .json collection files" } + }, + "required": ["path"] + } + } ]) } -/// Dispatch a `tools/call`. +// --------------------------------------------------------------------------- +// Tool dispatch +// --------------------------------------------------------------------------- + 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 i = |k: &str| a.get(k).and_then(|v| v.as_i64()); let mut argv: Vec = Vec::new(); match name { + // ---- engagements ---------------------------------------------------- "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); + argv.push("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(o) = s("objective") { argv.push("--objective".into()); argv.push(o); } if let Some(sf) = s("scope_file") { argv.push("--scope-file".into()); argv.push(sf); } + if let Some(c) = s("collection") { argv.push("--collection".into()); argv.push(c); } 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()); } + if let Some(n) = i("max_agents") { argv.push("--max-agents".into()); argv.push(n.to_string()); } + if b("quick") { argv.push("--quick".into()); } argv.push("-v".into()); } - "neurosploit_list_runs" => { argv.push("runs".into()); } + "neurosploit_whitebox" => { + let Some(path) = s("path") else { return tool_err(id, "path is required") }; + argv.push("whitebox".into()); argv.push(path); + 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 b("research") { argv.push("--research".into()); } + if let Some(n) = i("max_agents") { argv.push("--max-agents".into()); argv.push(n.to_string()); } + if b("quick") { argv.push("--quick".into()); } + argv.push("-v".into()); + } + "neurosploit_greybox" => { + let Some(repo) = s("repo") else { return tool_err(id, "repo is required") }; + let Some(url) = s("url") else { return tool_err(id, "url is required") }; + argv.push("greybox".into()); argv.push(repo); + argv.push("--url".into()); argv.push(url); + if let Some(m) = s("model") { argv.push("--model".into()); argv.push(m); } + if b("subscription") { argv.push("--subscription".into()); } + if let Some(c) = s("creds") { argv.push("--creds".into()); argv.push(c); } + if let Some(f) = s("focus") { argv.push("--focus".into()); argv.push(f); } + if let Some(col) = s("collection") { argv.push("--collection".into()); argv.push(col); } + if b("research") { argv.push("--research".into()); } + if let Some(n) = i("max_agents") { argv.push("--max-agents".into()); argv.push(n.to_string()); } + if b("quick") { argv.push("--quick".into()); } + argv.push("-v".into()); + } + "neurosploit_host" => { + let Some(target) = s("target") else { return tool_err(id, "target is required") }; + argv.push("host".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(c) = s("creds") { argv.push("--creds".into()); argv.push(c); } + if let Some(f) = s("focus") { argv.push("--focus".into()); argv.push(f); } + if let Some(n) = i("max_agents") { argv.push("--max-agents".into()); argv.push(n.to_string()); } + argv.push("-v".into()); + } + "neurosploit_aitest" => { + let Some(url) = s("url") else { return tool_err(id, "url is required") }; + argv.push("aitest".into()); argv.push(url); + if let Some(m) = s("model") { argv.push("--model".into()); argv.push(m); } + if b("subscription") { argv.push("--subscription".into()); } + if let Some(auth) = s("auth") { argv.push("--auth".into()); argv.push(auth); } + if let Some(f) = s("focus") { argv.push("--focus".into()); argv.push(f); } + if let Some(n) = i("max_agents") { argv.push("--max-agents".into()); argv.push(n.to_string()); } + argv.push("-v".into()); + } + "neurosploit_mobile" => { + let Some(path) = s("path") else { return tool_err(id, "path is required") }; + argv.push("mobile".into()); argv.push(path); + 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(n) = i("max_agents") { argv.push("--max-agents".into()); argv.push(n.to_string()); } + argv.push("-v".into()); + } + "neurosploit_container" => { + let Some(image) = s("image") else { return tool_err(id, "image is required") }; + argv.push("container".into()); argv.push(image); + 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); } + argv.push("-v".into()); + } + "neurosploit_skills" => { + let Some(path) = s("path") else { return tool_err(id, "path is required") }; + argv.push("skills".into()); argv.push(path); + if let Some(m) = s("model") { argv.push("--model".into()); argv.push(m); } + if b("subscription") { argv.push("--subscription".into()); } + argv.push("-v".into()); + } + "neurosploit_pr" => { + let Some(repo) = s("repo") else { return tool_err(id, "repo is required") }; + let Some(number) = i("number") else { return tool_err(id, "number is required") }; + argv.push("pr".into()); argv.push(repo); argv.push(number.to_string()); + if let Some(m) = s("model") { argv.push("--model".into()); argv.push(m); } + if b("subscription") { argv.push("--subscription".into()); } + if b("comment") { argv.push("--comment".into()); } + if let Some(fo) = s("fail_on") { argv.push("--fail-on".into()); argv.push(fo); } + if b("jira") { argv.push("--jira".into()); } + argv.push("-v".into()); + } + "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); } + } + + // ---- post-run tools ------------------------------------------------- + "neurosploit_list_runs" => return list_runs(id), "neurosploit_findings" => { let Some(run) = s("run") else { return tool_err(id, "run is required") }; return read_run_file(id, &run, "findings.json"); @@ -138,35 +463,71 @@ fn handle_call(id: Option, req: &Value, exe: &std::path::Path) -> Value { 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_sarif" => { let Some(run) = s("run") else { return tool_err(id, "run is required") }; argv.push("sarif".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_rebuild" => { + let Some(run) = s("run") else { return tool_err(id, "run is required") }; + argv.push("rebuild".into()); argv.push(run); } - "neurosploit_container" => { - let Some(image) = s("image") else { return tool_err(id, "image is required") }; - argv.push("container".into()); argv.push(image); - if let Some(m) = s("model") { argv.push("--model".into()); argv.push(m); } - if b("subscription") { argv.push("--subscription".into()); } - argv.push("-v".into()); + "neurosploit_sarif" => { + let Some(run) = s("run") else { return tool_err(id, "run is required") }; + argv.push("sarif".into()); argv.push(run); } "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); } } + "neurosploit_graph" => { + let Some(run) = s("run") else { return tool_err(id, "run is required") }; + argv.push("graph".into()); argv.push(run); + if let Some(ig) = s("internal") { argv.push("--internal".into()); argv.push(ig); } + if b("push") { argv.push("--push".into()); } + if let Some(t) = i("top") { argv.push("--top".into()); argv.push(t.to_string()); } + } + "neurosploit_audit" => { + let Some(run) = s("run") else { return tool_err(id, "run is required") }; + argv.push("audit".into()); argv.push(run); + if b("anchor") { argv.push("--anchor".into()); } + } + "neurosploit_poc" => { + let Some(run) = s("run") else { return tool_err(id, "run is required") }; + argv.push("poc".into()); argv.push(run); + if let Some(r) = i("repeats") { argv.push("--repeats".into()); argv.push(r.to_string()); } + if b("apply") { argv.push("--apply".into()); } + if let Some(o) = s("only") { argv.push("--only".into()); argv.push(o); } + argv.push("--json".into()); + } + "neurosploit_traffic" => { + let Some(run) = s("run") else { return tool_err(id, "run is required") }; + argv.push("traffic".into()); argv.push(run); + } + "neurosploit_assurance" => { + let Some(run) = s("run") else { return tool_err(id, "run is required") }; + argv.push("assurance".into()); argv.push(run); + if b("verify") { argv.push("--verify".into()); } + } + + // ---- info ----------------------------------------------------------- + "neurosploit_agents" => { argv.push("agents".into()); } + "neurosploit_models" => { argv.push("models".into()); } + + // ---- collection preview --------------------------------------------- + "neurosploit_import_collection" => { + let Some(path) = s("path") else { return tool_err(id, "path is required") }; + return import_collection(id, &path); + } + _ => 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); - } + run_cli(id, exe, &argv) +} - let out = std::process::Command::new(exe).args(&argv).output(); +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn run_cli(id: Option, exe: &std::path::Path, argv: &[String]) -> Value { + 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(); @@ -181,6 +542,27 @@ fn handle_call(id: Option, req: &Value, exe: &std::path::Path) -> Value { } } +fn import_collection(id: Option, path: &str) -> Value { + match harness::collection::parse_path(path) { + Ok(eps) => { + let kind = if std::path::Path::new(path).is_dir() { "directory" } else { "file" }; + let mut lines = vec![format!("Parsed {kind} {path}: {} endpoint(s)\n", eps.len())]; + for (i, ep) in eps.iter().enumerate().take(200) { + if ep.note.is_empty() { + lines.push(format!(" {}. {} {}", i + 1, ep.method, ep.url)); + } else { + lines.push(format!(" {}. {} {} ({})", i + 1, ep.method, ep.url, ep.note)); + } + } + if eps.len() > 200 { + lines.push(format!(" … and {} more", eps.len() - 200)); + } + tool_text(id, &lines.join("\n")) + } + Err(e) => tool_err(id, &format!("{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) }; @@ -201,7 +583,6 @@ fn list_runs(id: Option) -> Value { } 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 })) } diff --git a/neurosploit-rs/app/src/repl.rs b/neurosploit-rs/app/src/repl.rs index 64f111f..a8f247b 100644 --- a/neurosploit-rs/app/src/repl.rs +++ b/neurosploit-rs/app/src/repl.rs @@ -863,8 +863,12 @@ pub async fn repl(base: &Path, auth: SessionAuth) -> anyhow::Result<()> { } else if arg == "clear" { s.collection = None; println!(" api collection cleared"); } else { - match harness::collection::parse_file(arg) { - Ok(eps) => { s.collection = Some(arg.to_string()); println!(" api collection: imported {} endpoint(s) from {arg} — tested as known surface", eps.len()); } + match harness::collection::parse_path(arg) { + Ok(eps) => { + let kind = if std::path::Path::new(arg).is_dir() { "directory" } else { "file" }; + s.collection = Some(arg.to_string()); + println!(" api collection: imported {} endpoint(s) from {kind} {arg} — tested as known surface", eps.len()); + } Err(e) => println!(" \x1b[33m⚠ {e}\x1b[0m"), } } @@ -2724,7 +2728,7 @@ fn help() { h("/repo ", "source repo (greybox) OR skill/n8n file/folder to audit (skills scope)"); h("/auth ", "auth header (Bearer/cookie/key). Roles: /auth admin · /auth user "); h("/creds ", "creds: jwt/header/cookie/login + ssh/windows + aws/gcp/azure + roles"); - h("/collection ", "import an API collection (Postman/OpenAPI/HAR, JSON) as known surface to test"); + h("/collection ", "import API collection(s) — file or directory of .json (Postman/OpenAPI/HAR)"); h("/focus ", "steer the tests (or just type the instruction)"); h("/objective ", "engagement goal/context — shapes what agents prioritise & count as impact"); h("/scope-out ", "out-of-scope exclusions — host-shaped entries become ENFORCED denials"); diff --git a/neurosploit-rs/crates/harness/src/collection.rs b/neurosploit-rs/crates/harness/src/collection.rs index 7827a7d..00222c0 100644 --- a/neurosploit-rs/crates/harness/src/collection.rs +++ b/neurosploit-rs/crates/harness/src/collection.rs @@ -39,7 +39,50 @@ fn detect(v: &Value) -> &'static str { } } -/// Parse a collection file into a de-duplicated endpoint list. +/// Parse a path that is either a single collection file OR a directory of +/// `.json` collection files. When it is a directory every `.json` inside it +/// (recursively) is parsed and the endpoints are merged and de-duplicated. +pub fn parse_path(path: &str) -> Result> { + let p = std::path::Path::new(path); + if p.is_dir() { + parse_dir(path) + } else { + parse_file(path) + } +} + +/// Walk a directory tree, parse every `.json` file as a collection, merge and +/// de-duplicate. Files that are not valid collections are reported on stderr +/// but do not abort the import — at least one file must yield endpoints. +pub fn parse_dir(dir: &str) -> Result> { + let mut all = Vec::new(); + let mut ok_count = 0usize; + let mut err_count = 0usize; + for entry in walkdir::WalkDir::new(dir).follow_links(true).into_iter().filter_map(|e| e.ok()) { + let p = entry.path(); + if !p.is_file() { continue; } + match p.extension().and_then(|x| x.to_str()) { + Some("json" | "har") => {} + _ => continue, + } + match parse_file(&p.to_string_lossy()) { + Ok(eps) => { ok_count += 1; all.extend(eps); } + Err(e) => { err_count += 1; eprintln!(" \x1b[33m⚠ skipping {}: {e}\x1b[0m", p.display()); } + } + } + if ok_count == 0 { + return Err(anyhow!( + "no valid collection files found in '{dir}' ({err_count} file(s) skipped)" + )); + } + dedup(&mut all); + if all.is_empty() { + return Err(anyhow!("collections in '{dir}' contained no endpoints")); + } + Ok(all) +} + +/// Parse a single collection file into a de-duplicated endpoint list. pub fn parse_file(path: &str) -> Result> { let raw = std::fs::read_to_string(path) .map_err(|e| anyhow!("cannot read collection '{path}': {e}"))?; diff --git a/neurosploit-rs/crates/harness/src/pipeline.rs b/neurosploit-rs/crates/harness/src/pipeline.rs index af873f9..721476d 100644 --- a/neurosploit-rs/crates/harness/src/pipeline.rs +++ b/neurosploit-rs/crates/harness/src/pipeline.rs @@ -180,7 +180,7 @@ fn operator_directives(cfg: &RunConfig) -> String { // endpoints as surface to test directly. Parse errors are reported once at // launch (see the CLI), so render silently here. if let Some(col) = cfg.api_collection.as_deref().filter(|x| !x.trim().is_empty()) { - if let Ok(eps) = crate::collection::parse_file(col) { + if let Ok(eps) = crate::collection::parse_path(col) { s.push_str(&crate::collection::render_block(&eps, 200)); } } diff --git a/web/public/app.js b/web/public/app.js index 12601b4..669d294 100644 --- a/web/public/app.js +++ b/web/public/app.js @@ -1,5 +1,5 @@ 'use strict'; -/* NeuroSploit v4.2.1 — web console frontend. Vanilla JS, no build step. */ +/* NeuroSploit v4.3.2 — web console frontend. Vanilla JS, no build step. */ const $ = (sel, root = document) => root.querySelector(sel); const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel)); @@ -43,6 +43,7 @@ const state = { live: { sort: 'severity', dir: 1, query: '', sev: null }, detail: { sort: 'severity', dir: 1, query: '', sev: null }, }, + activeView: 'wizard', }; // --------------------------------------------------------------------------- @@ -96,6 +97,33 @@ function fieldError(id, msg) { } function clearFieldErrors() { $$('.field-error').forEach((el) => { el.textContent = ''; el.hidden = true; }); } +// --------------------------------------------------------------------------- +// view navigation +// --------------------------------------------------------------------------- + +const ALL_VIEWS = ['dashView','wizardView','liveView','detailView','pentestsView','prreviewsView','issuesView','reposView','domainsView','networksView','integrationsView','settingsView']; + +function switchView(viewName) { + state.activeView = viewName; + ALL_VIEWS.forEach(id => { const el = $(`#${id}`); if (el) el.hidden = true; }); + const target = $(`#${viewName}View`); + if (target) target.hidden = false; + $$('.sb-nav-item').forEach(btn => { + btn.classList.toggle('active', btn.dataset.view === viewName); + }); + const runHist = $('#sbRunHistory'); + if (runHist) runHist.style.display = ['dash','pentests','wizard'].includes(viewName) ? '' : 'none'; + if (viewName === 'pentests') loadPentests(); + if (viewName === 'issues') loadIssues(); + if (viewName === 'repos') loadRepos(); + if (viewName === 'domains') loadDomains(); + if (viewName === 'networks') loadNetworks(); + if (viewName === 'integrations') loadIntegrations(); + if (viewName === 'settings') loadSettings(); + if (viewName === 'prreviews') loadPrReviews(); + if (viewName === 'dash') renderDashboard(); +} + function timeAgo(ts) { if (!ts) return ''; const secs = Math.max(0, Math.floor(Date.now() / 1000 - ts)); @@ -1029,7 +1057,7 @@ function leaveLiveJob() { state.currentJob = null; termSyncTargets(); } -$('#btnBackToBoard').addEventListener('click', () => { leaveLiveJob(); show($('#liveView'), false); show($('#dashView'), false); show($('#wizardView'), true); }); +$('#btnBackToBoard').addEventListener('click', () => { leaveLiveJob(); switchView('wizard'); }); // Regenerating from the evidence already on disk, rather than re-running the // engagement: a run whose PDF was never produced (no `typst` at the time, or a // since-improved template) would otherwise be unreportable. @@ -1074,8 +1102,9 @@ $('#btnDeleteRun').addEventListener('click', async () => { } }); -$('#btnDetailBack').addEventListener('click', () => { clearInterval(state.detailPoll); show($('#detailView'), false); show($('#dashView'), false); show($('#wizardView'), true); }); -$('#btnNewEngagement').addEventListener('click', () => { leaveLiveJob(); clearInterval(state.detailPoll); show($('#detailView'), false); show($('#liveView'), false); show($('#dashView'), false); show($('#wizardView'), true); }); +$('#btnDetailBack').addEventListener('click', () => { clearInterval(state.detailPoll); switchView('wizard'); }); +$('#btnNewEngagement')?.addEventListener('click', () => { leaveLiveJob(); clearInterval(state.detailPoll); switchView('wizard'); }); +if ($('#btnNewPentest2')) $('#btnNewPentest2').addEventListener('click', () => { leaveLiveJob(); clearInterval(state.detailPoll); switchView('wizard'); }); // --------------------------------------------------------------------------- // Finding detail modal — full evidence/impact/remediation + any PoC script @@ -2611,16 +2640,19 @@ function openFairParams(params) { function showDashboard() { leaveLiveJob(); clearInterval(state.detailPoll); - show($('#wizardView'), false); - show($('#liveView'), false); - show($('#detailView'), false); - show($('#dashView'), true); - renderDashboard(); + switchView('dash'); } -$('#btnDashboard').addEventListener('click', showDashboard); -$('#btnDashRefresh').addEventListener('click', renderDashboard); -$('#dashRange').addEventListener('change', renderDashboard); -$('#runFilter').addEventListener('input', (e) => { state.runFilter = e.target.value; renderSidebar(); }); +// Sidebar nav items +$$('.sb-nav-item').forEach(btn => { + btn.addEventListener('click', () => { + if (btn.dataset.view === 'wizard') { leaveLiveJob(); clearInterval(state.detailPoll); } + switchView(btn.dataset.view); + if (window.matchMedia(NARROW).matches) setSidebar(false); + }); +}); +$('#btnDashRefresh')?.addEventListener('click', renderDashboard); +$('#dashRange')?.addEventListener('change', renderDashboard); +$('#runFilter')?.addEventListener('input', (e) => { state.runFilter = e.target.value; renderSidebar(); }); // --------------------------------------------------------------------------- // boot @@ -2641,8 +2673,7 @@ $('#sidebarScrim').addEventListener('click', () => setSidebar(false)); $('#sbGroups').addEventListener('click', (e) => { if (e.target.closest('.sb-run') && window.matchMedia(NARROW).matches) setSidebar(false); }); -$('#btnDashboard').addEventListener('click', () => { if (window.matchMedia(NARROW).matches) setSidebar(false); }); -$('#btnNewEngagement').addEventListener('click', () => { if (window.matchMedia(NARROW).matches) setSidebar(false); }); +$('#btnNewEngagement')?.addEventListener('click', () => { if (window.matchMedia(NARROW).matches) setSidebar(false); }); // Growing the window past the breakpoint leaves a scrim over a sidebar that is // no longer a drawer. window.matchMedia(NARROW).addEventListener('change', (m) => { if (!m.matches) setSidebar(false); }); @@ -3365,3 +3396,210 @@ const adoptGraph = (r) => { $('#btnGraphRecompute')?.addEventListener('click', (e) => cliAction(e.currentTarget, 'Recompute graph', '/graph', {}, adoptGraph)); $('#btnGraphPush')?.addEventListener('click', (e) => cliAction(e.currentTarget, 'Push to Neo4j', '/graph', { push: true }, adoptGraph)); $('#btnRetest')?.addEventListener('click', (e) => cliAction(e.currentTarget, 'Re-test PoCs', '/retest', {})); + +// --------------------------------------------------------------------------- +// new pages — pentests / PR reviews / issues / repos / domains / networks / +// integrations / settings +// --------------------------------------------------------------------------- + +async function loadPentests() { + const body = $('#pentestsBody'); + if (!body) return; + try { + const runs = await api('/api/runs'); + if (!runs.length) { body.innerHTML = '
No pentests yet. Click + New Pentest to start.
'; return; } + const rows = runs.map(r => { + const mode = (r.id || '').startsWith('ns-wb-') ? 'whitebox' : (r.id || '').startsWith('ns-gb-') ? 'greybox' : (r.id || '').startsWith('ns-host-') ? 'host' : 'blackbox'; + const when = r.when ? timeAgo(Math.floor(new Date(r.when).getTime() / 1000)) : ''; + return ` + ${esc(r.id)} + ${esc(r.target || r.name || '')} + ${esc(mode)} + ${r.findings ?? 0} + ${esc(r.state || 'done')} + ${when} + `; + }).join(''); + body.innerHTML = `${rows}
IDTargetModeFindingsStatusWhen
`; + $$('.pentest-row', body).forEach(row => { + row.style.cursor = 'pointer'; + row.addEventListener('click', () => { const run = runs.find(r => r.id === row.dataset.id); if (run) openRun(run); }); + }); + } catch (e) { body.innerHTML = `
Failed to load: ${esc(e.message)}
`; } +} + +async function loadPrReviews() { + const body = $('#prreviewsBody'); + if (!body) return; + try { + const runs = await api('/api/runs'); + const prs = runs.filter(r => r.id && r.id.includes('-pr-')); + if (!prs.length) { body.innerHTML = '
No PR reviews yet. Run neurosploit pr owner/repo 123 or use the MCP server.
'; return; } + const rows = prs.map(r => { + const when = r.when ? timeAgo(Math.floor(new Date(r.when).getTime() / 1000)) : ''; + return `${esc(r.id)}${esc(r.target || r.name || '')}${r.findings ?? 0}${esc(r.state || 'done')}${when}`; + }).join(''); + body.innerHTML = `${rows}
IDRepositoryFindingsStatusWhen
`; + } catch (e) { body.innerHTML = `
Failed to load: ${esc(e.message)}
`; } +} + +async function loadIssues() { + const body = $('#issuesBody'); + if (!body) return; + try { + const data = await api('/api/issues'); + if (!data.length) { body.innerHTML = '
No findings across any engagement yet.
'; return; } + const rows = data.map(f => { + const sc = sevClass(f.severity); + return `${esc(f.severity)}${esc(f.title)}${esc(f.target || '')}${esc(f.run || '')}${esc(f.status || 'open')}`; + }).join(''); + body.innerHTML = `${rows}
SeverityTitleTargetRunStatus
`; + } catch (e) { body.innerHTML = `
Failed to load: ${esc(e.message)}
`; } +} + +async function loadRepos() { + const body = $('#reposBody'); + if (!body) return; + try { + const repos = await api('/api/config/repos'); + if (!repos.length) { body.innerHTML = '
No repositories connected yet. Click + Add Repository to connect one.
'; return; } + body.innerHTML = '
' + repos.map(r => `
+
${esc(r.name || r.url)}
+
${esc(r.description || r.url)}
+
${esc(r.provider || 'git')} · ${esc(r.branch || 'main')}
+
+
`).join('') + '
'; + } catch { body.innerHTML = '
No repositories connected yet. Click + Add Repository to connect one.
'; } +} + +async function loadDomains() { + const body = $('#domainsBody'); + if (!body) return; + try { + const domains = await api('/api/config/domains'); + if (!domains.length) { body.innerHTML = '
No domains configured yet. Click + Add Domain to add one.
'; return; } + body.innerHTML = '
' + domains.map(d => `
+
${esc(d.domain)}
+
${esc(d.description || 'Target domain')}
+
${d.subdomains ? d.subdomains.length + ' subdomains' : 'No subdomains'} · ${esc(d.scope || 'in-scope')}
+
+
`).join('') + '
'; + } catch { body.innerHTML = '
No domains configured yet. Click + Add Domain to add one.
'; } +} + +async function loadNetworks() { + const body = $('#networksBody'); + if (!body) return; + try { + const nets = await api('/api/config/networks'); + if (!nets.length) { body.innerHTML = '
No networks configured yet. Click + Add Network to add one.
'; return; } + body.innerHTML = '
' + nets.map(n => `
+
${esc(n.name || n.cidr)}
+
${esc(n.description || n.cidr)}
+
${esc(n.cidr)} · ${esc(n.transport || 'direct')}
+
+
`).join('') + '
'; + } catch { body.innerHTML = '
No networks configured yet. Click + Add Network to add one.
'; } +} + +async function loadIntegrations() { + const body = $('#integrationsBody'); + if (!body) return; + try { + const integs = await api('/api/config/integrations'); + const defs = [ + { id: 'github', name: 'GitHub', desc: 'Pull request reviews, commit status checks, and repository scanning' }, + { id: 'gitlab', name: 'GitLab', desc: 'Merge request reviews and repository scanning' }, + { id: 'jira', name: 'Jira', desc: 'Create Jira tickets for confirmed findings' }, + { id: 'slack', name: 'Slack', desc: 'Notifications for completed engagements and critical findings' }, + { id: 'mcp', name: 'MCP Server', desc: 'Model Context Protocol — drive NeuroSploit from Claude Code, Codex, Cursor, Windsurf' }, + ]; + body.innerHTML = '
' + defs.map(c => { + const cfg = integs.find(i => i.id === c.id) || {}; + const on = cfg.enabled; + return `
+
${esc(c.name)}
+
${esc(c.desc)}
+
${on ? 'Connected' : 'Not configured'}
+
+
`; + }).join('') + '
'; + } catch { body.innerHTML = '
Failed to load integrations.
'; } +} + +async function loadSettings() { + const body = $('#settingsBody'); + if (!body) return; + try { + const cfg = await api('/api/config/settings'); + body.innerHTML = `
+
+
General
+
+
+
+
+
+
+
Network
+
+
+
+
+
+
+
MCP Server
+
Run NeuroSploit as an MCP server for Claude Code, Codex, Cursor, Windsurf and other MCP clients.
+
neurosploit mcp
Config: {"command":"neurosploit","args":["mcp"]}
+
+
`; + } catch { body.innerHTML = '
Failed to load settings.
'; } +} + +function deleteRepo(id) { api(`/api/config/repos/${id}`, { method: 'DELETE' }).then(() => loadRepos()).catch(e => toast(e.message, 'error')); } +function deleteDomain(id) { api(`/api/config/domains/${id}`, { method: 'DELETE' }).then(() => loadDomains()).catch(e => toast(e.message, 'error')); } +function deleteNetwork(id) { api(`/api/config/networks/${id}`, { method: 'DELETE' }).then(() => loadNetworks()).catch(e => toast(e.message, 'error')); } +function configureIntegration(id) { toast('Integration configuration — coming soon', 'info'); } +async function saveSettings() { + const data = { + defaultModel: $('#settingModel')?.value || '', + environment: $('#settingEnv')?.value || 'production', + typesafe: $('#settingTypesafe')?.value || 'auto', + transport: $('#settingTransport')?.value || '', + oobDomain: $('#settingOob')?.value || '', + intercept: $('#settingIntercept')?.value || '', + }; + try { await api('/api/config/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); toast('Settings saved', 'ok'); } + catch (e) { toast(`Failed: ${e.message}`, 'error'); } +} + +// Add-item modals +function addRepoPrompt() { + const url = prompt('Repository URL or owner/repo:'); + if (!url) return; + const name = prompt('Name (optional):') || ''; + api('/api/config/repos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url, name, branch: 'main' }) }) + .then(() => loadRepos()).catch(e => toast(e.message, 'error')); +} +function addDomainPrompt() { + const domain = prompt('Domain (e.g. app.example.com):'); + if (!domain) return; + api('/api/config/domains', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ domain }) }) + .then(() => loadDomains()).catch(e => toast(e.message, 'error')); +} +function addNetworkPrompt() { + const cidr = prompt('Network CIDR or host (e.g. 10.0.0.0/24):'); + if (!cidr) return; + const name = prompt('Name (optional):') || ''; + api('/api/config/networks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ cidr, name }) }) + .then(() => loadNetworks()).catch(e => toast(e.message, 'error')); +} diff --git a/web/public/index.html b/web/public/index.html index ea537a3..360ea99 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -3,7 +3,7 @@ -NeuroSploit v4.3.1 — Console +NeuroSploit v4.3.2 — Console @@ -35,18 +35,33 @@ - - + -
- - -
Imports the endpoints as known surface — every route is tested directly, alongside recon. JSON only (convert YAML specs first).
+ + +
Imports endpoints as known surface — file or directory of .json files. Every route is tested directly, alongside recon.
diff --git a/web/public/style.css b/web/public/style.css index 0ec13f8..c3f4f40 100644 --- a/web/public/style.css +++ b/web/public/style.css @@ -207,6 +207,79 @@ a { color: var(--accent); text-decoration: none; } } .sb-link:hover { background: var(--surface-3); color: var(--text); } +.sb-nav { display: flex; flex-direction: column; gap: 1px; padding: 0 var(--sp-2) var(--sp-3); } +.sb-nav-item { + display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-2) var(--sp-3); + border: none; border-radius: var(--radius-sm); + background: transparent; color: var(--text-dim); font-size: var(--fs-sm); text-align: left; + cursor: pointer; transition: background var(--t-fast), color var(--t-fast); +} +.sb-nav-item:hover { background: var(--surface-3); color: var(--text); } +.sb-nav-item.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; } +.sb-nav-icon { width: 18px; text-align: center; font-size: var(--fs-base); flex: none; } +.sb-nav-separator { height: 1px; background: var(--border); margin: var(--sp-2) var(--sp-3); } + +.sb-run-history { display: flex; flex-direction: column; flex: 1; overflow: hidden; } + +.page-view { display: none; flex-direction: column; flex: 1; overflow: hidden; } +.page-view:not([hidden]) { display: flex; } +.page-body { flex: 1; overflow-y: auto; padding: var(--sp-5); } + +.topbar-search { + padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius-sm); + background: var(--surface); color: var(--text); font-size: var(--fs-sm); width: 220px; +} + +.data-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); } +.data-table th { text-align: left; padding: var(--sp-2) var(--sp-3); font-weight: 600; color: var(--text-dim); border-bottom: 2px solid var(--border); font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .04em; } +.data-table td { padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--border); vertical-align: top; } +.data-table tr:hover td { background: var(--surface-2); } +.data-table .mono { font-family: var(--mono); font-size: var(--fs-xs); } + +.config-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: var(--sp-4); } +.config-card { + border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--sp-4); + background: var(--surface); transition: border-color var(--t-fast); +} +.config-card:hover { border-color: var(--border-strong); } +.config-card-title { font-size: var(--fs-lg); font-weight: 600; margin-bottom: var(--sp-2); display: flex; align-items: center; gap: var(--sp-2); } +.config-card-desc { font-size: var(--fs-sm); color: var(--text-dim); margin-bottom: var(--sp-3); } +.config-card-meta { font-size: var(--fs-xs); color: var(--text-faint); font-family: var(--mono); } +.config-card-actions { display: flex; gap: var(--sp-2); margin-top: var(--sp-3); padding-top: var(--sp-3); border-top: 1px solid var(--border); } + +.btn-accent { + background: var(--accent); color: var(--accent-contrast); border: 1px solid var(--accent); + padding: var(--sp-2) var(--sp-4); border-radius: var(--radius-sm); font-size: var(--fs-sm); font-weight: 600; +} +.btn-accent:hover { background: var(--accent-hover); border-color: var(--accent-hover); } + +.integration-card { display: flex; align-items: center; gap: var(--sp-4); } +.integration-icon { width: 40px; height: 40px; border-radius: var(--radius-sm); background: var(--surface-3); display: flex; align-items: center; justify-content: center; font-size: var(--fs-xl); flex: none; } +.integration-info { flex: 1; } +.integration-status { font-size: var(--fs-xs); display: flex; align-items: center; gap: var(--sp-1); } +.integration-dot { width: 8px; height: 8px; border-radius: 50%; } +.integration-dot.connected { background: var(--sev-low-fg); } +.integration-dot.disconnected { background: var(--text-faint); } + +.stats-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--sp-4); margin-bottom: var(--sp-5); } +.stat-card { + border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--sp-4); + background: var(--surface); +} +.stat-label { font-size: var(--fs-xs); color: var(--text-dim); text-transform: uppercase; letter-spacing: .04em; font-weight: 600; } +.stat-value { font-size: var(--fs-2xl); font-weight: 700; margin-top: var(--sp-1); } +.stat-sub { font-size: var(--fs-xs); color: var(--text-faint); margin-top: var(--sp-1); } + +.form-modal { max-width: 520px; } +.form-group { margin-bottom: var(--sp-4); } +.form-group label { display: block; font-size: var(--fs-sm); font-weight: 600; margin-bottom: var(--sp-1); } +.form-group input, .form-group select, .form-group textarea { + width: 100%; padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius-sm); + background: var(--surface); color: var(--text); font-size: var(--fs-sm); +} +.form-group .field-help { font-size: var(--fs-xs); color: var(--text-faint); margin-top: var(--sp-1); } +.form-actions { display: flex; gap: var(--sp-2); justify-content: flex-end; margin-top: var(--sp-5); } + .sb-search { position: relative; margin: 0 var(--sp-4) var(--sp-3); } .sb-search input { padding-left: 26px; font-size: var(--fs-sm); } .sb-search .search-icon { left: 8px; } diff --git a/web/server.js b/web/server.js index bf5ccab..8250dc4 100644 --- a/web/server.js +++ b/web/server.js @@ -1,7 +1,7 @@ #!/usr/bin/env node 'use strict'; /** - * NeuroSploit v4.3.1 — web console backend. + * NeuroSploit v4.3.2 — web console backend. * * Zero-dependency Node HTTP server that: * - serves the static SPA in ./public @@ -57,6 +57,22 @@ function saveEngagementName(runId, name) { // interrupted run/whitebox/greybox job, RESUME it (relaunch the REPL, which // auto-recovers the harness's own on-disk checkpoint and carries findings // forward). Secrets (API keys, creds-file contents) are never written here. +const CONFIG_DIR = path.join(ROOT, '.neurosploit'); +function configFile(name) { return path.join(CONFIG_DIR, `${name}.json`); } +async function ensureConfigDir() { await fsp.mkdir(CONFIG_DIR, { recursive: true }); } +async function readConfig(name) { + try { return JSON.parse(await fsp.readFile(configFile(name), 'utf8')); } + catch { return []; } +} +async function readConfigObj(name) { + try { return JSON.parse(await fsp.readFile(configFile(name), 'utf8')); } + catch { return {}; } +} +async function writeConfig(name, data) { + await ensureConfigDir(); + await fsp.writeFile(configFile(name), JSON.stringify(data, null, 2)); +} + const JOBS_DIR = path.join(ROOT, '.neurosploit', 'web-jobs'); function jobFile(id) { return path.join(JOBS_DIR, `${id}.json`); } function persistJob(job) { @@ -1942,6 +1958,99 @@ const server = http.createServer(async (req, res) => { return sendJson(res, 200, { ok: true }); } + // ---- Issues (aggregate findings across all runs) ---- + if (req.method === 'GET' && p === '/api/issues') { + const dirs = await fsp.readdir(RUNS_DIR).catch(() => []); + const all = []; + for (const d of dirs) { + const fp = path.join(RUNS_DIR, d, 'findings.json'); + try { + const raw = await fsp.readFile(fp, 'utf8'); + const findings = JSON.parse(raw); + for (const f of findings) all.push({ ...f, run: d, target: f.target || d }); + } catch { /* skip */ } + } + const sRank = (s) => { s = (s || '').toLowerCase(); return s.includes('crit') ? 0 : s.includes('high') ? 1 : s.includes('med') ? 2 : s.includes('low') ? 3 : 4; }; + all.sort((a, b) => sRank(a.severity) - sRank(b.severity)); + return sendJson(res, 200, all); + } + + // ---- Config CRUD: repos ---- + if (req.method === 'GET' && p === '/api/config/repos') return sendJson(res, 200, await readConfig('repos')); + if (req.method === 'POST' && p === '/api/config/repos') { + const body = await readBody(req); + const repos = await readConfig('repos'); + const item = { id: crypto.randomUUID(), ...body, createdAt: new Date().toISOString() }; + repos.push(item); + await writeConfig('repos', repos); + return sendJson(res, 200, item); + } + m = p.match(/^\/api\/config\/repos\/([^/]+)$/); + if (req.method === 'DELETE' && m) { + let repos = await readConfig('repos'); + repos = repos.filter(r => r.id !== decodeURIComponent(m[1])); + await writeConfig('repos', repos); + return sendJson(res, 200, { ok: true }); + } + + // ---- Config CRUD: domains ---- + if (req.method === 'GET' && p === '/api/config/domains') return sendJson(res, 200, await readConfig('domains')); + if (req.method === 'POST' && p === '/api/config/domains') { + const body = await readBody(req); + const domains = await readConfig('domains'); + const item = { id: crypto.randomUUID(), ...body, createdAt: new Date().toISOString() }; + domains.push(item); + await writeConfig('domains', domains); + return sendJson(res, 200, item); + } + m = p.match(/^\/api\/config\/domains\/([^/]+)$/); + if (req.method === 'DELETE' && m) { + let domains = await readConfig('domains'); + domains = domains.filter(d => d.id !== decodeURIComponent(m[1])); + await writeConfig('domains', domains); + return sendJson(res, 200, { ok: true }); + } + + // ---- Config CRUD: networks ---- + if (req.method === 'GET' && p === '/api/config/networks') return sendJson(res, 200, await readConfig('networks')); + if (req.method === 'POST' && p === '/api/config/networks') { + const body = await readBody(req); + const networks = await readConfig('networks'); + const item = { id: crypto.randomUUID(), ...body, createdAt: new Date().toISOString() }; + networks.push(item); + await writeConfig('networks', networks); + return sendJson(res, 200, item); + } + m = p.match(/^\/api\/config\/networks\/([^/]+)$/); + if (req.method === 'DELETE' && m) { + let networks = await readConfig('networks'); + networks = networks.filter(n => n.id !== decodeURIComponent(m[1])); + await writeConfig('networks', networks); + return sendJson(res, 200, { ok: true }); + } + + // ---- Config CRUD: integrations ---- + if (req.method === 'GET' && p === '/api/config/integrations') return sendJson(res, 200, await readConfig('integrations')); + m = p.match(/^\/api\/config\/integrations\/([^/]+)$/); + if (req.method === 'PUT' && m) { + const body = await readBody(req); + let integs = await readConfig('integrations'); + const id = decodeURIComponent(m[1]); + const idx = integs.findIndex(i => i.id === id); + if (idx >= 0) integs[idx] = { ...integs[idx], ...body }; + else integs.push({ id, ...body }); + await writeConfig('integrations', integs); + return sendJson(res, 200, integs.find(i => i.id === id)); + } + + // ---- Config: settings ---- + if (req.method === 'GET' && p === '/api/config/settings') return sendJson(res, 200, await readConfigObj('settings')); + if (req.method === 'PUT' && p === '/api/config/settings') { + const body = await readBody(req); + await writeConfig('settings', body); + return sendJson(res, 200, body); + } + sendJson(res, 404, { error: 'not found' }); } catch (err) { sendJson(res, 500, { error: err.message });