mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-08-15 14:10:22 +02:00
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:
co-authored by
Claude Opus 4.8
parent
a5badefc29
commit
56d3f0c723
@@ -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(())
|
||||
}
|
||||
@@ -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()))
|
||||
}
|
||||
Reference in New Issue
Block a user