recon: intense multi-round active recon (deep_recon) with tool auto-install

Recon was a single quick model pass — now it's deep and iterative:
- deep_recon(): an initial deep enumeration pass then follow-up EXPANSION rounds
  that chase discovered subdomains/hosts/endpoints/params, converging when a
  round finds nothing new. Rounds scale with intensity.
- recon_intensity_directive(): tells the agent HOW hard to recon and to INSTALL
  the tools it needs (apt/pip/go/npm/cargo) — subfinder/amass/httpx/gau/katana/
  gf/arjun/ffuf/nuclei/nmap/dnsx/linkfinder/whatweb/nikto/testssl — chained
  (subfinder->httpx->katana/gau->gf->ffuf); covers subdomains, crawl+wayback, JS,
  content/param discovery, ports, versions, API, exposures, TLS/headers.
- RunConfig.recon_intensity (default 3) + REPL /recon <1-4> + CLI --recon <1-4>
  (1 quick .. 4 exhaustive); shown in /show.
This commit is contained in:
CyberSecurityUP
2026-07-10 11:16:44 -03:00
parent b09367483a
commit d414dcb1f1
5 changed files with 140 additions and 28 deletions
+14
View File
@@ -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`**.
+30 -6
View File
@@ -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."));
+13 -3
View File
@@ -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 <mins>`.
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 <n>", 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::<usize>().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<RunRecord>) {
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 <n>", "number of validator votes per finding");
h("/chain <n>", "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 <min>", "idle guardrail: stop if no new finding in <min> (0 = off)");
h("/proxy <url>|off", "route agent HTTP through Burp/ZAP (/burp = default :8080)");
h("/ua <string>", "identifying User-Agent for NeuroSploit traffic (default = NeuroSploit)");
+74 -19
View File
@@ -241,25 +241,8 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
let _ = tx.send("recon: offline mode — skipping model calls".into()).await;
"{}".to_string()
} else {
let recon_user = format!(
"{}{}OBSERVED HTTP PROBE (real request/response facts — build on these, verify, and go deeper):\n{}\n\nTarget: {}",
operator_directives(&cfg), tool_doctrine(pool.mcp_config.is_some()), probe_facts, cfg.target);
match pool.complete_routed(Task::Recon, "recon", RECON_SYS, &recon_user).await {
Ok((m, t)) => {
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 <t>`, `pip install <t>`, `go install <pkg>@latest`, `npm i -g <t>`, or `cargo install <t>`. \
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>) -> 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::<String>().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, \
@@ -137,12 +137,20 @@ pub struct RunConfig {
/// Defaults to the NeuroSploit UA when unset.
#[serde(default)]
pub user_agent: Option<String>,
/// 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,
}
}
}