mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-06-29 23:05:30 +02:00
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:
@@ -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).
|
||||
|
||||
@@ -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)]) } }
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user