v3.5.0: REPL quick-wins (Tab-complete, @file/@dir/@line, multiline, /theme, /attach, /context) + installer + README

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 <path> 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) <noreply@anthropic.com>
This commit is contained in:
CyberSecurityUP
2026-06-24 21:19:56 -03:00
parent 1be053c4a2
commit 702f22a87a
3 changed files with 274 additions and 15 deletions
+24 -9
View File
@@ -1,4 +1,4 @@
<h1 align="center">NeuroSploit v3.4.1 🦀</h1>
<h1 align="center">🧠 NeuroSploit v3.5.0</h1>
<p align="center">
<a href="https://github.com/JoasASantos/NeuroSploit/stargazers"><img src="https://img.shields.io/github/stars/JoasASantos/NeuroSploit?style=for-the-badge&logo=github&color=8b5cf6" alt="Stars"></a>
@@ -8,7 +8,7 @@
</p>
<p align="center">
<img src="https://img.shields.io/badge/Version-3.4.1-blue?style=flat-square">
<img src="https://img.shields.io/badge/Version-3.5.0-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-303-red?style=flat-square">
@@ -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).
+160 -6
View File
@@ -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<Pair>)> {
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<Pair> {
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<ValidationResult> {
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<String>,
creds: Option<String>,
instructions: Option<String>,
attachments: Vec<String>,
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<DefaultEditor>, std::path::PathBuf),
Rl(Box<Editor<NsHelper, FileHistory>>, 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::<NsHelper, FileHistory>::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 <path>"); }
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<RunRecord>) {
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 <value> auth header (e.g. 'Authorization: Bearer <jwt>')");
println!(" /creds <file.yaml> credentials (jwt/header/cookie/login) for authed tests");
println!(" /focus <text> 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 <path> attach context · /context list attachments");
println!(" /mcp on|off Playwright MCP browser /offline on|off self-test");
println!(" /votes <n> /agents <n>");
println!(" /theme color|mono /config (=/show) /votes <n> /agents <n>");
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<String> = 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::<Vec<_>>().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)]) } }
Executable
+90
View File
@@ -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"