diff --git a/README.md b/README.md index b4d48fe..01e5fdc 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

🧠 NeuroSploit v4.2.1

+

🧠 NeuroSploit v4.2.2

Stars @@ -8,7 +8,7 @@

- + @@ -52,7 +52,22 @@ Control TUI**. ### Highlights -> **New in v4.2.1** β€” **SARIF 2.1.0 export**: every run now writes `report.sarif` +> **New in v4.2.2** β€” **free, LLM-directed exploration**: an exploit agent's named +> class is a starting point, not a cage β€” it maps what the app actually does and +> reports any class it can prove, with **authentication / identity** (login, signup, +> password reset, MFA, OAuth/OIDC/SAML, JWT, session) as a first-class target and +> business-logic / multi-step flows pursued on its own judgment; agent **selection** +> now covers the surface instead of collapsing into one family. **WAF-aware User-Agent** +> (`/ua browser`) uses a realistic browser UA for accuracy behind a CDN while keeping +> attribution in the `X-NeuroSploit-Scan` header. **Importable engagement configs**: +> `/authorize ` sets the whole scope in one line (no bug-bounty program needed), +> `/scope-file ` imports scope **and** target/models/focus/classes from one file; +> `/class idor,sqli,xss,ssrf` focuses a run on vuln classes. Plus **PoC/evidence files +> synthesized from recorded evidence** even on the API-key path (empty `pocs/`Β·`evidence/` +> fixed), **model-refusal detection** (a declined technique is reported as such, not a +> parse error), and version strings read from the build so they never go stale. +> +> **Also in v4.2.x** β€” **SARIF 2.1.0 export**: every run now writes `report.sarif` > next to the Markdown/JSON/HTML/PDF, and `neurosploit sarif ` (re)emits it > on demand, so findings drop straight into GitHub / Azure DevOps code-scanning > as severity-coloured, CWE-linked alerts (also exposed over MCP). Plus stronger diff --git a/TUTORIAL.md b/TUTORIAL.md index c1e2dd9..9b261c4 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -1,4 +1,4 @@ -# NeuroSploit β€” Tutorial & User Guide (v4.2.1) +# NeuroSploit β€” Tutorial & User Guide (v4.2.2) A complete, hands-on guide to installing, configuring and running NeuroSploit β€” the autonomous, multi-model penetration-testing harness. @@ -100,7 +100,7 @@ Agents **degrade gracefully**: if `rustscan` is absent they use `nmap`; if neith ### Verify ```bash -neurosploit --version # neurosploit 4.2.1 +neurosploit --version # neurosploit 4.2.2 neurosploit agents # {"vulns":255,...,"ai":30,...,"total":480} neurosploit models # all providers & models ``` diff --git a/neurosploit-rs/Cargo.lock b/neurosploit-rs/Cargo.lock index ee16cc6..213de3a 100644 --- a/neurosploit-rs/Cargo.lock +++ b/neurosploit-rs/Cargo.lock @@ -940,7 +940,7 @@ dependencies = [ [[package]] name = "neurosploit" -version = "4.2.1" +version = "4.2.2" dependencies = [ "anyhow", "clap", @@ -957,7 +957,7 @@ dependencies = [ [[package]] name = "neurosploit-harness" -version = "4.2.1" +version = "4.2.2" dependencies = [ "anyhow", "base64", diff --git a/neurosploit-rs/Cargo.toml b/neurosploit-rs/Cargo.toml index 6e08880..230bfd5 100644 --- a/neurosploit-rs/Cargo.toml +++ b/neurosploit-rs/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/harness", "app"] resolver = "2" [workspace.package] -version = "4.2.1" +version = "4.2.2" 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 dd1e5de..78e1300 100644 --- a/neurosploit-rs/app/src/main.rs +++ b/neurosploit-rs/app/src/main.rs @@ -13,8 +13,8 @@ use std::path::{Path, PathBuf}; #[command( name = "neurosploit", version, - about = "NeuroSploit v4.2.1 β€” multi-model autonomous pentest harness", - long_about = "NeuroSploit v4.2.1 β€” a Rust multi-model harness that drives a pool of LLMs \ + about = "NeuroSploit v4.2.2 β€” multi-model autonomous pentest harness", + long_about = "NeuroSploit v4.2.2 β€” a Rust multi-model harness that drives a pool of LLMs \ (API key or local subscription: Claude/Codex/Gemini/Grok/OpenCode/Hermes) 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\ diff --git a/neurosploit-rs/app/src/repl.rs b/neurosploit-rs/app/src/repl.rs index ccf306c..08e3971 100644 --- a/neurosploit-rs/app/src/repl.rs +++ b/neurosploit-rs/app/src/repl.rs @@ -148,7 +148,7 @@ struct LiveCheckpoint { pub(crate) const ACCEPTED: &[&str] = &[ "/?", "/agents", "/attach", "/audit", "/auth", "/burp", "/cap", "/capability", "/chain", "/changed", "/clear", "/config", "/context", "/continue", "/creds", "/diff", "/exclude", "/exit", "/expand", "/feed", - "/finding", "/findings", "/focus", "/forget", "/full", "/go", "/goal", "/graph", "/guardrail", "/guardrails", "/help", + "/pocs", "/poc", "/evidence", "/artifacts", "/finding", "/findings", "/focus", "/forget", "/full", "/go", "/goal", "/graph", "/guardrail", "/guardrails", "/help", "/history", "/idle", "/inscope", "/instructions", "/integration", "/integrations", "/key", "/log", "/authorize", "/grant", "/inscope-set", "/scope-file", "/scopefile", "/import-scope", "/authorization", "/authz", "/program", "/logs", "/mcp", "/memory", "/model", "/models", "/objective", "/objectives", "/observe", "/observe-only", "/offline", @@ -164,7 +164,7 @@ const COMMANDS: &[&str] = &[ "/help", "/onboard", "/show", "/config", "/providers", "/model", "/key", "/sub", "/target", "/scope-file", "/authorization", "/class", "/repo", "/auth", "/creds", "/focus", "/objective", "/scope-out", "/attach", "/context", "/mcp", "/offline", "/class", "/research", "/quick", "/economy", "/eco", "/votes", "/chain", "/recon", "/tempmail", "/timeout", "/proxy", "/burp", "/ua", "/agents", "/only", "/theme", "/clear", "/run", "/stop", "/pause", "/continue", "/runs", "/results", "/report", - "/status", "/logs", "/diff", "/retest", "/validate", "/finding", "/expand", "/integrations", + "/status", "/logs", "/diff", "/retest", "/validate", "/finding", "/pocs", "/expand", "/integrations", "/memory", "/forget", "/graph", "/inscope", "/observe", "/guardrail", "/policy", "/capability", "/audit", "/quit", ]; @@ -1277,6 +1277,31 @@ pub async fn repl(base: &Path, auth: SessionAuth) -> anyhow::Result<()> { if live_now { println!(" \x1b[2m(run still streaming in background β€” /logs for what happened while browsing)\x1b[0m"); } } } + "/pocs" | "/poc" | "/evidence" | "/artifacts" => { + // List the PoC and evidence artifacts a run produced (synthesized + // or agent-written), with their paths so they can be retrieved. + let h = history.lock().unwrap(); + let rec = if arg.trim().is_empty() { h.last() } else { pick(&h, arg) }; + match rec { + None => println!(" no run yet β€” /runs to list, or /pocs after a run"), + Some(r) if r.workdir.is_empty() => println!(" run #{} has no workdir on record", r.id), + Some(r) => { + let dir = std::path::Path::new(&r.workdir); + let mut any = false; + for (sub, label) in [("pocs", "PoC scripts"), ("evidence", "evidence")] { + let p = dir.join(sub); + let mut files: Vec = std::fs::read_dir(&p).map(|rd| rd.filter_map(|e| e.ok()).map(|e| e.file_name().to_string_lossy().to_string()).collect()).unwrap_or_default(); + files.sort(); + if !files.is_empty() { + any = true; + println!(" \x1b[1m{label}\x1b[0m ({}):", p.display()); + for f in files { println!(" {}", p.join(&f).display()); } + } + } + if !any { println!(" no pocs/ or evidence/ files for run #{} ({})", r.id, dir.display()); } + } + } + } "/finding" | "/findings" => { // Build the finding pool: live run if active, else a past run. let pool: Vec = match &active { diff --git a/neurosploit-rs/crates/harness/src/pipeline.rs b/neurosploit-rs/crates/harness/src/pipeline.rs index 2b0f7f9..de0154e 100644 --- a/neurosploit-rs/crates/harness/src/pipeline.rs +++ b/neurosploit-rs/crates/harness/src/pipeline.rs @@ -1935,26 +1935,35 @@ async fn validate(candidates: Vec, pool: &ModelPool, sys: &str, vote_n: f.review_status = "needs-review".into(); f.review_reason = format!("voter rejected the narrative, mechanic retained: {}", f.review_reason.trim()); f.confidence = f.confidence.min(0.5); - } else if grounded_receipt(&f) { - // Unanimously rejected, but the MECHANISM was demonstrated β€” - // a real engagement rejected "no rate limiting on the reset - // flow" because the agent claimed email flooding and only - // proved that 25 requests went through unthrottled. The - // claim was inflated; the measurement was real, and - // discarding it hid a genuine gap from the report. - // - // So an over-claimed finding is capped and flagged rather - // than deleted: the reader gets the fact, not the story - // that was built on it. + } else if grounded_receipt(&f) || has_http_receipt(&f) { + // Unanimously rejected by the opinion-vote, but the finding + // carries a concrete, REPRODUCIBLE receipt (a captured HTTP + // response, a header/cookie the class is proven by, or a + // file:line citation). Another person can reproduce this + // exact finding with one request β€” so it is NOT dropped. + // The adversarial validator is tuned to reject low-impact and + // "theoretical" issues, but "the response lacks HSTS" or "the + // cookie has no Secure flag" is a FACT, not a story. We cap + // the severity to what the receipt alone proves and flag it + // for human review, never delete it. let cap = "Low"; let was = f.severity.clone(); - f.severity = cap.to_string(); + if sev_rank(&f.severity) < sev_rank(cap) { f.severity = cap.to_string(); } f.review_status = "needs-review".into(); - f.review_reason = format!( - "impact not demonstrated β€” capped from {was} to {cap}. Validator: {}", - f.review_reason.trim() - ); - f.confidence = f.confidence.min(0.5); + f.review_reason = if was == f.severity { + format!("kept β€” reproducible receipt present; impact not independently demonstrated. Validator: {}", f.review_reason.trim()) + } else { + format!("kept & capped from {was} to {} β€” reproducible receipt present, impact not demonstrated. Validator: {}", f.severity, f.review_reason.trim()) + }; + f.confidence = f.confidence.max(0.3).min(0.6); + } + // Reproducibility: a kept finding that reaches a URL endpoint but + // carries no explicit repro steps gets a minimal, pasteable one, + // so another person can reproduce the exact finding. + if (f.validated || f.review_status == "needs-review") + && f.repro_steps.is_empty() + && (f.endpoint.starts_with("http://") || f.endpoint.starts_with("https://")) { + f.repro_steps = vec![format!("curl -i -s '{}' # inspect the response (status + headers + body) that proves this finding", f.endpoint)]; } let label = if f.validated { "CONFIRMED" } else if f.review_status == "needs-review" { "needs-review" } else { "rejected" }; let _ = txc.send(format!("vote {} β†’ {} ({})", f.title, label, f.votes)).await; @@ -2154,9 +2163,39 @@ fn grounded_receipt(f: &Finding) -> bool { if f.review_reason.contains("receipt_missing") || f.votes.contains("receipt_missing") { return false; } + if has_http_receipt(f) { + return true; + } crate::grounding::ground(f, "", crate::grounding::GroundMode::Either).ok } +/// True when the finding's evidence carries a concrete, reproducible HTTP +/// receipt β€” a captured response (status line / headers / Set-Cookie) or a +/// `file:line` code citation. This is the test for "another person can +/// reproduce this exact finding": if the proof is the response itself (a missing +/// security header, an insecure cookie flag, an internal IP leaked in a header, +/// a status code), it is reproducible with a single request and must never be +/// discarded by an opinion-based vote β€” demoted to needs-review at worst. +fn has_http_receipt(f: &Finding) -> bool { + if f.evidence_data.is_some() { return true; } + let hay = format!("{}\n{}", f.evidence, f.payload).to_lowercase(); + // A captured HTTP response or the headers/fields these deterministic classes + // are proven by. + const SIGNS: &[&str] = &[ + "http/1.1", "http/2", "http/1.0", "status: ", "status code", "status=", + "set-cookie", "strict-transport-security", "x-frame-options", + "content-security-policy", "access-control-allow-origin", "location:", + "server:", "www-authenticate", "< http", "=> http", "response:", "200 ok", + "301 ", "302 ", "401 ", "403 ", "404 ", "500 ", "curl ", + ]; + if SIGNS.iter().any(|s| hay.contains(s)) { return true; } + // A white-box file:line citation is also a reproducible receipt. + f.endpoint.contains(':') && f.endpoint.chars().any(|c| c.is_ascii_digit()) + && (f.endpoint.contains(".rs") || f.endpoint.contains(".py") || f.endpoint.contains(".js") + || f.endpoint.contains(".php") || f.endpoint.contains(".java") || f.endpoint.contains(".go") + || f.endpoint.contains(".ts") || f.endpoint.contains(".rb")) +} + /// Adversarial refutation pass: every confirmed **High/Critical** finding is /// re-examined by a skeptical panel that tries to prove it's a false positive. /// A finding that fails to withstand a majority of skeptics is dropped. Lower @@ -4440,6 +4479,34 @@ mod extraction_tests { assert!(!looks_like_refusal("No vulnerabilities were found in the tested endpoints.")); } + #[test] + fn a_response_backed_finding_is_a_reproducible_receipt_and_never_dropped() { + // Missing HSTS: the proof is the response headers β€” reproducible with one + // request, must never be discarded by an opinion-vote. + let hsts = Finding { + title: "No Strict-Transport-Security header".into(), + severity: "Low".into(), cwe: "CWE-319".into(), + endpoint: "https://scapi.rockstargames.com/".into(), + evidence: "HTTP/2 200\nserver: cloudflare\n(no strict-transport-security header present)".into(), + ..Default::default() + }; + assert!(has_http_receipt(&hsts), "a captured response IS a receipt"); + // Insecure cookie flags β€” Set-Cookie in evidence. + let cookie = Finding { + title: "Cookie without Secure/HttpOnly".into(), severity: "Low".into(), cwe: "CWE-614".into(), + endpoint: "https://scapi.rockstargames.com/".into(), + evidence: "Set-Cookie: bal=1; path=/ (no Secure, no HttpOnly, no SameSite)".into(), + ..Default::default() + }; + assert!(has_http_receipt(&cookie)); + // Pure prose with no receipt is NOT a reproducible receipt. + let vague = Finding { title: "Maybe vulnerable".into(), evidence: "the app seems insecure".into(), ..Default::default() }; + assert!(!has_http_receipt(&vague)); + // A white-box file:line citation IS a receipt. + let wb = Finding { title: "SQLi".into(), endpoint: "src/db.py:42".into(), evidence: "query = f\"...{id}\"".into(), ..Default::default() }; + assert!(has_http_receipt(&wb)); + } + /// The bare-array happy path for agent selection. #[test] fn a_string_array_parses_plain() { diff --git a/web/public/index.html b/web/public/index.html index 7420e92..c53adba 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -3,7 +3,7 @@ -NeuroSploit v4.2.1 β€” Console +NeuroSploit v4.2.2 β€” Console @@ -33,7 +33,7 @@

- v4.2.1 + v4.2.2
diff --git a/web/server.js b/web/server.js index a7909f3..d00bf89 100644 --- a/web/server.js +++ b/web/server.js @@ -1,7 +1,7 @@ #!/usr/bin/env node 'use strict'; /** - * NeuroSploit v4.2.1 β€” web console backend. + * NeuroSploit v4.2.2 β€” web console backend. * * Zero-dependency Node HTTP server that: * - serves the static SPA in ./public @@ -1487,7 +1487,7 @@ const server = http.createServer(async (req, res) => { } if (req.method === 'GET' && p === '/api/meta') { - return sendJson(res, 200, { version: '4.2.1', binary: BIN, root: ROOT }); + return sendJson(res, 200, { version: "4.2.2", binary: BIN, root: ROOT }); } // ---- providers / API keys (in-memory only, never persisted) ---- @@ -1521,7 +1521,7 @@ const server = http.createServer(async (req, res) => { loadPersistedJobs(); server.listen(PORT, () => { - console.log(`NeuroSploit v4.2.1 web console β†’ http://localhost:${PORT}`); + console.log(`NeuroSploit v4.2.2 web console β†’ http://localhost:${PORT}`); console.log(` binary : ${BIN || '(not found β€” build neurosploit-rs first)'}`); console.log(` agents : ${AGENTS_DIR}`); console.log(` runs : ${RUNS_DIR}`);