From f913af211d08c5841a614b8eaada91527e57b906 Mon Sep 17 00:00:00 2001 From: Joas A Santos <34966120+JoasASantos@users.noreply.github.com> Date: Mon, 3 Aug 2026 23:48:08 -0300 Subject: [PATCH] feat(3.6.6): local/uncensored llama.cpp provider, clippy clean, CI (#40) Version bump 3.6.5 -> 3.6.6. Local & uncensored models - New `llamacpp:` provider (llama-server, OpenAI-compatible, localhost:8080, no API key, CPU-only or GPU-offloaded). Override via LLAMACPP_BASE_URL; model name is the loaded gguf (pass-through). 15 -> 16 providers. - README: local/uncensored highlight, provider table + key-less note, badges. Quality - clippy clean under `-D warnings`: clamp(), sort_by_key(Reverse), struct-literal init, too_many_arguments allows, scoped await_holding_lock on the REPL blocking fallback (guard intentionally held across run().await), plus clippy --fix set. CI - examples/github-actions/ci.yml: cargo build/test/clippy -D warnings for the neurosploit-rs workspace (template, kept out of .github/workflows). Claude-Session: https://claude.ai/code/session_01QDses7zTSa9YF7pPRjphvh Co-authored-by: Claude Fable 5 --- README.md | 20 +++++-- examples/github-actions/README.md | 1 + examples/github-actions/ci.yml | 52 +++++++++++++++++++ neurosploit-rs/Cargo.lock | 4 +- neurosploit-rs/Cargo.toml | 2 +- neurosploit-rs/app/src/main.rs | 10 ++-- neurosploit-rs/app/src/repl.rs | 17 +++--- neurosploit-rs/app/src/tui.rs | 11 ++-- neurosploit-rs/crates/harness/src/hygiene.rs | 9 ++-- neurosploit-rs/crates/harness/src/models.rs | 10 +++- neurosploit-rs/crates/harness/src/pipeline.rs | 22 ++++---- neurosploit-rs/crates/harness/src/pool.rs | 34 ++++++------ neurosploit-rs/crates/harness/src/probe.rs | 2 +- neurosploit-rs/crates/harness/src/report.rs | 6 +-- 14 files changed, 137 insertions(+), 63 deletions(-) create mode 100644 examples/github-actions/ci.yml diff --git a/README.md b/README.md index 3b0a36e..05dd537 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

🧠 NeuroSploit v3.6.5

+

🧠 NeuroSploit v3.6.6

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

- + - +

@@ -108,6 +108,11 @@ Control TUI**. - πŸ“Έ **Proof screenshots in reports** β€” agents capture visual proof per finding (`evidence/-N.png`), embedded beside its vulnerability in the Typst/HTML/Markdown reports. +- πŸ–₯️ **Local, uncensored & CPU-only models** β€” `ollama:` and `llamacpp:` run the + whole engagement on your box with **no API key** and **no data leaving the + host**. `llamacpp:` speaks to a `llama-server` OpenAI-compatible endpoint + (`LLAMACPP_BASE_URL`, default localhost:8080); the `model` is whatever gguf you + loaded. Ideal for offline/air-gapped work and unfiltered offensive prompting. - πŸ•΅οΈ **Burp/ZAP proxy** β€” `/proxy ` (or `/burp`) routes agent traffic through your local intercepting proxy so you can inspect & replay in Burp. - πŸ—ΊοΈ **Attack graph & kill chain** β€” findings mapped to OWASP / CWE / MITRE @@ -412,7 +417,7 @@ export GROQ_API_KEY=... # groq:* export TOGETHER_API_KEY=... # together:* export MOONSHOT_API_KEY=... # moonshot:* (Kimi K3/K2) export OPENROUTER_API_KEY=... # openrouter:* -# ollama needs no key (local) +# ollama / llamacpp need no key (local) # then run via API (note: NO --subscription) ./target/release/neurosploit run http://testphp.vulnweb.com/ \ @@ -442,9 +447,16 @@ Or put the keys in a `.env` and source it (`cp .env.example .env`; edit; `set -a | `moonshot:` | `MOONSHOT_API_KEY` | api.moonshot.ai | | `openrouter:` | `OPENROUTER_API_KEY` | openrouter.ai | | `ollama:` | _(none)_ | localhost:11434 | +| `llamacpp:` | _(none)_ | localhost:8080 | Run `./target/release/neurosploit models` for the full provider/model list. +> **Local, uncensored & CPU-only** β€” `ollama:` and `llamacpp:` run entirely on +> your box with no API key and no data leaving the host. `llamacpp:` targets a +> [`llama-server`](https://github.com/ggml-org/llama.cpp) OpenAI-compatible +> endpoint (override with `LLAMACPP_BASE_URL`); the `model` is whatever gguf you +> loaded. Ideal for offline engagements and unfiltered offensive prompting. + #### 2) Via subscription (no API key) `--subscription` drives your local agentic-CLI login instead of an API key β€” diff --git a/examples/github-actions/README.md b/examples/github-actions/README.md index f9435c6..1db6255 100644 --- a/examples/github-actions/README.md +++ b/examples/github-actions/README.md @@ -9,6 +9,7 @@ variables β†’ Actions), or swap the `MODEL`/key for a provider you use. The buil |----------|--------------| | `neurosploit-pr-gate.yml` | Reviews every pull request and **blocks the merge** on a confirmed critical (fails the check + sets a `neurosploit/security` commit status + posts a REQUEST_CHANGES review). | | `neurosploit-mention.yml` | Comment **`@neurosploit`** on a PR/issue (writers only) to trigger a scan. Text after the mention steers it, in any language; a URL runs a black-box test, otherwise it reviews the PR. | +| `ci.yml` | Rust CI for the `neurosploit-rs/` workspace β€” `cargo build` / `test` / `clippy -D warnings` on every push & PR. | ## Enforce the PR gate as a merge block diff --git a/examples/github-actions/ci.yml b/examples/github-actions/ci.yml new file mode 100644 index 0000000..f4915a5 --- /dev/null +++ b/examples/github-actions/ci.yml @@ -0,0 +1,52 @@ +# NeuroSploit β€” Rust CI +# +# Build, test and lint the `neurosploit-rs/` Cargo workspace on every push and +# pull request. Copy to `.github/workflows/ci.yml` in your fork to enable it. +# +# Note: this lives here (not in `.github/workflows/`) as a template β€” like the +# other files in this folder β€” so the upstream repo doesn't run it on itself. + +name: ci + +on: + push: + branches: [main] + paths: ["neurosploit-rs/**", ".github/workflows/ci.yml"] + pull_request: + paths: ["neurosploit-rs/**"] + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + build-test: + name: build & test + runs-on: ubuntu-latest + defaults: + run: + working-directory: neurosploit-rs + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + run: rustup toolchain install stable --profile minimal --component clippy + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + neurosploit-rs/target + key: ${{ runner.os }}-cargo-${{ hashFiles('neurosploit-rs/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + + - name: Build + run: cargo build --workspace --locked + + - name: Test + run: cargo test --workspace --locked + + - name: Clippy + run: cargo clippy --workspace --all-targets -- -D warnings diff --git a/neurosploit-rs/Cargo.lock b/neurosploit-rs/Cargo.lock index 0147915..b4e4c3d 100644 --- a/neurosploit-rs/Cargo.lock +++ b/neurosploit-rs/Cargo.lock @@ -871,7 +871,7 @@ dependencies = [ [[package]] name = "neurosploit" -version = "3.6.5" +version = "3.6.6" dependencies = [ "anyhow", "clap", @@ -888,7 +888,7 @@ dependencies = [ [[package]] name = "neurosploit-harness" -version = "3.6.5" +version = "3.6.6" dependencies = [ "anyhow", "futures", diff --git a/neurosploit-rs/Cargo.toml b/neurosploit-rs/Cargo.toml index 3aa8cd2..df4ce3a 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.5" +version = "3.6.6" 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 6448d0c..0c3a93d 100644 --- a/neurosploit-rs/app/src/main.rs +++ b/neurosploit-rs/app/src/main.rs @@ -1,4 +1,4 @@ -//! NeuroSploit v3.6.5 β€” interactive harness + CLI (`run` / `whitebox` / `agents` / `models`). +//! NeuroSploit v3.6.6 β€” 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.5 β€” multi-model autonomous pentest harness", - long_about = "NeuroSploit v3.6.5 β€” a Rust multi-model harness that drives a pool of LLMs \ + about = "NeuroSploit v3.6.6 β€” multi-model autonomous pentest harness", + long_about = "NeuroSploit v3.6.6 β€” 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\ @@ -542,7 +542,7 @@ async fn main() -> anyhow::Result<()> { std::fs::remove_dir_all(&dest).ok(); let url = ig.authed_clone_url(&format!("https://github.com/{owner_repo}")); if run_git(&["clone", "--depth", "1", "--branch", &branch, &url, &dest.display().to_string()]).is_ok() { - let mut cfg = RunConfig::new(&dest.display().to_string()); + let mut cfg = RunConfig::new(dest.display().to_string()); cfg.subscription = subscription; cfg.verbose = verbose; if !models.is_empty() { cfg.models = models.clone(); } @@ -751,7 +751,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.5 Β· by Joas A Santos & Red Team Leaders"); + println!(" β”Œβ”€ NeuroSploit v3.6.6 Β· 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 522ef36..778f846 100644 --- a/neurosploit-rs/app/src/repl.rs +++ b/neurosploit-rs/app/src/repl.rs @@ -1,4 +1,4 @@ -//! NeuroSploit v3.6.5 β€” interactive session (Claude-Code / Codex / Cursor-CLI style). +//! NeuroSploit v3.6.6 β€” 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 @@ -361,12 +361,16 @@ impl Reader { } } +// The blocking (piped, no external printer) fallback holds the history +// MutexGuard across `run().await` on purpose β€” run() mutates that history for +// the whole async operation and no other task contends for it there. +#[allow(clippy::await_holding_lock)] pub async fn repl(base: &Path) -> anyhow::Result<()> { let lib = agents::load(base); let backends = harness::installed_cli_backends(); println!("\x1b[1m"); println!(" β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•— β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—"); - println!(" β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•— NeuroSploit v3.6.5"); + println!(" β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•— NeuroSploit v3.6.6"); println!(" β–ˆβ–ˆβ•”β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ interactive harness"); println!(" β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β• β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ by Joas A Santos"); println!(" β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β• & Red Team Leaders"); @@ -1626,12 +1630,9 @@ fn browse_results(history: &[RunRecord]) { }; print_finding_detail(&f[fi]); // Enter β†’ back to the vuln list; Esc β†’ back to the target list. - match dialoguer::Select::with_theme(&ColorfulTheme::default()) + if let Ok(None) = dialoguer::Select::with_theme(&ColorfulTheme::default()) .with_prompt("↡ back to vulnerabilities Β· Esc = back to targets") - .items(&["back"]).default(0).interact_opt() { - Ok(None) => break, - _ => {} - } + .items(&["back"]).default(0).interact_opt() { break } } } } @@ -2118,7 +2119,7 @@ fn attach_path(spec: &str, s: &mut Session) -> usize { Ok(content) => { let body = match range.and_then(parse_range) { Some((a, b)) => content.lines().enumerate() - .filter(|(i, _)| *i + 1 >= a && *i + 1 <= b) + .filter(|(i, _)| *i + 1 >= a && *i < b) .map(|(_, l)| l).collect::>().join("\n"), None => content.chars().take(8000).collect(), }; diff --git a/neurosploit-rs/app/src/tui.rs b/neurosploit-rs/app/src/tui.rs index 7d4a3aa..5fce20d 100644 --- a/neurosploit-rs/app/src/tui.rs +++ b/neurosploit-rs/app/src/tui.rs @@ -1,4 +1,4 @@ -//! NeuroSploit v3.6.5 β€” TUI "Mission Control" mode. +//! NeuroSploit v3.6.6 β€” 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: @@ -169,7 +169,7 @@ pub async fn run(base: &Path, mut cfg: RunConfig, mcp: bool, mode: Mode) -> anyh } }); - let out; + loop { // drain engagement events while let Ok(line) = rx.try_recv() { ui.ingest(line); } @@ -206,17 +206,14 @@ pub async fn run(base: &Path, mut cfg: RunConfig, mcp: bool, mode: Mode) -> anyh } } - out = (&mut task).await.unwrap_or_default(); + let out = (&mut task).await.unwrap_or_default(); // ---- restore terminal ---- execute!(stdout(), terminal::LeaveAlternateScreen)?; terminal::disable_raw_mode()?; // generate report unless discarded; print a plain summary after leaving the TUI - match harness::report::typst_report(&out.target, &out.findings, &workdir) { - Ok(p) => println!(" report β†’ {}", p.display()), - Err(_) => {} - } + if let Ok(p) = harness::report::typst_report(&out.target, &out.findings, &workdir) { println!(" report β†’ {}", p.display()) } crate::write_status_pub(&workdir, if cancel.load(Ordering::Relaxed) { "stopped" } else { "complete" }, ""); println!(" βœ“ {} validated finding(s) Β· {}", out.findings.len(), workdir.display()); Ok(()) diff --git a/neurosploit-rs/crates/harness/src/hygiene.rs b/neurosploit-rs/crates/harness/src/hygiene.rs index 3dde5fd..6cd9206 100644 --- a/neurosploit-rs/crates/harness/src/hygiene.rs +++ b/neurosploit-rs/crates/harness/src/hygiene.rs @@ -147,10 +147,11 @@ pub fn hygiene_summary(findings: &[Finding]) -> Vec { mod tests { use super::*; fn f(title: &str, sev: &str, cwe: &str, ep: &str, ev: &str, payload: &str) -> Finding { - let mut x = Finding::default(); - x.title = title.into(); x.severity = sev.into(); x.cwe = cwe.into(); - x.endpoint = ep.into(); x.evidence = ev.into(); x.payload = payload.into(); - x + Finding { + title: title.into(), severity: sev.into(), cwe: cwe.into(), + endpoint: ep.into(), evidence: ev.into(), payload: payload.into(), + ..Default::default() + } } #[test] diff --git a/neurosploit-rs/crates/harness/src/models.rs b/neurosploit-rs/crates/harness/src/models.rs index 04a0356..07a1462 100644 --- a/neurosploit-rs/crates/harness/src/models.rs +++ b/neurosploit-rs/crates/harness/src/models.rs @@ -60,6 +60,12 @@ pub fn providers() -> Vec { models: vec!["gpt-4o", "gpt-4o-mini", "gpt-5.1", "o4-mini"] }, Provider { key: "ollama", label: "Ollama (local)", base_url: "http://localhost:11434/v1", env_key: "OLLAMA_API_KEY", kind: "api", models: vec!["qwen2.5-coder:32b", "qwq:32b", "deepseek-r1:32b", "llama3.3:70b"] }, + // llama.cpp server (`llama-server`, OpenAI-compatible). Runs CPU-only or + // GPU-offloaded, fully local & uncensored β€” no API key. Point at your + // server with LLAMACPP_BASE_URL (default http://localhost:8080/v1); the + // `model` name is whatever gguf you loaded (pass-through). + Provider { key: "llamacpp", label: "llama.cpp (local)", base_url: "http://localhost:8080/v1", env_key: "LLAMACPP_API_KEY", kind: "api", + models: vec!["qwen2.5-coder-32b-instruct", "dolphin-2.9-llama3-70b", "deepseek-r1-distill-qwen-32b", "llama-3.3-70b-instruct"] }, ] } @@ -118,7 +124,7 @@ impl ChatClient { let p = provider_for(&m.provider) .ok_or_else(|| anyhow!("unknown provider '{}'", m.provider))?; let key = resolve_key(&p); - if key.is_empty() && p.key != "ollama" && p.key != "litellm" { + if key.is_empty() && p.key != "ollama" && p.key != "litellm" && p.key != "llamacpp" { let hint = if p.key == "gemini" { format!("{} (or GOOGLE_API_KEY)", p.env_key) } else { p.env_key.to_string() }; return Err(anyhow!("no API key ({}) for provider '{}'", hint, p.key)); } @@ -139,6 +145,7 @@ impl ChatClient { let base = match p.key { "litellm" => std::env::var("LITELLM_BASE_URL").unwrap_or_else(|_| p.base_url.to_string()), "ollama" => std::env::var("OLLAMA_BASE_URL").unwrap_or_else(|_| p.base_url.to_string()), + "llamacpp" => std::env::var("LLAMACPP_BASE_URL").unwrap_or_else(|_| p.base_url.to_string()), _ => p.base_url.to_string(), }; format!("{}/chat/completions", base.trim_end_matches('/')) @@ -178,6 +185,7 @@ impl ChatClient { /// When `mcp_config` is set (a path to an `.mcp.json`), Claude/Codex run with /// the MCP servers enabled and tool autonomy, so agents can actually drive /// **Playwright** (browse, execute JS, screenshot) during execution. + #[allow(clippy::too_many_arguments)] pub async fn chat_cli( &self, label: &str, diff --git a/neurosploit-rs/crates/harness/src/pipeline.rs b/neurosploit-rs/crates/harness/src/pipeline.rs index c49872e..c5b748d 100644 --- a/neurosploit-rs/crates/harness/src/pipeline.rs +++ b/neurosploit-rs/crates/harness/src/pipeline.rs @@ -375,8 +375,8 @@ fn identify_asset(p: &crate::probe::Probe) -> String { let tech = if p.tech.is_empty() { String::new() } else { format!(" [{}]", p.tech.join(", ")) }; // Prefer a KNOWN product; else the org/brand from the page; else the title. let name = product - .or_else(|| if brand.is_empty() { None } else { Some(brand) }) - .or_else(|| if title.is_empty() { None } else { Some(title) }); + .or(if brand.is_empty() { None } else { Some(brand) }) + .or(if title.is_empty() { None } else { Some(title) }); match name { Some(n) => format!("{n}{tech}"), None => if tech.is_empty() { "unidentified web asset".into() } else { format!("web asset{tech}") }, @@ -678,7 +678,7 @@ pub async fn run_greybox(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Se if !cfg.offline && !context.is_empty() { let code_cap = if cfg.max_agents > 0 { cfg.max_agents.min(lib.code.len()) } else { lib.code.len().min(12) }; let code_agents: Vec = lib.code.iter().take(code_cap).cloned().collect(); - let leads: Vec = stream::iter(code_agents.into_iter()) + let leads: Vec = stream::iter(code_agents) .map(|ag| { let ctx = context.clone(); let txc = tx.clone(); @@ -841,7 +841,7 @@ async fn attack_chain(pool: &ModelPool, cfg: &RunConfig, recon: &str, // Frontier = footholds to expand this round; start with confirmed, best-first. let mut frontier: Vec = confirmed.to_vec(); - frontier.sort_by(|a, b| sev_rank(&b.severity).cmp(&sev_rank(&a.severity))); + frontier.sort_by_key(|f| std::cmp::Reverse(sev_rank(&f.severity))); for round in 1..=max_rounds { if pool.stop_exploiting() || frontier.is_empty() { @@ -851,7 +851,7 @@ async fn attack_chain(pool: &ModelPool, cfg: &RunConfig, recon: &str, let _ = tx.send(format!("β›“ attack-chain round {round}/{max_rounds} β€” expanding {} foothold(s), {} loot item(s)", seeds.len(), loot.len())).await; let loot_snapshot = loot.clone(); - let results: Vec<(Vec, Vec)> = stream::iter(seeds.into_iter()) + let results: Vec<(Vec, Vec)> = stream::iter(seeds) .map(|seed| { let (dir, rc, rb, ls, txc) = (directives.clone(), recon_ctx.clone(), recipe_block.clone(), loot_snapshot.clone(), tx.clone()); async move { chain_from_seed(pool, &cfg.target, &dir, &rc, &rb, &seed, &ls, round, max_rounds, &txc).await } @@ -886,7 +886,7 @@ async fn attack_chain(pool: &ModelPool, cfg: &RunConfig, recon: &str, all_new.extend(validated.clone()); // Next round expands the freshly-validated footholds, best-first. frontier = validated; - frontier.sort_by(|a, b| sev_rank(&b.severity).cmp(&sev_rank(&a.severity))); + frontier.sort_by_key(|f| std::cmp::Reverse(sev_rank(&f.severity))); } if !all_new.is_empty() { let _ = tx.send(format!("β›“ attack-chaining added {} finding(s) across pivots", all_new.len())).await; @@ -896,6 +896,7 @@ async fn attack_chain(pool: &ModelPool, cfg: &RunConfig, recon: &str, /// Expand ONE foothold: the agent decides directions, does post-exploitation and /// pivots, and returns new findings + discovered loot. +#[allow(clippy::too_many_arguments)] async fn chain_from_seed(pool: &ModelPool, target: &str, directives: &str, recon_ctx: &str, recipe_block: &str, seed: &Finding, loot: &[String], round: usize, max: usize, tx: &Sender) -> (Vec, Vec) { @@ -1071,7 +1072,7 @@ fn heuristic_select(ranked: &[Agent], recon: &str, focus: &str, cap: usize) -> V (score, a) }) .collect(); - scored.sort_by(|x, y| y.0.cmp(&x.0)); + scored.sort_by_key(|x| std::cmp::Reverse(x.0)); let mut out: Vec = scored.iter().filter(|(s, _)| *s > 0).map(|(_, a)| (*a).clone()).collect(); if out.is_empty() { out = ranked.to_vec(); @@ -1082,7 +1083,7 @@ fn heuristic_select(ranked: &[Agent], recon: &str, focus: &str, cap: usize) -> V async fn validate(candidates: Vec, pool: &ModelPool, sys: &str, vote_n: usize, tx: &Sender) -> Vec { // 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.into_iter()) + let validated: Vec = stream::iter(candidates) .map(|mut f| { let txc = tx.clone(); let finder = finder.clone(); @@ -1157,6 +1158,7 @@ async fn refute_pass(findings: Vec, pool: &ModelPool, vote_n: usize, tx kept } +#[allow(clippy::too_many_arguments)] async fn finish(cfg: RunConfig, _lib: &Library, recon: String, transcript: String, mut findings: Vec, selected: Vec, rl: &mut RlState, gmode: crate::grounding::GroundMode, source_ctx: String, tx: Sender) -> RunOutput { @@ -1246,7 +1248,7 @@ async fn finish(cfg: RunConfig, _lib: &Library, recon: String, transcript: Strin let mut wm = crate::belief::WorldModel::new(); wm.deterministic = whitebox; for f in &findings { - wm.add(&f.id, crate::belief::Kind::Exploit, &f.title, f.confidence.max(0.05).min(0.99)); + wm.add(&f.id, crate::belief::Kind::Exploit, &f.title, f.confidence.clamp(0.05, 0.99)); } let unc = wm.uncertainty(None); if !findings.is_empty() { @@ -1275,7 +1277,7 @@ async fn finish(cfg: RunConfig, _lib: &Library, recon: String, transcript: Strin // confirmed high-severity bugs float to the top of selection on future runs. let mut hit: std::collections::HashMap<&str, f64> = Default::default(); for f in &findings { - let base = severity_reward(&f.severity) * f.confidence.max(0.2).min(1.0); + let base = severity_reward(&f.severity) * f.confidence.clamp(0.2, 1.0); let r = if f.review_status == "needs-review" { 0.15 } else { base }; let e = hit.entry(f.agent.as_str()).or_insert(0.0); *e = (*e + r).clamp(-1.0, 1.0); diff --git a/neurosploit-rs/crates/harness/src/pool.rs b/neurosploit-rs/crates/harness/src/pool.rs index 70c7e4a..fc9bc53 100644 --- a/neurosploit-rs/crates/harness/src/pool.rs +++ b/neurosploit-rs/crates/harness/src/pool.rs @@ -372,6 +372,23 @@ pub fn parse_verdict(text: &str) -> Verdict { Verdict::Unclear } +/// Severity-aware confirmation quorum. False High/Critical findings are the most +/// costly, so they require β‰₯2 validators AND β‰₯2/3 agreement; lower severities +/// pass on a strict majority (more than half). With only one validator available +/// (single-model panel) the majority rule applies to all severities. +pub fn quorum_confirmed(severity: &str, yes: usize, total: usize) -> bool { + if total == 0 { + return false; + } + let s = severity.to_lowercase(); + let high = s.starts_with("crit") || s.starts_with("high"); + if high && total >= 2 { + yes * 3 >= total * 2 // β‰₯ two-thirds + } else { + yes * 2 > total // strict majority + } +} + #[cfg(test)] mod verdict_tests { use super::*; @@ -405,20 +422,3 @@ mod verdict_tests { assert!(!quorum_confirmed("Low", 0, 2)); } } - -/// Severity-aware confirmation quorum. False High/Critical findings are the most -/// costly, so they require β‰₯2 validators AND β‰₯2/3 agreement; lower severities -/// pass on a strict majority (more than half). With only one validator available -/// (single-model panel) the majority rule applies to all severities. -pub fn quorum_confirmed(severity: &str, yes: usize, total: usize) -> bool { - if total == 0 { - return false; - } - let s = severity.to_lowercase(); - let high = s.starts_with("crit") || s.starts_with("high"); - if high && total >= 2 { - yes * 3 >= total * 2 // β‰₯ two-thirds - } else { - yes * 2 > total // strict majority - } -} diff --git a/neurosploit-rs/crates/harness/src/probe.rs b/neurosploit-rs/crates/harness/src/probe.rs index 8b6ca9a..f57fa8b 100644 --- a/neurosploit-rs/crates/harness/src/probe.rs +++ b/neurosploit-rs/crates/harness/src/probe.rs @@ -149,7 +149,7 @@ fn extract_brand(body: &str) -> String { // strip the marker + a year, take the first capitalised words. let cleaned = seg.replace('Β©', " ").replace("©", " "); let cleaned = cleaned.trim_start_matches(|c: char| !c.is_alphabetic()); - let name: String = cleaned.split(|c: char| c == '<' || c == '.' || c == '|' || c == '\n') + let name: String = cleaned.split(['<', '.', '|', '\n']) .next().unwrap_or("").chars().filter(|c| c.is_alphanumeric() || c.is_whitespace() || *c == '&' || *c == '-') .collect::().trim().to_string(); // drop a leading year like "2024 " diff --git a/neurosploit-rs/crates/harness/src/report.rs b/neurosploit-rs/crates/harness/src/report.rs index 290db74..dcecb83 100644 --- a/neurosploit-rs/crates/harness/src/report.rs +++ b/neurosploit-rs/crates/harness/src/report.rs @@ -232,7 +232,7 @@ fn needs_review(f: &Finding) -> bool { f.review_status == "needs-review" } /// Strip Markdown emphasis/backticks so prose renders cleanly inside Typst. fn strip_md(s: &str) -> String { - s.replace("**", "").replace('`', "").replace('*', "") + s.replace("**", "").replace(['`', '*'], "") } /// Written prose executive summary: names the asset, the counts, and the top risks. @@ -313,7 +313,7 @@ pub fn markdown(target: &str, findings: &[Finding], meta: &EngagementMeta) -> St if !meta.title.is_empty() { out.push_str(&format!("- **Page title:** {}\n", meta.title)); } if !meta.tech.is_empty() { out.push_str(&format!("- **Technology:** {}\n", meta.tech.join(", "))); } if !meta.server.is_empty() { out.push_str(&format!("- **Server:** {}\n", meta.server)); } - out.push_str("\n"); + out.push('\n'); // --- Executive summary --- out.push_str("## Executive summary\n\n"); @@ -333,7 +333,7 @@ pub fn markdown(target: &str, findings: &[Finding], meta: &EngagementMeta) -> St out.push_str(&format!("| {} | {} | {} | {} | {} | {} |\n", i + 1, f.title.replace('|', "\\|"), f.severity, owc.replace('|', "\\|"), status, auth)); } - out.push_str("\n"); + out.push('\n'); } // --- Test accounts created (from the vault cleanup finding) ---