mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-07-23 11:10:56 +02:00
fix(repl): show recon/probe activity + don't let the idle guardrail kill recon
Symptom: with a non-streaming subscription CLI (codex), a long/intense recon
showed nothing in the feed ("phase starting") and the 5-min idle guardrail killed
the run before any agent ran.
- render_compact now SHOWS recon/probe/ai-recon/skills-audit/loaded/running lines
(were dropped) so a long recon no longer looks frozen.
- Idle guardrail reworked: resets on ANY streamed activity (not only new
findings) and only ARMS after exploitation starts (agent launch / vote) — recon
can never trip it. Message: "no activity in N min".
- RunLive.ingest sets phase=recon on recon/probe lines (was stuck at "starting").
This commit is contained in:
@@ -1076,10 +1076,16 @@ pub(crate) fn render_compact(raw: &str) -> Option<String> {
|
||||
"ai" => return None, // skip verbose model chatter in background feed
|
||||
_ => {
|
||||
let low = line.to_lowercase();
|
||||
if low.contains("recon complete") { "\x1b[36m 🔍 recon complete\x1b[0m".into() }
|
||||
// Recon / probe activity — SHOW it so a long recon (esp. via a
|
||||
// non-streaming CLI like codex) doesn't look frozen.
|
||||
if low.starts_with("probe:") { format!("\x1b[36m 🔎 {}\x1b[0m", trunc1(line, 130)) }
|
||||
else if low.contains("recon complete") { "\x1b[36m 🔍 recon complete\x1b[0m".into() }
|
||||
else if low.starts_with("recon") || low.starts_with("ai-recon") || low.contains("recon round") || low.contains("intensity") { format!("\x1b[36m 🔍 {}\x1b[0m", trunc1(line, 130)) }
|
||||
else if low.starts_with("skills audit") || low.starts_with("ai engagement") { format!("\x1b[36m 🤖 {}\x1b[0m", trunc1(line, 130)) }
|
||||
else if low.starts_with("loaded ") || low.starts_with("running ") { format!("\x1b[36m 🧭 {}\x1b[0m", trunc1(line, 130)) }
|
||||
else if low.contains("selected") && low.contains("agent") { format!("\x1b[36m 🧭 {}\x1b[0m", trunc1(line, 110)) }
|
||||
else if low.starts_with("vote") && low.contains("confirmed") { format!("\x1b[1;32m ✓ {}\x1b[0m", trunc1(line, 110)) }
|
||||
else if low.starts_with("exploit") || low.starts_with("test ") || low.contains("launching agent") { format!("\x1b[35m 🧪 {}\x1b[0m", trunc1(line, 110)) }
|
||||
else if low.starts_with("exploit") || low.starts_with("test ") || low.starts_with("ai ") || low.starts_with("skill ") || low.contains("launching agent") { format!("\x1b[35m 🧪 {}\x1b[0m", trunc1(line, 110)) }
|
||||
else if low.starts_with("vote") { format!("\x1b[2m · {}\x1b[0m", trunc1(line, 110)) }
|
||||
else if low.contains("fail") || low.contains("error") { format!("\x1b[31m ✗ {}\x1b[0m", trunc1(line, 110)) }
|
||||
else { return None; }
|
||||
|
||||
@@ -50,7 +50,7 @@ impl RunLive {
|
||||
let low = line.to_lowercase();
|
||||
if low.contains("token/quota exhausted") || low.contains("run is paused") { self.phase = "paused (quota)".into(); }
|
||||
else if low.contains("resumed — retrying") { self.phase = "exploiting".into(); }
|
||||
else if low.contains("recon complete") { self.phase = "recon".into(); }
|
||||
else if low.starts_with("recon") || low.starts_with("ai-recon") || low.contains("recon round") || low.contains("intensity") || low.starts_with("probe:") { self.phase = "recon".into(); }
|
||||
else if low.contains("selected") && low.contains("agent") {
|
||||
self.phase = "planning".into();
|
||||
if let Some(n) = line.split_whitespace().find_map(|t| t.parse::<usize>().ok()) { self.agents = n; }
|
||||
@@ -1051,9 +1051,10 @@ async fn start_background(base: &Path, s: &Session, reader: &mut Reader,
|
||||
tokio::spawn(async move {
|
||||
let crate::Spawned { task, mut rx, workdir, .. } = sp;
|
||||
let mut last_saved = 0usize;
|
||||
let mut last_find = Instant::now(); // time of the last NEW finding
|
||||
let mut last_activity = Instant::now(); // last sign of PROGRESS (any activity)
|
||||
let mut idle_fired = false;
|
||||
let mut tool_events = 0usize; // exec/net/read/browser activity seen
|
||||
let mut exploiting = false; // guardrail only arms once exploitation starts
|
||||
let mut ticker = tokio::time::interval(std::time::Duration::from_secs(15));
|
||||
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||
loop {
|
||||
@@ -1061,14 +1062,20 @@ async fn start_background(base: &Path, s: &Session, reader: &mut Reader,
|
||||
maybe = rx.recv() => {
|
||||
let Some(line) = maybe else { break };
|
||||
live2.lock().unwrap().ingest(&line);
|
||||
let low = line.to_lowercase();
|
||||
if line.contains("exec:") || line.contains("net:") || line.contains("read:") || line.contains("browser") { tool_events += 1; }
|
||||
// ANY streamed line is progress → reset the idle clock (a long
|
||||
// recon or active tool use must NOT count as idle).
|
||||
last_activity = Instant::now();
|
||||
// Exploitation has begun once agents launch / vote — only then arm the guardrail.
|
||||
if low.contains("launching agent") || low.starts_with("exploit ") || low.starts_with("test ")
|
||||
|| low.starts_with("ai ") || low.starts_with("skill ") || low.starts_with("vote") { exploiting = true; }
|
||||
if let Some(out) = crate::render_compact(&line) { let _ = printer.print(out); }
|
||||
// Checkpoint on each new finding; also resets the idle clock.
|
||||
// Checkpoint on each new finding.
|
||||
let snap = {
|
||||
let l = live2.lock().unwrap();
|
||||
if l.full.len() != last_saved {
|
||||
last_saved = l.full.len();
|
||||
last_find = Instant::now();
|
||||
Some(LiveCheckpoint {
|
||||
target: l.target.clone(), mode: l.mode.into(), phase: l.phase.clone(),
|
||||
workdir: workdir.display().to_string(),
|
||||
@@ -1079,15 +1086,16 @@ async fn start_background(base: &Path, s: &Session, reader: &mut Reader,
|
||||
if let Some(c) = snap { save_checkpoint(&c); }
|
||||
}
|
||||
_ = ticker.tick() => {
|
||||
// Idle guardrail: no NEW finding within the window → soft-stop
|
||||
// (stop launching exploit agents, validate what was found).
|
||||
if idle_secs > 0 && !idle_fired && last_find.elapsed().as_secs() >= idle_secs
|
||||
// Idle guardrail: only after exploitation started AND no activity
|
||||
// (not just no finding) within the window → soft-stop & validate.
|
||||
// Recon never trips it — it streams progress lines that reset the clock.
|
||||
if idle_secs > 0 && !idle_fired && exploiting && last_activity.elapsed().as_secs() >= idle_secs
|
||||
&& !soft_task.load(Ordering::Relaxed) && !cancel_task.load(Ordering::Relaxed) {
|
||||
idle_fired = true;
|
||||
*choice2.lock().unwrap() = StopMode::Validate;
|
||||
soft_task.store(true, Ordering::Relaxed);
|
||||
let _ = printer.print(format!(
|
||||
"\x1b[33m⏹ idle guardrail: no new finding in {} min — stopping & validating what was found\x1b[0m",
|
||||
"\x1b[33m⏹ idle guardrail: no activity in {} min — stopping & validating what was found\x1b[0m",
|
||||
idle_secs / 60));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user