fix(3.6.8): better Ollama error messages, empty-evidence findings go to needs-review, single-model vote warning

- models.rs: detect connection-refused and timeout on local providers
  (ollama/litellm/llamacpp), show actionable error instead of raw reqwest
- pipeline.rs: findings with empty evidence skip adversarial vote (which
  always rejects per 'default to rejected' prompt) and go straight to
  needs-review for human triage
- pipeline.rs: warn when single-model panel + vote_n=1 (same model
  validates its own findings = weaker validation)
- Bump version 3.6.7 → 3.6.8

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011739wMqPJJPttTLLX6YoQH
This commit is contained in:
CyberSecurityUP
2026-08-06 15:21:37 -03:00
co-authored by Claude Opus 4.6
parent cb19e2194d
commit a0a477a2bf
9 changed files with 66 additions and 15 deletions
+1
View File
@@ -108,3 +108,4 @@ data/repl_history.txt
# Cloned source repos (whitebox/greybox from a git URL)
repos/
neurosploit-rs/repos/
target/
+17 -2
View File
@@ -1,11 +1,26 @@
# NeuroSploit v3.6.7 — Release Notes
# NeuroSploit v3.6.8 — Release Notes
**Release Date:** August 2026
**Codename:** Chain & Exploit
**License:** MIT
**Credits:** Joas A Santos & Red Team Leaders
## Highlights
## v3.6.8 — Bugfix: Ollama error handling, empty-evidence validation, single-model warnings
- **Better Ollama/local provider error messages.** Connection-refused and timeout
errors now name the provider, URL, and suggest checking if the server is running.
Previously showed raw reqwest errors.
- **Empty-evidence findings skip the vote and go straight to `needs-review`.**
Findings with no evidence are unverifiable by the adversarial validator (which
always rejects "no evidence" per its system prompt). Now they bypass the vote
and are flagged for human review instead of being silently dropped.
- **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).
---
## v3.6.7 Highlights
- **CVE exploitation pipeline — 4 new agents.** `cve_version_fingerprint` (pin
exact versions) → `cve_research_analyst` (map to NVD/GHSA, judge reachability) →
+2 -2
View File
@@ -871,7 +871,7 @@ dependencies = [
[[package]]
name = "neurosploit"
version = "3.6.7"
version = "3.6.8"
dependencies = [
"anyhow",
"clap",
@@ -888,7 +888,7 @@ dependencies = [
[[package]]
name = "neurosploit-harness"
version = "3.6.7"
version = "3.6.8"
dependencies = [
"anyhow",
"futures",
+1 -1
View File
@@ -3,7 +3,7 @@ members = ["crates/harness", "app"]
resolver = "2"
[workspace.package]
version = "3.6.7"
version = "3.6.8"
edition = "2021"
license = "MIT"
repository = "https://github.com/JoasASantos/NeuroSploit"
+4 -4
View File
@@ -1,4 +1,4 @@
//! NeuroSploit v3.6.7 — interactive harness + CLI (`run` / `whitebox` / `agents` / `models`).
//! NeuroSploit v3.6.8 — interactive harness + CLI (`run` / `whitebox` / `agents` / `models`).
mod repl;
mod tui;
@@ -11,8 +11,8 @@ use std::path::{Path, PathBuf};
#[command(
name = "neurosploit",
version,
about = "NeuroSploit v3.6.7 — multi-model autonomous pentest harness",
long_about = "NeuroSploit v3.6.7 — a Rust multi-model harness that drives a pool of LLMs \
about = "NeuroSploit v3.6.8 — multi-model autonomous pentest harness",
long_about = "NeuroSploit v3.6.8 — 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\
@@ -765,7 +765,7 @@ pub(crate) fn spawn_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, mode:
println!(" │ ua : {ua}");
write_status(&workdir, "running", &format!("\"target\":{:?}", cfg.target));
println!(" ┌─ NeuroSploit v3.6.7 · by Joas A Santos & Red Team Leaders");
println!(" ┌─ NeuroSploit v3.6.8 · by Joas A Santos & Red Team Leaders");
println!(" │ run id : {run_id}");
println!(" │ target : {}", cfg.target);
println!(" │ models : {}", cfg.models.join(", "));
+2 -2
View File
@@ -1,4 +1,4 @@
//! NeuroSploit v3.6.7 — interactive session (Claude-Code / Codex / Cursor-CLI style).
//! NeuroSploit v3.6.8 — interactive session (Claude-Code / Codex / Cursor-CLI style).
//!
//! Launched when `neurosploit` runs with no subcommand. A persistent REPL with
//! real line editing (arrow-key history recall, Ctrl-A/E/K, paste), model
@@ -370,7 +370,7 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
let backends = harness::installed_cli_backends();
println!("\x1b[1m");
println!(" ███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗");
println!(" ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗ NeuroSploit v3.6.7");
println!(" ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗ NeuroSploit v3.6.8");
println!(" ██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║ interactive harness");
println!(" ██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║ by Joas A Santos");
println!(" ██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝ & Red Team Leaders");
+1 -1
View File
@@ -1,4 +1,4 @@
//! NeuroSploit v3.6.7 — TUI "Mission Control" mode.
//! NeuroSploit v3.6.8 — TUI "Mission Control" mode.
//!
//! Concurrent panels that update live while the engagement runs in the
//! background, with a composer input that stays active during execution:
+14 -1
View File
@@ -163,7 +163,20 @@ impl ChatClient {
if !key.is_empty() {
if azure { req = req.header("api-key", &key); } else { req = req.bearer_auth(&key); }
}
let resp = req.send().await?;
let resp = req.send().await.map_err(|e| {
if e.is_connect() {
let local = matches!(p.key, "ollama" | "litellm" | "llamacpp");
if local {
anyhow!("{} connection refused at {} — is the server running? ({})", p.key, url, e)
} else {
anyhow!("{} connection error: {}", p.key, e)
}
} else if e.is_timeout() {
anyhow!("{} request timed out (120s) for model '{}' — model may be too large for available memory", p.key, m.model)
} else {
anyhow!("{} request error: {}", p.key, e)
}
})?;
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
if !status.is_success() {
+24 -2
View File
@@ -619,6 +619,9 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
let transcript = transcript_of(&raw);
let candidates = dedup_findings(raw.iter().flat_map(|(_, _, f)| f.clone()).collect());
let _ = tx.send(format!("{} candidate finding(s) (deduped) — validating by {}-model vote", candidates.len(), cfg.vote_n)).await;
if pool.candidates.len() == 1 && cfg.vote_n <= 1 {
let _ = tx.send("⚠ single-model panel with vote_n=1 — validation is weaker (same model validates its own findings). Consider --vote-n 2 or adding a second model for cross-validation.".into()).await;
}
// ---- 4. Validate by N-model voting ---------------------------------
let mut findings = validate(candidates, pool, VOTE_SYS, cfg.vote_n, &tx).await;
@@ -1148,9 +1151,26 @@ fn heuristic_select(ranked: &[Agent], recon: &str, focus: &str, cap: usize) -> V
}
async fn validate(candidates: Vec<Finding>, pool: &ModelPool, sys: &str, vote_n: usize, tx: &Sender<String>) -> Vec<Finding> {
// Fast-track: findings with no evidence are unverifiable — skip the vote
// and flag for human review instead of wasting a validator call that will
// always reject ("default to rejected when uncertain" + empty evidence).
let (have_evidence, no_evidence): (Vec<_>, Vec<_>) = candidates.into_iter().partition(|f| {
let e = f.evidence.trim();
!e.is_empty() && e != "N/A" && e != "n/a" && e != "none" && e != "-"
});
let mut flagged: Vec<Finding> = no_evidence.into_iter().map(|mut f| {
f.validated = false;
f.review_status = "needs-review".into();
f.review_reason = "no concrete evidence provided by agent — manual verification required".into();
f.votes = "0/0".into();
f
}).collect();
for f in &flagged {
let _ = tx.send(format!("vote {} → needs-review (no evidence)", f.title)).await;
}
// Prefer a model other than the primary (likely finder) to adjudicate.
let finder = pool.candidates.first().map(|m| m.label());
let validated: Vec<Finding> = stream::iter(candidates)
let validated: Vec<Finding> = stream::iter(have_evidence)
.map(|mut f| {
let txc = tx.clone();
let finder = finder.clone();
@@ -1184,7 +1204,9 @@ async fn validate(candidates: Vec<Finding>, pool: &ModelPool, sys: &str, vote_n:
.collect()
.await;
// Keep confirmed AND needs-review (human decides); drop only zero-support noise.
validated.into_iter().filter(|f| f.validated || f.review_status == "needs-review").collect()
// Include no-evidence flagged findings so the human loop sees them.
flagged.extend(validated.into_iter().filter(|f| f.validated || f.review_status == "needs-review"));
flagged
}
/// Adversarial refutation pass: every confirmed **High/Critical** finding is