mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-08-14 13:40:23 +02:00
v3.6.4: fix #33 — mode-aware grounding so white-box SAST findings aren't demoted
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.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<h1 align="center">🧠 NeuroSploit v3.6.3</h1>
|
||||
<h1 align="center">🧠 NeuroSploit v3.6.4</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/22624?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-22624" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/22624/daily?language=Python" alt="JoasASantos%2FNeuroSploit | Trendshift" width="250" height="55"/></a>
|
||||
@@ -12,7 +12,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/Version-3.6.3-blue?style=flat-square">
|
||||
<img src="https://img.shields.io/badge/Version-3.6.4-blue?style=flat-square">
|
||||
<img src="https://img.shields.io/badge/Harness-Rust%20%7C%20tokio-e6b673?style=flat-square">
|
||||
<img src="https://img.shields.io/badge/License-MIT-green?style=flat-square">
|
||||
<img src="https://img.shields.io/badge/MD%20Agents-417-red?style=flat-square">
|
||||
@@ -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
|
||||
|
||||
+26
-2
@@ -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
|
||||
|
||||
+6
-4
@@ -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.
|
||||
|
||||
Generated
+2
-2
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(", "));
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 /
|
||||
|
||||
@@ -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::<Vec<_>>().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<Finding>, context: &str, whitebox: bool) -> (Vec<Finding>, 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<Finding>, context: &str, mode: GroundMode) -> (Vec<Finding>, 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<Finding>, context: &str, whitebox: bool) -> (Vec<F
|
||||
findings.retain(|f| f.validated);
|
||||
(findings, demoted)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn sast_finding() -> 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<script>alert(1)</script>".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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -356,7 +356,7 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
|
||||
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 engagement: analyse a repository's source for vulnerabilities.
|
||||
@@ -418,7 +418,7 @@ pub async fn run_whitebox(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: S
|
||||
let _ = tx.send(format!("{} candidate finding(s) (deduped) — validating", candidates.len())).await;
|
||||
let findings = validate(candidates, pool, CODE_VOTE_SYS, cfg.vote_n, &tx).await;
|
||||
let findings = refute_pass(findings, pool, cfg.vote_n, &tx).await;
|
||||
finish(cfg, lib, "{}".into(), transcript, findings, selected, &mut rl, tx).await
|
||||
finish(cfg, lib, "{}".into(), transcript, findings, selected, &mut rl, crate::grounding::GroundMode::Symbolic, context, tx).await
|
||||
}
|
||||
|
||||
/// Greybox engagement: review the source code AND exploit the running app in one
|
||||
@@ -561,7 +561,7 @@ pub async fn run_greybox(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Se
|
||||
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::Either, context, tx).await
|
||||
}
|
||||
|
||||
const CHAIN_SYS: &str = "You are a post-exploitation & attack-chaining specialist. You are given ONE confirmed foothold plus any loot already gathered. DECIDE the most promising directions to expand from THIS foothold and pursue them with real tools: post-exploitation (loot credentials/tokens/keys/config/source), credential reuse, privilege escalation (horizontal AND vertical), lateral movement to adjacent services/hosts, data exfiltration, and reaching NEW attack surface the foothold exposes (e.g. SSRF→cloud metadata creds→IAM, SQLi→DB dump→credential reuse→admin, arbitrary file read→secrets→RCE, IDOR→account takeover, auth bypass→internal APIs). PROVE each escalated step with a real tool receipt. Report ONLY NEW findings beyond the input, plus any new loot you discovered (creds, tokens, hosts, internal endpoints) so later stages can reuse it. Authorized engagement; never destructive/DoS.";
|
||||
@@ -912,16 +912,28 @@ async fn refute_pass(findings: Vec<Finding>, pool: &ModelPool, vote_n: usize, tx
|
||||
}
|
||||
|
||||
async fn finish(cfg: RunConfig, _lib: &Library, recon: String, transcript: String, mut findings: Vec<Finding>,
|
||||
selected: Vec<Agent>, rl: &mut RlState, tx: Sender<String>) -> 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<Agent>, rl: &mut RlState, gmode: crate::grounding::GroundMode, source_ctx: String,
|
||||
tx: Sender<String>) -> 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<Agent> = 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<Finding>)> = 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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}}</style></head><body>\
|
||||
<h1><span class=b>NeuroSploit</span> Penetration Test Report</h1>\
|
||||
<div class=meta>Target: <b>{t}</b> · v3.6.3 Rust harness · multi-model validated</div>\
|
||||
<div class=meta>Target: <b>{t}</b> · v3.6.4 Rust harness · multi-model validated</div>\
|
||||
<div>{chips}</div>{graph_block}<h2>Findings ({n})</h2>{body}\
|
||||
<p class=meta>Authorized testing only. Findings confirmed by multi-model adversarial voting.<br>NeuroSploit v3.6.3 · by <b>Joas A Santos</b> & <b>Red Team Leaders</b></p></body></html>",
|
||||
<p class=meta>Authorized testing only. Findings confirmed by multi-model adversarial voting.<br>NeuroSploit v3.6.4 · by <b>Joas A Santos</b> & <b>Red Team Leaders</b></p></body></html>",
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user