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>