Files
NeuroSploit/neurosploit-rs/app/src/repl.rs
T
CyberSecurityUP 435463979b v3.5.0: Claude-Code-style interactive harness (REPL) + instruction-steered testing
- New persistent interactive session (app/src/repl.rs), launched when run with no args:
  banner, model selection, API-key config (/key) or subscription (/sub), then a live
  session to set /target, /repo, /auth, and free-text /focus instructions (or just type
  them) that STEER which agents run and how.
- Slash-commands: /model /providers /key /sub /target /repo /auth /focus /mcp /votes
  /agents /show /run /quit  (+ bare text = focus).
- RunConfig gains `instructions` and `auth`:
  * instructions bias both LLM agent-selection and the heuristic (focus keywords →
    injection/access-control/etc. agents get a strong boost)
  * operator directives (focus + auth) injected into recon and exploit prompts so agents
    test as an authenticated user and prioritise the requested vuln classes
- bump 3.4.1 → 3.5.0 (CLI, harness, reports, credits)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 19:58:35 -03:00

219 lines
10 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! NeuroSploit v3.5.0 — interactive session (Claude-Code / Codex / Cursor-CLI style).
//!
//! Launched when `neurosploit` runs with no subcommand. A persistent REPL where
//! you pick models, set an API key (or use a subscription login), point at a URL
//! or a repo, configure authentication, and write free-text instructions that
//! steer which agents run and how — e.g. "find injection and broken access
//! control". `/run` then executes the engagement with that configuration.
use harness::{agents, types::RunConfig};
use std::io::Write;
use std::path::Path;
/// Mutable session state edited via slash-commands and consumed by `/run`.
struct Session {
models: Vec<String>,
subscription: bool,
mcp: bool,
vote_n: usize,
max_agents: usize,
target: Option<String>,
repo: Option<String>,
auth: Option<String>,
instructions: Option<String>,
}
impl Default for Session {
fn default() -> Self {
Session {
models: vec!["anthropic:claude-opus-4-8".into()],
subscription: harness::installed_cli_backends().contains(&"claude"),
mcp: false,
vote_n: 3,
max_agents: 0,
target: None,
repo: None,
auth: None,
instructions: None,
}
}
}
const PROMPT: &str = "\x1b[35mneurosploit\x1b[0m ";
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.5.0");
println!(" ██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║ interactive harness");
println!(" ██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║ by Joas A Santos");
println!(" ██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝ & Red Team Leaders");
println!(" ╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝\x1b[0m");
println!(" {} agents loaded · detected logins: {}", lib.total(),
if backends.is_empty() { "none (use API keys)".into() } else { backends.join(", ") });
println!(" Type \x1b[36m/help\x1b[0m to get started, \x1b[36m/run\x1b[0m to launch, \x1b[36m/quit\x1b[0m to exit.\n");
let mut s = Session::default();
show(&s);
let stdin = std::io::stdin();
loop {
print!("{PROMPT}");
std::io::stdout().flush().ok();
let mut line = String::new();
if stdin.read_line(&mut line).unwrap_or(0) == 0 {
println!();
break; // EOF (Ctrl-D)
}
let line = line.trim();
if line.is_empty() {
continue;
}
// A bare line that isn't a command is treated as test instructions.
if !line.starts_with('/') {
s.instructions = Some(line.to_string());
println!(" focus set: {line}");
continue;
}
let mut parts = line.splitn(2, char::is_whitespace);
let cmd = parts.next().unwrap_or("");
let arg = parts.next().unwrap_or("").trim();
match cmd {
"/help" | "/?" => help(),
"/show" | "/config" => show(&s),
"/providers" => {
for p in harness::providers() {
println!(" [{}] {:<14} {}", p.kind, p.key,
p.models.iter().map(|m| format!("{}:{}", p.key, m)).collect::<Vec<_>>().join(" "));
}
}
"/model" | "/models" => {
if arg.is_empty() {
println!(" current: {}", s.models.join(", "));
} else {
s.models = arg.split([',', ' ']).filter(|x| !x.is_empty()).map(String::from).collect();
println!(" models: {}", s.models.join(", "));
}
}
"/key" => {
// /key <PROVIDER> <KEY> → sets the provider's env var for this session
let mut kp = arg.splitn(2, char::is_whitespace);
match (kp.next(), kp.next()) {
(Some(prov), Some(key)) if !key.trim().is_empty() => {
match harness::provider_for(prov) {
Some(p) => {
std::env::set_var(p.env_key, key.trim());
s.subscription = false;
println!(" set {} (API mode)", p.env_key);
}
None => println!(" unknown provider '{prov}' (see /providers)"),
}
}
_ => println!(" usage: /key <provider> <api-key> e.g. /key anthropic sk-ant-..."),
}
}
"/sub" | "/subscription" => {
s.subscription = !matches!(arg, "off" | "false" | "0" | "no");
println!(" subscription: {}", onoff(s.subscription));
}
"/target" | "/url" => {
let t = if arg.starts_with("http") || arg.is_empty() { arg.to_string() } else { format!("https://{arg}") };
s.target = if t.is_empty() { None } else { Some(t) };
s.repo = None;
println!(" target: {}", s.target.clone().unwrap_or_else(|| "(none)".into()));
}
"/repo" => {
s.repo = if arg.is_empty() { None } else { Some(arg.to_string()) };
s.target = None;
println!(" repo: {}", s.repo.clone().unwrap_or_else(|| "(none)".into()));
}
"/auth" => {
s.auth = if arg.is_empty() { None } else { Some(arg.to_string()) };
println!(" auth: {}", s.auth.clone().unwrap_or_else(|| "(none)".into()));
}
"/focus" | "/instructions" => {
s.instructions = if arg.is_empty() { None } else { Some(arg.to_string()) };
println!(" focus: {}", s.instructions.clone().unwrap_or_else(|| "(none)".into()));
}
"/mcp" => {
s.mcp = !matches!(arg, "off" | "false" | "0" | "no");
println!(" Playwright MCP: {}", onoff(s.mcp));
}
"/votes" => { s.vote_n = arg.parse().unwrap_or(s.vote_n); println!(" votes: {}", s.vote_n); }
"/agents" => { s.max_agents = arg.parse().unwrap_or(s.max_agents); println!(" max agents: {} ", s.max_agents); }
"/clear" => { print!("\x1b[2J\x1b[H"); }
"/run" | "/go" => run(base, &s).await,
"/quit" | "/exit" | "/q" => { println!(" bye."); break; }
other => println!(" unknown command '{other}' — try /help"),
}
}
Ok(())
}
async fn run(base: &Path, s: &Session) {
let (target, whitebox) = match (&s.repo, &s.target) {
(Some(r), _) => (r.clone(), true),
(_, Some(t)) => (t.clone(), false),
_ => {
println!(" \x1b[31m✗ set a /target <url> or /repo <path> first.\x1b[0m");
return;
}
};
let mut cfg = RunConfig::new(&target);
cfg.models = s.models.clone();
cfg.subscription = s.subscription;
cfg.vote_n = s.vote_n;
cfg.max_agents = s.max_agents;
cfg.verbose = true;
cfg.instructions = s.instructions.clone();
cfg.auth = s.auth.clone();
match crate::run_engagement(base, cfg, s.mcp && !whitebox, whitebox).await {
Ok(out) => crate::print_findings(&out),
Err(e) => println!(" \x1b[31m✗ run failed: {e}\x1b[0m"),
}
}
fn show(s: &Session) {
println!(" ┌─ session");
println!(" │ models : {}", s.models.join(", "));
println!(" │ auth mode: {}", if s.subscription { "subscription (CLI login)" } else { "API key" });
println!(" │ target : {}", s.target.clone().unwrap_or_else(|| "(none)".into()));
println!(" │ repo : {}", s.repo.clone().unwrap_or_else(|| "(none)".into()));
println!(" │ auth : {}", s.auth.clone().unwrap_or_else(|| "(none)".into()));
println!(" │ focus : {}", s.instructions.clone().unwrap_or_else(|| "(none — tests everything)".into()));
println!(" │ mcp : {} votes: {} max-agents: {}", onoff(s.mcp), s.vote_n, s.max_agents);
println!(" └─ /run to launch");
}
fn help() {
println!(" Commands:");
println!(" /model a:b[,c:d] set model panel (1st primary; rest fail over + vote)");
println!(" /providers list providers & models");
println!(" /key <prov> <key> set a provider API key (switches to API mode)");
println!(" /sub on|off use local subscription login instead of API key");
println!(" /target <url> black-box target URL");
println!(" /repo <path> white-box: analyse a local repository");
println!(" /auth <value> auth to send (e.g. 'Authorization: Bearer <jwt>' or 'Cookie: s=..')");
println!(" /focus <text> steer the tests, e.g. 'injection and broken access control'");
println!(" (or just type the instruction with no slash)");
println!(" /mcp on|off enable Playwright MCP browser (subscription path)");
println!(" /votes <n> validator votes per finding");
println!(" /agents <n> cap agents (0 = all matching)");
println!(" /show show current session config");
println!(" /run launch the engagement");
println!(" /quit exit");
println!();
println!(" Example:");
println!(" /model anthropic:claude-opus-4-8");
println!(" /target http://testphp.vulnweb.com/");
println!(" find injection and broken access control");
println!(" /run");
}
fn onoff(b: bool) -> &'static str {
if b { "on" } else { "off" }
}