Merge NeuroSploit v3.4.0 — Rust multi-model harness into main

This commit is contained in:
CyberSecurityUP
2026-06-21 19:59:33 -03:00
18 changed files with 3268 additions and 0 deletions
+3
View File
@@ -91,3 +91,6 @@ logs/webgui.log
# generated reports
reports/report.*
reports/*.pdf
# Rust build artifacts (v3.4.0)
neurosploit-rs/target/
+15
View File
@@ -19,6 +19,21 @@ and a **reinforcement-learning** loop that gets smarter every run.
> The previous Python orchestration now lives in [`legacy/`](legacy/README.md).
> **🦀 v3.4.0 — Rust multi-model harness.** A new high-performance harness lives
> in [`neurosploit-rs/`](neurosploit-rs/): a single Rust binary (`tokio` + `axum`)
> that drives a **pool of LLM models** with concurrency, **provider failover**,
> and **N-model validator voting** (N models must agree a finding is real before
> it counts). It serves its own solid web dashboard. Build & run:
> ```bash
> cd neurosploit-rs && cargo build --release
> ./target/release/neurosploit serve # web dashboard → :8788
> ./target/release/neurosploit run https://target.example --model anthropic:claude-opus-4-8 --model openai:gpt-5.1
> ./target/release/neurosploit run https://t.example --offline # pipeline self-test, no API keys
> ```
> 11 OpenAI-compatible providers / 31 models (Claude, GPT, Grok, NVIDIA NIM,
> DeepSeek, Mistral, Qwen, Groq, Together, OpenRouter, Ollama). Reads the same
> `agents_md/` library (213 agents).
---
## Why this architecture
+46
View File
@@ -1,3 +1,49 @@
# NeuroSploit v3.4.0 — Release Notes
**Release Date:** June 2026
**Codename:** Rust Multi-Model Harness
**License:** MIT
---
## TL;DR
A new **Rust harness** (`neurosploit-rs/`) re-implements the autonomous runtime
as a single, fast binary built on `tokio` + `axum`. It drives a **pool of LLM
models** with concurrency limits, **provider failover**, and **N-model validator
voting** — multiple models must independently agree a finding is real before it
is reported — then serves its own solid web dashboard. It reuses the existing
`agents_md/` library (213 agents) unchanged.
## Highlights
- **`neurosploit-rs/` cargo workspace**: `harness` lib crate + `neurosploit`
binary. `cargo build --release` → one static-ish binary.
- **Multi-model pool** (`pool.rs`): bounded concurrency + automatic **failover**
across providers; the same panel is reused as the **validator voting** jury.
- **Pipeline** (`pipeline.rs`): recon → parallel agent exploitation (semaphore
bounded) → **N-model adversarial vote** → score → report. Streams live
progress over a channel.
- **11 providers / 31 models** (`models.rs`), all OpenAI-compatible: Anthropic,
OpenAI, xAI, NVIDIA NIM, DeepSeek, Mistral, Qwen, Groq, Together, OpenRouter,
Ollama. Models like **Qwen / DeepSeek / Llama** usable directly.
- **Axum web dashboard** (`app/`): multi-model selection panel, live execution
console, findings, agent browser, embedded HTML report. Single binary serves
the SPA — no npm/build.
- **CLI**: `neurosploit serve | run <url> | agents | models`, plus `--offline`
mode to exercise the full pipeline without any API keys.
## Usage
```bash
cd neurosploit-rs && cargo build --release
./target/release/neurosploit serve # → http://127.0.0.1:8788
./target/release/neurosploit run https://t.example \
--model anthropic:claude-opus-4-8 --model openai:gpt-5.1 --vote-n 3
```
---
# NeuroSploit v3.3.0 — Release Notes
**Release Date:** June 2026
+2019
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
[workspace]
members = ["crates/harness", "app"]
resolver = "2"
[workspace.package]
version = "3.4.0"
edition = "2021"
license = "MIT"
repository = "https://github.com/JoasASantos/NeuroSploit"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
anyhow = "1"
futures = "0.3"
[profile.release]
opt-level = 2
lto = "thin"
+21
View File
@@ -0,0 +1,21 @@
[package]
name = "neurosploit"
version.workspace = true
edition.workspace = true
license.workspace = true
[[bin]]
name = "neurosploit"
path = "src/main.rs"
[dependencies]
harness = { package = "neurosploit-harness", path = "../crates/harness" }
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
anyhow.workspace = true
futures.workspace = true
axum = { version = "0.7", features = ["ws"] }
tower-http = { version = "0.5", features = ["cors"] }
clap = { version = "4", features = ["derive"] }
uuid = { version = "1", features = ["v4"] }
+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()))
}
+170
View File
@@ -0,0 +1,170 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>NeuroSploit v3.4.0</title>
<style>
:root{--bg:#080910;--bg2:#0d0f17;--panel:#13151f;--panel2:#1a1d29;--line:#252938;--text:#e7e9ee;
--muted:#878da1;--accent:#8b5cf6;--accent2:#a855f7;--cy:#22d3ee;--ok:#34d399;--warn:#fbbf24;--crit:#f87171;--high:#fb923c}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--text);font:14px/1.55 ui-sans-serif,system-ui,Segoe UI,Roboto,sans-serif;
display:grid;grid-template-columns:228px 1fr;min-height:100vh}
.side{background:linear-gradient(180deg,var(--bg2),#0a0b12);border-right:1px solid var(--line);padding:20px 14px;
display:flex;flex-direction:column;gap:4px;position:sticky;top:0;height:100vh}
.brand{display:flex;align-items:center;gap:10px;margin:2px 6px 20px}
.logo{width:34px;height:34px;border-radius:9px;background:linear-gradient(135deg,var(--accent),var(--accent2));
display:grid;place-items:center;font-weight:800;color:#fff;box-shadow:0 6px 22px rgba(139,92,246,.4)}
.brand b{font-size:15px}.brand span{color:var(--muted);font-size:11px;display:block;margin-top:-2px}
.nav{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:9px;color:var(--muted);cursor:pointer;font-size:13.5px}
.nav:hover{background:var(--panel);color:var(--text)}
.nav.on{background:linear-gradient(135deg,rgba(139,92,246,.22),rgba(168,85,247,.12));color:#fff;box-shadow:inset 0 0 0 1px rgba(139,92,246,.35)}
.sf{margin-top:auto;font-size:11px;color:var(--muted);padding:10px 8px;border-top:1px solid var(--line)}
.badge-rs{display:inline-block;font-size:9px;font-weight:700;color:#fff;background:#dea584;border-radius:4px;padding:1px 5px;margin-left:6px;vertical-align:middle}
main{padding:26px 32px;max-width:1060px}
h1{font-size:20px;margin:0}.sub{color:var(--muted);font-size:12.5px}
.head{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:18px}
.view{display:none}.view.on{display:block}
.card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:20px;margin-bottom:18px}
label{display:block;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin:0 0 6px}
.field{margin-bottom:15px}
input,select,textarea{width:100%;background:var(--panel2);border:1px solid var(--line);color:var(--text);border-radius:9px;
padding:10px 11px;font-size:13.5px;outline:none;font-family:inherit}
textarea{resize:vertical;min-height:70px;font-family:ui-monospace,Menlo,monospace;font-size:12.5px}
input:focus,select:focus,textarea:focus{border-color:var(--accent)}
.row{display:flex;gap:12px}.row>*{flex:1}
.mpanel{max-height:200px;overflow:auto;border:1px solid var(--line);border-radius:9px;padding:6px}
.mopt{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:7px;font-size:12.5px;cursor:pointer}
.mopt:hover{background:var(--panel2)}.mopt input{accent-color:var(--accent);width:15px;height:15px}
.kind{font-size:9px;padding:1px 6px;border-radius:4px;background:var(--line);color:var(--muted);text-transform:uppercase}
.kind.cli{background:rgba(139,92,246,.2);color:#c4b5fd}.kind.meta{background:rgba(34,211,238,.14);color:var(--cy)}
.toggles{display:flex;gap:10px;flex-wrap:wrap;margin:6px 0 14px}
.toggle{display:flex;align-items:center;gap:8px;background:var(--panel2);border:1px solid var(--line);border-radius:9px;padding:9px 12px;cursor:pointer;font-size:12.5px}
.toggle.on{border-color:var(--accent)}.toggle input{accent-color:var(--accent)}
.btns{display:flex;gap:10px}
button{border:0;border-radius:10px;padding:11px 16px;font-size:13.5px;font-weight:600;cursor:pointer}
.run{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;flex:1}.run:hover{filter:brightness(1.08)}
.ghost{background:var(--panel2);color:var(--text);border:1px solid var(--line)}.ghost:hover{border-color:var(--accent)}
button:disabled{opacity:.5;cursor:not-allowed}
.pill{display:inline-flex;gap:5px;background:var(--panel2);border:1px solid var(--line);border-radius:999px;padding:4px 11px;font-size:11.5px;color:var(--muted);margin-right:7px}
.pill b{color:var(--text)}
.term{background:#06070c;border:1px solid var(--line);border-radius:11px;padding:13px 15px;margin-top:14px;
font:12px/1.6 ui-monospace,Menlo,monospace;max-height:260px;overflow:auto;white-space:pre-wrap;color:#cbd3e6;display:none}
.term .h{color:var(--accent2)}.term .ok{color:var(--ok)}.term .e{color:var(--crit)}.term .v{color:var(--cy)}
.sevrow{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0;display:none}
.sev{border-radius:8px;padding:5px 11px;font-size:12px;font-weight:700}
.sev.Critical{background:rgba(248,113,113,.16);color:var(--crit)}.sev.High{background:rgba(251,146,60,.16);color:var(--high)}
.sev.Medium{background:rgba(251,191,36,.15);color:var(--warn)}.sev.Low{background:rgba(34,211,238,.14);color:var(--cy)}
.sev.none{background:rgba(52,211,153,.13);color:var(--ok)}
.find{border:1px solid var(--line);border-radius:11px;padding:14px;margin:10px 0;background:var(--panel2)}
.find h4{margin:0 0 5px;font-size:14px}.find .m{color:var(--muted);font-size:12px}
.find pre{background:#06070c;border-radius:7px;padding:9px;font-size:11.5px;overflow:auto;margin:7px 0}
.alist{max-height:430px;overflow:auto;border:1px solid var(--line);border-radius:10px}
.arow{display:flex;gap:10px;padding:9px 13px;border-bottom:1px solid var(--line);font-size:13px;align-items:center}
.arow:last-child{border:0}.arow code{color:var(--accent2)}.arow .t{color:var(--muted);margin-left:auto;font-size:11.5px}
.muted{color:var(--muted);font-size:12.5px}a{color:var(--accent2)}
.dl{display:inline-flex;gap:7px;background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:8px 13px;text-decoration:none;color:var(--text);font-size:12.5px}
.dl:hover{border-color:var(--accent)}iframe{width:100%;height:520px;border:1px solid var(--line);border-radius:10px;background:#fff;margin-top:12px}
</style>
</head>
<body>
<aside class="side">
<div class="brand"><div class="logo">N</div><div><b>NeuroSploit<span class="badge-rs">RUST</span></b><span>v3.4.0 · Multi-Model Harness</span></div></div>
<div class="nav on" data-v="run">▶ Run</div>
<div class="nav" data-v="agents">⛓ Agents</div>
<div class="nav" data-v="models">🧠 Models</div>
<div class="nav" data-v="reports">📄 Report</div>
<div class="sf" id="sf">loading…</div>
</aside>
<main>
<section class="view on" id="v-run">
<div class="head"><div><h1>Run engagement</h1><div class="sub">Parallel agents · provider failover · N-model validator voting.</div></div></div>
<div class="card">
<div class="field"><label>Targets (one URL per line)</label><textarea id="targets" placeholder="https://target-one.example&#10;https://target-two.example"></textarea></div>
<div class="field"><label>Model panel (1st = primary · others fail over &amp; vote)</label><div class="mpanel" id="mpanel"></div></div>
<div class="row">
<div class="field"><label>Validator votes (N)</label><input id="voten" type="number" value="3" min="1" max="9"/></div>
<div class="field"><label>Max agents (0 = all)</label><input id="maxa" type="number" value="8" min="0"/></div>
</div>
<div class="toggles"><label class="toggle on" id="t-off"><input type="checkbox" id="offline" checked/> Offline (no API calls — pipeline self-test)</label></div>
<div class="btns"><button class="run" id="go">▶ Run harness</button></div>
<div class="term" id="term"></div>
<div class="sevrow" id="sevrow"></div>
<div id="findings"></div>
</div>
</section>
<section class="view" id="v-agents">
<div class="head"><div><h1>Agent library</h1><div class="sub" id="agentsub"></div></div></div>
<div class="card"><div class="field"><input id="asearch" placeholder="🔎 filter agents"/></div><div class="alist" id="alist"></div></div>
</section>
<section class="view" id="v-models">
<div class="head"><div><h1>Models</h1><div class="sub">OpenAI-compatible providers — CLI &amp; API.</div></div></div>
<div class="card" id="modelcard"></div>
</section>
<section class="view" id="v-reports">
<div class="head"><div><h1>Report</h1><div class="sub">Last engagement.</div></div></div>
<div class="card" id="reportcard"><span class="muted">Run an engagement to generate a report.</span></div>
</section>
</main>
<script>
const $=s=>document.querySelector(s),$$=s=>[...document.querySelectorAll(s)];
let INFO=null,AGENTS=[],lastRun=null;
$$('.nav').forEach(n=>n.onclick=()=>{$$('.nav').forEach(x=>x.classList.remove('on'));n.classList.add('on');
$$('.view').forEach(v=>v.classList.remove('on'));$('#v-'+n.dataset.v).classList.add('on');});
$('#t-off').querySelector('input').onchange=e=>$('#t-off').classList.toggle('on',e.target.checked);
async function init(){
INFO=await (await fetch('/api/info')).json();
$('#sf').textContent=`${INFO.agents.total} agents · ${INFO.providers.length} providers`;
$('#agentsub').textContent=`${INFO.agents.vulns} vuln specialists · ${INFO.agents.meta} meta-agents`;
// model panel
let mh='',first=true;
INFO.providers.forEach(p=>p.models.forEach(m=>{const id=p.key+':'+m;
mh+=`<label class="mopt"><input type="checkbox" value="${id}" ${first?'checked':''}/> <span class="kind ${p.kind}">${p.kind}</span> <code>${id}</code></label>`;first=false;}));
$('#mpanel').innerHTML=mh;
// models tab
$('#modelcard').innerHTML=INFO.providers.map(p=>`<div style="margin-bottom:12px"><b>${p.label}</b> <span class="kind ${p.kind}">${p.kind}</span>
<div class="muted" style="margin-top:4px">${p.models.map(m=>'<code>'+m+'</code>').join(' · ')}</div></div>`).join('');
AGENTS=(await (await fetch('/api/agents')).json()).agents;renderAgents();
}
function selectedModels(){return $$('#mpanel input:checked').map(i=>i.value);}
function logLine(t){const T=$('#term');T.style.display='block';const d=document.createElement('div');
d.className=t.startsWith('===')?'h':t.startsWith('vote')&&t.includes('CONFIRMED')?'ok':t.includes('failed')||t.startsWith('ERROR')?'e':t.startsWith('recon')||t.startsWith('exploit')?'v':'';
d.textContent=t;T.appendChild(d);T.scrollTop=T.scrollHeight;}
let seen=0;
async function run(){
const targets=$('#targets').value.split('\n').map(s=>s.trim()).filter(Boolean);
if(!targets.length){$('#targets').focus();$('#targets').style.borderColor='var(--crit)';return;}
$('#go').disabled=true;$('#term').innerHTML='';$('#sevrow').style.display='none';$('#findings').innerHTML='';
const body={targets,models:selectedModels(),vote_n:+$('#voten').value,max_agents:+$('#maxa').value,offline:$('#offline').checked};
logLine('Queued '+targets.length+' target(s) · panel: '+(body.models.join(', ')||'default'));
const r=await (await fetch('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})).json();
if(r.error){logLine('ERROR: '+r.error);$('#go').disabled=false;return;}
poll(r.run_id);
}
async function poll(id){
const st=await (await fetch('/api/status/'+id)).json();
(st.log||[]).slice(seen).forEach(logLine);seen=(st.log||[]).length;
if(!st.done){setTimeout(()=>poll(id),600);return;}
seen=0;$('#go').disabled=false;lastRun=id;render(st.result||{});
}
function render(res){
const sr=$('#sevrow');sr.style.display='flex';
const f=res.findings||[],by={};f.forEach(x=>by[x.severity]=(by[x.severity]||0)+1);
sr.innerHTML=f.length?Object.entries(by).map(([k,v])=>`<span class="sev ${k}">${k}: ${v}</span>`).join('')
:`<span class="sev none">✓ complete — ${(res.agents_ran||[]).length} agents ran, 0 validated findings</span>`;
$('#findings').innerHTML=f.map(x=>`<div class="find"><h4><span class="sev ${x.severity}" style="font-size:11px">${x.severity}</span> ${x.title||''}</h4>
<div class="m">${x.agent||''} · ${x.cwe||''} · votes ${x.votes||'-'} · conf ${(x.confidence||0).toFixed(2)} · ${x.endpoint||''}</div>
${x.payload?`<pre>${(x.payload+'').replace(/</g,'&lt;')}</pre>`:''}${x.evidence?`<div class="m">Evidence: ${x.evidence}</div>`:''}</div>`).join('');
const rc=$('#reportcard');
rc.innerHTML=`<a class="dl" href="/report/${lastRun}" target="_blank">⬇ open HTML report</a><iframe src="/report/${lastRun}"></iframe>`;
}
function renderAgents(){const q=$('#asearch').value.toLowerCase();
$('#alist').innerHTML=AGENTS.filter(a=>!q||(a.name+a.title+a.cwe).toLowerCase().includes(q)).slice(0,400)
.map(a=>`<div class="arow"><span class="kind ${a.kind}">${a.kind}</span> <code>${a.name}</code> <span class="t">${(a.title||'').replace(' Agent','')} ${a.cwe?'· '+a.cwe:''}</span></div>`).join('')||'<div class="arow muted">no match</div>';}
$('#asearch').oninput=renderAgents;
$('#go').onclick=run;$('#targets').oninput=()=>$('#targets').style.borderColor='';
init();
</script>
</body>
</html>
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "neurosploit-harness"
version.workspace = true
edition.workspace = true
license.workspace = true
[lib]
name = "harness"
path = "src/lib.rs"
[dependencies]
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
reqwest.workspace = true
anyhow.workspace = true
futures.workspace = true
walkdir = "2"
regex = "1"
@@ -0,0 +1,77 @@
use regex::Regex;
use serde::Serialize;
use std::path::Path;
use walkdir::WalkDir;
/// One markdown specialist/meta agent.
#[derive(Clone, Debug, Serialize)]
pub struct Agent {
pub name: String,
pub title: String,
pub cwe: String,
pub kind: String, // "vuln" | "meta"
#[serde(skip)]
pub system: String,
#[serde(skip)]
pub user: String,
}
/// The loaded `agents_md/` library.
#[derive(Default)]
pub struct Library {
pub vulns: Vec<Agent>,
pub meta: Vec<Agent>,
}
impl Library {
pub fn total(&self) -> usize {
self.vulns.len() + self.meta.len()
}
}
/// Load `<base>/agents_md/{vulns,meta}/*.md`.
pub fn load(base: &Path) -> Library {
let root = base.join("agents_md");
Library {
vulns: load_dir(&root.join("vulns"), "vuln"),
meta: load_dir(&root.join("meta"), "meta"),
}
}
fn load_dir(dir: &Path, kind: &str) -> Vec<Agent> {
let title_re = Regex::new(r"(?m)^#\s+(.+?)\s*$").unwrap();
let cwe_re = Regex::new(r"CWE-\d+").unwrap();
let user_re = Regex::new(r"(?s)##\s*User Prompt\s*\n(.*?)(?:\n##\s|\z)").unwrap();
let sys_re = Regex::new(r"(?s)##\s*System Prompt\s*\n(.*?)(?:\n##\s|\z)").unwrap();
let mut out = Vec::new();
if !dir.is_dir() {
return out;
}
for entry in WalkDir::new(dir).max_depth(1).into_iter().flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("md") {
continue;
}
let text = std::fs::read_to_string(path).unwrap_or_default();
let name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("").to_string();
let title = title_re
.captures(&text)
.and_then(|c| c.get(1))
.map(|m| m.as_str().trim().to_string())
.unwrap_or_else(|| name.clone());
let cwe = cwe_re.find(&text).map(|m| m.as_str().to_string()).unwrap_or_default();
let user = user_re
.captures(&text)
.and_then(|c| c.get(1))
.map(|m| m.as_str().trim().to_string())
.unwrap_or_default();
let system = sys_re
.captures(&text)
.and_then(|c| c.get(1))
.map(|m| m.as_str().trim().to_string())
.unwrap_or_default();
out.push(Agent { name, title, cwe, kind: kind.to_string(), system, user });
}
out.sort_by(|a, b| a.name.cmp(&b.name));
out
}
+20
View File
@@ -0,0 +1,20 @@
//! NeuroSploit v3.4.0 harness — a robust multi-model runtime for the
//! markdown-driven autonomous pentest engine.
//!
//! The harness loads the `agents_md/` library, drives a *pool* of LLM models
//! (any OpenAI-compatible provider) with concurrency + provider failover, runs
//! the specialist agents in parallel, then validates every candidate finding by
//! **N-model voting** before scoring and reporting.
pub mod agents;
pub mod models;
pub mod pipeline;
pub mod pool;
pub mod report;
pub mod types;
pub use agents::{Agent, Library};
pub use models::{provider_for, providers, ChatClient, ModelRef, Provider};
pub use pipeline::run;
pub use pool::ModelPool;
pub use types::{Finding, RunConfig};
+134
View File
@@ -0,0 +1,134 @@
use anyhow::{anyhow, Result};
use serde::Serialize;
use std::time::Duration;
/// A model provider exposing an OpenAI-compatible `/chat/completions` endpoint.
#[derive(Clone, Debug, Serialize)]
pub struct Provider {
pub key: &'static str,
pub label: &'static str,
pub base_url: &'static str,
pub env_key: &'static str,
/// "cli" (also drivable by an agentic CLI) | "api"
pub kind: &'static str,
pub models: Vec<&'static str>,
}
/// The full provider registry. Every entry speaks the OpenAI chat schema
/// (Anthropic, xAI, NVIDIA NIM, DeepSeek, Mistral, Qwen, Groq, Together,
/// OpenRouter, Gemini-compat, Ollama).
pub fn providers() -> Vec<Provider> {
vec![
Provider { key: "anthropic", label: "Anthropic Claude", base_url: "https://api.anthropic.com/v1", env_key: "ANTHROPIC_API_KEY", kind: "cli",
models: vec!["claude-opus-4-8", "claude-sonnet-4-6", "claude-haiku-4-5"] },
Provider { key: "openai", label: "OpenAI", base_url: "https://api.openai.com/v1", env_key: "OPENAI_API_KEY", kind: "cli",
models: vec!["gpt-5.1", "o4"] },
Provider { key: "xai", label: "xAI Grok", base_url: "https://api.x.ai/v1", env_key: "XAI_API_KEY", kind: "cli",
models: vec!["grok-4", "grok-4-fast"] },
Provider { key: "nvidia_nim", label: "NVIDIA NIM", base_url: "https://integrate.api.nvidia.com/v1", env_key: "NVIDIA_NIM_API_KEY", kind: "api",
models: vec!["nvidia/llama-3.3-nemotron-super-49b-v1", "deepseek-ai/deepseek-r1", "qwen/qwen2.5-coder-32b-instruct"] },
Provider { key: "deepseek", label: "DeepSeek", base_url: "https://api.deepseek.com/v1", env_key: "DEEPSEEK_API_KEY", kind: "api",
models: vec!["deepseek-reasoner", "deepseek-chat"] },
Provider { key: "mistral", label: "Mistral", base_url: "https://api.mistral.ai/v1", env_key: "MISTRAL_API_KEY", kind: "api",
models: vec!["mistral-large-latest", "codestral-latest"] },
Provider { key: "qwen", label: "Qwen (DashScope)", base_url: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", env_key: "DASHSCOPE_API_KEY", kind: "api",
models: vec!["qwen-max", "qwen2.5-coder-32b-instruct", "qwq-plus"] },
Provider { key: "groq", label: "Groq", base_url: "https://api.groq.com/openai/v1", env_key: "GROQ_API_KEY", kind: "api",
models: vec!["llama-3.3-70b-versatile", "qwen-2.5-coder-32b"] },
Provider { key: "together", label: "Together AI", base_url: "https://api.together.xyz/v1", env_key: "TOGETHER_API_KEY", kind: "api",
models: vec!["Qwen/Qwen2.5-Coder-32B-Instruct", "deepseek-ai/DeepSeek-R1", "meta-llama/Llama-3.3-70B-Instruct-Turbo"] },
Provider { key: "openrouter", label: "OpenRouter", base_url: "https://openrouter.ai/api/v1", env_key: "OPENROUTER_API_KEY", kind: "api",
models: vec!["anthropic/claude-opus-4-8", "qwen/qwen-2.5-coder-32b-instruct", "deepseek/deepseek-r1", "meta-llama/llama-3.3-70b-instruct"] },
Provider { key: "ollama", label: "Ollama (local)", base_url: "http://localhost:11434/v1", env_key: "OLLAMA_API_KEY", kind: "api",
models: vec!["qwen2.5-coder:32b", "qwq:32b", "deepseek-r1:32b", "llama3.3:70b"] },
]
}
pub fn provider_for(key: &str) -> Option<Provider> {
providers().into_iter().find(|p| p.key == key)
}
/// A `provider:model` selection.
#[derive(Clone, Debug)]
pub struct ModelRef {
pub provider: String,
pub model: String,
}
impl ModelRef {
pub fn parse(s: &str) -> ModelRef {
match s.split_once(':') {
Some((p, m)) => ModelRef { provider: p.to_string(), model: m.to_string() },
None => ModelRef { provider: "anthropic".into(), model: s.to_string() },
}
}
pub fn label(&self) -> String {
format!("{}:{}", self.provider, self.model)
}
}
/// OpenAI-compatible chat client shared across the model pool.
#[derive(Clone)]
pub struct ChatClient {
http: reqwest::Client,
}
impl ChatClient {
pub fn new() -> Self {
let http = reqwest::Client::builder()
.timeout(Duration::from_secs(120))
.build()
.unwrap_or_else(|_| reqwest::Client::new());
ChatClient { http }
}
/// One chat completion. Errors (missing key, network, non-2xx) propagate so
/// the pool can fail over to the next candidate model.
pub async fn chat(&self, m: &ModelRef, system: &str, user: &str) -> Result<String> {
let p = provider_for(&m.provider)
.ok_or_else(|| anyhow!("unknown provider '{}'", m.provider))?;
let key = std::env::var(p.env_key).unwrap_or_default();
if key.is_empty() && p.key != "ollama" {
return Err(anyhow!("no API key ({}) for provider '{}'", p.env_key, p.key));
}
let url = format!("{}/chat/completions", p.base_url.trim_end_matches('/'));
let body = serde_json::json!({
"model": m.model,
"max_tokens": 4096,
"temperature": 0.2,
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": user}
]
});
let mut req = self.http.post(&url).json(&body);
if !key.is_empty() {
req = req.bearer_auth(&key);
}
let resp = req.send().await?;
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
if !status.is_success() {
return Err(anyhow!("{} returned {}: {}", p.key, status, truncate(&text, 200)));
}
let v: serde_json::Value = serde_json::from_str(&text)?;
let content = v["choices"][0]["message"]["content"]
.as_str()
.ok_or_else(|| anyhow!("no content in response"))?;
Ok(content.to_string())
}
}
impl Default for ChatClient {
fn default() -> Self {
Self::new()
}
}
fn truncate(s: &str, n: usize) -> String {
if s.len() <= n {
s.to_string()
} else {
format!("{}", &s[..n])
}
}
@@ -0,0 +1,161 @@
use crate::agents::{Agent, Library};
use crate::pool::ModelPool;
use crate::types::{Finding, RunConfig};
use futures::stream::{self, StreamExt};
use serde::Serialize;
use tokio::sync::mpsc::Sender;
/// Result of an engagement run.
#[derive(Default, Serialize)]
pub struct RunOutput {
pub findings: Vec<Finding>,
pub agents_ran: Vec<String>,
pub candidates: usize,
}
const RECON_SYS: &str = "You are a web recon specialist. Map the target's attack surface and reply with a compact JSON object (tech, endpoints, auth, apis, ai_features). No prose.";
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.";
/// Run the full harness pipeline, streaming human-readable progress over `tx`.
pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<String>) -> RunOutput {
let _ = tx
.send(format!(
"Loaded {} agents ({} vuln / {} meta) · models: {} · vote_n={} · concurrency={}",
lib.total(),
lib.vulns.len(),
lib.meta.len(),
pool.candidates.iter().map(|m| m.label()).collect::<Vec<_>>().join(", "),
cfg.vote_n,
cfg.concurrency,
))
.await;
// ---- 1. Recon -------------------------------------------------------
let recon = if cfg.offline {
let _ = tx.send("recon: offline mode — skipping model calls".into()).await;
"{}".to_string()
} else {
match pool.complete(RECON_SYS, &format!("Target: {}", cfg.target)).await {
Ok((m, t)) => {
let _ = tx.send(format!("recon complete via {}", m.label())).await;
t
}
Err(e) => {
let _ = tx.send(format!("recon failed ({e}) — continuing with empty recon")).await;
"{}".to_string()
}
}
};
// ---- 2. Select agents ----------------------------------------------
let cap = if cfg.max_agents > 0 { cfg.max_agents } else { lib.vulns.len() };
let selected: Vec<Agent> = lib.vulns.iter().take(cap).cloned().collect();
let _ = tx.send(format!("selected {} specialist agents", selected.len())).await;
if cfg.offline {
let _ = tx.send("offline: no exploitation performed (provide API keys to run live)".into()).await;
return RunOutput {
findings: vec![],
agents_ran: selected.iter().map(|a| a.name.clone()).collect(),
candidates: 0,
};
}
// ---- 3. Exploit (parallel, bounded by the pool semaphore) ----------
let target = cfg.target.clone();
let candidates: Vec<Finding> = stream::iter(selected.iter().cloned())
.map(|ag| {
let target = target.clone();
let recon = recon.clone();
let txc = tx.clone();
async move {
let user = format!(
"{}\n\nReply ONLY with a JSON array of confirmed findings (may be empty []). \
Each item: {{id,title,severity,cwe,endpoint,payload,evidence,impact,remediation,confidence}}.",
ag.user.replace("{target}", &target).replace("{recon_json}", &recon)
);
match pool.complete(&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;
f
}
Err(e) => {
let _ = txc.send(format!("exploit {} failed: {e}", ag.name)).await;
vec![]
}
}
}
})
.buffer_unordered(cfg.concurrency)
.collect::<Vec<Vec<Finding>>>()
.await
.into_iter()
.flatten()
.collect();
let _ = tx.send(format!("{} candidate finding(s) — validating by {}-model vote", candidates.len(), cfg.vote_n)).await;
// ---- 4. Validate by N-model voting ---------------------------------
let vote_n = cfg.vote_n;
let validated: Vec<Finding> = stream::iter(candidates.into_iter())
.map(|mut f| {
let txc = tx.clone();
async move {
let q = format!(
"Finding: {} | severity {} | {} | endpoint {} | payload {} | evidence {}",
f.title, f.severity, f.cwe, f.endpoint, f.payload, f.evidence
);
let (yes, total) = pool.vote(VOTE_SYS, &q, vote_n).await;
f.validated = total > 0 && yes * 2 >= total;
f.votes = format!("{yes}/{total}");
if f.confidence == 0.0 && total > 0 {
f.confidence = yes as f64 / total as f64;
}
let _ = txc
.send(format!("vote {}{} ({})", f.title, if f.validated { "CONFIRMED" } else { "rejected" }, f.votes))
.await;
f
}
})
.buffer_unordered(cfg.concurrency)
.collect::<Vec<Finding>>()
.await;
let candidates = validated.len();
let findings: Vec<Finding> = validated.into_iter().filter(|f| f.validated).collect();
let _ = tx.send(format!("{} validated finding(s)", findings.len())).await;
RunOutput {
findings,
agents_ran: selected.iter().map(|a| a.name.clone()).collect(),
candidates,
}
}
/// Pull a JSON array (or object) of findings out of a model's reply.
fn extract_findings(text: &str, agent: &str) -> Vec<Finding> {
let slice = match (text.find('['), text.rfind(']')) {
(Some(a), Some(b)) if b > a => &text[a..=b],
_ => match (text.find('{'), text.rfind('}')) {
(Some(a), Some(b)) if b > a => &text[a..=b],
_ => return vec![],
},
};
let mut out: Vec<Finding> = if let Ok(v) = serde_json::from_str::<Vec<Finding>>(slice) {
v
} else if let Ok(one) = serde_json::from_str::<Finding>(slice) {
vec![one]
} else {
return vec![];
};
for f in out.iter_mut() {
f.agent = agent.to_string();
if f.id.is_empty() {
f.id = format!("{}-{}", agent, &f.title.chars().take(12).collect::<String>());
}
}
out
}
+68
View File
@@ -0,0 +1,68 @@
use crate::models::{ChatClient, ModelRef};
use anyhow::{anyhow, Result};
use std::sync::Arc;
use tokio::sync::Semaphore;
/// A pool of candidate models with a global concurrency cap and provider
/// failover. The same panel of models is reused for validator voting.
pub struct ModelPool {
client: ChatClient,
sem: Arc<Semaphore>,
pub candidates: Vec<ModelRef>,
}
impl ModelPool {
pub fn new(models: Vec<ModelRef>, concurrency: usize) -> Self {
let concurrency = concurrency.max(1);
ModelPool {
client: ChatClient::new(),
sem: Arc::new(Semaphore::new(concurrency)),
candidates: if models.is_empty() {
vec![ModelRef::parse("anthropic:claude-opus-4-8")]
} else {
models
},
}
}
/// 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)> {
let _permit = self.sem.acquire().await.expect("semaphore closed");
let mut last = anyhow!("no candidate models");
for m in &self.candidates {
match self.client.chat(m, system, user).await {
Ok(text) => return Ok((m.clone(), text)),
Err(e) => last = e,
}
}
Err(last)
}
/// 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.
pub async fn vote(&self, system: &str, user: &str, n: usize) -> (usize, usize) {
let panel: Vec<ModelRef> = self.candidates.iter().take(n.max(1)).cloned().collect();
let mut confirmed = 0usize;
let mut total = 0usize;
for m in &panel {
let _permit = match self.sem.acquire().await {
Ok(p) => p,
Err(_) => break,
};
if let Ok(text) = self.client.chat(m, system, user).await {
total += 1;
let t = text.to_lowercase();
if t.contains("\"verdict\": \"confirmed\"")
|| t.trim_start().starts_with("yes")
|| t.contains("confirmed: true")
|| t.contains("is_real\": true")
{
confirmed += 1;
}
}
}
(confirmed, total)
}
}
@@ -0,0 +1,82 @@
use crate::types::Finding;
fn sev_rank(s: &str) -> u8 {
match s {
"Critical" => 0,
"High" => 1,
"Medium" => 2,
"Low" => 3,
_ => 4,
}
}
fn sev_color(s: &str) -> &'static str {
match s {
"Critical" => "#c0392b",
"High" => "#e67e22",
"Medium" => "#f1c40f",
"Low" => "#3498db",
_ => "#7f8c8d",
}
}
fn esc(s: &str) -> String {
s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;")
}
/// Render an HTML report for the validated findings.
pub fn html(target: &str, findings: &[Finding]) -> String {
let mut sorted = findings.to_vec();
sorted.sort_by_key(|f| sev_rank(&f.severity));
let mut counts: std::collections::BTreeMap<&str, usize> = Default::default();
for f in &sorted {
*counts.entry(f.severity.as_str()).or_default() += 1;
}
let chips: String = if counts.is_empty() {
"<span class=chip style=background:#27ae60>No validated findings</span>".into()
} else {
counts
.iter()
.map(|(s, n)| format!("<span class=chip style=background:{}>{}: {}</span>", sev_color(s), s, n))
.collect()
};
let rows: String = sorted
.iter()
.enumerate()
.map(|(i, f)| {
format!(
"<section class=finding><h3><span class=sev style=background:{}>{}</span> {}. {}</h3>\
<div class=m>{} · {} · CVSS {} · votes {} · conf {:.2}</div>\
<div class=m>Endpoint: {}</div>\
<h4>Payload</h4><pre>{}</pre><h4>Evidence</h4><pre>{}</pre>\
<h4>Impact</h4><p>{}</p><h4>Remediation</h4><p>{}</p></section>",
sev_color(&f.severity), esc(&f.severity), i + 1, esc(&f.title),
esc(&f.agent), esc(&f.cwe), esc(&f.cvss), esc(&f.votes), f.confidence,
esc(&f.endpoint), esc(&f.payload), esc(&f.evidence), esc(&f.impact), esc(&f.remediation),
)
})
.collect();
let body = if rows.is_empty() {
"<p><em>No validated findings were produced for this engagement.</em></p>".to_string()
} else {
rows
};
format!(
"<!DOCTYPE html><html><head><meta charset=utf-8><title>NeuroSploit Report — {t}</title><style>\
body{{font:14px/1.6 -apple-system,Segoe UI,Roboto,sans-serif;color:#1a1a1a;max-width:860px;margin:40px auto;padding:0 24px}}\
h1{{margin:0}}.meta{{color:#666;margin:4px 0 18px}}.chip{{color:#fff;border-radius:999px;padding:4px 12px;margin-right:8px;font-size:13px;font-weight:600}}\
.finding{{border:1px solid #e3e3e3;border-radius:12px;padding:16px 20px;margin:16px 0}}.finding h3{{margin:0 0 8px;font-size:16px}}\
.sev{{color:#fff;border-radius:6px;padding:2px 8px;font-size:12px;margin-right:8px}}.m{{color:#666;font-size:12px}}\
pre{{background:#0f1117;color:#dfe6f3;padding:11px;border-radius:8px;overflow:auto;font-size:12.5px}}\
h4{{margin:12px 0 3px;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:#8b5cf6}}\
.b{{color:#8b5cf6;font-weight:800}}</style></head><body>\
<h1><span class=b>NeuroSploit</span> Penetration Test Report</h1>\
<div class=meta>Target: <b>{t}</b> · v3.4.0 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>",
t = esc(target), chips = chips, n = sorted.len(), body = body,
)
}
@@ -0,0 +1,93 @@
use serde::{Deserialize, Serialize};
/// A validated (or candidate) security finding.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Finding {
pub id: String,
pub agent: String,
pub title: String,
pub severity: String,
#[serde(default)]
pub cwe: String,
#[serde(default)]
pub cvss: String,
#[serde(default)]
pub endpoint: String,
#[serde(default)]
pub payload: String,
#[serde(default)]
pub evidence: String,
#[serde(default)]
pub impact: String,
#[serde(default)]
pub remediation: String,
#[serde(default)]
pub confidence: f64,
#[serde(default)]
pub validated: bool,
/// Per-model vote summary, e.g. "3/4 confirmed".
#[serde(default)]
pub votes: String,
}
impl Default for Finding {
fn default() -> Self {
Finding {
id: String::new(),
agent: String::new(),
title: String::new(),
severity: "Info".into(),
cwe: String::new(),
cvss: String::new(),
endpoint: String::new(),
payload: String::new(),
evidence: String::new(),
impact: String::new(),
remediation: String::new(),
confidence: 0.0,
validated: false,
votes: String::new(),
}
}
}
/// Configuration for a single engagement run.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunConfig {
pub target: String,
/// Model references in `provider:model` form. The first is primary; the
/// rest are failover candidates and also the voting panel.
pub models: Vec<String>,
/// Number of models that cross-check each candidate finding.
#[serde(default = "default_vote")]
pub vote_n: usize,
/// Max concurrent model calls.
#[serde(default = "default_concurrency")]
pub concurrency: usize,
/// Cap on specialist agents to run (0 = all).
#[serde(default)]
pub max_agents: usize,
/// Offline mode: exercise the full pipeline without calling any model API.
#[serde(default)]
pub offline: bool,
}
fn default_vote() -> usize {
3
}
fn default_concurrency() -> usize {
8
}
impl RunConfig {
pub fn new(target: impl Into<String>) -> Self {
RunConfig {
target: target.into(),
models: vec!["anthropic:claude-opus-4-8".into()],
vote_n: 3,
concurrency: 8,
max_agents: 0,
offline: false,
}
}
}
+1
View File
@@ -0,0 +1 @@
<!DOCTYPE html><html><head><meta charset=utf-8><title>NeuroSploit Report — https://demo.testfire.net</title><style>body{font:14px/1.6 -apple-system,Segoe UI,Roboto,sans-serif;color:#1a1a1a;max-width:860px;margin:40px auto;padding:0 24px}h1{margin:0}.meta{color:#666;margin:4px 0 18px}.chip{color:#fff;border-radius:999px;padding:4px 12px;margin-right:8px;font-size:13px;font-weight:600}.finding{border:1px solid #e3e3e3;border-radius:12px;padding:16px 20px;margin:16px 0}.finding h3{margin:0 0 8px;font-size:16px}.sev{color:#fff;border-radius:6px;padding:2px 8px;font-size:12px;margin-right:8px}.m{color:#666;font-size:12px}pre{background:#0f1117;color:#dfe6f3;padding:11px;border-radius:8px;overflow:auto;font-size:12.5px}h4{margin:12px 0 3px;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:#8b5cf6}.b{color:#8b5cf6;font-weight:800}</style></head><body><h1><span class=b>NeuroSploit</span> Penetration Test Report</h1><div class=meta>Target: <b>https://demo.testfire.net</b> · v3.4.0 Rust harness · multi-model validated</div><div><span class=chip style=background:#27ae60>No validated findings</span></div><h2>Findings (0)</h2><p><em>No validated findings were produced for this engagement.</em></p><p class=meta>Authorized testing only. Findings confirmed by multi-model adversarial voting.</p></body></html>