mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-07-15 07:17:19 +02:00
v3.4.1: CLI-only Rust harness — interactive wizard, smart selection, tool doctrine, Typst, status
- Remove Rust web server (axum/tower-http); CLI-only binary - Verbose logging (-v) + unique run-id output folder runs/ns-<ts>-<target>/ - status.json lifecycle (running → complete) + ✓ COMPLETE summary - Interactive wizard when run with no args; detailed --help with testphp/DVWA examples + Kali tip - Tool-usage doctrine injected into recon/exploit prompts: curl + rustscan/nmap (apt/brew/cargo install guidance) + browser via Playwright when present, else curl - Smart recon-aware selection: map recon signals → agent categories, only run matching agents; heuristic fallback when LLM selection is empty - Cross-model false-positive validation: voting prefers a model other than the finder - Playwright MCP auto-provision (npx) + per-backend support (claude/codex; gemini/grok degrade) - Gemini provider (API + gemini CLI subscription) - Typst report (report.typ + compiled report.pdf) via blank structured template - Lenient finding parsing (confidence as word/number) — fixes empty-results bug - bump version 3.4.0 -> 3.4.1 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+138
-42
@@ -1,26 +1,39 @@
|
||||
//! NeuroSploit v3.4.0 — single binary: `serve` (web dashboard) or `run` (CLI).
|
||||
|
||||
mod web;
|
||||
//! NeuroSploit v3.4.1 — CLI: `run` (black-box) / `whitebox` (source) / `agents` / `models`.
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
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")]
|
||||
#[command(
|
||||
name = "neurosploit",
|
||||
version,
|
||||
about = "NeuroSploit v3.4.1 — multi-model autonomous pentest harness",
|
||||
long_about = "NeuroSploit v3.4.1 — a Rust multi-model harness that drives a pool of LLMs \
|
||||
(API key or local subscription: Claude/Codex/Gemini/Grok) to autonomously test a target. \
|
||||
After recon it INTELLIGENTLY selects only the agents matching the discovered surface, runs \
|
||||
them in parallel, then validates every finding by cross-model voting before reporting.\n\n\
|
||||
Run with NO arguments for an interactive wizard.\n\n\
|
||||
EXAMPLES:\n \
|
||||
# Black-box against a known test site (subscription, Opus, browser via Playwright if present)\n \
|
||||
neurosploit run http://testphp.vulnweb.com/ --subscription --model anthropic:claude-opus-4-8 --mcp -v\n\n \
|
||||
# Black-box via API keys with a multi-model voting panel\n \
|
||||
neurosploit run http://testphp.vulnweb.com/ --model anthropic:claude-opus-4-8 --model openai:gpt-5.1 --vote-n 3\n\n \
|
||||
# White-box source review of a cloned repo (DVWA)\n \
|
||||
git clone https://github.com/digininja/DVWA /tmp/DVWA\n \
|
||||
neurosploit whitebox /tmp/DVWA --subscription --model anthropic:claude-opus-4-8 -v\n\n \
|
||||
# Offline pipeline self-test (no keys/login)\n \
|
||||
neurosploit run http://testphp.vulnweb.com/ --offline\n\n\
|
||||
TIP: run inside Kali Linux (or `docker run -it kalilinux/kali-rolling`) so curl/nmap/rustscan/ffuf are available."
|
||||
)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
cmd: Cmd,
|
||||
cmd: Option<Cmd>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Cmd {
|
||||
/// Start the web dashboard.
|
||||
Serve {
|
||||
#[arg(long, default_value_t = 8788)]
|
||||
port: u16,
|
||||
},
|
||||
/// Run an engagement from the CLI.
|
||||
/// Black-box: recon → intelligent agent selection → exploit → vote → report.
|
||||
Run {
|
||||
url: String,
|
||||
/// Models as provider:model (repeatable). First is primary; rest fail over + vote.
|
||||
@@ -30,20 +43,21 @@ enum Cmd {
|
||||
max_agents: usize,
|
||||
#[arg(long, default_value_t = 3)]
|
||||
vote_n: usize,
|
||||
/// Exercise the pipeline without calling any model API.
|
||||
#[arg(long)]
|
||||
offline: bool,
|
||||
/// Use local agentic CLI subscriptions (Claude Code / Codex / Grok)
|
||||
/// instead of HTTP API keys.
|
||||
/// Use local agentic CLI subscription (Claude/Codex/Gemini/Grok login).
|
||||
#[arg(long)]
|
||||
subscription: bool,
|
||||
/// Enable Playwright MCP (browser proof) on the subscription/CLI path.
|
||||
/// Enable Playwright MCP (auto-installed if missing; backends that don't
|
||||
/// support MCP fall back to their built-in tools).
|
||||
#[arg(long)]
|
||||
mcp: bool,
|
||||
/// Verbose: log each agent as it launches, recon, and votes.
|
||||
#[arg(short, long)]
|
||||
verbose: 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>,
|
||||
@@ -55,6 +69,8 @@ enum Cmd {
|
||||
offline: bool,
|
||||
#[arg(long)]
|
||||
subscription: bool,
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
},
|
||||
/// Show agent library counts.
|
||||
Agents,
|
||||
@@ -62,8 +78,7 @@ enum Cmd {
|
||||
Models,
|
||||
}
|
||||
|
||||
/// Locate the repo root that holds `agents_md/` (walk up from CWD, then fall
|
||||
/// back to the crate's compile-time location).
|
||||
/// Locate the repo root that holds `agents_md/`.
|
||||
fn find_base() -> PathBuf {
|
||||
if let Ok(b) = std::env::var("NEUROSPLOIT_BASE") {
|
||||
return PathBuf::from(b);
|
||||
@@ -80,7 +95,6 @@ fn find_base() -> PathBuf {
|
||||
}
|
||||
}
|
||||
}
|
||||
// crate is at <root>/neurosploit-rs/app → root is two levels up
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.and_then(|p| p.parent())
|
||||
@@ -93,10 +107,18 @@ async fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
let base = find_base();
|
||||
|
||||
match cli.cmd {
|
||||
let cmd = match cli.cmd {
|
||||
Some(c) => c,
|
||||
None => interactive(&base).await?, // no args → wizard
|
||||
};
|
||||
|
||||
match cmd {
|
||||
Cmd::Agents => {
|
||||
let lib = agents::load(&base);
|
||||
println!("{{\"vulns\":{},\"meta\":{},\"total\":{}}}", lib.vulns.len(), lib.meta.len(), lib.total());
|
||||
println!(
|
||||
"{{\"vulns\":{},\"recon\":{},\"code\":{},\"meta\":{},\"total\":{}}}",
|
||||
lib.vulns.len(), lib.recon.len(), lib.code.len(), lib.meta.len(), lib.total()
|
||||
);
|
||||
}
|
||||
Cmd::Models => {
|
||||
for p in harness::providers() {
|
||||
@@ -106,55 +128,76 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Cmd::Run { url, models, max_agents, vote_n, offline, subscription, mcp } => {
|
||||
Cmd::Run { url, models, max_agents, vote_n, offline, subscription, mcp, verbose } => {
|
||||
let url = if url.starts_with("http") { url } else { format!("https://{url}") };
|
||||
let mut cfg = RunConfig::new(&url);
|
||||
cfg.max_agents = max_agents;
|
||||
cfg.vote_n = vote_n;
|
||||
cfg.offline = offline;
|
||||
cfg.subscription = subscription;
|
||||
cfg.verbose = verbose;
|
||||
if !models.is_empty() {
|
||||
cfg.models = models;
|
||||
}
|
||||
let out = run_engagement(&base, cfg, mcp, false).await?;
|
||||
print_findings(&out);
|
||||
}
|
||||
Cmd::Whitebox { path, models, max_agents, vote_n, offline, subscription } => {
|
||||
Cmd::Whitebox { path, models, max_agents, vote_n, offline, subscription, verbose } => {
|
||||
let mut cfg = RunConfig::new(&path);
|
||||
cfg.max_agents = max_agents;
|
||||
cfg.vote_n = vote_n;
|
||||
cfg.offline = offline;
|
||||
cfg.subscription = subscription;
|
||||
cfg.verbose = verbose;
|
||||
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?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shared engagement runner for CLI `run` / `whitebox`.
|
||||
/// Shared engagement runner for `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()));
|
||||
|
||||
// Unique, sortable run id → runs/<id>/
|
||||
let run_id = format!("ns-{}-{}", now_ts(), sanitize(&cfg.target));
|
||||
let workdir = base.join("runs").join(&run_id);
|
||||
std::fs::create_dir_all(&workdir).ok();
|
||||
cfg.workdir = Some(workdir.display().to_string());
|
||||
cfg.rl_path = Some(base.join("data").join("rl_state_rs.json").display().to_string());
|
||||
write_status(&workdir, "running", &format!("\"target\":{:?}", cfg.target));
|
||||
|
||||
println!(" ┌─ NeuroSploit v3.4.1");
|
||||
println!(" │ run id : {run_id}");
|
||||
println!(" │ target : {}", cfg.target);
|
||||
println!(" │ models : {}", cfg.models.join(", "));
|
||||
println!(" │ output : {}", workdir.display());
|
||||
println!(" └─ mode : {}{}{}",
|
||||
if whitebox { "white-box" } else { "black-box" },
|
||||
if cfg.subscription { " · subscription" } else { " · api" },
|
||||
if mcp { " · mcp" } else { "" });
|
||||
|
||||
// Playwright MCP: only for backends that support it; auto-provision if asked.
|
||||
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
|
||||
let providers: Vec<String> = cfg.models.iter().map(|m| ModelRef::parse(m).provider).collect();
|
||||
if providers.iter().any(|p| harness::mcp_supported(p)) {
|
||||
match harness::ensure_playwright_mcp() {
|
||||
Ok(()) => match harness::write_mcp_config(&workdir) {
|
||||
Ok(p) => {
|
||||
println!(" [*] Playwright MCP ready → {}", p.display());
|
||||
Some(p.display().to_string())
|
||||
}
|
||||
Err(e) => { eprintln!(" [!] MCP config failed: {e}"); None }
|
||||
},
|
||||
Err(e) => { eprintln!(" [!] Playwright MCP unavailable ({e}); using built-in tools"); None }
|
||||
}
|
||||
} else {
|
||||
eprintln!(" [!] selected backend(s) don't support MCP; using built-in tools");
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
@@ -175,6 +218,14 @@ async fn run_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, whitebox: bo
|
||||
harness::run(cfg, &lib, &pool, tx).await
|
||||
};
|
||||
let _ = printer.await;
|
||||
|
||||
// Final report via Typst (PDF if the `typst` binary is present) + HTML/MD already written.
|
||||
match harness::report::typst_report(&out.target, &out.findings, &workdir) {
|
||||
Ok(p) => println!(" [*] report → {}", p.display()),
|
||||
Err(e) => eprintln!(" [!] typst report skipped: {e}"),
|
||||
}
|
||||
write_status(&workdir, "complete", &format!("\"findings\":{},\"agents_ran\":{}", out.findings.len(), out.agents_ran.len()));
|
||||
println!(" ✓ COMPLETE — {} validated finding(s) · status: {}/status.json", out.findings.len(), workdir.display());
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
@@ -189,16 +240,61 @@ fn print_findings(out: &RunOutput) {
|
||||
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);
|
||||
o.truncate(40);
|
||||
let o = o.trim_matches('_').to_string();
|
||||
if o.is_empty() {
|
||||
"target".into()
|
||||
} else {
|
||||
o
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
fn write_status(workdir: &Path, state: &str, extra: &str) {
|
||||
let p = workdir.join("status.json");
|
||||
let _ = std::fs::write(&p, format!("{{\"state\":\"{state}\",\"ts\":{}{}}}", now_ts(),
|
||||
if extra.is_empty() { String::new() } else { format!(",{extra}") }));
|
||||
}
|
||||
|
||||
fn prompt(q: &str, default: &str) -> String {
|
||||
use std::io::Write;
|
||||
print!(" {q}{}: ", if default.is_empty() { String::new() } else { format!(" [{default}]") });
|
||||
std::io::stdout().flush().ok();
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s).ok();
|
||||
let s = s.trim().to_string();
|
||||
if s.is_empty() { default.to_string() } else { s }
|
||||
}
|
||||
|
||||
/// Interactive wizard launched when `neurosploit` is run with no subcommand.
|
||||
async fn interactive(base: &Path) -> anyhow::Result<Cmd> {
|
||||
let lib = agents::load(base);
|
||||
let backends = harness::installed_cli_backends();
|
||||
println!("\n ┌────────────────────────────────────────────┐");
|
||||
println!(" │ NeuroSploit v3.4.1 — interactive │");
|
||||
println!(" └────────────────────────────────────────────┘");
|
||||
println!(" agents: {} · detected CLI logins: {}\n",
|
||||
lib.total(), if backends.is_empty() { "none".into() } else { backends.join(", ") });
|
||||
|
||||
let mode = prompt("Mode — (b)lack-box URL or (w)hite-box repo?", "b").to_lowercase();
|
||||
let whitebox = mode.starts_with('w');
|
||||
let target = if whitebox {
|
||||
prompt("Repository path", "/tmp/DVWA")
|
||||
} else {
|
||||
prompt("Target URL", "http://testphp.vulnweb.com/")
|
||||
};
|
||||
let model = prompt("Model (provider:model)", "anthropic:claude-opus-4-8");
|
||||
let sub = prompt("Use subscription login (no API key)? (y/n)", "y").to_lowercase().starts_with('y');
|
||||
let mcp = if whitebox { false } else {
|
||||
prompt("Use Playwright MCP browser if available? (y/n)", "y").to_lowercase().starts_with('y')
|
||||
};
|
||||
let max_agents: usize = prompt("Max agents (0 = all matching)", "5").parse().unwrap_or(5);
|
||||
let vote_n: usize = prompt("Validator votes (N)", "3").parse().unwrap_or(3);
|
||||
|
||||
let models = vec![model];
|
||||
Ok(if whitebox {
|
||||
Cmd::Whitebox { path: target, models, max_agents, vote_n, offline: false, subscription: sub, verbose: true }
|
||||
} else {
|
||||
Cmd::Run { url: target, models, max_agents, vote_n, offline: false, subscription: sub, mcp, verbose: true }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
//! Axum web dashboard for the v3.4.0 harness.
|
||||
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::Html,
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use harness::{agents, models::ModelRef, pool::ModelPool, report, types::RunConfig};
|
||||
use serde_json::{json, Value};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
struct RunState {
|
||||
log: Vec<String>,
|
||||
done: bool,
|
||||
result: Option<Value>,
|
||||
report: Option<String>,
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
base: PathBuf,
|
||||
runs: Mutex<HashMap<String, RunState>>,
|
||||
}
|
||||
|
||||
pub async fn serve(base: PathBuf, port: u16) -> anyhow::Result<()> {
|
||||
let state = Arc::new(AppState { base, runs: Mutex::new(HashMap::new()) });
|
||||
let app = Router::new()
|
||||
.route("/", get(index))
|
||||
.route("/api/info", get(info))
|
||||
.route("/api/agents", get(agents_list))
|
||||
.route("/api/models", get(models_list))
|
||||
.route("/api/run", post(run))
|
||||
.route("/api/status/:id", get(status))
|
||||
.route("/report/:id", get(report_html))
|
||||
.with_state(state);
|
||||
|
||||
let addr = format!("127.0.0.1:{port}");
|
||||
println!("NeuroSploit v3.4.0 dashboard → http://{addr}");
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||
axum::serve(listener, app).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn index() -> Html<&'static str> {
|
||||
Html(include_str!("../web/index.html"))
|
||||
}
|
||||
|
||||
async fn info(State(st): State<Arc<AppState>>) -> Json<Value> {
|
||||
let lib = agents::load(&st.base);
|
||||
let provs: Vec<Value> = harness::providers()
|
||||
.iter()
|
||||
.map(|p| json!({"key": p.key, "label": p.label, "kind": p.kind, "models": p.models}))
|
||||
.collect();
|
||||
Json(json!({
|
||||
"version": "3.4.0",
|
||||
"agents": {"vulns": lib.vulns.len(), "meta": lib.meta.len(), "recon": lib.recon.len(), "code": lib.code.len(), "total": lib.total()},
|
||||
"providers": provs,
|
||||
"cli_backends": harness::installed_cli_backends(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn agents_list(State(st): State<Arc<AppState>>) -> Json<Value> {
|
||||
let lib = agents::load(&st.base);
|
||||
let v: Vec<Value> = lib
|
||||
.vulns
|
||||
.iter()
|
||||
.chain(lib.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();
|
||||
Json(json!({ "agents": v }))
|
||||
}
|
||||
|
||||
async fn models_list() -> Json<Value> {
|
||||
let provs: Vec<Value> = harness::providers()
|
||||
.iter()
|
||||
.map(|p| json!({"key": p.key, "label": p.label, "kind": p.kind, "models": p.models}))
|
||||
.collect();
|
||||
Json(json!({ "providers": provs }))
|
||||
}
|
||||
|
||||
fn norm(u: &str) -> String {
|
||||
if u.starts_with("http") {
|
||||
u.to_string()
|
||||
} else {
|
||||
format!("https://{u}")
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(State(st): State<Arc<AppState>>, Json(body): Json<Value>) -> Json<Value> {
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
st.runs
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(id.clone(), RunState { log: vec![], done: false, result: None, report: None });
|
||||
|
||||
let st2 = st.clone();
|
||||
let id2 = id.clone();
|
||||
tokio::spawn(async move {
|
||||
let base = st2.base.clone();
|
||||
|
||||
let mut targets: Vec<String> = Vec::new();
|
||||
if let Some(arr) = body.get("targets").and_then(|v| v.as_array()) {
|
||||
for t in arr {
|
||||
if let Some(s) = t.as_str() {
|
||||
if !s.trim().is_empty() {
|
||||
targets.push(norm(s.trim()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if targets.is_empty() {
|
||||
if let Some(u) = body.get("url").and_then(|v| v.as_str()) {
|
||||
if !u.trim().is_empty() {
|
||||
targets.push(norm(u.trim()));
|
||||
}
|
||||
}
|
||||
}
|
||||
let models: Vec<String> = body
|
||||
.get("models")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|a| a.iter().filter_map(|x| x.as_str().map(|s| s.to_string())).collect())
|
||||
.unwrap_or_default();
|
||||
let vote_n = body.get("vote_n").and_then(|v| v.as_u64()).unwrap_or(3) as usize;
|
||||
let max_agents = body.get("max_agents").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
|
||||
let offline = body.get("offline").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
let 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() {
|
||||
vec![ModelRef::parse("anthropic:claude-opus-4-8")]
|
||||
} else {
|
||||
models.iter().map(|s| ModelRef::parse(s)).collect()
|
||||
};
|
||||
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();
|
||||
let idf = id2.clone();
|
||||
let fwd = tokio::spawn(async move {
|
||||
while let Some(line) = rx.recv().await {
|
||||
if let Ok(mut g) = stf.runs.lock() {
|
||||
if let Some(r) = g.get_mut(&idf) {
|
||||
r.log.push(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut all_findings = Vec::new();
|
||||
let mut all_ran = Vec::new();
|
||||
for url in &targets {
|
||||
let mut cfg = RunConfig::new(url);
|
||||
cfg.models = if models.is_empty() {
|
||||
vec!["anthropic:claude-opus-4-8".into()]
|
||||
} else {
|
||||
models.clone()
|
||||
};
|
||||
cfg.vote_n = vote_n;
|
||||
cfg.max_agents = max_agents;
|
||||
cfg.offline = offline;
|
||||
cfg.subscription = subscription;
|
||||
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);
|
||||
}
|
||||
drop(tx);
|
||||
let _ = fwd.await;
|
||||
|
||||
let report_html = report::html(targets.first().map(|s| s.as_str()).unwrap_or(""), &all_findings);
|
||||
let result = json!({"findings": all_findings, "agents_ran": all_ran, "targets": targets});
|
||||
if let Ok(mut g) = st2.runs.lock() {
|
||||
if let Some(r) = g.get_mut(&id2) {
|
||||
r.result = Some(result);
|
||||
r.report = Some(report_html);
|
||||
r.done = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Json(json!({ "run_id": id }))
|
||||
}
|
||||
|
||||
async fn status(Path(id): Path<String>, State(st): State<Arc<AppState>>) -> Json<Value> {
|
||||
let g = st.runs.lock().unwrap();
|
||||
match g.get(&id) {
|
||||
Some(r) => Json(json!({"log": r.log, "done": r.done, "result": r.result, "has_report": r.report.is_some()})),
|
||||
None => Json(json!({"error": "unknown run"})),
|
||||
}
|
||||
}
|
||||
|
||||
async fn report_html(Path(id): Path<String>, State(st): State<Arc<AppState>>) -> Html<String> {
|
||||
let g = st.runs.lock().unwrap();
|
||||
Html(g.get(&id).and_then(|r| r.report.clone()).unwrap_or_else(|| "<h1>no report</h1>".into()))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user