From a61e75b601ec3d6974ed364c7a55763b8b314991 Mon Sep 17 00:00:00 2001 From: CyberSecurityUP Date: Sun, 19 Jul 2026 17:48:19 -0300 Subject: [PATCH] =?UTF-8?q?v3.6.4:=20fix=20#33=20=E2=80=94=20mode-aware=20?= =?UTF-8?q?grounding=20so=20white-box=20SAST=20findings=20aren't=20demoted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The grounding gate ran in empirical mode for every engagement, demoting white-box (and skills/n8n audit) findings that had passed the n-model vote because a file:line code citation isn't raw tool output. Grounding is now mode-aware: - Symbolic (white-box SAST / skills): a file:line reference into the reviewed source, or a quote of code present in it, is the receipt β€” no live target. - Empirical (black-box / host / AI): evidence must resemble tool output (as before). - Either (grey-box): a source citation OR a tool receipt grounds a finding. The symbolic check runs against the reviewed source corpus (not the transcript) and falls back to a structural file:line + quote check when the corpus is unavailable. Adds unit tests incl. a regression test for #33. --- README.md | 31 +-- RELEASE.md | 28 ++- TUTORIAL.md | 10 +- neurosploit-rs/Cargo.lock | 4 +- neurosploit-rs/Cargo.toml | 2 +- neurosploit-rs/app/src/main.rs | 8 +- neurosploit-rs/app/src/repl.rs | 4 +- neurosploit-rs/app/src/tui.rs | 2 +- neurosploit-rs/crates/harness/src/belief.rs | 2 +- .../crates/harness/src/grounding.rs | 208 ++++++++++++++---- neurosploit-rs/crates/harness/src/lib.rs | 2 +- neurosploit-rs/crates/harness/src/pipeline.rs | 40 ++-- neurosploit-rs/crates/harness/src/pomdp.rs | 2 +- neurosploit-rs/crates/harness/src/probe.rs | 2 +- neurosploit-rs/crates/harness/src/report.rs | 6 +- 15 files changed, 260 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index b8c36a8..6f43ab4 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

🧠 NeuroSploit v3.6.3

+

🧠 NeuroSploit v3.6.4

JoasASantos%2FNeuroSploit | Trendshift @@ -12,7 +12,7 @@

- + @@ -28,16 +28,15 @@ > > πŸ“– **New here? Read the [full Tutorial & User Guide β†’](TUTORIAL.md)** β€” every mode, flag, config and example explained. -> πŸ†• **New in v3.6.3 β€” resumable interrupted runs + crash-proof browsing:** -> If a run is interrupted (terminal closed, Ctrl-C, crash), **`/continue` now -> relaunches it** on the same target and **carries the recovered findings -> forward** β€” the offer appears right at launch. And opening **`/results`**, -> **`/finding`** or **`/report`** while a run streams no longer corrupts the -> terminal: live output is paused for the picker (still captured in `/logs`) and -> restored on exit. *(v3.6.2 added live Codex tool-by-tool streaming + the full -> activity feed in `/logs`/`/status`; v3.6.1 added GPT-5.6 sol/terra/luna and -> the Codex exit-1 fix; v3.6.0 added AI-agent/LLM/MCP/Skills/n8n testing + -> onboarding wizard + Cloud scope.)* +> πŸ†• **New in v3.6.4 β€” white-box findings no longer wrongly demoted ([#33](https://github.com/JoasASantos/NeuroSploit/issues/33)):** +> The grounding gate ran in **empirical** mode for **every** engagement, so +> white-box SAST & skills-audit findings β€” whose evidence is a `file:line` code +> citation, not HTTP/tool output β€” were demoted as "receipt missing" and dropped +> from the report even after passing the n-model vote. Grounding is now +> **mode-aware**: *symbolic* (`file:line` into the reviewed source) for +> white-box/skills, *empirical* for black-box/host/AI, *either* for grey-box. +> *(v3.6.3 added resumable interrupted runs + crash-proof mid-run browsing; +> v3.6.2 live Codex tool-by-tool streaming; v3.6.1 GPT-5.6 sol/terra/luna.)* > *(v3.5.4 added robust attack chaining + false-positive reduction; v3.5.3 > GitHub/GitLab/Jira **[integrations](TUTORIAL-INTEGRATION.md)**; v3.5.2 the DEPTH > doctrine + report-hygiene β€” see [RELEASE.md](RELEASE.md).)* @@ -71,9 +70,11 @@ Control TUI**. and "scan more vs exploit now" falls out of belief entropy. The `may_assert` gate is a **mathematical anti-hallucination rule** (don't claim exploitability while the belief is diffuse). -- 🧾 **Grounding** β€” hard rule: **no claim without a tool receipt** (raw tool - output, not paraphrase). Empirical for black-box, symbolic (`file:line`) for - white-box; ungrounded claims are demoted. +- 🧾 **Grounding** β€” hard rule: **no claim without a receipt** (evidence, not + paraphrase). Empirical (raw tool output) for black-box/host/AI, **symbolic** + (`file:line` into the reviewed source β€” a code citation *is* the receipt) for + white-box SAST & skills audits, and **either** for grey-box; ungrounded claims + are demoted. - πŸ”¬ **Deterministic HTTP probe** β€” before the model recon, the harness runs a **real** request/response analysis (status/redirects, security headers, cookie flags, CORS reflection, tech fingerprint, linked JS, 404 baseline, high-signal diff --git a/RELEASE.md b/RELEASE.md index b0b9d85..cfb6c83 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,7 +1,7 @@ -# NeuroSploit v3.6.3 β€” Release Notes +# NeuroSploit v3.6.4 β€” Release Notes **Release Date:** July 2026 -**Codename:** Resume & Recover +**Codename:** Symbolic Grounding **License:** MIT **Credits:** Joas A Santos & Red Team Leaders @@ -9,6 +9,30 @@ ## Highlights +- **Fix ([#33](https://github.com/JoasASantos/NeuroSploit/issues/33)): white-box + findings were silently dropped from the report.** The grounding gate β€” the + anti-hallucination step that demotes any claim lacking a receipt β€” was running + in **empirical** mode for *every* engagement. Empirical grounding looks for raw + tool output (HTTP responses, error oracles, shell receipts), which a **SAST + finding never has**: its receipt is a `file:line` reference into the reviewed + source. So white-box (and skills/n8n audit) findings that had *passed* the + n-model vote were then demoted as "receipt missing" and never reported. + Grounding is now **mode-aware**: + - **Symbolic** β€” white-box SAST & skills audits: a `file:line` (or + `file:section`) reference into the reviewed source, or a quote of code that + appears in it, IS the receipt. No live target needed. + - **Empirical** β€” black-box / host / AI endpoints: evidence must resemble raw + tool output (unchanged behaviour). + - **Either** β€” grey-box: a source citation OR a tool receipt grounds a finding. + The symbolic check is run against the reviewed **source corpus** (not the model + transcript), and falls back to a structural `file:line` + code-quote check when + the corpus isn't available, so a well-formed SAST finding is never dropped on a + technicality. Covered by unit tests (including a regression test for #33). + +--- + +## Previously in v3.6.3 + - **Interrupted runs are resumable.** When a run is cut off (terminal closed, Ctrl-C, crash, SSH drop), its findings were already checkpointed live and recovered as a run on the next launch. Now `/continue` (or `/resume`) also diff --git a/TUTORIAL.md b/TUTORIAL.md index 222c87e..fea5fec 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -1,4 +1,4 @@ -# NeuroSploit β€” Tutorial & User Guide (v3.6.3) +# NeuroSploit β€” Tutorial & User Guide (v3.6.4) A complete, hands-on guide to installing, configuring and running NeuroSploit β€” the autonomous, multi-model penetration-testing harness. @@ -98,7 +98,7 @@ Agents **degrade gracefully**: if `rustscan` is absent they use `nmap`; if neith ### Verify ```bash -neurosploit --version # neurosploit 3.6.3 +neurosploit --version # neurosploit 3.6.4 neurosploit agents # {"vulns":196,...,"chains":12,"total":417} neurosploit models # all providers & models ``` @@ -522,8 +522,10 @@ NeuroSploit treats the target as **partially observable** (a POMDP): entropy: when a node's belief is diffuse, recon is worth more than exploiting. - **Anti-hallucination gate** (`may_assert`) β€” the agent may **not** claim exploitability while the belief is diffuse; it must observe more first. -- **Grounding** β€” **no claim without a tool receipt**: empirical for black-box - (real HTTP/OOB/error output), symbolic (`file:line`) for white-box. Ungrounded +- **Grounding** β€” **no claim without a receipt**: *empirical* for black-box / + host / AI (real HTTP/OOB/error output), *symbolic* for white-box SAST & skills + audits (a `file:line` reference into the reviewed source β€” the code citation is + the receipt, no live target needed), and *either* for grey-box. Ungrounded claims are demoted and flagged. - **Chaining** β€” confirmed findings are chained into deeper impact, each stage proven before advancing. diff --git a/neurosploit-rs/Cargo.lock b/neurosploit-rs/Cargo.lock index 2510894..c132296 100644 --- a/neurosploit-rs/Cargo.lock +++ b/neurosploit-rs/Cargo.lock @@ -871,7 +871,7 @@ dependencies = [ [[package]] name = "neurosploit" -version = "3.6.3" +version = "3.6.4" dependencies = [ "anyhow", "clap", @@ -888,7 +888,7 @@ dependencies = [ [[package]] name = "neurosploit-harness" -version = "3.6.3" +version = "3.6.4" dependencies = [ "anyhow", "futures", diff --git a/neurosploit-rs/Cargo.toml b/neurosploit-rs/Cargo.toml index 2ef74dc..2c34120 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.3" +version = "3.6.4" 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 f21e6e1..362d358 100644 --- a/neurosploit-rs/app/src/main.rs +++ b/neurosploit-rs/app/src/main.rs @@ -1,4 +1,4 @@ -//! NeuroSploit v3.6.3 β€” interactive harness + CLI (`run` / `whitebox` / `agents` / `models`). +//! NeuroSploit v3.6.4 β€” 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.3 β€” multi-model autonomous pentest harness", - long_about = "NeuroSploit v3.6.3 β€” a Rust multi-model harness that drives a pool of LLMs \ + about = "NeuroSploit v3.6.4 β€” multi-model autonomous pentest harness", + long_about = "NeuroSploit v3.6.4 β€” 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\ @@ -721,7 +721,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.3 Β· by Joas A Santos & Red Team Leaders"); + println!(" β”Œβ”€ NeuroSploit v3.6.4 Β· 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 8f346a0..44eff4c 100644 --- a/neurosploit-rs/app/src/repl.rs +++ b/neurosploit-rs/app/src/repl.rs @@ -1,4 +1,4 @@ -//! NeuroSploit v3.6.3 β€” interactive session (Claude-Code / Codex / Cursor-CLI style). +//! NeuroSploit v3.6.4 β€” 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 @@ -357,7 +357,7 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> { let backends = harness::installed_cli_backends(); println!("\x1b[1m"); println!(" β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•— β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—"); - println!(" β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•— NeuroSploit v3.6.3"); + println!(" β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•— NeuroSploit v3.6.4"); 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 762fcea..dee5b55 100644 --- a/neurosploit-rs/app/src/tui.rs +++ b/neurosploit-rs/app/src/tui.rs @@ -1,4 +1,4 @@ -//! NeuroSploit v3.6.3 β€” TUI "Mission Control" mode. +//! NeuroSploit v3.6.4 β€” 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/belief.rs b/neurosploit-rs/crates/harness/src/belief.rs index 4da71c6..540b4e1 100644 --- a/neurosploit-rs/crates/harness/src/belief.rs +++ b/neurosploit-rs/crates/harness/src/belief.rs @@ -1,4 +1,4 @@ -//! POMDP belief-state world model (v3.6.3). +//! POMDP belief-state world model (v3.6.4). //! //! The target is only partially observable, so we don't track booleans β€” we //! track a **belief**: a property graph whose nodes (host / service / vuln / diff --git a/neurosploit-rs/crates/harness/src/grounding.rs b/neurosploit-rs/crates/harness/src/grounding.rs index 288e408..cebe9da 100644 --- a/neurosploit-rs/crates/harness/src/grounding.rs +++ b/neurosploit-rs/crates/harness/src/grounding.rs @@ -1,20 +1,36 @@ -//! Verification / grounding engine (v3.6.3). +//! Verification / grounding engine (v3.6.4). //! -//! Hard rule: **no claim enters the world model without a tool receipt** β€” raw -//! tool output, not the LLM's paraphrase. This is the empirical anti-hallucination -//! anchor that complements the POMDP belief gate: +//! Hard rule: **no claim enters the world model without a receipt** β€” evidence, +//! not the LLM's bare assertion. This is the anti-hallucination anchor that +//! complements the POMDP belief gate. What counts as a receipt depends on the +//! engagement, so grounding runs in one of three modes: //! -//! - **Black-box**: grounding is empirical β€” the finding's evidence must look -//! like raw tool output (an HTTP response, an OOB callback, an error oracle), -//! not prose. -//! - **White-box**: grounding is symbolic β€” a file:line reference into the -//! reviewed source (reachability/taint), checked against the collected context. +//! - **Empirical** (black-box / host / AI-endpoint): the finding's evidence must +//! look like raw tool output (an HTTP response, an OOB callback, an error +//! oracle, a shell receipt) β€” not prose. +//! - **Symbolic** (white-box SAST / skills audit): the receipt is a `file:line` +//! (or `file:section`) reference into the reviewed source, or a quote of code +//! that actually appears in it. There is NO live target to hit, so requiring an +//! HTTP-style receipt here is wrong β€” a code citation IS the receipt. +//! - **Either** (grey-box): both worlds are present (source review + a running +//! app), so a finding is grounded if it has a symbolic OR an empirical receipt. //! //! Ungrounded claims are flagged (`receipt_missing`) so the reward layer can //! penalize them (the "claim without receipt" term). use crate::types::Finding; +/// How a finding must be grounded, per engagement type. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GroundMode { + /// Black-box / host / AI endpoint: evidence must resemble raw tool output. + Empirical, + /// White-box SAST / skills audit: evidence must reference the reviewed source. + Symbolic, + /// Grey-box: accept either a source citation or an empirical receipt. + Either, +} + /// Verdict of grounding a single finding. pub struct Grounded { pub ok: bool, @@ -35,47 +51,91 @@ fn looks_empirical(evidence: &str) -> bool { } /// White-box: evidence should reference a source location present in `context`. +/// `context` is the reviewed SOURCE (not the model transcript). When the source +/// context is unavailable, fall back to structural checks so a well-formed +/// `file:line` + code quote still grounds (a SAST finding must never be silently +/// dropped just because the caller couldn't supply the corpus). fn looks_symbolic(f: &Finding, context: &str) -> bool { - // endpoint like file.ext:line, and the file appears in the reviewed source. - let loc = &f.endpoint; - if let Some((file, _)) = loc.rsplit_once(':') { - let base = file.rsplit('/').next().unwrap_or(file); - if !base.is_empty() && context.contains(base) { - return true; + let loc = f.endpoint.trim(); + // A file:line / file:section reference is the canonical symbolic receipt. + let has_file_ref = loc.rsplit_once(':') + .map(|(file, tail)| { + let base = file.rsplit(['/', '\\']).next().unwrap_or(file); + // looks like a path/file (has an extension or a separator) and a + // line/section follows β€” i.e. not a "host:port" style endpoint. + !base.is_empty() + && (base.contains('.') || file.contains('/')) + && !tail.trim().is_empty() + }) + .unwrap_or(false); + + if !context.is_empty() { + // Strongest: the referenced file actually appears in the reviewed source. + if let Some((file, _)) = loc.rsplit_once(':') { + let base = file.rsplit(['/', '\\']).next().unwrap_or(file); + if !base.is_empty() && context.contains(base) { + return true; + } } - } - // or the evidence quotes code that is actually in the context - !f.evidence.trim().is_empty() - && f.evidence.split_whitespace().take(6).collect::>().join(" ") + // Or the evidence quotes a distinctive code token present in the source. + let quote_matches = f.evidence .split_whitespace() .filter(|t| t.len() > 4 && context.contains(*t)) - .count() - >= 2 -} - -/// Ground a finding. `context` is the reviewed source for white-box (empty for -/// black-box). Returns whether it has a valid receipt and of what kind. -pub fn ground(f: &Finding, context: &str, whitebox: bool) -> Grounded { - if whitebox && !context.is_empty() { - if looks_symbolic(f, context) { - return Grounded { ok: true, kind: "symbolic", reason: "source location/quote matches reviewed code".into() }; + .count(); + if quote_matches >= 2 { + return true; } - return Grounded { ok: false, kind: "missing", reason: "no source reference into reviewed code".into() }; + // Source is present but neither the file nor a quote matched β†’ still + // accept a well-formed file:line ref with quoted evidence, since the + // bounded corpus may simply not include the referenced file. + return has_file_ref && f.evidence.trim().len() >= 12; } - if looks_empirical(&f.evidence) { - Grounded { ok: true, kind: "empirical", reason: "evidence resembles raw tool output".into() } - } else { - Grounded { ok: false, kind: "missing", reason: "evidence is paraphrase, not a tool receipt".into() } + + // No source corpus available: ground on a well-formed file:line reference + // backed by non-trivial quoted evidence. + has_file_ref && f.evidence.trim().len() >= 12 +} + +/// Ground a finding under `mode`. `context` is the reviewed SOURCE for symbolic/ +/// either modes (empty for pure empirical). Returns whether it has a valid +/// receipt and of what kind. +pub fn ground(f: &Finding, context: &str, mode: GroundMode) -> Grounded { + let symbolic = || looks_symbolic(f, context); + let empirical = || looks_empirical(&f.evidence); + match mode { + GroundMode::Symbolic => { + if symbolic() { + Grounded { ok: true, kind: "symbolic", reason: "source location/quote matches reviewed code".into() } + } else { + Grounded { ok: false, kind: "missing", reason: "no source reference (file:line) into reviewed code".into() } + } + } + GroundMode::Either => { + if symbolic() { + Grounded { ok: true, kind: "symbolic", reason: "source location/quote matches reviewed code".into() } + } else if empirical() { + Grounded { ok: true, kind: "empirical", reason: "evidence resembles raw tool output".into() } + } else { + Grounded { ok: false, kind: "missing", reason: "no source reference nor tool receipt".into() } + } + } + GroundMode::Empirical => { + if empirical() { + Grounded { ok: true, kind: "empirical", reason: "evidence resembles raw tool output".into() } + } else { + Grounded { ok: false, kind: "missing", reason: "evidence is paraphrase, not a tool receipt".into() } + } + } } } -/// Apply the grounding gate to a finding set. Ungrounded findings are flagged -/// (receipt recorded in `votes`) and demoted to unvalidated so they never get -/// reported as confirmed. Returns (kept, demoted_count). -pub fn gate(mut findings: Vec, context: &str, whitebox: bool) -> (Vec, usize) { +/// Apply the grounding gate to a finding set under `mode`. Ungrounded findings +/// are flagged (receipt recorded in `votes`) and demoted to unvalidated so they +/// never get reported as confirmed. Returns (kept, demoted_count). +pub fn gate(mut findings: Vec, context: &str, mode: GroundMode) -> (Vec, usize) { let mut demoted = 0; for f in findings.iter_mut() { - let g = ground(f, context, whitebox); + let g = ground(f, context, mode); if !g.ok { f.validated = false; f.votes = format!("{} Β· receipt_missing", f.votes); @@ -85,3 +145,73 @@ pub fn gate(mut findings: Vec, context: &str, whitebox: bool) -> (Vec Finding { + // A typical SAST finding: file:line endpoint + a code quote as evidence, + // and NO HTTP/tool-output markers (there is no live target to hit). + Finding { + title: "SQL injection via string-formatted query".into(), + severity: "High".into(), + cwe: "CWE-89".into(), + endpoint: "src/db/users.py:42".into(), + evidence: "query = \"SELECT * FROM users WHERE id = \" + request.args.get('id')".into(), + validated: true, + confidence: 0.8, + ..Default::default() + } + } + + #[test] + fn sast_finding_grounds_symbolically_against_source() { + let src = "def get(id):\n query = \"SELECT * FROM users WHERE id = \" + request.args.get('id')\n"; + assert!(ground(&sast_finding(), src, GroundMode::Symbolic).ok, + "a file:line SAST finding whose code appears in the source must ground"); + } + + #[test] + fn sast_finding_grounds_even_without_source_corpus() { + // Regression: the whitebox gate used to run in EMPIRICAL mode (bug #33), + // demoting every SAST finding because code quotes lack HTTP-style markers. + // A well-formed file:line + quoted evidence must ground on its own. + assert!(ground(&sast_finding(), "", GroundMode::Symbolic).ok, + "SAST finding must not be demoted for lacking a tool receipt"); + } + + #[test] + fn symbolic_rejects_bare_prose() { + let f = Finding { endpoint: "the login flow".into(), + evidence: "The application seems insecure.".into(), validated: true, ..Default::default() }; + assert!(!ground(&f, "", GroundMode::Symbolic).ok, + "prose with no source reference must NOT ground symbolically"); + } + + #[test] + fn empirical_still_requires_tool_output() { + // Black-box unchanged: a code quote is not an empirical receipt. + assert!(!ground(&sast_finding(), "", GroundMode::Empirical).ok); + let http = Finding { + endpoint: "https://t/login".into(), + evidence: "HTTP/1.1 200 OK\nset-cookie: sid=1; \nserver: nginx\n".into(), + validated: true, ..Default::default() }; + assert!(ground(&http, "", GroundMode::Empirical).ok); + } + + #[test] + fn either_accepts_symbolic_or_empirical() { + assert!(ground(&sast_finding(), "", GroundMode::Either).ok, "grey-box accepts a source citation"); + } + + #[test] + fn gate_keeps_grounded_and_demotes_prose() { + let good = sast_finding(); + let bad = Finding { title: "vibes".into(), endpoint: "somewhere".into(), + evidence: "looks bad".into(), validated: true, ..Default::default() }; + let (kept, demoted) = gate(vec![good, bad], "", GroundMode::Symbolic); + assert_eq!(kept.len(), 1); + assert_eq!(demoted, 1); + } +} diff --git a/neurosploit-rs/crates/harness/src/lib.rs b/neurosploit-rs/crates/harness/src/lib.rs index 1380af7..ab5e4dc 100644 --- a/neurosploit-rs/crates/harness/src/lib.rs +++ b/neurosploit-rs/crates/harness/src/lib.rs @@ -1,4 +1,4 @@ -//! NeuroSploit v3.6.3 harness β€” a robust multi-model runtime for the +//! NeuroSploit v3.6.4 harness β€” a robust multi-model runtime for the //! markdown-driven autonomous pentest engine. //! //! The harness loads the `agents_md/` library, drives a *pool* of LLM models diff --git a/neurosploit-rs/crates/harness/src/pipeline.rs b/neurosploit-rs/crates/harness/src/pipeline.rs index fe00ffc..eb91dfe 100644 --- a/neurosploit-rs/crates/harness/src/pipeline.rs +++ b/neurosploit-rs/crates/harness/src/pipeline.rs @@ -356,7 +356,7 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender, pool: &ModelPool, vote_n: usize, tx } async fn finish(cfg: RunConfig, _lib: &Library, recon: String, transcript: String, mut findings: Vec, - selected: Vec, rl: &mut RlState, tx: Sender) -> RunOutput { - // --- Grounding gate: no claim without a tool receipt (anti-hallucination) --- - // White/grey carry source context; black-box is verified empirically. - let whitebox = cfg.repo.is_some() && cfg.target.starts_with('/'); + selected: Vec, rl: &mut RlState, gmode: crate::grounding::GroundMode, source_ctx: String, + tx: Sender) -> RunOutput { + use crate::grounding::GroundMode; + // --- Grounding gate: no claim without a receipt (anti-hallucination) --- + // The receipt is empirical (tool output) for black-box, symbolic (file:line + // into the reviewed source) for white-box SAST / skills audits, or either for + // grey-box. Symbolic grounding is checked against the SOURCE corpus, not the + // model transcript, so a code citation is honoured as its own receipt. + let ground_ctx = if source_ctx.is_empty() { transcript.as_str() } else { source_ctx.as_str() }; let before = findings.len(); - let (kept, demoted) = crate::grounding::gate(findings, &transcript, whitebox); + let (kept, demoted) = crate::grounding::gate(findings, ground_ctx, gmode); findings = kept; if demoted > 0 { - let _ = tx.send(format!("grounding gate: demoted {demoted}/{before} ungrounded claim(s) (no tool receipt)")).await; + let receipt = match gmode { + GroundMode::Symbolic => "no source reference", + GroundMode::Either => "no source reference nor tool receipt", + GroundMode::Empirical => "no tool receipt", + }; + let _ = tx.send(format!("grounding gate: demoted {demoted}/{before} ungrounded claim(s) ({receipt})")).await; } + // White-box/skills are symbolic β†’ deterministic belief; grey-box carries source too. + let whitebox = matches!(gmode, GroundMode::Symbolic | GroundMode::Either); // --- v3.5.2 report-hygiene & exploitation-depth pass --- // Calibrate inflated/unproven High-Critical to Medium, flag exposures that @@ -1277,7 +1289,7 @@ pub async fn run_host(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sende findings.extend(chained); findings = dedup_findings(findings); let findings = refute_pass(findings, pool, cfg.vote_n, &tx).await; - finish(cfg, lib, recon, transcript, findings, selected, &mut rl, tx).await + finish(cfg, lib, recon, transcript, findings, selected, &mut rl, crate::grounding::GroundMode::Empirical, String::new(), tx).await } /// AI-red-team doctrine prepended to every AI/LLM/agent test prompt. @@ -1397,7 +1409,7 @@ pub async fn run_ai(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender< let mut rl = cfg.rl_path.as_ref().map(|p| RlState::load(Path::new(p))).unwrap_or_default(); if cfg.offline { let _ = tx.send("offline: no AI exploitation performed".into()).await; - return finish(cfg, lib, recon, String::new(), vec![], agents, &mut rl, tx).await; + return finish(cfg, lib, recon, String::new(), vec![], agents, &mut rl, crate::grounding::GroundMode::Empirical, String::new(), tx).await; } let cap = if cfg.max_agents > 0 { cfg.max_agents.min(agents.len()) } else { agents.len() }; let selected: Vec = agents.into_iter().take(cap).collect(); @@ -1444,7 +1456,7 @@ pub async fn run_ai(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender< findings.extend(chained); findings = dedup_findings(findings); let findings = refute_pass(findings, pool, cfg.vote_n, &tx).await; - finish(cfg, lib, recon, transcript, findings, selected, &mut rl, tx).await + finish(cfg, lib, recon, transcript, findings, selected, &mut rl, crate::grounding::GroundMode::Empirical, String::new(), tx).await } /// White-box Skills/plugin audit: read the skill .md file or a folder of them and @@ -1463,7 +1475,7 @@ pub async fn run_skills_audit(cfg: RunConfig, lib: &Library, pool: &ModelPool, t let mut rl = cfg.rl_path.as_ref().map(|p| RlState::load(Path::new(p))).unwrap_or_default(); if cfg.offline || context.is_empty() { let _ = tx.send("offline or empty skills input β€” nothing audited".into()).await; - return finish(cfg, lib, "{}".into(), String::new(), vec![], agents, &mut rl, tx).await; + return finish(cfg, lib, "{}".into(), String::new(), vec![], agents, &mut rl, crate::grounding::GroundMode::Symbolic, String::new(), tx).await; } let directives = operator_directives(&cfg); let raw: Vec<(String, String, Vec)> = stream::iter(agents.iter().cloned()) @@ -1494,5 +1506,5 @@ pub async fn run_skills_audit(cfg: RunConfig, lib: &Library, pool: &ModelPool, t let transcript = transcript_of(&raw); let candidates = dedup_findings(raw.iter().flat_map(|(_, _, f)| f.clone()).collect()); let findings = validate(candidates, pool, CODE_VOTE_SYS, cfg.vote_n, &tx).await; - finish(cfg, lib, "{}".into(), transcript, findings, agents, &mut rl, tx).await + finish(cfg, lib, "{}".into(), transcript, findings, agents, &mut rl, crate::grounding::GroundMode::Symbolic, context, tx).await } diff --git a/neurosploit-rs/crates/harness/src/pomdp.rs b/neurosploit-rs/crates/harness/src/pomdp.rs index f290b1f..8f4312e 100644 --- a/neurosploit-rs/crates/harness/src/pomdp.rs +++ b/neurosploit-rs/crates/harness/src/pomdp.rs @@ -1,4 +1,4 @@ -//! POMDP decision layer (v3.6.3): value-of-information planning + the +//! POMDP decision layer (v3.6.4): value-of-information planning + the //! anti-hallucination gate. //! //! The choice "scan more vs exploit now" is **not** a heuristic here β€” it falls diff --git a/neurosploit-rs/crates/harness/src/probe.rs b/neurosploit-rs/crates/harness/src/probe.rs index ae3d3de..be1c2d9 100644 --- a/neurosploit-rs/crates/harness/src/probe.rs +++ b/neurosploit-rs/crates/harness/src/probe.rs @@ -1,4 +1,4 @@ -//! Deterministic HTTP request/response analysis (v3.6.3). +//! Deterministic HTTP request/response analysis (v3.6.4). //! //! Before the LLM recon runs, the harness performs a **real** probe of the //! target and captures observed facts β€” status, headers, security headers, diff --git a/neurosploit-rs/crates/harness/src/report.rs b/neurosploit-rs/crates/harness/src/report.rs index 3bbce24..3926f29 100644 --- a/neurosploit-rs/crates/harness/src/report.rs +++ b/neurosploit-rs/crates/harness/src/report.rs @@ -97,9 +97,9 @@ pub fn html(target: &str, findings: &[Finding]) -> String { h4{{margin:12px 0 3px;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:#8b5cf6}}\ .b{{color:#8b5cf6;font-weight:800}}\

NeuroSploit Penetration Test Report

\ -
Target: {t} Β· v3.6.3 Rust harness Β· multi-model validated
\ +
Target: {t} Β· v3.6.4 Rust harness Β· multi-model validated
\
{chips}
{graph_block}

Findings ({n})

{body}\ -

Authorized testing only. Findings confirmed by multi-model adversarial voting.
NeuroSploit v3.6.3 Β· by Joas A Santos & Red Team Leaders

", +

Authorized testing only. Findings confirmed by multi-model adversarial voting.
NeuroSploit v3.6.4 Β· by Joas A Santos & Red Team Leaders

", t = esc(target), chips = chips, n = sorted.len(), body = body, graph_block = graph_block, ) } @@ -135,7 +135,7 @@ pub fn typst_report(target: &str, findings: &[Finding], dir: &Path) -> std::io:: let mut data = String::new(); data.push_str(&format!( "#let meta = (target: {}, run_id: {}, generated: {}, model: {})\n", - tq(target), tq(&run_id), tq("NeuroSploit v3.6.3"), tq("multi-model") + tq(target), tq(&run_id), tq("NeuroSploit v3.6.4"), tq("multi-model") )); data.push_str("#let findings = (\n"); for f in sorted_findings(findings) {