From e956b482b9e8d27f02fe3cb2f653196baf356f8f Mon Sep 17 00:00:00 2001 From: CyberSecurityUP Date: Fri, 7 Aug 2026 09:51:50 -0300 Subject: [PATCH] fix(3.6.8): JSON parse resilience + diagnostics for local model failures - extract_findings: log when model output has no JSON (was silent drop) - extract_findings: auto-fix trailing-comma JSON (common LLM mistake) - pipeline: emit response tail when agent returns 0 parseable findings - Helps diagnose why small/local models produce 0 findings on valid targets Co-Authored-By: Claude Opus 4.6 --- RELEASE.md | 7 ++++++ neurosploit-rs/crates/harness/src/pipeline.rs | 23 +++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index b2ef237..6d8f7d2 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -17,6 +17,13 @@ - **Single-model + vote_n=1 warning.** When only one model is configured and vote_n is 1, the pipeline emits a warning that validation is weaker (same model validates its own findings). +- **JSON parse resilience for local models.** `extract_findings` now logs when a + model returns text but no parseable JSON (previously silent drop — 0 findings + with no diagnostic). Also auto-fixes trailing-comma JSON (`[...,]`) which small + models commonly produce. +- **Visible diagnostics when agents return 0 findings.** Pipeline emits the + response tail so the operator can see what the model actually returned (helps + debug model quality issues with local/small models). --- diff --git a/neurosploit-rs/crates/harness/src/pipeline.rs b/neurosploit-rs/crates/harness/src/pipeline.rs index 8261869..61760b8 100644 --- a/neurosploit-rs/crates/harness/src/pipeline.rs +++ b/neurosploit-rs/crates/harness/src/pipeline.rs @@ -598,6 +598,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; + if f.is_empty() && !text.trim().is_empty() && text.trim() != "[]" { + let tail: String = text.chars().rev().take(120).collect::().chars().rev().collect(); + let _ = txc.send(format!("⚠ agent {} returned text but 0 parseable findings (model may have produced malformed JSON). Tail: {:?}", ag.name, tail)).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; @@ -1458,12 +1462,27 @@ fn extract_findings(text: &str, agent: &str) -> Vec { (Some(a), Some(b)) if b > a => &text[a..=b], _ => match (text.find('{'), text.rfind('}')) { (Some(a), Some(b)) if b > a => &text[a..=b], - _ => return vec![], + _ => { + if !text.trim().is_empty() && text.trim() != "[]" { + eprintln!("[extract_findings] agent {agent}: model returned text but no JSON array/object found (len={}); raw tail: {:?}", + text.len(), &text[text.len().saturating_sub(200)..]); + } + return vec![]; + } }, }; let val: serde_json::Value = match serde_json::from_str(slice) { Ok(v) => v, - Err(_) => return vec![], + Err(e) => { + eprintln!("[extract_findings] agent {agent}: JSON parse failed: {e}; slice head: {:?}", + &slice[..slice.len().min(300)]); + // Attempt to salvage: strip trailing comma before ] (common LLM mistake) + let fixed = slice.replace(",]", "]").replace(",}", "}"); + match serde_json::from_str(&fixed) { + Ok(v) => v, + Err(_) => return vec![], + } + } }; let items: Vec = match val { serde_json::Value::Array(a) => a,