diff --git a/.gitignore b/.gitignore index a37e537..7b06be9 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,4 @@ data/repl_history.txt # Cloned source repos (whitebox/greybox from a git URL) repos/ neurosploit-rs/repos/ +target/ diff --git a/RELEASE.md b/RELEASE.md index b26570f..b2ef237 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -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) → diff --git a/neurosploit-rs/Cargo.lock b/neurosploit-rs/Cargo.lock index 0eff1d0..7f000d8 100644 --- a/neurosploit-rs/Cargo.lock +++ b/neurosploit-rs/Cargo.lock @@ -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", diff --git a/neurosploit-rs/Cargo.toml b/neurosploit-rs/Cargo.toml index dede2a4..fcb863b 100644 --- a/neurosploit-rs/Cargo.toml +++ b/neurosploit-rs/Cargo.toml @@ -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" diff --git a/neurosploit-rs/app/src/main.rs b/neurosploit-rs/app/src/main.rs index a153bea..6bd45ce 100644 --- a/neurosploit-rs/app/src/main.rs +++ b/neurosploit-rs/app/src/main.rs @@ -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(", ")); diff --git a/neurosploit-rs/app/src/repl.rs b/neurosploit-rs/app/src/repl.rs index f769275..c1a69d1 100644 --- a/neurosploit-rs/app/src/repl.rs +++ b/neurosploit-rs/app/src/repl.rs @@ -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"); diff --git a/neurosploit-rs/app/src/tui.rs b/neurosploit-rs/app/src/tui.rs index 06dcc45..0d615d1 100644 --- a/neurosploit-rs/app/src/tui.rs +++ b/neurosploit-rs/app/src/tui.rs @@ -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: diff --git a/neurosploit-rs/crates/harness/src/models.rs b/neurosploit-rs/crates/harness/src/models.rs index 07a1462..ef7dc04 100644 --- a/neurosploit-rs/crates/harness/src/models.rs +++ b/neurosploit-rs/crates/harness/src/models.rs @@ -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() { diff --git a/neurosploit-rs/crates/harness/src/pipeline.rs b/neurosploit-rs/crates/harness/src/pipeline.rs index f809a5c..8261869 100644 --- a/neurosploit-rs/crates/harness/src/pipeline.rs +++ b/neurosploit-rs/crates/harness/src/pipeline.rs @@ -619,6 +619,9 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender V } async fn validate(candidates: Vec, pool: &ModelPool, sys: &str, vote_n: usize, tx: &Sender) -> Vec { + // 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 = 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 = stream::iter(candidates) + let validated: Vec = stream::iter(have_evidence) .map(|mut f| { let txc = tx.clone(); let finder = finder.clone(); @@ -1184,7 +1204,9 @@ async fn validate(candidates: Vec, 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