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:
CyberSecurityUP
2026-06-23 11:39:56 -03:00
parent bf56184912
commit 3ca3f269ee
53 changed files with 3684 additions and 209 deletions
+100 -23
View File
@@ -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)
}
+41 -4
View File
@@ -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)
}
+281 -128
View File
@@ -3,171 +3,324 @@
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>NeuroSploit v3.4.0</title>
<title>NeuroSploit</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}
:root{
--bg:#0a0b0f;--bg2:#0e1016;--panel:#13151d;--panel2:#191c26;--panel3:#1e2230;
--line:#242838;--line2:#2e3346;--text:#eef1f6;--muted:#9aa1b4;--dim:#6b7186;
--accent:#7c5cff;--accent2:#a855f7;--cy:#2dd4bf;--ok:#34d399;--warn:#fbbf24;
--crit:#f5556d;--high:#fb923c;--med:#fbbf24;--low:#38bdf8;--info:#94a3b8;
--radius:14px;--shadow:0 16px 50px rgba(0,0,0,.5);
}
*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
body{background:var(--bg);color:var(--text);font:14px/1.55 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
-webkit-font-smoothing:antialiased}
::-webkit-scrollbar{width:9px;height:9px}::-webkit-scrollbar-thumb{background:var(--line2);border-radius:9px}
.app{display:grid;grid-template-columns:240px 1fr;min-height:100vh}
/* ===== sidebar ===== */
.side{background:linear-gradient(180deg,var(--bg2),#08090d);border-right:1px solid var(--line);
padding:22px 16px;display:flex;flex-direction:column;gap:3px;position:sticky;top:0;height:100vh}
.brand{display:flex;align-items:center;gap:11px;margin:2px 6px 24px}
.logo{width:38px;height:38px;border-radius:11px;background:linear-gradient(135deg,var(--accent),var(--accent2));
display:grid;place-items:center;font-weight:800;font-size:19px;color:#fff;box-shadow:0 8px 26px rgba(124,92,255,.45)}
.brand .nm{font-size:16px;font-weight:700;letter-spacing:.2px}
.brand .vr{font-size:10.5px;color:var(--muted);margin-top:-1px}
.badge-rs{font-size:8.5px;font-weight:800;color:#1a1209;background:#e6b673;border-radius:4px;padding:1px 5px;margin-left:6px;vertical-align:middle}
.navlabel{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:var(--dim);margin:16px 10px 6px}
.nav{display:flex;align-items:center;gap:11px;padding:10px 12px;border-radius:10px;color:var(--muted);
cursor:pointer;font-size:13.5px;font-weight:500;transition:.13s}
.nav .ic{width:18px;height:18px;opacity:.8;flex:none}
.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)}
.nav.on{background:linear-gradient(135deg,rgba(124,92,255,.2),rgba(168,85,247,.08));color:#fff;
box-shadow:inset 0 0 0 1px rgba(124,92,255,.32)}
.nav.on .ic{opacity:1}
.side .foot{margin-top:auto;border-top:1px solid var(--line);padding-top:12px;font-size:11px;color:var(--dim)}
.stat{display:flex;justify-content:space-between;padding:3px 8px}.stat b{color:var(--text)}
/* ===== main ===== */
main{padding:0;overflow:hidden}
.topbar{display:flex;align-items:center;justify-content:space-between;padding:18px 32px;border-bottom:1px solid var(--line);
background:rgba(14,16,22,.6);backdrop-filter:blur(8px);position:sticky;top:0;z-index:5}
.topbar h1{font-size:18px;font-weight:650}.topbar .crumb{color:var(--dim);font-size:12.5px;margin-top:2px}
.chipline{display:flex;gap:8px}
.mono{font-family:ui-monospace,"SF Mono",Menlo,monospace}
.wrap{padding:28px 32px;max-width:1180px}
.view{display:none;animation:fade .25s ease}.view.on{display:block}
@keyframes fade{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
.grid2{display:grid;grid-template-columns:1.55fr 1fr;gap:20px;align-items:start}
@media(max-width:980px){.app{grid-template-columns:1fr}.side{position:static;height:auto;flex-direction:row;flex-wrap:wrap}
.grid2{grid-template-columns:1fr}.navlabel{display:none}.side .foot{display:none}}
.card{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:22px;margin-bottom:20px}
.card h2{font-size:14px;font-weight:650;margin-bottom:4px;display:flex;align-items:center;gap:8px}
.card .desc{color:var(--muted);font-size:12.5px;margin-bottom:16px}
label{display:block;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin:0 0 7px;font-weight:600}
.field{margin-bottom:16px}
input,select,textarea{width:100%;background:var(--panel2);border:1px solid var(--line2);color:var(--text);
border-radius:10px;padding:11px 13px;font-size:13.5px;outline:none;font-family:inherit;transition:.14s}
textarea{resize:vertical;min-height:76px;font-family:ui-monospace,Menlo,monospace;font-size:12.5px}
input:focus,select:focus,textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(124,92,255,.13)}
.row{display:flex;gap:13px;flex-wrap:wrap}.row>*{flex:1;min-width:120px}
/* segmented mode switch */
.seg{display:inline-flex;background:var(--panel2);border:1px solid var(--line2);border-radius:10px;padding:3px;gap:3px;margin-bottom:18px}
.seg button{background:transparent;border:0;color:var(--muted);padding:8px 16px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;transition:.13s}
.seg button.on{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff}
.toggles{display:flex;gap:10px;flex-wrap:wrap;margin:4px 0 18px}
.tg{display:flex;align-items:center;gap:9px;background:var(--panel2);border:1px solid var(--line2);border-radius:10px;
padding:10px 13px;cursor:pointer;font-size:12.5px;user-select:none;transition:.13s}
.tg.on{border-color:var(--accent);background:rgba(124,92,255,.1);color:#fff}
.tg input{accent-color:var(--accent);width:15px;height:15px}
.btns{display:flex;gap:11px}
button.act{border:0;border-radius:11px;padding:12px 18px;font-size:14px;font-weight:650;cursor:pointer;transition:.13s}
.primary{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;flex:1;box-shadow:0 8px 22px rgba(124,92,255,.3)}
.primary:hover{filter:brightness(1.09)}
.ghost{background:var(--panel2);color:var(--text);border:1px solid var(--line2)}.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}
.chip{display:inline-flex;align-items:center;gap:6px;background:var(--panel2);border:1px solid var(--line2);
border-radius:999px;padding:4px 11px;font-size:11.5px;color:var(--muted)}
.chip b{color:var(--text)}.dot{width:7px;height:7px;border-radius:50%;background:var(--ok);box-shadow:0 0 8px var(--ok)}
/* model panel */
.mpanel{max-height:230px;overflow:auto;border:1px solid var(--line2);border-radius:10px;padding:5px;background:var(--bg2)}
.mopt{display:flex;align-items:center;gap:9px;padding:7px 9px;border-radius:8px;font-size:12.5px;cursor:pointer}
.mopt:hover{background:var(--panel2)}.mopt input{accent-color:var(--accent)}
.tag{font-size:9px;font-weight:700;padding:2px 6px;border-radius:5px;text-transform:uppercase;letter-spacing:.4px}
.tag.cli{background:rgba(124,92,255,.2);color:#c4b5fd}.tag.api{background:rgba(45,212,191,.15);color:var(--cy)}
.tag.meta{background:rgba(45,212,191,.15);color:var(--cy)}.tag.recon{background:rgba(56,189,248,.16);color:var(--low)}
.tag.code{background:rgba(251,146,60,.16);color:var(--high)}.tag.vuln{background:rgba(245,85,109,.15);color:var(--crit)}
/* console */
.term{background:#070810;border:1px solid var(--line2);border-radius:12px;padding:14px 16px;
font:12px/1.65 ui-monospace,Menlo,monospace;max-height:340px;overflow:auto;white-space:pre-wrap;color:#c3cad8;min-height:120px}
.term .h{color:var(--accent2);font-weight:600}.term .ok{color:var(--ok)}.term .e{color:var(--crit)}
.term .v{color:var(--cy)}.term .s{color:var(--warn)}
.term .empty{color:var(--dim)}
/* findings */
.sevbar{display:flex;gap:9px;flex-wrap:wrap;margin-bottom:14px}
.scount{display:flex;flex-direction:column;align-items:center;background:var(--panel2);border:1px solid var(--line2);
border-radius:10px;padding:9px 16px;min-width:74px}
.scount .n{font-size:20px;font-weight:750}.scount .l{font-size:10px;text-transform:uppercase;letter-spacing:.5px;color:var(--muted)}
.scount.Critical .n{color:var(--crit)}.scount.High .n{color:var(--high)}.scount.Medium .n{color:var(--med)}
.scount.Low .n{color:var(--low)}.scount.Info .n{color:var(--info)}
.find{border:1px solid var(--line2);border-left-width:3px;border-radius:11px;padding:15px 17px;margin:11px 0;background:var(--panel2)}
.find.Critical{border-left-color:var(--crit)}.find.High{border-left-color:var(--high)}.find.Medium{border-left-color:var(--med)}
.find.Low{border-left-color:var(--low)}.find.Info{border-left-color:var(--info)}
.find h4{font-size:14px;margin-bottom:5px;display:flex;align-items:center;gap:9px}
.sev{font-size:10px;font-weight:700;padding:3px 8px;border-radius:6px;text-transform:uppercase}
.sev.Critical{background:rgba(245,85,109,.18);color:var(--crit)}.sev.High{background:rgba(251,146,60,.18);color:var(--high)}
.sev.Medium{background:rgba(251,191,36,.16);color:var(--med)}.sev.Low{background:rgba(56,189,248,.16);color:var(--low)}
.sev.Info{background:rgba(148,163,184,.16);color:var(--info)}
.find .m{color:var(--muted);font-size:11.5px;margin-bottom:6px}
.find pre{background:#070810;border:1px solid var(--line);border-radius:8px;padding:10px;font-size:11.5px;overflow:auto;margin:7px 0}
.empty-state{text-align:center;color:var(--dim);padding:36px 10px;font-size:13px}
/* agents list */
.toolbar{display:flex;gap:10px;margin-bottom:14px;flex-wrap:wrap}.toolbar input{flex:1;min-width:160px}
.fbtn{background:var(--panel2);border:1px solid var(--line2);color:var(--muted);border-radius:8px;padding:8px 13px;font-size:12px;cursor:pointer;font-weight:600}
.fbtn.on{border-color:var(--accent);color:#fff;background:rgba(124,92,255,.12)}
.alist{max-height:560px;overflow:auto;border:1px solid var(--line2);border-radius:11px}
.arow{display:flex;gap:11px;padding:10px 14px;border-bottom:1px solid var(--line);font-size:13px;align-items:center}
.arow:last-child{border:0}.arow:hover{background:var(--panel2)}.arow code{color:var(--accent2);font-size:12.5px}
.arow .t{color:var(--muted);margin-left:auto;font-size:11.5px;text-align:right}
.muted{color:var(--muted)}.dim{color:var(--dim)}a{color:var(--accent2);text-decoration:none}a:hover{text-decoration:underline}
.dl{display:inline-flex;gap:8px;background:var(--panel2);border:1px solid var(--line2);border-radius:9px;padding:9px 14px;margin:0 9px 9px 0;color:var(--text);font-size:12.5px}
.dl:hover{border-color:var(--accent);text-decoration:none}
iframe{width:100%;height:560px;border:1px solid var(--line2);border-radius:11px;background:#fff;margin-top:12px}
.mcard{padding:14px 16px;border:1px solid var(--line2);border-radius:11px;margin-bottom:11px;background:var(--panel2)}
.mcard h3{font-size:13.5px;margin-bottom:7px;display:flex;align-items:center;gap:8px}
.keyrow{display:flex;align-items:center;gap:8px;margin:5px 0;font-size:12px;color:var(--muted)}
.progress{height:3px;background:var(--line);border-radius:3px;overflow:hidden;margin-top:14px;display:none}
.progress.on{display:block}.progress .bar{height:100%;width:30%;background:linear-gradient(90deg,var(--accent),var(--accent2));
border-radius:3px;animation:slide 1.1s infinite ease-in-out}
@keyframes slide{0%{margin-left:-30%}100%{margin-left:100%}}
</style>
</head>
<body>
<div class="app">
<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>
<div class="brand"><div class="logo">N</div><div><div class="nm">NeuroSploit<span class="badge-rs">RUST</span></div><div class="vr">v3.4.0 · Multi-Model Harness</div></div></div>
<div class="navlabel">Operate</div>
<div class="nav on" data-v="run"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg> Engagement</div>
<div class="nav" data-v="findings"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg> Findings</div>
<div class="nav" data-v="report"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg> Report</div>
<div class="navlabel">Library</div>
<div class="nav" data-v="agents"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v6m0 6v6m11-7h-6m-6 0H1"/></svg> Agents</div>
<div class="nav" data-v="models"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 9h6v6H9z"/></svg> Models</div>
<div class="foot">
<div class="stat"><span>Agents</span><b id="sf-agents"></b></div>
<div class="stat"><span>Providers</span><b id="sf-prov"></b></div>
<div class="stat"><span>Backends</span><b id="sf-cli"></b></div>
</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 (pipeline self-test)</label>
<label class="toggle" id="t-sub"><input type="checkbox" id="subscription"/> Use subscription (Claude/Codex login)</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 class="topbar">
<div><h1 id="bar-title">Engagement</h1><div class="crumb" id="bar-crumb">Configure and launch an autonomous run</div></div>
<div class="chipline"><span class="chip"><span class="dot"></span> <b>online</b></span><span class="chip mono" id="chip-models"></span></div>
</div>
<!-- ENGAGEMENT -->
<section class="view on" id="v-run"><div class="wrap">
<div class="seg" id="modeSeg">
<button class="on" data-m="web">🌐 Black-box (URL)</button>
<button data-m="whitebox">📦 White-box (repo)</button>
</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>
<div class="grid2">
<div>
<div class="card">
<h2>Target</h2>
<div class="desc" id="targetDesc">One or more URLs — the harness recons each, then intelligently selects matching agents.</div>
<div class="field" id="urlField"><label>Targets (one per line)</label><textarea id="targets" placeholder="https://target-one.example&#10;https://target-two.example"></textarea></div>
<div class="field" id="repoField" style="display:none"><label>Repository path (local)</label><input id="repo" placeholder="/path/to/repo"/></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="0" min="0"/></div>
</div>
<div class="toggles">
<label class="tg on" id="tg-off"><input type="checkbox" id="offline" checked/> Offline self-test</label>
<label class="tg" id="tg-sub"><input type="checkbox" id="subscription"/> Subscription (Claude/Codex/Gemini login)</label>
<label class="tg" id="tg-mcp"><input type="checkbox" id="mcp"/> Playwright MCP</label>
</div>
<div class="btns"><button class="act primary" id="go">▶ Launch engagement</button></div>
<div class="progress" id="prog"><div class="bar"></div></div>
</div>
</div>
<div>
<div class="card">
<h2>Model panel</h2>
<div class="desc">1st = primary · others fail over &amp; form the validator jury.</div>
<div class="mpanel" id="mpanel"></div>
</div>
</div>
</div>
<div class="card">
<h2>Live execution</h2>
<div class="desc">Recon → intelligent agent selection → parallel exploitation → N-model voting → report. Artifacts saved to <span class="mono">runs/</span>.</div>
<div class="term" id="term"><span class="empty">— idle. Launch an engagement to stream activity. —</span></div>
</div>
</div></section>
<!-- FINDINGS -->
<section class="view" id="v-findings"><div class="wrap">
<div class="card">
<h2>Validated findings</h2>
<div class="desc">Only findings confirmed by multi-model adversarial voting appear here.</div>
<div class="sevbar" id="sevbar"></div>
<div id="findings"><div class="empty-state">No findings yet — run an engagement.</div></div>
</div>
</div></section>
<!-- REPORT -->
<section class="view" id="v-report"><div class="wrap">
<div class="card">
<h2>Report</h2>
<div class="desc">HTML report + JSON/MD artifacts for reuse by other tools/AIs.</div>
<div id="reportcard"><div class="empty-state">Run an engagement to generate a report.</div></div>
</div>
</div></section>
<!-- AGENTS -->
<section class="view" id="v-agents"><div class="wrap">
<div class="card">
<h2>Agent library</h2>
<div class="desc" id="agentsub"></div>
<div class="toolbar">
<input id="asearch" placeholder="🔎 filter by name / title / CWE"/>
<button class="fbtn on" data-k="all">All</button>
<button class="fbtn" data-k="vuln">Vuln</button>
<button class="fbtn" data-k="recon">Recon</button>
<button class="fbtn" data-k="code">Code</button>
<button class="fbtn" data-k="meta">Meta</button>
</div>
<div class="alist" id="alist"></div>
</div>
</div></section>
<!-- MODELS -->
<section class="view" id="v-models"><div class="wrap">
<div class="card">
<h2>Providers &amp; models</h2>
<div class="desc">Use via <b>API</b> key or <b>subscription</b> (local CLI login). CLI-capable providers are tagged.</div>
<div id="modelcard"></div>
</div>
</div></section>
</main>
</div>
<script>
const $=s=>document.querySelector(s),$$=s=>[...document.querySelectorAll(s)];
let INFO=null,AGENTS=[],lastRun=null;
let INFO=null,AGENTS=[],lastRun=null,mode='web',filter='all';
const TITLES={run:['Engagement','Configure and launch an autonomous run'],findings:['Findings','Validated, multi-model-confirmed results'],
report:['Report','Generated deliverables & artifacts'],agents:['Agents','The markdown agent library'],models:['Models','Providers, models & auth']};
$$('.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);
$('#t-sub').querySelector('input').onchange=e=>$('#t-sub').classList.toggle('on',e.target.checked);
$$('.view').forEach(v=>v.classList.remove('on'));$('#v-'+n.dataset.v).classList.add('on');
const t=TITLES[n.dataset.v];$('#bar-title').textContent=t[0];$('#bar-crumb').textContent=t[1];});
// mode switch
$$('#modeSeg button').forEach(b=>b.onclick=()=>{$$('#modeSeg button').forEach(x=>x.classList.remove('on'));b.classList.add('on');
mode=b.dataset.m;const wb=mode==='whitebox';
$('#urlField').style.display=wb?'none':'';$('#repoField').style.display=wb?'':'none';
$('#targetDesc').textContent=wb?'A local repository path — code agents review the source for vulnerabilities.':'One or more URLs — the harness recons each, then intelligently selects matching agents.';});
// toggles
['off','sub','mcp'].forEach(k=>{const map={off:'offline',sub:'subscription',mcp:'mcp'};
$('#'+map[k]).onchange=e=>$('#tg-'+k).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
const a=INFO.agents;
$('#sf-agents').textContent=a.total;$('#sf-prov').textContent=INFO.providers.length;
$('#sf-cli').textContent=(INFO.cli_backends||[]).length;
$('#chip-models').textContent=(INFO.cli_backends||[]).join(' · ')||'api-only';
$('#agentsub').textContent=`${a.vulns} vuln · ${a.recon||0} recon · ${a.code||0} code · ${a.meta} meta — ${a.total} total`;
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;}));
mh+=`<label class="mopt"><input type="checkbox" value="${id}" ${first?'checked':''}/> <span class="tag ${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('');
$('#modelcard').innerHTML=INFO.providers.map(p=>`<div class="mcard"><h3><span class="tag ${p.kind}">${p.kind}</span> ${p.label}
${(INFO.cli_backends||[]).some(b=>['claude','codex','grok','gemini'].includes(b))&&p.kind==='cli'?'<span class="dim" style="font-size:11px">· subscription-capable</span>':''}</h3>
<div class="muted" style="font-size:12px">${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;}
function logLine(t){const T=$('#term');if(T.querySelector('.empty'))T.innerHTML='';const d=document.createElement('div');
d.className=t.startsWith('===')?'h':t.includes('CONFIRMED')||t.includes('validated finding')||t.includes('updated')||t.startsWith('artifacts')?'ok':
t.includes('failed')||t.startsWith('ERROR')?'e':t.startsWith('recon')||t.startsWith('exploit')||t.startsWith('analyze')||t.startsWith('intelligently')||t.startsWith('agent selection')?'v':
t.startsWith('selected')||t.includes('candidate')?'s':'';
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,subscription:$('#subscription').checked};
logLine('Queued '+targets.length+' target(s) · panel: '+(body.models.join(', ')||'default'));
let body={models:selectedModels(),vote_n:+$('#voten').value,max_agents:+$('#maxa').value,
offline:$('#offline').checked,subscription:$('#subscription').checked,mcp:$('#mcp').checked,mode};
if(mode==='whitebox'){const r=$('#repo').value.trim();if(!r){$('#repo').focus();$('#repo').style.borderColor='var(--crit)';return;}body.repo=r;}
else{const t=$('#targets').value.split('\n').map(s=>s.trim()).filter(Boolean);if(!t.length){$('#targets').focus();$('#targets').style.borderColor='var(--crit)';return;}body.targets=t;}
$('#go').disabled=true;$('#prog').classList.add('on');$('#term').innerHTML='';seen=0;
logLine((mode==='whitebox'?'White-box repo: '+body.repo:'Black-box targets: '+body.targets.length)+' · 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;}
if(r.error){logLine('ERROR: '+r.error);$('#go').disabled=false;$('#prog').classList.remove('on');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||{});
$('#go').disabled=false;$('#prog').classList.remove('on');lastRun=id;render(st.result||{});
logLine('done.');
}
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>`;
const f=res.findings||[],by={Critical:0,High:0,Medium:0,Low:0,Info:0};f.forEach(x=>by[x.severity]=(by[x.severity]||0)+1);
$('#sevbar').innerHTML=Object.entries(by).map(([k,v])=>`<div class="scount ${k}"><span class="n">${v}</span><span class="l">${k}</span></div>`).join('');
$('#findings').innerHTML=f.length?f.map(x=>`<div class="find ${x.severity}"><h4><span class="sev ${x.severity}">${x.severity}</span> ${esc(x.title||'')}</h4>
<div class="m mono">${esc(x.agent||'')} · ${esc(x.cwe||'')} · votes ${esc(x.votes||'-')} · conf ${(x.confidence||0).toFixed(2)} · ${esc(x.endpoint||'')}</div>
${x.payload?`<pre>${esc(x.payload)}</pre>`:''}${x.evidence?`<div class="m">Evidence: ${esc(x.evidence)}</div>`:''}
${x.remediation?`<div class="m">Fix: ${esc(x.remediation)}</div>`:''}</div>`).join('')
:`<div class="empty-state">✓ Run complete — ${(res.agents_ran||[]).length} agents ran, 0 validated findings.<br><span class="dim">${$('#offline').checked?'Offline mode performs no exploitation — enable a model (API key or subscription) to find issues.':''}</span></div>`;
$('#reportcard').innerHTML=`<a class="dl" href="/report/${lastRun}" target="_blank">⬇ Open HTML report</a><iframe src="/report/${lastRun}"></iframe>`;
}
function esc(s){return (s+'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
$$('.fbtn').forEach(b=>b.onclick=()=>{$$('.fbtn').forEach(x=>x.classList.remove('on'));b.classList.add('on');filter=b.dataset.k;renderAgents();});
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>';}
const rows=AGENTS.filter(a=>(filter==='all'||a.kind===filter)&&(!q||(a.name+a.title+a.cwe).toLowerCase().includes(q)));
$('#alist').innerHTML=rows.slice(0,500).map(a=>`<div class="arow"><span class="tag ${a.kind}">${a.kind}</span> <code>${a.name}</code>
<span class="t">${esc((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='';
$('#go').onclick=run;
$('#targets').oninput=()=>$('#targets').style.borderColor='';$('#repo').oninput=()=>$('#repo').style.borderColor='';
init();
</script>
</body>
+6 -2
View File
@@ -21,20 +21,24 @@ pub struct Agent {
pub struct Library {
pub vulns: Vec<Agent>,
pub meta: Vec<Agent>,
pub recon: Vec<Agent>,
pub code: Vec<Agent>,
}
impl Library {
pub fn total(&self) -> usize {
self.vulns.len() + self.meta.len()
self.vulns.len() + self.meta.len() + self.recon.len() + self.code.len()
}
}
/// Load `<base>/agents_md/{vulns,meta}/*.md`.
/// Load `<base>/agents_md/{vulns,meta,recon,code}/*.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"),
recon: load_dir(&root.join("recon"), "recon"),
code: load_dir(&root.join("code"), "code"),
}
}
+4 -1
View File
@@ -11,12 +11,15 @@ pub mod models;
pub mod pipeline;
pub mod pool;
pub mod report;
pub mod rl;
pub mod types;
pub use agents::{Agent, Library};
pub use models::{
cli_binary_for, installed_cli_backends, provider_for, providers, ChatClient, ModelRef, Provider,
cli_binary_for, installed_cli_backends, provider_for, providers, write_mcp_config, ChatClient,
ModelRef, Provider,
};
pub use pipeline::{run_whitebox, RunOutput};
pub use pipeline::run;
pub use pool::ModelPool;
pub use types::{Finding, RunConfig};
+47 -5
View File
@@ -28,6 +28,8 @@ pub fn providers() -> Vec<Provider> {
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: "gemini", label: "Google Gemini", base_url: "https://generativelanguage.googleapis.com/v1beta/openai", env_key: "GEMINI_API_KEY", kind: "cli",
models: vec!["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash"] },
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",
@@ -124,9 +126,20 @@ impl ChatClient {
impl ChatClient {
/// Complete via a locally-installed **agentic CLI subscription** (Claude
/// Code / Codex / Grok) instead of an API key. This uses the user's logged-in
/// subscription, so no provider key is required.
pub async fn chat_cli(&self, provider: &str, model: &str, system: &str, user: &str) -> Result<String> {
/// Code / Codex / Grok / Gemini) instead of an API key. This uses the user's
/// logged-in subscription, so no provider key is required.
///
/// When `mcp_config` is set (a path to an `.mcp.json`), Claude/Codex run with
/// the MCP servers enabled and tool autonomy, so agents can actually drive
/// **Playwright** (browse, execute JS, screenshot) during execution.
pub async fn chat_cli(
&self,
provider: &str,
model: &str,
system: &str,
user: &str,
mcp_config: Option<&str>,
) -> Result<String> {
let bin = cli_binary_for(provider)
.ok_or_else(|| anyhow!("no CLI/subscription backend for provider '{}'", provider))?;
let prompt = format!("{system}\n\n{user}");
@@ -135,10 +148,24 @@ impl ChatClient {
// Claude Code headless print mode (uses the Claude subscription login).
"claude" => {
cmd.arg("-p").arg("--model").arg(model);
if let Some(mcp) = mcp_config {
cmd.arg("--mcp-config").arg(mcp).arg("--dangerously-skip-permissions");
// Required to allow tool autonomy when running as root.
cmd.env("IS_SANDBOX", "1");
}
}
// Codex non-interactive exec (uses the ChatGPT/Codex login), prompt on stdin.
"codex" => {
cmd.arg("exec").arg("--model").arg(model).arg("-");
cmd.arg("exec").arg("--model").arg(model);
if let Some(mcp) = mcp_config {
cmd.arg("--config").arg(format!("mcp_config_file={mcp}"))
.arg("--dangerously-bypass-approvals-and-sandbox");
}
cmd.arg("-");
}
// Google Gemini CLI (uses the Gemini subscription login).
"gemini" => {
cmd.arg("-m").arg(model);
}
// Grok CLI, prompt on stdin (best-effort flags).
"grok" => {
@@ -166,6 +193,7 @@ pub fn cli_binary_for(provider: &str) -> Option<&'static str> {
"anthropic" => Some("claude"),
"openai" => Some("codex"),
"xai" => Some("grok"),
"gemini" => Some("gemini"),
_ => None,
}
}
@@ -179,7 +207,21 @@ pub fn binary_in_path(name: &str) -> bool {
/// Which subscription CLI backends are installed locally.
pub fn installed_cli_backends() -> Vec<&'static str> {
["claude", "codex", "grok"].into_iter().filter(|b| binary_in_path(b)).collect()
["claude", "codex", "grok", "gemini"].into_iter().filter(|b| binary_in_path(b)).collect()
}
/// 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> {
std::fs::create_dir_all(dir)?;
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)?;
Ok(path)
}
impl Default for ChatClient {
+258 -42
View File
@@ -1,8 +1,11 @@
use crate::agents::{Agent, Library};
use crate::pool::ModelPool;
use crate::rl::{severity_reward, RlState};
use crate::types::{Finding, RunConfig};
use crate::report;
use futures::stream::{self, StreamExt};
use serde::Serialize;
use std::path::{Path, PathBuf};
use tokio::sync::mpsc::Sender;
/// Result of an engagement run.
@@ -11,26 +14,28 @@ pub struct RunOutput {
pub findings: Vec<Finding>,
pub agents_ran: Vec<String>,
pub candidates: usize,
pub recon: String,
/// Paths to persisted artifacts (recon/exploit/findings/report), if any.
pub artifacts: Vec<String>,
}
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.";
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\":\"...\"}.";
/// Run the full harness pipeline, streaming human-readable progress over `tx`.
/// 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
.send(format!(
"Loaded {} agents ({} vuln / {} meta) · models: {} · vote_n={} · concurrency={}",
lib.total(),
lib.vulns.len(),
lib.meta.len(),
"Loaded {} agents ({} vuln / {} recon / {} code / {} meta) · models: {} · vote_n={} · concurrency={}{}",
lib.total(), lib.vulns.len(), lib.recon.len(), lib.code.len(), lib.meta.len(),
pool.candidates.iter().map(|m| m.label()).collect::<Vec<_>>().join(", "),
cfg.vote_n,
cfg.concurrency,
cfg.vote_n, cfg.concurrency,
if pool.mcp_config.is_some() { " · Playwright MCP ON" } else { "" },
))
.await;
// ---- 1. Recon -------------------------------------------------------
// ---- 1. Recon ------------------------------------------------------
let recon = if cfg.offline {
let _ = tx.send("recon: offline mode — skipping model calls".into()).await;
"{}".to_string()
@@ -47,23 +52,42 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
}
};
// ---- 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;
// ---- 2. Intelligent, RL-ranked agent selection ---------------------
let mut rl = cfg.rl_path.as_ref().map(|p| RlState::load(Path::new(p))).unwrap_or_default();
let mut ranked: Vec<Agent> = lib.vulns.clone();
ranked.sort_by(|a, b| rl.weight(&b.name).partial_cmp(&rl.weight(&a.name)).unwrap_or(std::cmp::Ordering::Equal));
let cap = if cfg.max_agents > 0 { cfg.max_agents.min(ranked.len()) } else { ranked.len() };
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,
};
let selected: Vec<Agent> = ranked.into_iter().take(cap).collect();
let _ = tx.send(format!("selected {} specialist agents (RL-ranked)", selected.len())).await;
let _ = tx.send("offline: no exploitation performed (provide API keys or --subscription to run live)".into()).await;
let artifacts = persist(&cfg, &recon, "", &[]);
return RunOutput { findings: vec![], agents_ran: selected.iter().map(|a| a.name.clone()).collect(), candidates: 0, recon, artifacts };
}
// ---- 3. Exploit (parallel, bounded by the pool semaphore) ----------
// Use the model to pick the agents whose preconditions match the recon —
// the harness reasons about *which* specialists to run, not all of them.
let chosen = select_agents(pool, &recon, &ranked, &tx).await;
let selected: Vec<Agent> = {
let mut sel: Vec<Agent> = if chosen.is_empty() {
ranked.clone()
} else {
ranked.iter().filter(|a| chosen.iter().any(|c| c == &a.name)).cloned().collect()
};
if sel.is_empty() {
sel = ranked.clone();
}
sel.into_iter().take(cap).collect()
};
let _ = tx
.send(format!("intelligently selected {} agent(s) matching recon: {}", selected.len(),
selected.iter().map(|a| a.name.clone()).collect::<Vec<_>>().join(", ")))
.await;
// ---- 3. Exploit (parallel) -----------------------------------------
let target = cfg.target.clone();
let candidates: Vec<Finding> = stream::iter(selected.iter().cloned())
let raw: Vec<(String, String, Vec<Finding>)> = stream::iter(selected.iter().cloned())
.map(|ag| {
let target = target.clone();
let recon = recon.clone();
@@ -77,64 +101,221 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
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
let _ = txc.send(format!("exploit {} via {}{} candidate(s)", ag.name, m.label(), f.len())).await;
(ag.name.clone(), text, f)
}
Err(e) => {
let _ = txc.send(format!("exploit {} failed: {e}", ag.name)).await;
vec![]
(ag.name.clone(), format!("ERROR: {e}"), vec![])
}
}
}
})
.buffer_unordered(cfg.concurrency)
.collect::<Vec<Vec<Finding>>>()
.await
.into_iter()
.flatten()
.collect();
.collect()
.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;
// ---- 4. Validate by N-model voting ---------------------------------
let vote_n = cfg.vote_n;
let findings = validate(candidates, pool, VOTE_SYS, cfg.vote_n, &tx).await;
finish(cfg, lib, recon, transcript, findings, selected, &mut rl, tx).await
}
/// White-box engagement: analyse a repository's source for vulnerabilities.
pub async fn run_whitebox(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<String>) -> RunOutput {
let _ = tx.send(format!("WHITEBOX · repo: {} · {} code agents · models: {}", cfg.target, lib.code.len(),
pool.candidates.iter().map(|m| m.label()).collect::<Vec<_>>().join(", "))).await;
let context = collect_repo_context(Path::new(&cfg.target), 200, 120_000);
let bytes = context.len();
let _ = tx.send(format!("collected {} bytes of source context", bytes)).await;
if bytes == 0 {
let _ = tx.send("no readable source found at the given path".into()).await;
}
let mut rl = cfg.rl_path.as_ref().map(|p| RlState::load(Path::new(p))).unwrap_or_default();
let mut ranked: Vec<Agent> = if lib.code.is_empty() { lib.vulns.clone() } else { lib.code.clone() };
ranked.sort_by(|a, b| rl.weight(&b.name).partial_cmp(&rl.weight(&a.name)).unwrap_or(std::cmp::Ordering::Equal));
let cap = if cfg.max_agents > 0 { cfg.max_agents.min(ranked.len()) } else { ranked.len() };
let selected: Vec<Agent> = ranked.into_iter().take(cap).collect();
let _ = tx.send(format!("selected {} code-analysis agents", selected.len())).await;
if cfg.offline || bytes == 0 {
let artifacts = persist(&cfg, "{}", &context, &[]);
return RunOutput { findings: vec![], agents_ran: selected.iter().map(|a| a.name.clone()).collect(), candidates: 0, recon: String::new(), artifacts };
}
let raw: Vec<(String, String, Vec<Finding>)> = stream::iter(selected.iter().cloned())
.map(|ag| {
let ctx = context.clone();
let txc = tx.clone();
async move {
let user = format!(
"{}\n\nSOURCE CODE TO REVIEW:\n```\n{}\n```\n\nReply ONLY with a JSON array of findings (may be empty []). \
Each item: {{id,title,severity,cwe,endpoint,payload,evidence,impact,remediation,confidence}} \
where `endpoint` is the file:line and `evidence` quotes the vulnerable code.",
ag.user.replace("{target}", "the provided repository").replace("{recon_json}", "{}"),
ctx
);
match pool.complete(&ag.system, &user).await {
Ok((m, text)) => {
let f = extract_findings(&text, &ag.name);
let _ = txc.send(format!("analyze {} via {}{} candidate(s)", ag.name, m.label(), f.len())).await;
(ag.name.clone(), text, f)
}
Err(e) => {
let _ = txc.send(format!("analyze {} failed: {e}", ag.name)).await;
(ag.name.clone(), format!("ERROR: {e}"), vec![])
}
}
}
})
.buffer_unordered(cfg.concurrency)
.collect()
.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 findings = validate(candidates, pool, CODE_VOTE_SYS, cfg.vote_n, &tx).await;
finish(cfg, lib, "{}".into(), transcript, findings, selected, &mut rl, tx).await
}
// --------------------------------------------------------------------------- shared
const SELECT_SYS: &str = "You are a penetration-test orchestrator. Given recon of a target and a catalog of specialist agents, choose ONLY the agents whose preconditions clearly match the target's attack surface. Be selective. Reply with a JSON array of agent names (strings) drawn exactly from the catalog. No prose.";
/// Ask the model which agents to run for this recon. Returns chosen agent names
/// (empty on failure → caller falls back to RL-ranked agents).
async fn select_agents(pool: &ModelPool, recon: &str, catalog: &[Agent], tx: &Sender<String>) -> Vec<String> {
let list = catalog
.iter()
.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 {
Ok((m, text)) => {
let names = parse_string_array(&text);
let _ = tx.send(format!("agent selection via {}{} agent(s) chosen", m.label(), names.len())).await;
names
}
Err(e) => {
let _ = tx.send(format!("agent selection failed ({e}) — falling back to RL ranking")).await;
vec![]
}
}
}
fn parse_string_array(text: &str) -> Vec<String> {
match (text.find('['), text.rfind(']')) {
(Some(a), Some(b)) if b > a => serde_json::from_str::<Vec<String>>(&text[a..=b]).unwrap_or_default(),
_ => vec![],
}
}
async fn validate(candidates: Vec<Finding>, pool: &ModelPool, sys: &str, vote_n: usize, tx: &Sender<String>) -> Vec<Finding> {
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 {}",
"Finding: {} | severity {} | {} | at {} | 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;
let (yes, total) = pool.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;
let _ = txc.send(format!("vote {}{} ({})", f.title, if f.validated { "CONFIRMED" } else { "rejected" }, f.votes)).await;
f
}
})
.buffer_unordered(cfg.concurrency)
.collect::<Vec<Finding>>()
.buffer_unordered(pool.candidates.len().max(2))
.collect()
.await;
validated.into_iter().filter(|f| f.validated).collect()
}
let candidates = validated.len();
let findings: Vec<Finding> = validated.into_iter().filter(|f| f.validated).collect();
async fn finish(cfg: RunConfig, _lib: &Library, recon: String, transcript: String, findings: Vec<Finding>,
selected: Vec<Agent>, rl: &mut RlState, tx: Sender<String>) -> RunOutput {
let _ = tx.send(format!("{} validated finding(s)", findings.len())).await;
// RL update: reward agents that produced validated findings; gently decay idle.
let hit: std::collections::HashMap<&str, f64> = findings.iter().fold(Default::default(), |mut m, f| {
let e = m.entry(f.agent.as_str()).or_insert(0.0);
*e = (*e + severity_reward(&f.severity)).min(1.0);
m
});
for a in &selected {
let r = hit.get(a.name.as_str()).copied().unwrap_or(-0.05);
rl.update(&a.name, r);
}
rl.runs += 1;
if let Some(p) = &cfg.rl_path {
rl.save(Path::new(p));
let _ = tx.send("RL rewards updated".into()).await;
}
let artifacts = persist(&cfg, &recon, &transcript, &findings);
if !artifacts.is_empty() {
let _ = tx.send(format!("artifacts saved: {}", artifacts.join(", "))).await;
}
RunOutput {
candidates: findings.len(),
findings,
agents_ran: selected.iter().map(|a| a.name.clone()).collect(),
candidates,
recon,
artifacts,
}
}
/// Write recon/exploit/findings/report as json+md for downstream reuse.
fn persist(cfg: &RunConfig, recon: &str, transcript: &str, findings: &[Finding]) -> Vec<String> {
let Some(dir) = &cfg.workdir else { return vec![] };
let dir = PathBuf::from(dir);
if std::fs::create_dir_all(&dir).is_err() {
return vec![];
}
let mut written = Vec::new();
let mut put = |name: &str, content: String| {
let p = dir.join(name);
if std::fs::write(&p, content).is_ok() {
written.push(p.display().to_string());
}
};
put("recon.json", recon.to_string());
put("recon.md", format!("# Recon — {}\n\n```json\n{}\n```\n", cfg.target, recon));
if !transcript.is_empty() {
put("exploitation.md", format!("# Agent transcript — {}\n\n{}", cfg.target, transcript));
}
put("findings.json", serde_json::to_string_pretty(findings).unwrap_or_else(|_| "[]".into()));
put("findings.md", findings_md(&cfg.target, findings));
put("report.html", report::html(&cfg.target, findings));
written
}
fn findings_md(target: &str, findings: &[Finding]) -> String {
let mut s = format!("# NeuroSploit findings — {}\n\n{} validated finding(s).\n", target, findings.len());
for (i, f) in findings.iter().enumerate() {
s.push_str(&format!(
"\n## {}. [{}] {}\n- agent: `{}` CWE: {} CVSS: {} votes: {} confidence: {:.2}\n- endpoint: {}\n\n**Payload**\n```\n{}\n```\n\n**Evidence**\n{}\n\n**Impact:** {}\n\n**Remediation:** {}\n",
i + 1, f.severity, f.title, f.agent, f.cwe, f.cvss, f.votes, f.confidence, f.endpoint, f.payload, f.evidence, f.impact, f.remediation
));
}
s
}
fn transcript_of(raw: &[(String, String, Vec<Finding>)]) -> String {
raw.iter().map(|(n, t, f)| format!("## {} ({} candidate)\n\n{}\n", n, f.len(), t)).collect::<Vec<_>>().join("\n")
}
/// 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(']')) {
@@ -154,7 +335,42 @@ fn extract_findings(text: &str, agent: &str) -> Vec<Finding> {
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>());
f.id = format!("{}-{}", agent, f.title.chars().take(12).collect::<String>());
}
}
out
}
/// Concatenate source files under `root` into a bounded review context.
fn collect_repo_context(root: &Path, max_files: usize, max_bytes: usize) -> String {
const EXTS: &[&str] = &[
"rs", "py", "js", "ts", "tsx", "jsx", "go", "java", "php", "rb", "c", "cc", "cpp", "h", "hpp",
"cs", "kt", "swift", "scala", "sh", "sql", "html", "vue", "yml", "yaml", "tf",
];
let mut out = String::new();
let mut files = 0usize;
if !root.exists() {
return out;
}
for entry in walkdir::WalkDir::new(root).max_depth(8).into_iter().flatten() {
if files >= max_files || out.len() >= max_bytes {
break;
}
let path = entry.path();
let s = path.to_string_lossy();
if s.contains("/.git/") || s.contains("/node_modules/") || s.contains("/target/") || s.contains("/vendor/") {
continue;
}
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if !EXTS.contains(&ext) {
continue;
}
if let Ok(content) = std::fs::read_to_string(path) {
let rel = path.strip_prefix(root).unwrap_or(path).to_string_lossy();
let budget = max_bytes.saturating_sub(out.len());
let take = content.len().min(budget).min(8_000);
out.push_str(&format!("\n// ===== file: {} =====\n{}\n", rel, &content[..take]));
files += 1;
}
}
out
+15 -4
View File
@@ -13,14 +13,21 @@ pub struct ModelPool {
sem: Arc<Semaphore>,
pub candidates: Vec<ModelRef>,
pub subscription: bool,
/// Path to an `.mcp.json` (Playwright) used on the subscription/CLI path.
pub mcp_config: Option<String>,
}
impl ModelPool {
pub fn new(models: Vec<ModelRef>, concurrency: usize) -> Self {
Self::with_auth(models, concurrency, false)
Self::with_auth(models, concurrency, false, None)
}
pub fn with_auth(models: Vec<ModelRef>, concurrency: usize, subscription: bool) -> Self {
pub fn with_auth(
models: Vec<ModelRef>,
concurrency: usize,
subscription: bool,
mcp_config: Option<String>,
) -> Self {
let concurrency = concurrency.max(1);
ModelPool {
client: ChatClient::new(),
@@ -31,13 +38,17 @@ impl ModelPool {
models
},
subscription,
mcp_config,
}
}
/// One completion for a model, via subscription CLI or HTTP API.
/// One completion for a model, via subscription CLI (optionally with MCP) or HTTP API.
async fn one(&self, m: &ModelRef, system: &str, user: &str) -> Result<String> {
if self.subscription && cli_binary_for(&m.provider).is_some() {
return self.client.chat_cli(&m.provider, &m.model, system, user).await;
return self
.client
.chat_cli(&m.provider, &m.model, system, user, self.mcp_config.as_deref())
.await;
}
self.client.chat(m, system, user).await
}
+60
View File
@@ -0,0 +1,60 @@
//! Lightweight reinforcement-learning reward store for the harness.
//!
//! Each agent carries a weight in [0.05, 1.0]; validated findings reward it,
//! idle runs decay it slightly. Weights bias agent ordering on future runs and
//! persist to a JSON file so the harness gets sharper over time.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
#[derive(Default, Serialize, Deserialize)]
pub struct RlState {
#[serde(default)]
pub weights: HashMap<String, f64>,
#[serde(default)]
pub runs: u64,
}
const ALPHA: f64 = 0.3;
const WMIN: f64 = 0.05;
const WMAX: f64 = 1.0;
impl RlState {
pub fn load(path: &Path) -> RlState {
std::fs::read_to_string(path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
}
pub fn weight(&self, agent: &str) -> f64 {
*self.weights.get(agent).unwrap_or(&0.5)
}
/// Reward in [-1, 1]; e.g. severity-weighted hits positive, idle negative.
pub fn update(&mut self, agent: &str, reward: f64) {
let w = self.weights.entry(agent.to_string()).or_insert(0.5);
*w = (*w + ALPHA * (reward - *w)).clamp(WMIN, WMAX);
}
pub fn save(&self, path: &Path) {
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Ok(s) = serde_json::to_string_pretty(self) {
let _ = std::fs::write(path, s);
}
}
}
/// Severity → reward weight.
pub fn severity_reward(sev: &str) -> f64 {
match sev {
"Critical" => 1.0,
"High" => 0.7,
"Medium" => 0.4,
"Low" => 0.2,
_ => 0.05,
}
}
@@ -74,6 +74,12 @@ pub struct RunConfig {
/// of HTTP API keys.
#[serde(default)]
pub subscription: bool,
/// Directory to persist run artifacts (recon/exploit/findings json+md).
#[serde(default)]
pub workdir: Option<String>,
/// Path to the RL reward state file.
#[serde(default)]
pub rl_path: Option<String>,
}
fn default_vote() -> usize {
@@ -93,6 +99,8 @@ impl RunConfig {
max_agents: 0,
offline: false,
subscription: false,
workdir: None,
rl_path: None,
}
}
}
+24
View File
@@ -0,0 +1,24 @@
HTTP/2 200
date: Mon, 22 Jun 2026 20:03:23 GMT
content-type: text/html; charset=UTF-8
cf-ray: a0fddb2608004cf7-GRU
cf-cache-status: DYNAMIC
cache-control: no-store, no-cache, must-revalidate
expires: Thu, 19 Nov 1981 08:52:00 GMT
server: cloudflare
set-cookie: PHPSESSID=8e1ac50a4f9a4bec19d456ecc11ba781; path=/; secure; HttpOnly; SameSite=Lax
set-cookie: user_language=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/
set-cookie: user_language=pt; expires=Tue, 22 Jun 2027 20:03:23 GMT; Max-Age=31536000; path=/; domain=.hackersec.com; secure; HttpOnly; SameSite=Lax
strict-transport-security: max-age=15552000; includeSubDomains; preload
vary: Accept-Encoding
pragma: no-cache
content-security-policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com https://googleads.g.doubleclick.net https://www.google.com https://static.hotjar.com https://script.hotjar.com https://snap.licdn.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://www.google.com https://*.google.com https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com https://*.g.doubleclick.net https://*.hotjar.com https://*.hotjar.io wss://ws.hotjar.com https://snap.licdn.com https://px.ads.linkedin.com; frame-src 'self' https://www.googletagmanager.com https://td.doubleclick.net https://vars.hotjar.com https://www.google.com https://maps.google.com https://www.google.com/maps/embed; frame-ancestors 'self'; form-action 'self'; base-uri 'self'
expect-ct: max-age=86400, enforce
referrer-policy: same-origin
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
report-to: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=VHXHHEFxf1FeB7p%2FSZOs6ttzdPjv0%2BZKnuq964Yjrm44aGqGaLbxIKorQ46QqRSB1aJH40Lg5h5k0Opf%2FbY8%2Fqt1bz2b4uVVdD0G4CU%2B4Yf1ACKi%2F1z8YD90k2RTk7U%3D"}]}
nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
alt-svc: h3=":443"; ma=86400
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
# HackerSec - Robots.txt
# https://hackersec.com
User-agent: *
Allow: /
# Sitemap
Sitemap: https://hackersec.com/sitemap.php
File diff suppressed because one or more lines are too long
+100
View File
@@ -0,0 +1,100 @@
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200
200