From 702f22a87a532035ff0301af75e5d4dc7f2d2af3 Mon Sep 17 00:00:00 2001 From: CyberSecurityUP Date: Wed, 24 Jun 2026 21:19:56 -0300 Subject: [PATCH] v3.5.0: REPL quick-wins (Tab-complete, @file/@dir/@line, multiline, /theme, /attach, /context) + installer + README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit REPL (rustyline Helper): - Tab autocomplete for /commands and @filesystem-paths. - @path attach: @file, @folder, @file:LINE / @file:START-END fold scope files / stack traces into the agent context; /attach and /context to manage. - Multiline input: end a line with `\` to continue (validator-driven). - /theme color|mono, /config (=/show); history (↑/↓) persists as before. - Attachments are merged into the run's instruction context. Install: - setup.sh: `curl … | bash` β€” auto-installs Rust, clones to ~/.neurosploit, builds release, links neurosploit into ~/.local/bin; idempotent; env-tunable. README: v3.5.0, 🧠 (back to "neuro"), one-line install section, neurosploit-on-PATH usage. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 33 +++++-- neurosploit-rs/app/src/repl.rs | 166 +++++++++++++++++++++++++++++++-- setup.sh | 90 ++++++++++++++++++ 3 files changed, 274 insertions(+), 15 deletions(-) create mode 100755 setup.sh diff --git a/README.md b/README.md index d3fa022..10d7ceb 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

NeuroSploit v3.4.1 πŸ¦€

+

🧠 NeuroSploit v3.5.0

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

- + @@ -37,17 +37,32 @@ discovered surface**, runs them in parallel, then validates every finding by --- +## πŸ“¦ Install (one line) + +```bash +curl -fsSL https://raw.githubusercontent.com/JoasASantos/NeuroSploit/main/setup.sh | bash +``` + +The installer auto-installs Rust if needed, clones the repo to `~/.neurosploit`, +builds the release binary, and links `neurosploit` into `~/.local/bin`. Re-run it +any time to update. Tweak with env vars: `NEUROSPLOIT_REF` (branch/tag), +`NEUROSPLOIT_DIR`, `PREFIX`. + +Prefer to build by hand? + +```bash +git clone https://github.com/JoasASantos/NeuroSploit && cd NeuroSploit/neurosploit-rs +cargo build --release # β†’ target/release/neurosploit +``` + ## ⚑ Quick start (60 seconds) ```bash -# 1. build -cd neurosploit-rs && cargo build --release +# easiest path β€” just run it; the interactive session asks everything: +neurosploit -# 2. easiest path β€” just run it, the wizard asks everything: -./target/release/neurosploit - -# 3. or one-liner (subscription login, no API key needed): -./target/release/neurosploit run http://testphp.vulnweb.com/ --subscription --model anthropic:claude-opus-4-8 -v +# or one-liner (subscription login, no API key needed): +neurosploit run http://testphp.vulnweb.com/ --subscription --model anthropic:claude-opus-4-8 -v ``` No login? Use an **API key** instead β€” see [Authentication](#authentication--run-via-api-key-or-subscription). diff --git a/neurosploit-rs/app/src/repl.rs b/neurosploit-rs/app/src/repl.rs index 8b4add2..0b1d440 100644 --- a/neurosploit-rs/app/src/repl.rs +++ b/neurosploit-rs/app/src/repl.rs @@ -7,12 +7,84 @@ use dialoguer::{theme::ColorfulTheme, MultiSelect}; use harness::{agents, types::Finding, types::RunConfig}; +use rustyline::completion::{Completer, Pair}; use rustyline::error::ReadlineError; -use rustyline::DefaultEditor; +use rustyline::highlight::Highlighter; +use rustyline::hint::Hinter; +use rustyline::history::FileHistory; +use rustyline::validate::{ValidationContext, ValidationResult, Validator}; +use rustyline::{Config, Context, Editor, Helper}; use serde::{Deserialize, Serialize}; use std::io::IsTerminal; use std::path::Path; +/// All slash-commands, for Tab completion. +const COMMANDS: &[&str] = &[ + "/help", "/show", "/config", "/providers", "/model", "/key", "/sub", "/target", + "/repo", "/auth", "/creds", "/focus", "/attach", "/context", "/mcp", "/offline", + "/votes", "/agents", "/theme", "/clear", "/run", "/runs", "/results", "/report", + "/status", "/quit", +]; + +/// rustyline helper: Tab-completes `/commands` and `@filesystem-paths`, +/// and supports multiline input (a line ending with `\` continues). +struct NsHelper; + +impl Completer for NsHelper { + type Candidate = Pair; + fn complete(&self, line: &str, pos: usize, _ctx: &Context<'_>) -> rustyline::Result<(usize, Vec)> { + let head = &line[..pos]; + // current "word" = text after the last whitespace + let start = head.rfind(char::is_whitespace).map(|i| i + 1).unwrap_or(0); + let word = &head[start..]; + if let Some(p) = word.strip_prefix('@') { + return Ok((start, complete_path(p))); + } + if word.starts_with('/') || (start == 0 && word.is_empty()) { + let cands = COMMANDS.iter() + .filter(|c| c.starts_with(word)) + .map(|c| Pair { display: c.to_string(), replacement: format!("{c} ") }) + .collect(); + return Ok((start, cands)); + } + Ok((start, vec![])) + } +} + +fn complete_path(prefix: &str) -> Vec { + let (dir, frag) = match prefix.rfind('/') { + Some(i) => (&prefix[..=i], &prefix[i + 1..]), + None => ("", prefix), + }; + let read_dir = if dir.is_empty() { ".".to_string() } else { dir.to_string() }; + let mut out = Vec::new(); + if let Ok(entries) = std::fs::read_dir(&read_dir) { + for e in entries.flatten() { + let name = e.file_name().to_string_lossy().to_string(); + if name.starts_with(frag) { + let is_dir = e.path().is_dir(); + let full = format!("@{dir}{name}{}", if is_dir { "/" } else { "" }); + out.push(Pair { display: format!("{name}{}", if is_dir { "/" } else { "" }), replacement: full }); + } + } + } + out.truncate(40); + out +} + +impl Hinter for NsHelper { type Hint = String; } +impl Highlighter for NsHelper {} +impl Validator for NsHelper { + fn validate(&self, ctx: &mut ValidationContext<'_>) -> rustyline::Result { + if ctx.input().ends_with('\\') { + Ok(ValidationResult::Incomplete) // multiline: backslash continues + } else { + Ok(ValidationResult::Valid(None)) + } + } +} +impl Helper for NsHelper {} + /// A run completed within this session (persisted to disk for /runs across sessions). #[derive(Serialize, Deserialize, Clone)] struct RunRecord { @@ -35,6 +107,8 @@ struct Session { auth: Option, creds: Option, instructions: Option, + attachments: Vec, + color: bool, } impl Default for Session { @@ -51,22 +125,27 @@ impl Default for Session { auth: None, creds: None, instructions: None, + attachments: Vec::new(), + color: true, } } } const PROMPT: &str = "\x1b[35mneurosploitβ€Ί\x1b[0m "; -/// Line reader: full rustyline editing when interactive, plain stdin when piped. +/// Line reader: full rustyline editing (Tab-complete, history, multiline) when +/// interactive, plain stdin when piped. enum Reader { - Rl(Box, std::path::PathBuf), + Rl(Box>, std::path::PathBuf), Plain(std::io::Stdin), } impl Reader { fn new(base: &Path) -> Reader { if std::io::stdin().is_terminal() { - if let Ok(mut ed) = DefaultEditor::new() { + let cfg = Config::builder().auto_add_history(false).build(); + if let Ok(mut ed) = Editor::::with_config(cfg) { + ed.set_helper(Some(NsHelper)); let hist = base.join("data").join("repl_history.txt"); std::fs::create_dir_all(hist.parent().unwrap()).ok(); let _ = ed.load_history(&hist); @@ -82,6 +161,8 @@ impl Reader { match self { Reader::Rl(ed, hist) => match ed.readline(PROMPT) { Ok(l) => { + // Join multiline input: a trailing `\` continued the line. + let l = l.replace("\\\n", " ").replace('\n', " "); if !l.trim().is_empty() { let _ = ed.add_history_entry(l.as_str()); let _ = ed.save_history(hist); @@ -134,8 +215,10 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> { continue; } if !line.starts_with('/') { + let attached = expand_ats(line, &mut s); s.instructions = Some(line.to_string()); println!(" focus set: {line}"); + if attached > 0 { println!(" ({attached} @attachment(s) added to context)"); } continue; } let mut parts = line.splitn(2, char::is_whitespace); @@ -184,6 +267,16 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> { s.instructions = if arg.is_empty() { None } else { Some(arg.to_string()) }; println!(" focus: {}", s.instructions.clone().unwrap_or_else(|| "(none)".into())); } + "/attach" => { let n = attach_path(arg.trim_start_matches('@'), &mut s); if n > 0 { println!(" attached ({} total)", s.attachments.len()); } } + "/context" => { + if s.attachments.is_empty() { println!(" no attachments β€” add with @path or /attach "); } + else { println!(" context attachments ({}):", s.attachments.len()); + for a in &s.attachments { println!(" β€’ {}", a.lines().next().unwrap_or("").trim_start_matches("// ")); } } + } + "/theme" => { + s.color = !matches!(arg, "off" | "mono" | "no-color" | "plain"); + println!(" theme: {}", if s.color { "color" } else { "mono" }); + } "/mcp" => { s.mcp = !matches!(arg, "off" | "false" | "0" | "no"); println!(" Playwright MCP: {}", onoff(s.mcp)); } "/offline" => { s.offline = !matches!(arg, "off" | "false" | "0" | "no"); println!(" offline: {}", onoff(s.offline)); } "/votes" => { s.vote_n = arg.parse().unwrap_or(s.vote_n); println!(" votes: {}", s.vote_n); } @@ -300,7 +393,14 @@ async fn run(base: &Path, s: &Session, history: &mut Vec) { cfg.max_agents = s.max_agents; cfg.verbose = true; cfg.offline = s.offline; - cfg.instructions = s.instructions.clone(); + // Fold @attachments (scope files / stack traces) into the instruction context. + cfg.instructions = match (s.instructions.clone(), s.attachments.is_empty()) { + (instr, true) => instr, + (instr, false) => { + let ctx = s.attachments.join("\n\n"); + Some(format!("{}\n\nATTACHED CONTEXT:\n{ctx}", instr.unwrap_or_default())) + } + }; cfg.auth = s.auth.clone(); if let M::Grey { repo, .. } = &m { cfg.repo = Some(repo.clone()); @@ -430,11 +530,65 @@ fn help() { println!(" /auth auth header (e.g. 'Authorization: Bearer ')"); println!(" /creds credentials (jwt/header/cookie/login) for authed tests"); println!(" /focus steer the tests (or just type it); e.g. injection + access control"); + println!(" @path @dir @f:1-20 attach a file/folder/line-range to context (Tab-completes)"); + println!(" /attach attach context Β· /context list attachments"); println!(" /mcp on|off Playwright MCP browser /offline on|off self-test"); - println!(" /votes /agents "); + println!(" /theme color|mono /config (=/show) /votes /agents "); + println!(" Tab completes commands & @paths Β· ↑/↓ history Β· end a line with \\ for multiline"); println!(" /run launch Β· /runs /results [n] /report [n] /status [n]"); println!(" /quit exit"); } +/// Scan a line for @path tokens, attach each referenced file/dir to context. +fn expand_ats(line: &str, s: &mut Session) -> usize { + let mut n = 0; + for tok in line.split_whitespace() { + if let Some(p) = tok.strip_prefix('@') { + n += attach_path(p, s); + } + } + n +} + +/// Attach a file's content (capped) or a directory listing to session context. +/// Supports @file, @folder, and @file:LINE / @file:START-END. +fn attach_path(spec: &str, s: &mut Session) -> usize { + if spec.is_empty() { return 0; } + let (path, range) = match spec.split_once(':') { + Some((p, r)) => (p, Some(r)), + None => (spec, None), + }; + let pb = Path::new(path); + if pb.is_dir() { + let mut items: Vec = std::fs::read_dir(pb).map(|rd| rd.flatten() + .map(|e| e.file_name().to_string_lossy().to_string()).collect()).unwrap_or_default(); + items.sort(); + s.attachments.push(format!("// dir {path}:\n{}", items.join("\n"))); + println!(" + folder {path} ({} entries)", items.len()); + return 1; + } + match std::fs::read_to_string(pb) { + Ok(content) => { + let body = match range.and_then(parse_range) { + Some((a, b)) => content.lines().enumerate() + .filter(|(i, _)| *i + 1 >= a && *i + 1 <= b) + .map(|(_, l)| l).collect::>().join("\n"), + None => content.chars().take(8000).collect(), + }; + println!(" + file {spec} ({} bytes)", body.len()); + s.attachments.push(format!("// file {spec}:\n{body}")); + 1 + } + Err(_) => { println!(" \x1b[31mβœ— cannot read @{spec}\x1b[0m"); 0 } + } +} + +fn parse_range(r: &str) -> Option<(usize, usize)> { + match r.split_once('-') { + Some((a, b)) => Some((a.trim().parse().ok()?, b.trim().parse().ok()?)), + None => { let n: usize = r.trim().parse().ok()?; Some((n, n)) } + } +} + fn onoff(b: bool) -> &'static str { if b { "on" } else { "off" } } fn trunc(s: &str, n: usize) -> String { if s.len() <= n { s.to_string() } else { format!("{}…", &s[..n.saturating_sub(1)]) } } diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..2b32a24 --- /dev/null +++ b/setup.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# NeuroSploit installer β€” by Joas A Santos & Red Team Leaders +# +# curl -fsSL https://raw.githubusercontent.com/JoasASantos/NeuroSploit/main/setup.sh | bash +# +# Builds the v3.5.0 Rust harness and installs the `neurosploit` binary. +# Safe to re-run (idempotent). Honors: +# NEUROSPLOIT_DIR install/clone dir (default: ~/.neurosploit) +# NEUROSPLOIT_REF git branch/tag (default: main) +# PREFIX bin install prefix (default: ~/.local/bin) +set -euo pipefail + +REPO="https://github.com/JoasASantos/NeuroSploit.git" +DIR="${NEUROSPLOIT_DIR:-$HOME/.neurosploit}" +REF="${NEUROSPLOIT_REF:-main}" +PREFIX="${PREFIX:-$HOME/.local/bin}" + +c() { printf '\033[%sm%s\033[0m\n' "$1" "$2"; } +say() { c '1;35' " β–Œ $*"; } +ok() { c '1;32' " βœ“ $*"; } +warn(){ c '1;33' " ! $*"; } +die() { c '1;31' " βœ— $*"; exit 1; } + +cat <<'BANNER' + + β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•— β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— + β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•— NeuroSploit installer + β–ˆβ–ˆβ•”β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ v3.5.0 β€” Rust harness + β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β• β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ by Joas A Santos + β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β• & Red Team Leaders + β•šβ•β• β•šβ•β•β•β•β•šβ•β•β•β•β•β•β• β•šβ•β•β•β•β•β• β•šβ•β• β•šβ•β• β•šβ•β•β•β•β•β• +BANNER + +OS="$(uname -s)" +say "Detected OS: $OS" + +# 1) git +command -v git >/dev/null 2>&1 || die "git is required. Install git and re-run." + +# 2) Rust toolchain (rustup) +if ! command -v cargo >/dev/null 2>&1; then + [ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env" || true +fi +if ! command -v cargo >/dev/null 2>&1; then + say "Rust not found β€” installing rustup (stable, minimal)…" + curl --proto '=https' --tlsv1.2 -fsSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal + . "$HOME/.cargo/env" +fi +ok "Rust: $(cargo --version)" + +# 3) clone or update +if [ -d "$DIR/.git" ]; then + say "Updating existing checkout at $DIR…" + git -C "$DIR" fetch --depth 1 origin "$REF" && git -C "$DIR" checkout -q "$REF" && git -C "$DIR" reset -q --hard "origin/$REF" 2>/dev/null || git -C "$DIR" pull -q +else + say "Cloning $REPO ($REF) β†’ $DIR…" + git clone --depth 1 --branch "$REF" "$REPO" "$DIR" 2>/dev/null || git clone --depth 1 "$REPO" "$DIR" +fi + +# 4) build +say "Building release binary (first build downloads crates; grab a coffee)…" +( cd "$DIR/neurosploit-rs" && cargo build --release ) +BIN="$DIR/neurosploit-rs/target/release/neurosploit" +[ -x "$BIN" ] || die "build did not produce $BIN" +ok "Built: $("$BIN" --version 2>/dev/null || echo neurosploit)" + +# 5) install on PATH +mkdir -p "$PREFIX" +ln -sf "$BIN" "$PREFIX/neurosploit" +ok "Installed β†’ $PREFIX/neurosploit" + +# 6) optional tooling hints (don't fail if absent) +say "Recommended tools for richer testing (optional):" +for t in curl nmap rustscan ffuf node npx typst; do + if command -v "$t" >/dev/null 2>&1; then ok "$t present"; else warn "$t missing"; fi +done +echo +warn "Best run on Kali Linux β†’ docker run -it --rm kalilinux/kali-rolling" +warn "typst (PDF reports): cargo install typst-cli Β· rustscan: cargo install rustscan" + +case ":$PATH:" in + *":$PREFIX:"*) ;; + *) warn "Add to PATH: echo 'export PATH=\"$PREFIX:\$PATH\"' >> ~/.bashrc && source ~/.bashrc" ;; +esac + +echo +ok "Done. Authenticate a model, then launch:" +echo " neurosploit # interactive session" +echo " neurosploit run http://testphp.vulnweb.com/ --subscription --model anthropic:claude-opus-4-8 -v" +echo " neurosploit --help"