diff --git a/RELEASE.md b/RELEASE.md index 78d4147..284bb0c 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -41,6 +41,20 @@ and pick your engagement type up front in a new **onboarding wizard**. Library Skills/Plugins/n8n** — then the box type (black/white/grey for web) and the minimal setup, so a plain `/run` does the right thing. Scope shown in `/show`. +## Intense, multi-round recon + +- Recon is no longer a single quick pass. **`deep_recon`** runs an initial deep + enumeration then **follow-up expansion rounds** that chase what the previous + round found (new subdomains/hosts, unmapped endpoints, promising paths/params), + converging when nothing new appears. +- Agents are told to **install the tools they need** (apt/pip/go/npm/cargo) — + subfinder/amass, httpx, gau/waybackurls/katana/hakrawler, gf, arjun/paramspider, + ffuf/feroxbuster, nuclei, nmap/rustscan, dnsx, linkfinder, whatweb, nikto, + testssl — and chain them (subfinder→httpx→katana/gau→gf→ffuf). +- **`/recon <1-4>`** (REPL) and **`--recon <1-4>`** (CLI) set the intensity: + 1 quick · 2 standard · 3 deep (default) · 4 exhaustive — more rounds + wider + enumeration at higher levels. Best on Kali; degrades to curl/nc if installs fail. + ## Models - Added **`anthropic:claude-sonnet-5`** and **`xai:grok-4.5`**. diff --git a/neurosploit-rs/app/src/main.rs b/neurosploit-rs/app/src/main.rs index ba464b0..31b5cc0 100644 --- a/neurosploit-rs/app/src/main.rs +++ b/neurosploit-rs/app/src/main.rs @@ -49,6 +49,9 @@ enum Cmd { /// Attack-chaining rounds (post-exploitation pivots; 0 disables). #[arg(long, default_value_t = 2)] chain_depth: usize, + /// Recon intensity 1-4 (1 quick .. 4 exhaustive; installs tools). + #[arg(long, default_value_t = 3)] + recon: usize, #[arg(long)] offline: bool, /// Use local agentic CLI subscription (Claude/Codex/Gemini/Grok login). @@ -85,6 +88,9 @@ enum Cmd { /// Attack-chaining rounds (post-exploitation pivots; 0 disables). #[arg(long, default_value_t = 2)] chain_depth: usize, + /// Recon intensity 1-4 (1 quick .. 4 exhaustive; installs tools). + #[arg(long, default_value_t = 3)] + recon: usize, #[arg(long)] offline: bool, #[arg(long)] @@ -117,6 +123,9 @@ enum Cmd { /// Attack-chaining rounds (post-exploitation pivots; 0 disables). #[arg(long, default_value_t = 2)] chain_depth: usize, + /// Recon intensity 1-4 (1 quick .. 4 exhaustive; installs tools). + #[arg(long, default_value_t = 3)] + recon: usize, #[arg(long)] offline: bool, #[arg(long)] @@ -145,6 +154,9 @@ enum Cmd { /// Attack-chaining rounds (post-exploitation pivots; 0 disables). #[arg(long, default_value_t = 2)] chain_depth: usize, + /// Recon intensity 1-4 (1 quick .. 4 exhaustive; installs tools). + #[arg(long, default_value_t = 3)] + recon: usize, #[arg(long)] subscription: bool, #[arg(long)] @@ -169,6 +181,9 @@ enum Cmd { /// Attack-chaining rounds (post-exploitation pivots; 0 disables). #[arg(long, default_value_t = 2)] chain_depth: usize, + /// Recon intensity 1-4 (1 quick .. 4 exhaustive; installs tools). + #[arg(long, default_value_t = 3)] + recon: usize, #[arg(long)] offline: bool, #[arg(long)] @@ -228,6 +243,9 @@ enum Cmd { /// Attack-chaining rounds (post-exploitation pivots; 0 disables). #[arg(long, default_value_t = 2)] chain_depth: usize, + /// Recon intensity 1-4 (1 quick .. 4 exhaustive; installs tools). + #[arg(long, default_value_t = 3)] + recon: usize, #[arg(long)] subscription: bool, /// Post a summary comment back on the PR (needs github integration on). @@ -349,12 +367,13 @@ async fn main() -> anyhow::Result<()> { } } } - Cmd::Run { url, models, max_agents, vote_n, chain_depth, offline, subscription, mcp, creds, focus, jira, verbose } => { + Cmd::Run { url, models, max_agents, vote_n, chain_depth, recon, offline, subscription, mcp, creds, focus, jira, verbose } => { let url = if url.starts_with("http") { url } else { format!("https://{url}") }; let mut cfg = RunConfig::new(&url); cfg.max_agents = max_agents; cfg.vote_n = vote_n; cfg.chain_depth = chain_depth; + cfg.recon_intensity = recon; cfg.offline = offline; cfg.subscription = subscription; cfg.verbose = verbose; @@ -368,12 +387,13 @@ async fn main() -> anyhow::Result<()> { let ig = harness::integrations::Integrations::load(&repl::proj_dir()); post_integrations(&ig, &url, &out, jira, false, None).await; } - Cmd::Whitebox { path, models, max_agents, vote_n, chain_depth, offline, subscription, jira, verbose } => { + Cmd::Whitebox { path, models, max_agents, vote_n, chain_depth, recon, offline, subscription, jira, verbose } => { let path = resolve_source(&base, &path)?; // local path OR github URL/owner/repo let mut cfg = RunConfig::new(&path); cfg.max_agents = max_agents; cfg.vote_n = vote_n; cfg.chain_depth = chain_depth; + cfg.recon_intensity = recon; cfg.offline = offline; cfg.subscription = subscription; cfg.verbose = verbose; @@ -385,7 +405,7 @@ async fn main() -> anyhow::Result<()> { let ig = harness::integrations::Integrations::load(&repl::proj_dir()); post_integrations(&ig, &path, &out, jira, false, None).await; } - Cmd::Greybox { repo, url, models, creds, focus, max_agents, vote_n, chain_depth, offline, subscription, mcp, verbose } => { + Cmd::Greybox { repo, url, models, creds, focus, max_agents, vote_n, chain_depth, recon, offline, subscription, mcp, verbose } => { let repo = resolve_source(&base, &repo)?; // local path OR github URL/owner/repo let url = if url.starts_with("http") { url } else { format!("https://{url}") }; let mut cfg = RunConfig::new(&url); @@ -393,6 +413,7 @@ async fn main() -> anyhow::Result<()> { cfg.max_agents = max_agents; cfg.vote_n = vote_n; cfg.chain_depth = chain_depth; + cfg.recon_intensity = recon; cfg.offline = offline; cfg.subscription = subscription; cfg.verbose = verbose; @@ -404,13 +425,14 @@ async fn main() -> anyhow::Result<()> { let out = run_greybox_engagement(&base, cfg, mcp).await?; print_findings(&out); } - Cmd::Tui { url, models, repo, creds, focus, max_agents, vote_n, chain_depth, subscription, mcp } => { + Cmd::Tui { url, models, repo, creds, focus, max_agents, vote_n, chain_depth, recon, subscription, mcp } => { let repo = match repo { Some(r) => Some(resolve_source(&base, &r)?), None => None }; // github URL ok let url = if url.starts_with("http") { url } else { format!("https://{url}") }; let mut cfg = RunConfig::new(&url); cfg.max_agents = max_agents; cfg.vote_n = vote_n; cfg.chain_depth = chain_depth; + cfg.recon_intensity = recon; cfg.subscription = subscription; cfg.instructions = focus; cfg.repo = repo.clone(); @@ -421,11 +443,12 @@ async fn main() -> anyhow::Result<()> { let mode = if repo.is_some() { Mode::Grey } else { Mode::Black }; tui::run(&base, cfg, mcp, mode).await?; } - Cmd::Host { target, models, creds, focus, max_agents, vote_n, chain_depth, offline, subscription, verbose } => { + Cmd::Host { target, models, creds, focus, max_agents, vote_n, chain_depth, recon, offline, subscription, verbose } => { let mut cfg = RunConfig::new(&target); cfg.max_agents = max_agents; cfg.vote_n = vote_n; cfg.chain_depth = chain_depth; + cfg.recon_intensity = recon; cfg.offline = offline; cfg.subscription = subscription; cfg.verbose = verbose; @@ -462,7 +485,7 @@ async fn main() -> anyhow::Result<()> { let out = run_mode(&base, cfg, false, Mode::Skills).await?; print_findings(&out); } - Cmd::Pr { repo, number, models, vote_n, chain_depth, subscription, comment, jira, verbose } => { + Cmd::Pr { repo, number, models, vote_n, chain_depth, recon, subscription, comment, jira, verbose } => { let ig = harness::integrations::Integrations::load(&repl::proj_dir()); let owner_repo = normalize_repo(&repo); let path = clone_pr(&base, &ig, &owner_repo, number)?; @@ -470,6 +493,7 @@ async fn main() -> anyhow::Result<()> { let mut cfg = RunConfig::new(&path); cfg.vote_n = vote_n; cfg.chain_depth = chain_depth; + cfg.recon_intensity = recon; cfg.subscription = subscription; cfg.verbose = verbose; cfg.instructions = Some(format!("This is the code of pull request #{number} of {owner_repo}. Focus on vulnerabilities introduced or touched by this change.")); diff --git a/neurosploit-rs/app/src/repl.rs b/neurosploit-rs/app/src/repl.rs index 2642d99..57ca7b3 100644 --- a/neurosploit-rs/app/src/repl.rs +++ b/neurosploit-rs/app/src/repl.rs @@ -119,7 +119,7 @@ struct LiveCheckpoint { const COMMANDS: &[&str] = &[ "/help", "/onboard", "/show", "/config", "/providers", "/model", "/key", "/sub", "/target", "/repo", "/auth", "/creds", "/focus", "/attach", "/context", "/mcp", "/offline", - "/votes", "/chain", "/timeout", "/proxy", "/burp", "/ua", "/agents", "/theme", "/clear", "/run", "/stop", "/continue", "/runs", "/results", "/report", + "/votes", "/chain", "/recon", "/timeout", "/proxy", "/burp", "/ua", "/agents", "/theme", "/clear", "/run", "/stop", "/continue", "/runs", "/results", "/report", "/status", "/diff", "/retest", "/validate", "/finding", "/expand", "/integrations", "/quit", ]; @@ -214,6 +214,7 @@ struct Session { vote_n: usize, max_agents: usize, chain_depth: usize, + recon_intensity: usize, /// Idle guardrail: stop a run if no NEW finding lands in this many seconds /// (0 = disabled). Set in minutes via `/timeout `. idle_secs: u64, @@ -244,6 +245,7 @@ impl Default for Session { vote_n: 3, max_agents: 0, chain_depth: 2, + recon_intensity: 3, idle_secs: 300, // 5-minute idle guardrail by default proxy: None, user_agent: None, @@ -567,6 +569,11 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> { if arg.is_empty() { println!(" attack-chain depth: {} (0 disables) — set with /chain ", s.chain_depth); } else { s.chain_depth = arg.parse().unwrap_or(s.chain_depth); println!(" attack-chain depth: {}", s.chain_depth); } } + "/recon" => { + let lvl = |n: usize| ["", "quick", "standard", "deep", "exhaustive"].get(n).copied().unwrap_or("deep"); + if arg.is_empty() { println!(" recon intensity: {} ({}) — set with /recon <1-4> [1 quick · 2 standard · 3 deep · 4 exhaustive]", s.recon_intensity, lvl(s.recon_intensity)); } + else { s.recon_intensity = arg.parse::().unwrap_or(s.recon_intensity).clamp(1, 4); println!(" recon intensity: {} ({}) — more rounds, more enumeration, auto-installs tools", s.recon_intensity, lvl(s.recon_intensity)); } + } "/agents" => { if arg == "list" || arg == "ls" { let lib = agents::load(base); @@ -932,6 +939,7 @@ async fn run(base: &Path, s: &Session, history: &mut Vec) { cfg.subscription = s.subscription; cfg.vote_n = s.vote_n; cfg.chain_depth = s.chain_depth; + cfg.recon_intensity = s.recon_intensity; cfg.proxy = s.proxy.clone(); cfg.user_agent = s.user_agent.clone(); cfg.max_agents = s.max_agents; @@ -1007,6 +1015,7 @@ async fn start_background(base: &Path, s: &Session, reader: &mut Reader, cfg.subscription = s.subscription; cfg.vote_n = s.vote_n; cfg.chain_depth = s.chain_depth; + cfg.recon_intensity = s.recon_intensity; cfg.proxy = s.proxy.clone(); cfg.user_agent = s.user_agent.clone(); cfg.max_agents = s.max_agents; @@ -1467,8 +1476,8 @@ fn show(s: &Session) { println!(" │ proxy : {}", s.proxy.clone().unwrap_or_else(|| "(none — /proxy for Burp/ZAP)".into())); println!(" │ user-agent: {}", s.user_agent.clone().unwrap_or_else(|| "NeuroSploit (default)".into())); println!(" │ focus : {}", s.instructions.clone().unwrap_or_else(|| "(none — tests everything)".into())); - println!(" │ opts : mcp={} offline={} votes={} chain-depth={} max-agents={} idle-stop={}", - onoff(s.mcp), onoff(s.offline), s.vote_n, s.chain_depth, s.max_agents, + println!(" │ opts : mcp={} offline={} votes={} recon={} chain-depth={} max-agents={} idle-stop={}", + onoff(s.mcp), onoff(s.offline), s.vote_n, s.recon_intensity, s.chain_depth, s.max_agents, if s.idle_secs == 0 { "off".to_string() } else { format!("{}m", s.idle_secs / 60) }); // Integrations at a glance (see /integrations for detail). { @@ -1535,6 +1544,7 @@ fn help() { h("/offline on|off", "pipeline self-test (no API keys / no model calls)"); h("/votes ", "number of validator votes per finding"); h("/chain ", "attack-chain depth (post-exploitation pivots; 0 = off)"); + h("/recon <1-4>", "recon intensity: 1 quick · 2 standard · 3 deep · 4 exhaustive (installs tools)"); h("/timeout ", "idle guardrail: stop if no new finding in (0 = off)"); h("/proxy |off", "route agent HTTP through Burp/ZAP (/burp = default :8080)"); h("/ua ", "identifying User-Agent for NeuroSploit traffic (default = NeuroSploit)"); diff --git a/neurosploit-rs/crates/harness/src/pipeline.rs b/neurosploit-rs/crates/harness/src/pipeline.rs index 3872a2f..d2a7926 100644 --- a/neurosploit-rs/crates/harness/src/pipeline.rs +++ b/neurosploit-rs/crates/harness/src/pipeline.rs @@ -241,25 +241,8 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender { - let _ = tx.send(format!("recon complete via {}", m.label())).await; - if cfg.verbose { - let snip: String = t.chars().take(280).collect(); - let _ = tx.send(format!(" recon> {}", snip.replace('\n', " "))).await; - } - // Keep the deterministic probe facts alongside the model recon so - // exploitation agents always see the observed evidence. - format!("{}\n\nMODEL RECON:\n{}", probe_facts, t) - } - Err(e) => { - let _ = tx.send(format!("recon failed ({e}) — continuing with probe facts only")).await; - probe_facts.clone() - } - } + // Intense, multi-round active recon (installs tools, expands the surface). + deep_recon(&cfg, pool, &probe_facts, &tx).await }; // ---- 2. Intelligent, RL-ranked agent selection --------------------- @@ -1303,6 +1286,78 @@ and the model's own response. Map every finding to OWASP LLM Top 10 (2025) and, Exchange. NON-DESTRUCTIVE: never exfiltrate real user data or weaponise the model against third parties — a redacted, \ minimal proof is enough. Chain findings (e.g. system-prompt leak → tailored injection → excessive-agency tool abuse).\n\n"; +/// Recon-phase directive by intensity — tells the agent HOW HARD to recon and +/// to INSTALL the tools it needs (the user wants an intense, active recon, not a +/// quick one-shot). Best on Kali; degrades to curl/nc if installs fail. +fn recon_intensity_directive(level: usize) -> String { + let (label, rounds, extra) = match level { + 0 | 1 => ("QUICK", "one focused pass", ""), + 2 => ("STANDARD", "crawl + JS + params", ""), + 3 => ("DEEP", "multi-angle active enumeration", + "Go WIDE and DEEP — do NOT stop after the homepage. This should take real effort."), + _ => ("EXHAUSTIVE", "leave no stone unturned", + "Be EXHAUSTIVE — enumerate everything, brute wordlists, chase every referenced host/asset."), + }; + format!( + "RECON INTENSITY: {label} — {rounds}. {extra}\n\ + INSTALL WHAT YOU NEED (authorized): if a recon tool is missing, install it before falling back — \ + `apt-get install -y `, `pip install `, `go install @latest`, `npm i -g `, or `cargo install `. \ + Recommended arsenal: subfinder/amass/assetfinder (subdomains), httpx/httprobe (probe live), \ + gau/waybackurls/katana/hakrawler/gospider (URL harvest & crawl), gf (pattern-filter urls), \ + arjun/paramspider (params), ffuf/feroxbuster/dirsearch (content discovery), nuclei (targeted templates), \ + nmap/rustscan/naabu (ports), dnsx (dns), subjs/linkfinder/getjs (JS endpoints), whatweb/wappalyzer (fingerprint), \ + nikto (server issues), testssl.sh/sslscan (TLS). Chain them: subfinder→httpx→katana/gau→gf→ffuf.\n\ + COVER, at this intensity: (1) subdomain & vhost enumeration + resolve live; (2) full crawl + historical \ + URLs (wayback/gau) + JS analysis (endpoints, params, secrets, source maps); (3) content & parameter \ + discovery with wordlists; (4) port/service scan; (5) tech + EXACT version fingerprinting; (6) auth/API \ + (REST+GraphQL) mapping; (7) classic exposures (.git/.env/backups/swagger/actuator, dangling CNAMEs); \ + (8) TLS/headers/cookies. Report counts (how many subdomains/urls/params/endpoints you actually found).\n\n") +} + +/// Intense, multi-round recon: an initial deep pass, then follow-up rounds that +/// EXPAND the surface (chase discovered subdomains/endpoints/params, install +/// tools, dig where the previous round found signal). Returns the merged recon +/// text. Rounds scale with `recon_intensity` (2→1 extra, 3→2, 4→3). +async fn deep_recon(cfg: &RunConfig, pool: &ModelPool, probe_facts: &str, tx: &Sender) -> String { + let intensity = cfg.recon_intensity.max(1); + let extra_rounds = intensity.saturating_sub(1).min(3); + let doctrine = tool_doctrine(pool.mcp_config.is_some()); + let intensity_dir = recon_intensity_directive(intensity); + let dir = operator_directives(cfg); + let mut accum = format!("OBSERVED HTTP PROBE:\n{probe_facts}"); + + // Initial deep pass. + let user = format!("{dir}{intensity_dir}{doctrine}OBSERVED HTTP PROBE (build on these, verify, go deeper):\n{probe_facts}\n\nTarget: {}", cfg.target); + let _ = tx.send(format!("recon: intensity {} — actively enumerating (installing tools as needed)…", intensity)).await; + match pool.complete_routed(Task::Recon, "recon", RECON_SYS, &user).await { + Ok((m, t)) => { let _ = tx.send(format!("recon round 1 complete via {}", m.label())).await; accum.push_str(&format!("\n\nMODEL RECON (round 1):\n{t}")); } + Err(e) => { let _ = tx.send(format!("recon round 1 failed ({e}) — probe facts only")).await; return accum; } + } + + // Follow-up expansion rounds — each digs further using what's known so far. + for r in 0..extra_rounds { + if pool.stop_exploiting() { break; } + let round = r + 2; + let known: String = accum.chars().rev().take(3000).collect::().chars().rev().collect(); + let follow = format!( + "{dir}{intensity_dir}{doctrine}CONTINUE the recon — this is round {round}. Here is what recon has found so far:\n{known}\n\n\ + Now EXPAND: pick the most promising leads and go deeper — resolve & probe any NEW subdomains/hosts, crawl \ + and harvest URLs for endpoints not yet mapped, run content/parameter discovery where you saw interesting \ + paths, fingerprint exact versions of anything unclear, and enumerate the API/GraphQL further. Install any \ + tool you still need. Report ONLY the NEW facts found this round as the same COMPACT JSON schema. No repetition of prior facts.", + ); + match pool.complete_routed(Task::Recon, "recon", RECON_SYS, &follow).await { + Ok((m, t)) => { + let novel = t.trim(); + if novel.len() > 20 { let _ = tx.send(format!("recon round {round} via {} — expanded surface", m.label())).await; accum.push_str(&format!("\n\nMODEL RECON (round {round}):\n{novel}")); } + else { let _ = tx.send(format!("recon round {round}: no new surface — recon converged")).await; break; } + } + Err(e) => { let _ = tx.send(format!("recon round {round} failed ({e})")).await; break; } + } + } + accum +} + /// AI recon system prompt. const AI_RECON_SYS: &str = "You are an AI-security recon specialist on an AUTHORIZED engagement. Probe the AI endpoint: \ identify the model/provider if leaked, the system/assistant behaviour, available tools/functions/MCP servers, RAG/retrieval, \ diff --git a/neurosploit-rs/crates/harness/src/types.rs b/neurosploit-rs/crates/harness/src/types.rs index 03d2324..1331438 100644 --- a/neurosploit-rs/crates/harness/src/types.rs +++ b/neurosploit-rs/crates/harness/src/types.rs @@ -137,12 +137,20 @@ pub struct RunConfig { /// Defaults to the NeuroSploit UA when unset. #[serde(default)] pub user_agent: Option, + /// Recon intensity (1=quick, 2=standard, 3=deep, 4=exhaustive). Higher = + /// more recon rounds, more active enumeration, and auto-installing tools. + #[serde(default = "default_recon")] + pub recon_intensity: usize, } fn default_vote() -> usize { 3 } +fn default_recon() -> usize { + 3 +} + fn default_chain_depth() -> usize { 2 } @@ -170,6 +178,7 @@ impl RunConfig { chain_depth: 2, proxy: None, user_agent: None, + recon_intensity: 3, } } }