From 94404555aaaefbcca4e4a0c1a99753e0af0dc9da Mon Sep 17 00:00:00 2001 From: CyberSecurityUP Date: Sun, 4 Oct 2026 08:01:29 -0300 Subject: [PATCH] v4.2.4: full Kali sandbox orchestration for recon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spin Kali up for the engagement, run tool-recon in it, let the LLM refine on top, tear it down after. - kali_provision_recon_tools(): installs the recon toolbox (subfinder/httpx/ katana/gau/waybackurls/nuclei/naabu/dnsx/assetfinder/gf/qsreplace/anew via go install + apt) in the Kali sandbox on demand, idempotent, once per run. - kali_tool_recon(): deterministic tool-recon phase โ€” gau/waybackurls/katana URL harvest + targeted nuclei (exposures/misconfig/takeovers, high-signal only) + gf-flagged candidate URLs by class โ€” over the live hosts, then folded into the recon context so the LLM works on top of the tool output and confirms each. Runs in the sandbox (--sandbox) or on host tools via recon_tool(). - Engine autostart: if the container engine is installed but not running, start it automatically (colima start / open -a Docker / systemctl start docker / podman machine start) and poll until up โ€” a --sandbox run no longer fails just because the daemon wasn't started. Clear guidance if it can't be started. - Teardown: the Kali container is removed at the end of the run (override with NEUROSPLOIT_KEEP_SANDBOX=1). Version 4.2.4 across CLI/clap/web/README/TUTORIAL. 423 tests. Co-Authored-By: Claude Opus 4.8 --- README.md | 6 +- TUTORIAL.md | 4 +- neurosploit-rs/Cargo.lock | 4 +- neurosploit-rs/Cargo.toml | 2 +- neurosploit-rs/app/src/main.rs | 4 +- neurosploit-rs/crates/harness/src/pipeline.rs | 137 +++++++++++++++++- neurosploit-rs/crates/harness/src/sandbox.rs | 39 +++++ web/public/index.html | 4 +- web/server.js | 6 +- 9 files changed, 190 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 31dcfcc..e774ad3 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

๐Ÿง  NeuroSploit v4.2.3

+

๐Ÿง  NeuroSploit v4.2.4

Stars @@ -8,7 +8,7 @@

- + @@ -52,7 +52,7 @@ Control TUI**. ### Highlights -> **New in v4.2.3** โ€” **free, LLM-directed exploration**: an exploit agent's named +> **New in v4.2.4** โ€” **free, LLM-directed exploration**: an exploit agent's named > class is a starting point, not a cage โ€” it maps what the app actually does and > reports any class it can prove, with **authentication / identity** (login, signup, > password reset, MFA, OAuth/OIDC/SAML, JWT, session) as a first-class target and diff --git a/TUTORIAL.md b/TUTORIAL.md index 46d3468..d6dcc0b 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -1,4 +1,4 @@ -# NeuroSploit โ€” Tutorial & User Guide (v4.2.3) +# NeuroSploit โ€” Tutorial & User Guide (v4.2.4) A complete, hands-on guide to installing, configuring and running NeuroSploit โ€” the autonomous, multi-model penetration-testing harness. @@ -102,7 +102,7 @@ Agents **degrade gracefully**: if `rustscan` is absent they use `nmap`; if neith ### Verify ```bash -neurosploit --version # neurosploit 4.2.3 +neurosploit --version # neurosploit 4.2.4 neurosploit agents # {"vulns":255,...,"ai":30,...,"total":480} neurosploit models # all providers & models ``` diff --git a/neurosploit-rs/Cargo.lock b/neurosploit-rs/Cargo.lock index 01b2b7c..904c8c4 100644 --- a/neurosploit-rs/Cargo.lock +++ b/neurosploit-rs/Cargo.lock @@ -940,7 +940,7 @@ dependencies = [ [[package]] name = "neurosploit" -version = "4.2.3" +version = "4.2.4" dependencies = [ "anyhow", "clap", @@ -957,7 +957,7 @@ dependencies = [ [[package]] name = "neurosploit-harness" -version = "4.2.3" +version = "4.2.4" dependencies = [ "anyhow", "base64", diff --git a/neurosploit-rs/Cargo.toml b/neurosploit-rs/Cargo.toml index f364c42..f3f752b 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.2.3" +version = "4.2.4" 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 ee45b63..796b25e 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.2.3 โ€” multi-model autonomous pentest harness", - long_about = "NeuroSploit v4.2.3 โ€” a Rust multi-model harness that drives a pool of LLMs \ + about = "NeuroSploit v4.2.4 โ€” multi-model autonomous pentest harness", + long_about = "NeuroSploit v4.2.4 โ€” 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\ diff --git a/neurosploit-rs/crates/harness/src/pipeline.rs b/neurosploit-rs/crates/harness/src/pipeline.rs index 387ef50..5ebbcdb 100644 --- a/neurosploit-rs/crates/harness/src/pipeline.rs +++ b/neurosploit-rs/crates/harness/src/pipeline.rs @@ -1057,7 +1057,24 @@ pub async fn run(mut cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender // in-scope subdomains and fold them into the surface, so the run tests the // whole authorized domain, not just the seed host. let subdomains = if cfg.offline { String::new() } else { enumerate_subdomains(&cfg, &tx).await }; - let probe_facts = if subdomains.is_empty() { probe_facts } else { format!("{probe_facts}{subdomains}") }; + // Live hosts discovered (parsed from the block) โ€” fed to the tool-recon pass. + let live_hosts: Vec = subdomains.lines() + .filter_map(|l| l.trim().strip_prefix("- https://").map(|r| r.split_whitespace().next().unwrap_or("").to_string())) + .filter(|h| !h.is_empty()).collect(); + + // KALI TOOL-RECON: run the real recon pipeline (gau/katana URL harvest + + // targeted nuclei) inside the sandbox (or on host tools), then let the LLM + // refine on top of the output. Provisions the toolbox in Kali on demand. + let tool_recon = if cfg.offline { + String::new() + } else { + if let Some(sb) = engagement_sandbox(&cfg) { + kali_provision_recon_tools(&sb, &tx).await; + } + kali_tool_recon(&cfg, &live_hosts, &tx).await + }; + + let probe_facts = format!("{probe_facts}{subdomains}{tool_recon}"); let recon = if cfg.offline { let _ = tx.send("recon: offline mode โ€” skipping model calls".into()).await; @@ -2810,6 +2827,15 @@ async fn finish(cfg: RunConfig, _lib: &Library, pool: &ModelPool, recon: String, let _ = tx.send(format!("notify: phase complete โ€” {} validated finding(s) [{}]", findings.len(), sev)).await; } + // Tear the Kali sandbox down at the end of the engagement (spin up for the + // run, kill it after), unless the operator asked to keep it. + if cfg.sandbox.is_some() && !std::env::var("NEUROSPLOIT_KEEP_SANDBOX").map(|v| v == "1" || v == "true").unwrap_or(false) { + if let Some(sb) = engagement_sandbox(&cfg) { + sb.teardown().await; + let _ = tx.send("notify: ๐Ÿ“ฆ Kali sandbox torn down".into()).await; + } + } + RunOutput { target: cfg.target.clone(), workdir: cfg.workdir.clone().unwrap_or_default(), @@ -4104,6 +4130,115 @@ async fn recon_tool(cfg: &RunConfig, tool: &str, command: &str) -> Option Option { + let image = cfg.sandbox.as_deref()?; + let mut sc = crate::sandbox::SandboxConfig::default(); + if !image.trim().is_empty() { sc = sc.with_image(image); } + crate::sandbox::Sandbox::new(sc).ok() +} + +/// Install the recon toolbox in the Kali sandbox on demand (idempotent โ€” skips +/// anything already present). Runs once per engagement. Best-effort: a tool that +/// fails to install just won't be used. +async fn kali_provision_recon_tools(sb: &crate::sandbox::Sandbox, tx: &Sender) { + // Which recon tools are missing? + let check = sb.exec("for t in subfinder httpx katana gau waybackurls nuclei naabu dnsx assetfinder gf qsreplace anew; do command -v $t >/dev/null 2>&1 || echo $t; done").await; + let missing: Vec = check.map(|o| o.stdout.lines().map(|l| l.trim().to_string()).filter(|l| !l.is_empty()).collect()).unwrap_or_default(); + if missing.is_empty() { return; } + let _ = tx.send(format!("๐Ÿ“ฆ provisioning Kali recon tools: {}", missing.join(", "))).await; + // ProjectDiscovery + bug-bounty tools install via `go install`; a couple via apt. + // Install Go first if needed, then the PD suite in one shot. + let _ = sb.exec("command -v go >/dev/null 2>&1 || (apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq golang-go) ; true").await; + let go = |pkg: &str| format!("GOBIN=/usr/local/bin go install -v {pkg}@latest 2>/dev/null; true"); + let pd: &[(&str, &str)] = &[ + ("subfinder", "github.com/projectdiscovery/subfinder/v2/cmd/subfinder"), + ("httpx", "github.com/projectdiscovery/httpx/cmd/httpx"), + ("katana", "github.com/projectdiscovery/katana/cmd/katana"), + ("nuclei", "github.com/projectdiscovery/nuclei/v3/cmd/nuclei"), + ("naabu", "github.com/projectdiscovery/naabu/v2/cmd/naabu"), + ("dnsx", "github.com/projectdiscovery/dnsx/cmd/dnsx"), + ("gau", "github.com/lc/gau/v2/cmd/gau"), + ("waybackurls", "github.com/tomnomnom/waybackurls"), + ("assetfinder", "github.com/tomnomnom/assetfinder"), + ("gf", "github.com/tomnomnom/gf"), + ("qsreplace", "github.com/tomnomnom/qsreplace"), + ("anew", "github.com/tomnomnom/anew"), + ]; + for (bin, pkg) in pd { + if missing.iter().any(|m| m == bin) { let _ = sb.exec(&go(pkg)).await; } + } + // nuclei templates (quietly). + if missing.iter().any(|m| m == "nuclei") { let _ = sb.exec("nuclei -update-templates -silent 2>/dev/null; true").await; } +} + +/// Deterministic TOOL-RECON phase: run the real bug-bounty recon pipeline inside +/// the Kali sandbox (or on the host) against the live hosts, and return a +/// structured block (URL harvest + targeted nuclei quick-wins + gf-flagged +/// candidate URLs) to feed the LLM refine phase. This is the "recon by tools, +/// then the LLM works on top of the output" the operator asked for. +async fn kali_tool_recon(cfg: &RunConfig, hosts: &[String], tx: &Sender) -> String { + // Need either a sandbox or host tools; the recon_tool() helper handles both. + let seed = crate::scope::host_of(&cfg.target); + let mut targets: Vec = Vec::new(); + if !seed.is_empty() { targets.push(seed); } + for h in hosts { if !targets.contains(h) { targets.push(h.clone()); } } + targets.truncate(12); // bound the deterministic pass; the LLM covers the rest + if targets.is_empty() { return String::new(); } + + let _ = tx.send(format!("๐Ÿ›  tool-recon over {} host(s) (gau/katana + targeted nuclei)โ€ฆ", targets.len())).await; + let mut urls: std::collections::BTreeSet = std::collections::BTreeSet::new(); + let mut nuclei_hits: Vec = Vec::new(); + + for host in &targets { + // URL harvest (historical + crawl). + let harvest = format!("( echo {host} | gau --threads 20 2>/dev/null; echo {host} | waybackurls 2>/dev/null; katana -u https://{host} -d 2 -jc -silent 2>/dev/null ) | sort -u | head -400"); + if let Some(out) = recon_tool(cfg, "gau", &harvest).await { + for l in out.lines() { let u = l.trim(); if u.starts_with("http") { urls.insert(u.to_string()); } } + } + // Targeted nuclei quick-wins (exposures/misconfig/takeover), high signal only. + let nuc = format!("nuclei -u https://{host} -t exposures/,misconfiguration/,takeovers/ -severity critical,high,medium -silent -nc 2>/dev/null | head -40"); + if let Some(out) = recon_tool(cfg, "nuclei", &nuc).await { + for l in out.lines() { let t = l.trim(); if !t.is_empty() { nuclei_hits.push(t.to_string()); } } + } + } + if urls.is_empty() && nuclei_hits.is_empty() { + let _ = tx.send("๐Ÿ›  tool-recon: no tools available or no output (LLM recon continues)".into()).await; + return String::new(); + } + // gf-flag candidate URLs by class for the exploitation phase. + let url_list = urls.iter().cloned().collect::>().join("\n"); + let mut gf_block = String::new(); + if !url_list.is_empty() { + for pat in ["sqli", "xss", "ssrf", "redirect", "lfi", "idor"] { + let cmd = format!("printf '%s' {:?} | gf {pat} 2>/dev/null | head -15", url_list.chars().take(8000).collect::()); + if let Some(out) = recon_tool(cfg, "gf", &cmd).await { + let hits: Vec<&str> = out.lines().map(|l| l.trim()).filter(|l| l.starts_with("http")).collect(); + if !hits.is_empty() { gf_block.push_str(&format!(" {pat}: {}\n", hits.join(" "))); } + } + } + } + + let _ = tx.send(format!("๐Ÿ›  tool-recon: {} URL(s) harvested, {} nuclei hit(s){}", urls.len(), nuclei_hits.len(), + if gf_block.is_empty() { String::new() } else { " ยท gf-flagged candidates added".into() })).await; + + let mut block = String::from("\n\nTOOL-RECON OUTPUT (deterministic โ€” work on top of this, verify each before reporting):\n"); + if !nuclei_hits.is_empty() { + block.push_str("Nuclei quick-wins (CONFIRM each with your own request โ€” do not report a template name as a finding):\n"); + for h in nuclei_hits.iter().take(40) { block.push_str(&format!("- {h}\n")); } + } + if !gf_block.is_empty() { + block.push_str("Candidate URLs by vuln class (gf-flagged โ€” TEST these parameters):\n"); + block.push_str(&gf_block); + } + if !urls.is_empty() { + block.push_str(&format!("Harvested URLs (sample of {}):\n", urls.len())); + for u in urls.iter().take(80) { block.push_str(&format!("- {u}\n")); } + } + block +} + async fn enumerate_subdomains(cfg: &RunConfig, tx: &Sender) -> String { // Which apex(es) are authorized domain-wide? let apexes: Vec = cfg.scope.hard.iter().filter_map(|p| { diff --git a/neurosploit-rs/crates/harness/src/sandbox.rs b/neurosploit-rs/crates/harness/src/sandbox.rs index b41edb6..d6e0a2a 100644 --- a/neurosploit-rs/crates/harness/src/sandbox.rs +++ b/neurosploit-rs/crates/harness/src/sandbox.rs @@ -209,7 +209,46 @@ impl Sandbox { /// /// Returns a human-readable status. Idempotent: a second call on an /// already-running container is a no-op, so the pipeline can call it freely. + /// Is the container ENGINE (daemon) responsive? ` info` only succeeds + /// when the daemon is up โ€” the binary being installed is not enough. + async fn engine_up(&self) -> bool { + run(self.runtime.bin(), &["info"], Duration::from_secs(10)).await + .map(|o| o.code == 0).unwrap_or(false) + } + + /// Start the container engine if it's installed but not running โ€” so a run + /// with `--sandbox` doesn't fail just because Docker Desktop/Colima/the + /// docker daemon/the podman machine wasn't started. Tries the common starts + /// for the platform, then polls ` info` until it comes up. + async fn start_engine(&self) -> Result<(), String> { + if self.engine_up().await { return Ok(()); } + let starters: &[(&str, &[&str])] = match self.runtime { + Runtime::Docker => &[ + ("colima", &["start"]), // macOS/Linux, common + ("systemctl", &["start", "docker"]), // Linux systemd + ("service", &["docker", "start"]), // Linux sysv + ("open", &["-a", "Docker"]), // macOS Docker Desktop + ], + Runtime::Podman => &[ + ("podman", &["machine", "start"]), + ], + }; + for (bin, args) in starters { + if !which(bin) { continue; } + let _ = run(bin, args, Duration::from_secs(120)).await; + // Poll for the daemon to come up (engines take a few seconds). + for _ in 0..30 { + if self.engine_up().await { return Ok(()); } + tokio::time::sleep(Duration::from_secs(2)).await; + } + } + if self.engine_up().await { Ok(()) } + else { Err(format!("the {} engine is installed but not running, and could not be started automatically โ€” start it (e.g. `colima start`, `open -a Docker`, or `sudo systemctl start docker`) and retry", self.runtime.bin())) } + } + pub async fn ensure(&self) -> Result { + // Bring the engine up first โ€” installed-but-not-running is the common case. + self.start_engine().await?; if self.is_up().await { return Ok(format!("{} container `{}` already running", self.runtime.bin(), self.cfg.name)); } diff --git a/web/public/index.html b/web/public/index.html index 2e852a4..c76c26f 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -3,7 +3,7 @@ -NeuroSploit v4.2.3 โ€” Console +NeuroSploit v4.2.4 โ€” Console @@ -33,7 +33,7 @@

- v4.2.3 + v4.2.4
diff --git a/web/server.js b/web/server.js index cb3cb2b..0cd5b07 100644 --- a/web/server.js +++ b/web/server.js @@ -1,7 +1,7 @@ #!/usr/bin/env node 'use strict'; /** - * NeuroSploit v4.2.3 โ€” web console backend. + * NeuroSploit v4.2.4 โ€” web console backend. * * Zero-dependency Node HTTP server that: * - serves the static SPA in ./public @@ -1487,7 +1487,7 @@ const server = http.createServer(async (req, res) => { } if (req.method === 'GET' && p === '/api/meta') { - return sendJson(res, 200, { version: "4.2.3", binary: BIN, root: ROOT }); + return sendJson(res, 200, { version: "4.2.4", binary: BIN, root: ROOT }); } // ---- providers / API keys (in-memory only, never persisted) ---- @@ -1521,7 +1521,7 @@ const server = http.createServer(async (req, res) => { loadPersistedJobs(); server.listen(PORT, () => { - console.log(`NeuroSploit v4.2.3 web console โ†’ http://localhost:${PORT}`); + console.log(`NeuroSploit v4.2.4 web console โ†’ http://localhost:${PORT}`); console.log(` binary : ${BIN || '(not found โ€” build neurosploit-rs first)'}`); console.log(` agents : ${AGENTS_DIR}`); console.log(` runs : ${RUNS_DIR}`);