mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-07-04 10:27:50 +02:00
v3.4.x: intelligent agent selection, whitebox, recon/code agents, Gemini, artifacts, RL, XBOW GUI
Harness intelligence: - After recon, the model SELECTS which specialist agents match the target (select_agents) — runs the relevant subset, not blindly top-N - RL reward store (rl.rs): per-agent weights persist to data/rl_state_rs.json, reward validated findings (severity-weighted), decay idle, bias next run - Run artifacts persisted as JSON + MD (recon, exploitation transcript, findings, html report) under runs/<target>-<ts>/ for reuse by other AIs Whitebox mode: - run_whitebox: walks a repo, builds bounded source context, runs code agents, validates by adversarial vote. CLI `whitebox <path>` + web "White-box" mode Agents: +12 recon (subdomain/tech/js/api/secrets/dns/content/param/waf/cloud/ graphql/osint) and +24 code SAST reviewers (sqli/cmdi/path/ssrf/xss/deser/ secrets/crypto/authz/idor/xxe/redirect/ssti/race/eval/csrf/random/logging/ upload/mass-assign/jwt/cors). Loader gains recon/ + code/ categories → 249 total Models: +Google Gemini provider (API + gemini CLI subscription); installed_cli_ backends now detects gemini; chat_cli handles gemini/codex/grok + optional Playwright MCP (.mcp.json) on the subscription path with autonomy flags GUI: full XBOW-style redesign — sidebar (Operate/Library), topbar status, mode segment (black-box/white-box), model panel, live console, severity cards, agent browser with category filters, models view; responsive + aligned Verified: cargo build --release clean; CLI agents/whitebox; LIVE subscription run shows model selecting 23→4 agents, RL update, artifacts written; GUI + white-box toggle in Playwright. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+100
-23
@@ -3,8 +3,8 @@
|
||||
mod web;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use harness::{agents, models::ModelRef, pool::ModelPool, report, types::RunConfig};
|
||||
use std::path::PathBuf;
|
||||
use harness::{agents, models::ModelRef, pool::ModelPool, types::RunConfig, RunOutput};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "neurosploit", version, about = "NeuroSploit v3.4.0 — multi-model autonomous pentest harness")]
|
||||
@@ -37,6 +37,24 @@ enum Cmd {
|
||||
/// instead of HTTP API keys.
|
||||
#[arg(long)]
|
||||
subscription: bool,
|
||||
/// Enable Playwright MCP (browser proof) on the subscription/CLI path.
|
||||
#[arg(long)]
|
||||
mcp: bool,
|
||||
},
|
||||
/// White-box: analyse a local repository's source code for vulnerabilities.
|
||||
Whitebox {
|
||||
/// Path to the repository to analyse.
|
||||
path: String,
|
||||
#[arg(long = "model")]
|
||||
models: Vec<String>,
|
||||
#[arg(long, default_value_t = 0)]
|
||||
max_agents: usize,
|
||||
#[arg(long, default_value_t = 2)]
|
||||
vote_n: usize,
|
||||
#[arg(long)]
|
||||
offline: bool,
|
||||
#[arg(long)]
|
||||
subscription: bool,
|
||||
},
|
||||
/// Show agent library counts.
|
||||
Agents,
|
||||
@@ -88,7 +106,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Cmd::Run { url, models, max_agents, vote_n, offline, subscription } => {
|
||||
Cmd::Run { url, models, max_agents, vote_n, offline, subscription, mcp } => {
|
||||
let url = if url.starts_with("http") { url } else { format!("https://{url}") };
|
||||
let mut cfg = RunConfig::new(&url);
|
||||
cfg.max_agents = max_agents;
|
||||
@@ -98,26 +116,20 @@ async fn main() -> anyhow::Result<()> {
|
||||
if !models.is_empty() {
|
||||
cfg.models = models;
|
||||
}
|
||||
let lib = agents::load(&base);
|
||||
let refs: Vec<ModelRef> = cfg.models.iter().map(|s| ModelRef::parse(s)).collect();
|
||||
let pool = ModelPool::with_auth(refs, cfg.concurrency, cfg.subscription);
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(256);
|
||||
let printer = tokio::spawn(async move {
|
||||
while let Some(line) = rx.recv().await {
|
||||
println!(" [*] {line}");
|
||||
}
|
||||
});
|
||||
let out = harness::run(cfg.clone(), &lib, &pool, tx).await;
|
||||
let _ = printer.await;
|
||||
|
||||
println!("\n=== {} validated finding(s) ===", out.findings.len());
|
||||
println!("{}", serde_json::to_string_pretty(&out.findings)?);
|
||||
let html = report::html(&url, &out.findings);
|
||||
std::fs::create_dir_all(base.join("reports")).ok();
|
||||
let rp = base.join("reports").join("report_rs.html");
|
||||
std::fs::write(&rp, html).ok();
|
||||
println!("report → {}", rp.display());
|
||||
let out = run_engagement(&base, cfg, mcp, false).await?;
|
||||
print_findings(&out);
|
||||
}
|
||||
Cmd::Whitebox { path, models, max_agents, vote_n, offline, subscription } => {
|
||||
let mut cfg = RunConfig::new(&path);
|
||||
cfg.max_agents = max_agents;
|
||||
cfg.vote_n = vote_n;
|
||||
cfg.offline = offline;
|
||||
cfg.subscription = subscription;
|
||||
if !models.is_empty() {
|
||||
cfg.models = models;
|
||||
}
|
||||
let out = run_engagement(&base, cfg, false, true).await?;
|
||||
print_findings(&out);
|
||||
}
|
||||
Cmd::Serve { port } => {
|
||||
web::serve(base, port).await?;
|
||||
@@ -125,3 +137,68 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shared engagement runner for CLI `run` / `whitebox`.
|
||||
async fn run_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, whitebox: bool) -> anyhow::Result<RunOutput> {
|
||||
let lib = agents::load(base);
|
||||
let workdir = base.join("runs").join(format!("{}-{}", sanitize(&cfg.target), now_ts()));
|
||||
cfg.workdir = Some(workdir.display().to_string());
|
||||
cfg.rl_path = Some(base.join("data").join("rl_state_rs.json").display().to_string());
|
||||
|
||||
let mcp_config = if mcp && cfg.subscription {
|
||||
match harness::write_mcp_config(&workdir) {
|
||||
Ok(p) => {
|
||||
println!(" [*] Playwright MCP enabled → {}", p.display());
|
||||
Some(p.display().to_string())
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(" [!] MCP config failed: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let refs: Vec<ModelRef> = cfg.models.iter().map(|s| ModelRef::parse(s)).collect();
|
||||
let pool = ModelPool::with_auth(refs, cfg.concurrency, cfg.subscription, mcp_config);
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(256);
|
||||
let printer = tokio::spawn(async move {
|
||||
while let Some(line) = rx.recv().await {
|
||||
println!(" [*] {line}");
|
||||
}
|
||||
});
|
||||
let out = if whitebox {
|
||||
harness::run_whitebox(cfg, &lib, &pool, tx).await
|
||||
} else {
|
||||
harness::run(cfg, &lib, &pool, tx).await
|
||||
};
|
||||
let _ = printer.await;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn print_findings(out: &RunOutput) {
|
||||
println!("\n=== {} validated finding(s) ===", out.findings.len());
|
||||
println!("{}", serde_json::to_string_pretty(&out.findings).unwrap_or_default());
|
||||
if !out.artifacts.is_empty() {
|
||||
println!("artifacts: {}", out.artifacts.join(", "));
|
||||
}
|
||||
}
|
||||
|
||||
fn sanitize(s: &str) -> String {
|
||||
let s = s.replace("https://", "").replace("http://", "");
|
||||
let mut o: String = s.chars().map(|c| if c.is_alphanumeric() { c } else { '_' }).collect();
|
||||
o.truncate(50);
|
||||
let o = o.trim_matches('_').to_string();
|
||||
if o.is_empty() {
|
||||
"target".into()
|
||||
} else {
|
||||
o
|
||||
}
|
||||
}
|
||||
|
||||
fn now_ts() -> u64 {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ async fn info(State(st): State<Arc<AppState>>) -> Json<Value> {
|
||||
.collect();
|
||||
Json(json!({
|
||||
"version": "3.4.0",
|
||||
"agents": {"vulns": lib.vulns.len(), "meta": lib.meta.len(), "total": lib.total()},
|
||||
"agents": {"vulns": lib.vulns.len(), "meta": lib.meta.len(), "recon": lib.recon.len(), "code": lib.code.len(), "total": lib.total()},
|
||||
"providers": provs,
|
||||
"cli_backends": harness::installed_cli_backends(),
|
||||
}))
|
||||
@@ -68,6 +68,8 @@ async fn agents_list(State(st): State<Arc<AppState>>) -> Json<Value> {
|
||||
let v: Vec<Value> = lib
|
||||
.vulns
|
||||
.iter()
|
||||
.chain(lib.recon.iter())
|
||||
.chain(lib.code.iter())
|
||||
.chain(lib.meta.iter())
|
||||
.map(|a| json!({"name": a.name, "title": a.title, "cwe": a.cwe, "kind": a.kind}))
|
||||
.collect();
|
||||
@@ -128,6 +130,16 @@ async fn run(State(st): State<Arc<AppState>>, Json(body): Json<Value>) -> Json<V
|
||||
let max_agents = body.get("max_agents").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
|
||||
let offline = body.get("offline").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
let subscription = body.get("subscription").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
let mcp = body.get("mcp").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
let mode = body.get("mode").and_then(|v| v.as_str()).unwrap_or("web").to_string();
|
||||
// Whitebox uses a repo path instead of URLs.
|
||||
if mode == "whitebox" {
|
||||
if let Some(p) = body.get("repo").and_then(|v| v.as_str()) {
|
||||
if !p.trim().is_empty() {
|
||||
targets = vec![p.trim().to_string()];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let lib = agents::load(&base);
|
||||
let refs: Vec<ModelRef> = if models.is_empty() {
|
||||
@@ -135,7 +147,13 @@ async fn run(State(st): State<Arc<AppState>>, Json(body): Json<Value>) -> Json<V
|
||||
} else {
|
||||
models.iter().map(|s| ModelRef::parse(s)).collect()
|
||||
};
|
||||
let pool = ModelPool::with_auth(refs, 8, subscription);
|
||||
let mcp_config = if mcp && subscription {
|
||||
harness::write_mcp_config(&base.join("runs").join("_mcp")).ok().map(|p| p.display().to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let pool = ModelPool::with_auth(refs, 8, subscription, mcp_config);
|
||||
let rl_path = base.join("data").join("rl_state_rs.json").display().to_string();
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(256);
|
||||
let stf = st2.clone();
|
||||
@@ -163,8 +181,14 @@ async fn run(State(st): State<Arc<AppState>>, Json(body): Json<Value>) -> Json<V
|
||||
cfg.max_agents = max_agents;
|
||||
cfg.offline = offline;
|
||||
cfg.subscription = subscription;
|
||||
let _ = tx.send(format!("=== target: {url} ===")).await;
|
||||
let out = harness::run(cfg, &lib, &pool, tx.clone()).await;
|
||||
cfg.rl_path = Some(rl_path.clone());
|
||||
cfg.workdir = Some(base.join("runs").join(format!("{}-{}", slug(url), now_ts())).display().to_string());
|
||||
let _ = tx.send(format!("=== {}: {url} ===", if mode == "whitebox" { "whitebox repo" } else { "target" })).await;
|
||||
let out = if mode == "whitebox" {
|
||||
harness::run_whitebox(cfg, &lib, &pool, tx.clone()).await
|
||||
} else {
|
||||
harness::run(cfg, &lib, &pool, tx.clone()).await
|
||||
};
|
||||
all_findings.extend(out.findings);
|
||||
all_ran.extend(out.agents_ran);
|
||||
}
|
||||
@@ -197,3 +221,16 @@ async fn report_html(Path(id): Path<String>, State(st): State<Arc<AppState>>) ->
|
||||
let g = st.runs.lock().unwrap();
|
||||
Html(g.get(&id).and_then(|r| r.report.clone()).unwrap_or_else(|| "<h1>no report</h1>".into()))
|
||||
}
|
||||
|
||||
fn slug(s: &str) -> String {
|
||||
let s = s.replace("https://", "").replace("http://", "");
|
||||
let mut o: String = s.chars().map(|c| if c.is_alphanumeric() { c } else { '_' }).collect();
|
||||
o.truncate(50);
|
||||
let o = o.trim_matches('_').to_string();
|
||||
if o.is_empty() { "target".into() } else { o }
|
||||
}
|
||||
|
||||
fn now_ts() -> u64 {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user