From 78653e45cd83d5c710cdd64867f6c1ea2fd50a36 Mon Sep 17 00:00:00 2001 From: CyberSecurityUP Date: Wed, 24 Jun 2026 21:43:24 -0300 Subject: [PATCH] =?UTF-8?q?v3.5.1:=20live=20findings=20feed=20+=20?= =?UTF-8?q?=F0=9F=94=94=20notifications=20+=20automatic=20partial=20summar?= =?UTF-8?q?y?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Live findings feed: each candidate is surfaced (✦ possible finding [sev] title @ endpoint) the moment an agent returns it, not only at the end. - 🔔 notifications in the feed: evidence saved, phase complete (with severity breakdown = automatic partial summary). Renderer styles notify/finding tags. Co-Authored-By: Claude Opus 4.8 (1M context) --- neurosploit-rs/app/src/main.rs | 4 +++- neurosploit-rs/crates/harness/src/pipeline.rs | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/neurosploit-rs/app/src/main.rs b/neurosploit-rs/app/src/main.rs index 856bae8..81343d7 100644 --- a/neurosploit-rs/app/src/main.rs +++ b/neurosploit-rs/app/src/main.rs @@ -437,10 +437,12 @@ fn render_line(raw: &str) { } } let (tag, rest) = match line.split_once(": ") { - Some((t, r)) if matches!(t, "exec" | "danger" | "read" | "edit" | "tool" | "net" | "ai" | "plan" | "tokens") => (t, r), + Some((t, r)) if matches!(t, "exec" | "danger" | "read" | "edit" | "tool" | "net" | "ai" | "plan" | "tokens" | "notify" | "finding") => (t, r), _ => ("", line), }; match tag { + "notify" => println!(" \x1b[1;36m🔔 {}\x1b[0m", rest.trim()), + "finding" => println!(" \x1b[1;33m✦ possible finding\x1b[0m {who}{}", rest.trim()), "exec" => card(&format!("{who}⌘ command"), rest, "\x1b[33m"), "danger" => card(&format!("{who}⚠ DANGEROUS command"), rest, "\x1b[1;31m"), "read" => state("📄", "reading", &format!("{who}{rest}"), "\x1b[34m"), diff --git a/neurosploit-rs/crates/harness/src/pipeline.rs b/neurosploit-rs/crates/harness/src/pipeline.rs index 5dc7c83..c3d1dbb 100644 --- a/neurosploit-rs/crates/harness/src/pipeline.rs +++ b/neurosploit-rs/crates/harness/src/pipeline.rs @@ -181,6 +181,10 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender { let f = extract_findings(&text, &ag.name); let _ = txc.send(format!("exploit {} via {} → {} candidate(s)", ag.name, m.label(), f.len())).await; + // Live findings feed: surface each candidate the moment it appears. + for c in &f { + let _ = txc.send(format!("finding: [{}] {} @ {}", c.severity, c.title, c.endpoint)).await; + } (ag.name.clone(), text, f) } Err(e) => { @@ -647,8 +651,17 @@ async fn finish(cfg: RunConfig, _lib: &Library, recon: String, transcript: Strin let artifacts = persist(&cfg, &recon, &transcript, &findings); if !artifacts.is_empty() { + let _ = tx.send(format!("notify: evidence saved → {}", cfg.workdir.clone().unwrap_or_default())).await; let _ = tx.send(format!("artifacts saved: {}", artifacts.join(", "))).await; } + // Automatic partial summary (phase complete). + { + let mut by: std::collections::BTreeMap<&str, usize> = Default::default(); + for f in &findings { *by.entry(f.severity.as_str()).or_insert(0) += 1; } + let sev = if by.is_empty() { "none".to_string() } + else { by.iter().map(|(k, v)| format!("{k}:{v}")).collect::>().join(" ") }; + let _ = tx.send(format!("notify: phase complete — {} validated finding(s) [{}]", findings.len(), sev)).await; + } RunOutput { target: cfg.target.clone(),