v3.5.1: live findings feed + 🔔 notifications + automatic partial summary

- 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) <noreply@anthropic.com>
This commit is contained in:
CyberSecurityUP
2026-06-24 21:43:24 -03:00
co-authored by Claude Opus 4.8
parent a8676fee0a
commit 78653e45cd
2 changed files with 16 additions and 1 deletions
@@ -181,6 +181,10 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
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;
// 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::<Vec<_>>().join(" ") };
let _ = tx.send(format!("notify: phase complete — {} validated finding(s) [{}]", findings.len(), sev)).await;
}
RunOutput {
target: cfg.target.clone(),