NeuroSploit v3.4.0 — Rust multi-model harness + Axum dashboard

New cargo workspace `neurosploit-rs/` (single `neurosploit` binary):

harness crate:
- models.rs: 11 OpenAI-compatible providers / 31 models (Claude, GPT, Grok,
  NVIDIA NIM, DeepSeek, Mistral, Qwen, Groq, Together, OpenRouter, Ollama)
- pool.rs: ModelPool with bounded concurrency, provider failover, and N-model
  validator voting (the panel doubles as the jury)
- agents.rs: loads the existing agents_md/ library (213 agents)
- pipeline.rs: recon → parallel exploit (semaphore-bounded) → N-model
  adversarial vote → score; streams live progress over a channel
- report.rs: HTML report
- tokio + reqwest(rustls); offline mode runs the pipeline without API keys

app binary:
- clap CLI: serve | run | agents | models  (run supports --model x N, --vote-n,
  --max-agents, --offline)
- axum web dashboard with multi-model panel, live console, findings, agent
  browser, embedded report; single binary serves the SPA (no npm/build)

Verified: cargo build clean; agents/models/offline-run CLI; server endpoints
(/api/info, /api/run lifecycle, /report); dashboard + live run in Playwright.

Docs: README v3.4.0 callout + RELEASE.md notes. target/ gitignored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
CyberSecurityUP
2026-06-21 19:58:43 -03:00
co-authored by Claude Opus 4.8
parent a5badefc29
commit 56d3f0c723
18 changed files with 3268 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
//! NeuroSploit v3.4.0 — single binary: `serve` (web dashboard) or `run` (CLI).
mod web;
use clap::{Parser, Subcommand};
use harness::{agents, models::ModelRef, pool::ModelPool, report, types::RunConfig};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "neurosploit", version, about = "NeuroSploit v3.4.0 — multi-model autonomous pentest harness")]
struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
/// Start the web dashboard.
Serve {
#[arg(long, default_value_t = 8788)]
port: u16,
},
/// Run an engagement from the CLI.
Run {
url: String,
/// Models as provider:model (repeatable). First is primary; rest fail over + vote.
#[arg(long = "model")]
models: Vec<String>,
#[arg(long, default_value_t = 0)]
max_agents: usize,
#[arg(long, default_value_t = 3)]
vote_n: usize,
/// Exercise the pipeline without calling any model API.
#[arg(long)]
offline: bool,
},
/// Show agent library counts.
Agents,
/// List providers and models.
Models,
}
/// Locate the repo root that holds `agents_md/` (walk up from CWD, then fall
/// back to the crate's compile-time location).
fn find_base() -> PathBuf {
if let Ok(b) = std::env::var("NEUROSPLOIT_BASE") {
return PathBuf::from(b);
}
if let Ok(cwd) = std::env::current_dir() {
let mut dir = cwd.as_path();
for _ in 0..6 {
if dir.join("agents_md").is_dir() {
return dir.to_path_buf();
}
match dir.parent() {
Some(p) => dir = p,
None => break,
}
}
}
// crate is at <root>/neurosploit-rs/app → root is two levels up
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("."))
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
let base = find_base();
match cli.cmd {
Cmd::Agents => {
let lib = agents::load(&base);
println!("{{\"vulns\":{},\"meta\":{},\"total\":{}}}", lib.vulns.len(), lib.meta.len(), lib.total());
}
Cmd::Models => {
for p in harness::providers() {
println!("{:<4} {:<14} {} models [{}]", p.kind, p.key, p.models.len(), p.label);
for m in &p.models {
println!(" {}:{}", p.key, m);
}
}
}
Cmd::Run { url, models, max_agents, vote_n, offline } => {
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.offline = offline;
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::new(refs, cfg.concurrency);
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());
}
Cmd::Serve { port } => {
web::serve(base, port).await?;
}
}
Ok(())
}
+196
View File
@@ -0,0 +1,196 @@
//! Axum web dashboard for the v3.4.0 harness.
use axum::{
extract::{Path, State},
response::Html,
routing::{get, post},
Json, Router,
};
use harness::{agents, models::ModelRef, pool::ModelPool, report, types::RunConfig};
use serde_json::{json, Value};
use std::{
collections::HashMap,
path::PathBuf,
sync::{Arc, Mutex},
};
struct RunState {
log: Vec<String>,
done: bool,
result: Option<Value>,
report: Option<String>,
}
pub struct AppState {
base: PathBuf,
runs: Mutex<HashMap<String, RunState>>,
}
pub async fn serve(base: PathBuf, port: u16) -> anyhow::Result<()> {
let state = Arc::new(AppState { base, runs: Mutex::new(HashMap::new()) });
let app = Router::new()
.route("/", get(index))
.route("/api/info", get(info))
.route("/api/agents", get(agents_list))
.route("/api/models", get(models_list))
.route("/api/run", post(run))
.route("/api/status/:id", get(status))
.route("/report/:id", get(report_html))
.with_state(state);
let addr = format!("127.0.0.1:{port}");
println!("NeuroSploit v3.4.0 dashboard → http://{addr}");
let listener = tokio::net::TcpListener::bind(&addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
async fn index() -> Html<&'static str> {
Html(include_str!("../web/index.html"))
}
async fn info(State(st): State<Arc<AppState>>) -> Json<Value> {
let lib = agents::load(&st.base);
let provs: Vec<Value> = harness::providers()
.iter()
.map(|p| json!({"key": p.key, "label": p.label, "kind": p.kind, "models": p.models}))
.collect();
Json(json!({
"version": "3.4.0",
"agents": {"vulns": lib.vulns.len(), "meta": lib.meta.len(), "total": lib.total()},
"providers": provs,
}))
}
async fn agents_list(State(st): State<Arc<AppState>>) -> Json<Value> {
let lib = agents::load(&st.base);
let v: Vec<Value> = lib
.vulns
.iter()
.chain(lib.meta.iter())
.map(|a| json!({"name": a.name, "title": a.title, "cwe": a.cwe, "kind": a.kind}))
.collect();
Json(json!({ "agents": v }))
}
async fn models_list() -> Json<Value> {
let provs: Vec<Value> = harness::providers()
.iter()
.map(|p| json!({"key": p.key, "label": p.label, "kind": p.kind, "models": p.models}))
.collect();
Json(json!({ "providers": provs }))
}
fn norm(u: &str) -> String {
if u.starts_with("http") {
u.to_string()
} else {
format!("https://{u}")
}
}
async fn run(State(st): State<Arc<AppState>>, Json(body): Json<Value>) -> Json<Value> {
let id = uuid::Uuid::new_v4().to_string();
st.runs
.lock()
.unwrap()
.insert(id.clone(), RunState { log: vec![], done: false, result: None, report: None });
let st2 = st.clone();
let id2 = id.clone();
tokio::spawn(async move {
let base = st2.base.clone();
let mut targets: Vec<String> = Vec::new();
if let Some(arr) = body.get("targets").and_then(|v| v.as_array()) {
for t in arr {
if let Some(s) = t.as_str() {
if !s.trim().is_empty() {
targets.push(norm(s.trim()));
}
}
}
}
if targets.is_empty() {
if let Some(u) = body.get("url").and_then(|v| v.as_str()) {
if !u.trim().is_empty() {
targets.push(norm(u.trim()));
}
}
}
let models: Vec<String> = body
.get("models")
.and_then(|v| v.as_array())
.map(|a| a.iter().filter_map(|x| x.as_str().map(|s| s.to_string())).collect())
.unwrap_or_default();
let vote_n = body.get("vote_n").and_then(|v| v.as_u64()).unwrap_or(3) as usize;
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 lib = agents::load(&base);
let refs: Vec<ModelRef> = if models.is_empty() {
vec![ModelRef::parse("anthropic:claude-opus-4-8")]
} else {
models.iter().map(|s| ModelRef::parse(s)).collect()
};
let pool = ModelPool::new(refs, 8);
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(256);
let stf = st2.clone();
let idf = id2.clone();
let fwd = tokio::spawn(async move {
while let Some(line) = rx.recv().await {
if let Ok(mut g) = stf.runs.lock() {
if let Some(r) = g.get_mut(&idf) {
r.log.push(line);
}
}
}
});
let mut all_findings = Vec::new();
let mut all_ran = Vec::new();
for url in &targets {
let mut cfg = RunConfig::new(url);
cfg.models = if models.is_empty() {
vec!["anthropic:claude-opus-4-8".into()]
} else {
models.clone()
};
cfg.vote_n = vote_n;
cfg.max_agents = max_agents;
cfg.offline = offline;
let _ = tx.send(format!("=== target: {url} ===")).await;
let out = harness::run(cfg, &lib, &pool, tx.clone()).await;
all_findings.extend(out.findings);
all_ran.extend(out.agents_ran);
}
drop(tx);
let _ = fwd.await;
let report_html = report::html(targets.first().map(|s| s.as_str()).unwrap_or(""), &all_findings);
let result = json!({"findings": all_findings, "agents_ran": all_ran, "targets": targets});
if let Ok(mut g) = st2.runs.lock() {
if let Some(r) = g.get_mut(&id2) {
r.result = Some(result);
r.report = Some(report_html);
r.done = true;
}
}
});
Json(json!({ "run_id": id }))
}
async fn status(Path(id): Path<String>, State(st): State<Arc<AppState>>) -> Json<Value> {
let g = st.runs.lock().unwrap();
match g.get(&id) {
Some(r) => Json(json!({"log": r.log, "done": r.done, "result": r.result, "has_report": r.report.is_some()})),
None => Json(json!({"error": "unknown run"})),
}
}
async fn report_html(Path(id): Path<String>, State(st): State<Arc<AppState>>) -> Html<String> {
let g = st.runs.lock().unwrap();
Html(g.get(&id).and_then(|r| r.report.clone()).unwrap_or_else(|| "<h1>no report</h1>".into()))
}