v3.4.1: harness intelligence — router, ReAct, dedup, token-trim, configurable MCP, +54 code agents, credits

- Task-based model ROUTER (recon/select prefer a fast model; exploit prefers primary; validate uses a different model than the finder)
- ReAct doctrine injected into exploit prompts (Thought→Action→Observation, token-efficient)
- Dedup: unique agents per run + findings deduped by CWE/endpoint/title (highest confidence kept)
- Token economy: recon blob capped for selector + per-agent context
- Configurable MCP: merge user mcp.servers.json into the pipeline's .mcp.json
- +54 white-box/code-analysis agents (NoSQLi, LDAP/XPath, JWT-none, Java/.NET/PHP/Go/Node/Python
  specifics, SSTI, ReDoS, deserialization, etc.) → 303 agents total (78 code)
- Credits: Joas A Santos & Red Team Leaders (CLI banner, interactive header, HTML+Typst report)
- README: GitHub stars/forks badges, 60-second quick start, full API config steps, intuitive layout

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
CyberSecurityUP
2026-06-24 19:49:01 -03:00
parent deca20d11f
commit 5d83e8848e
62 changed files with 2445 additions and 43 deletions
+10 -3
View File
@@ -171,7 +171,7 @@ async fn run_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, whitebox: bo
cfg.rl_path = Some(base.join("data").join("rl_state_rs.json").display().to_string());
write_status(&workdir, "running", &format!("\"target\":{:?}", cfg.target));
println!(" ┌─ NeuroSploit v3.4.1");
println!(" ┌─ NeuroSploit v3.4.1 · by Joas A Santos & Red Team Leaders");
println!(" │ run id : {run_id}");
println!(" │ target : {}", cfg.target);
println!(" │ models : {}", cfg.models.join(", "));
@@ -186,13 +186,19 @@ async fn run_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, whitebox: bo
let providers: Vec<String> = cfg.models.iter().map(|m| ModelRef::parse(m).provider).collect();
if providers.iter().any(|p| harness::mcp_supported(p)) {
match harness::ensure_playwright_mcp() {
Ok(()) => match harness::write_mcp_config(&workdir) {
Ok(()) => {
// Optional user-supplied extra MCP servers merged into the pipeline.
let extra = base.join("mcp.servers.json");
let extra_ref = if extra.is_file() { Some(extra.as_path()) } else { None };
match harness::write_mcp_config(&workdir, extra_ref) {
Ok(p) => {
if extra_ref.is_some() { println!(" [*] merged extra MCP servers from mcp.servers.json"); }
println!(" [*] Playwright MCP ready → {}", p.display());
Some(p.display().to_string())
}
Err(e) => { eprintln!(" [!] MCP config failed: {e}"); None }
},
}
}
Err(e) => { eprintln!(" [!] Playwright MCP unavailable ({e}); using built-in tools"); None }
}
} else {
@@ -272,6 +278,7 @@ async fn interactive(base: &Path) -> anyhow::Result<Cmd> {
let backends = harness::installed_cli_backends();
println!("\n ┌────────────────────────────────────────────┐");
println!(" │ NeuroSploit v3.4.1 — interactive │");
println!(" │ by Joas A Santos & Red Team Leaders │");
println!(" └────────────────────────────────────────────┘");
println!(" agents: {} · detected CLI logins: {}\n",
lib.total(), if backends.is_empty() { "none".into() } else { backends.join(", ") });
+1 -1
View File
@@ -21,5 +21,5 @@ pub use models::{
};
pub use pipeline::{run_whitebox, RunOutput};
pub use pipeline::run;
pub use pool::ModelPool;
pub use pool::{ModelPool, Task};
pub use types::{Finding, RunConfig};
+23 -9
View File
@@ -265,17 +265,31 @@ pub fn ensure_playwright_mcp() -> Result<()> {
}
}
/// Write a Playwright `.mcp.json` into `dir` and return its path, so the agentic
/// CLI can drive a real browser (DOM/JS/network/screenshots) during execution.
pub fn write_mcp_config(dir: &std::path::Path) -> std::io::Result<std::path::PathBuf> {
/// Write an `.mcp.json` into `dir` (Playwright by default) and return its path,
/// so the agentic CLI can drive a real browser during execution. If
/// `extra_servers` points at a JSON file shaped like `{ "mcpServers": {...} }`
/// (or just `{...}` of servers), those servers are MERGED in — letting users
/// plug additional MCP tools into the pipeline to potentiate testing.
pub fn write_mcp_config(dir: &std::path::Path, extra_servers: Option<&std::path::Path>) -> std::io::Result<std::path::PathBuf> {
std::fs::create_dir_all(dir)?;
let mut servers = serde_json::json!({
"playwright": { "command": "npx", "args": ["-y", "@playwright/mcp@latest", "--headless", "--isolated"] }
});
if let Some(extra) = extra_servers {
if let Ok(txt) = std::fs::read_to_string(extra) {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&txt) {
let add = v.get("mcpServers").cloned().unwrap_or(v);
if let (Some(dst), Some(src)) = (servers.as_object_mut(), add.as_object()) {
for (k, val) in src {
dst.insert(k.clone(), val.clone());
}
}
}
}
}
let cfg = serde_json::json!({ "mcpServers": servers });
let path = dir.join(".mcp.json");
let cfg = r#"{
"mcpServers": {
"playwright": { "command": "npx", "args": ["-y", "@playwright/mcp@latest", "--headless", "--isolated"] }
}
}"#;
std::fs::write(&path, cfg)?;
std::fs::write(&path, serde_json::to_string_pretty(&cfg).unwrap_or_default())?;
Ok(path)
}
+50 -20
View File
@@ -1,5 +1,5 @@
use crate::agents::{Agent, Library};
use crate::pool::ModelPool;
use crate::pool::{ModelPool, Task};
use crate::rl::{severity_reward, RlState};
use crate::types::{Finding, RunConfig};
use crate::report;
@@ -45,6 +45,12 @@ fn tool_doctrine(mcp_on: bool) -> String {
const VOTE_SYS: &str = "You are an adversarial security validator. Decide if the candidate finding is a REAL, reproducible, exploitable vulnerability with proof. Reply with JSON {\"verdict\":\"confirmed\"|\"rejected\",\"reason\":\"...\"}. Default to rejected when uncertain.";
const CODE_VOTE_SYS: &str = "You are an adversarial source-code reviewer. Decide if the reported issue is a REAL vulnerability in the provided code (reachable, exploitable, not a false positive). Reply JSON {\"verdict\":\"confirmed\"|\"rejected\",\"reason\":\"...\"}.";
/// ReAct loop directive: make the agent reason → act with a tool → observe →
/// iterate, instead of one-shot guessing. Keeps it grounded in real evidence.
const REACT_DOCTRINE: &str = "METHOD (ReAct): work in explicit Thought → Action → Observation cycles. \
Each Action runs ONE concrete tool command (e.g. a curl request); read its real Observation before the next Thought. \
Base every claim on an actual observed response — never assume. Stop when you've either proven an issue or exhausted reasonable checks. Be token-efficient: no filler, no repetition.\n\n";
/// Black-box web engagement: recon → parallel exploit → N-model vote → report.
pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<String>) -> RunOutput {
let _ = tx
@@ -63,7 +69,7 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
"{}".to_string()
} else {
let recon_user = format!("{}Target: {}", tool_doctrine(pool.mcp_config.is_some()), cfg.target);
match pool.complete(RECON_SYS, &recon_user).await {
match pool.complete_routed(Task::Recon, RECON_SYS, &recon_user).await {
Ok((m, t)) => {
let _ = tx.send(format!("recon complete via {}", m.label())).await;
if cfg.verbose {
@@ -109,6 +115,11 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
let _ = tx.send("selection empty — using recon-keyword heuristic".into()).await;
heuristic_select(&ranked, &recon, cap)
};
// Dedup: never run the same agent twice in one engagement.
let selected: Vec<Agent> = {
let mut seen = std::collections::HashSet::new();
selected.into_iter().filter(|a| seen.insert(a.name.clone())).collect()
};
let _ = tx
.send(format!("intelligently selected {} agent(s) matching recon: {}", selected.len(),
selected.iter().map(|a| a.name.clone()).collect::<Vec<_>>().join(", ")))
@@ -118,26 +129,29 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
let target = cfg.target.clone();
let verbose = cfg.verbose;
let mcp_on = pool.mcp_config.is_some();
// Token economy: each agent gets a capped recon context, not the full blob.
let recon_ctx: String = recon.chars().take(3500).collect();
let raw: Vec<(String, String, Vec<Finding>)> = stream::iter(selected.iter().cloned())
.map(|ag| {
let target = target.clone();
let recon = recon.clone();
let recon = recon_ctx.clone();
let txc = tx.clone();
async move {
let user = format!(
"AUTHORIZED engagement — you have explicit permission to test {target}. \
Do not ask for confirmation — proceed and PROVE each issue.\n\n\
{doctrine}{body}\n\nWhen done, reply with ONLY a JSON array of confirmed findings (may be empty []). \
Each item: {{id,title,severity,cwe,endpoint,payload,evidence,impact,remediation,confidence}}. \
`evidence` must contain the concrete proof (request/response excerpt).",
target = target,
doctrine = tool_doctrine(mcp_on),
body = ag.user.replace("{target}", &target).replace("{recon_json}", &recon),
);
if verbose {
let _ = txc.send(format!(" ▶ launching agent: {} ({})", ag.name, ag.title.replace(" Agent", ""))).await;
}
match pool.complete(&ag.system, &user).await {
let user = format!(
"AUTHORIZED engagement — you have explicit permission to test {target}. \
Do not ask for confirmation — proceed and PROVE each issue.\n\n\
{react}{doctrine}{body}\n\nWhen done, reply with ONLY a JSON array of confirmed findings (may be empty []). \
Each item: {{id,title,severity,cwe,endpoint,payload,evidence,impact,remediation,confidence}}. \
`evidence` must contain the concrete proof (request/response excerpt).",
target = target,
react = REACT_DOCTRINE,
doctrine = tool_doctrine(mcp_on),
body = ag.user.replace("{target}", &target).replace("{recon_json}", &recon),
);
match pool.complete_routed(Task::Exploit, &ag.system, &user).await {
Ok((m, text)) => {
let f = extract_findings(&text, &ag.name);
let _ = txc.send(format!("exploit {} via {}{} candidate(s)", ag.name, m.label(), f.len())).await;
@@ -155,8 +169,8 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
.await;
let transcript = transcript_of(&raw);
let candidates: Vec<Finding> = raw.iter().flat_map(|(_, _, f)| f.clone()).collect();
let _ = tx.send(format!("{} candidate finding(s) — validating by {}-model vote", candidates.len(), cfg.vote_n)).await;
let candidates = dedup_findings(raw.iter().flat_map(|(_, _, f)| f.clone()).collect());
let _ = tx.send(format!("{} candidate finding(s) (deduped) — validating by {}-model vote", candidates.len(), cfg.vote_n)).await;
// ---- 4. Validate by N-model voting ---------------------------------
let findings = validate(candidates, pool, VOTE_SYS, cfg.vote_n, &tx).await;
@@ -217,8 +231,8 @@ pub async fn run_whitebox(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: S
.await;
let transcript = transcript_of(&raw);
let candidates: Vec<Finding> = raw.iter().flat_map(|(_, _, f)| f.clone()).collect();
let _ = tx.send(format!("{} candidate finding(s) — validating", candidates.len())).await;
let candidates = dedup_findings(raw.iter().flat_map(|(_, _, f)| f.clone()).collect());
let _ = tx.send(format!("{} candidate finding(s) (deduped) — validating", candidates.len())).await;
let findings = validate(candidates, pool, CODE_VOTE_SYS, cfg.vote_n, &tx).await;
finish(cfg, lib, "{}".into(), transcript, findings, selected, &mut rl, tx).await
}
@@ -235,8 +249,10 @@ async fn select_agents(pool: &ModelPool, recon: &str, catalog: &[Agent], tx: &Se
.map(|a| format!("{}{} [{}]", a.name, a.title.replace(" Agent", ""), a.cwe))
.collect::<Vec<_>>()
.join("\n");
let user = format!("RECON:\n{recon}\n\nAGENT CATALOG (name — title [cwe]):\n{list}\n\nReturn a JSON array of agent names to run.");
match pool.complete(SELECT_SYS, &user).await {
// Token economy: cap the recon blob fed to the selector.
let recon_trim: String = recon.chars().take(3000).collect();
let user = format!("RECON:\n{recon_trim}\n\nAGENT CATALOG (name — title [cwe]):\n{list}\n\nReturn a JSON array of agent names to run.");
match pool.complete_routed(Task::Select, SELECT_SYS, &user).await {
Ok((m, text)) => {
let names = parse_string_array(&text);
if names.is_empty() {
@@ -522,6 +538,20 @@ fn conf(v: Option<&serde_json::Value>) -> f64 {
}
}
/// Drop duplicate findings (same CWE + endpoint + lowercased title) that
/// different agents/models may each report, keeping the highest-confidence one.
fn dedup_findings(mut v: Vec<Finding>) -> Vec<Finding> {
v.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal));
let mut seen = std::collections::HashSet::new();
v.into_iter()
.filter(|f| {
let key = format!("{}|{}|{}", f.cwe.to_lowercase(), f.endpoint.to_lowercase(),
f.title.to_lowercase().chars().take(40).collect::<String>());
seen.insert(key)
})
.collect()
}
fn norm_sev(s: &str) -> String {
match s.to_lowercase().as_str() {
x if x.starts_with("crit") => "Critical",
+44 -1
View File
@@ -3,6 +3,22 @@ use anyhow::{anyhow, Result};
use std::sync::Arc;
use tokio::sync::Semaphore;
/// Task type used by the model router to pick the best model for the step.
#[derive(Clone, Copy, Debug)]
pub enum Task {
Recon,
Select,
Exploit,
Validate,
Default,
}
/// Heuristic: is this a fast/cheap model id (good for recon/triage)?
fn is_fast(model: &str) -> bool {
let m = model.to_lowercase();
["haiku", "flash", "fast", "mini", "lite", "chat", "small"].iter().any(|k| m.contains(k))
}
/// A pool of candidate models with a global concurrency cap and provider
/// failover. The same panel of models is reused for validator voting.
///
@@ -73,9 +89,17 @@ impl ModelPool {
/// Complete a prompt, trying each candidate model until one succeeds.
/// Returns the model that answered and its text.
pub async fn complete(&self, system: &str, user: &str) -> Result<(ModelRef, String)> {
self.complete_routed(Task::Default, system, user).await
}
/// Router-aware completion: reorder the candidate panel by task before the
/// failover loop. Recon/triage prefer a fast/cheap model to save tokens and
/// latency; exploitation prefers the strongest (primary) model.
pub async fn complete_routed(&self, task: Task, system: &str, user: &str) -> Result<(ModelRef, String)> {
let _permit = self.sem.acquire().await.expect("semaphore closed");
let order = self.route(task);
let mut last = anyhow!("no candidate models");
for m in &self.candidates {
for m in &order {
match self.one(m, system, user).await {
Ok(text) => return Ok((m.clone(), text)),
Err(e) => last = e,
@@ -84,6 +108,25 @@ impl ModelPool {
Err(last)
}
/// Reorder candidates for a task. With a single-model panel this is a no-op.
pub fn route(&self, task: Task) -> Vec<ModelRef> {
let mut order = self.candidates.clone();
if order.len() < 2 {
return order;
}
match task {
// Prefer a fast/cheap model for recon & selection.
Task::Recon | Task::Select => {
order.sort_by_key(|m| !is_fast(&m.model)); // fast first
}
// Strongest (panel order = primary first) for exploitation.
Task::Exploit | Task::Default => {}
// Validation handled by vote() rotation (different model than finder).
Task::Validate => {}
}
order
}
/// Ask up to `n` distinct models the same yes/no validation question and
/// return (confirmations, total_votes). A model answering "yes"/"confirmed"
/// counts as a confirmation. Used to cut false positives.
+1 -1
View File
@@ -81,7 +81,7 @@ pub fn html(target: &str, findings: &[Finding]) -> String {
<h1><span class=b>NeuroSploit</span> Penetration Test Report</h1>\
<div class=meta>Target: <b>{t}</b> · v3.4.1 Rust harness · multi-model validated</div>\
<div>{chips}</div><h2>Findings ({n})</h2>{body}\
<p class=meta>Authorized testing only. Findings confirmed by multi-model adversarial voting.</p></body></html>",
<p class=meta>Authorized testing only. Findings confirmed by multi-model adversarial voting.<br>NeuroSploit v3.4.1 · by <b>Joas A Santos</b> &amp; <b>Red Team Leaders</b></p></body></html>",
t = esc(target), chips = chips, n = sorted.len(), body = body,
)
}
+2
View File
@@ -40,6 +40,8 @@
#text(13pt)[Target: #strong(meta.target)]
#v(4pt)
#text(10pt, fill: gray)[Run #meta.run_id · #meta.generated · models: #meta.model]
#v(8pt)
#text(9pt, fill: gray)[by #strong[Joas A Santos] & #strong[Red Team Leaders]]
]
#pagebreak()