mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-08-15 06:00:28 +02:00
v3.5.1: attack-chain agents (12) + per-project .neurosploit/ persistence & resume
Chaining: - agents_md/chains/ (12 multi-stage exploitation playbooks): SQLi→RCE→LPE, SSRF→AWS-creds, SSRF→RCE, upload→RCE, upload→LFI→RCE→LPE, XSS→ATO, IDOR→ATO, SSTI→RCE→cloud, default-creds→domain, deserialization→RCE, exposed-git→RCE, subdomain-takeover→trusted-abuse. Each stage proven by a tool receipt before advancing; reports chains_from edges. - Loaded as a `chains` category (→ 329 agents). chain_round now injects the chain recipes as a menu so the LLM applies proven multi-stage paths. Persistence (no DB — structured state): - Per-project `<cwd>/.neurosploit/` holding session.json (config), runs.json (history), history.txt (readline). REPL resumes target/repo/auth/focus/models on reopen; saves on /run and /quit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f8d70ce9c5
commit
639c2209f7
@@ -201,8 +201,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
Cmd::Agents => {
|
||||
let lib = agents::load(&base);
|
||||
println!(
|
||||
"{{\"vulns\":{},\"recon\":{},\"code\":{},\"infra\":{},\"meta\":{},\"total\":{}}}",
|
||||
lib.vulns.len(), lib.recon.len(), lib.code.len(), lib.infra.len(), lib.meta.len(), lib.total()
|
||||
"{{\"vulns\":{},\"recon\":{},\"code\":{},\"infra\":{},\"chains\":{},\"meta\":{},\"total\":{}}}",
|
||||
lib.vulns.len(), lib.recon.len(), lib.code.len(), lib.infra.len(), lib.chains.len(), lib.meta.len(), lib.total()
|
||||
);
|
||||
}
|
||||
Cmd::Models => {
|
||||
|
||||
@@ -139,13 +139,12 @@ enum Reader {
|
||||
}
|
||||
|
||||
impl Reader {
|
||||
fn new(base: &Path) -> Reader {
|
||||
fn new(_base: &Path) -> Reader {
|
||||
if std::io::stdin().is_terminal() {
|
||||
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 hist = proj_dir().join("history.txt");
|
||||
let _ = ed.load_history(&hist);
|
||||
return Reader::Rl(Box::new(ed), hist);
|
||||
}
|
||||
@@ -200,9 +199,11 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
|
||||
println!(" Type \x1b[36m/help\x1b[0m to start, \x1b[36m/run\x1b[0m to launch, \x1b[36m/quit\x1b[0m to exit. (↑/↓ recalls commands)\n");
|
||||
|
||||
let mut s = Session::default();
|
||||
let resumed = load_session(&mut s);
|
||||
let mut history: Vec<RunRecord> = load_runs(base);
|
||||
if !history.is_empty() {
|
||||
println!(" loaded {} past run(s) — /runs to list\n", history.len());
|
||||
if resumed || !history.is_empty() {
|
||||
println!(" ↻ resumed project session from {} — {} past run(s)\n",
|
||||
proj_dir().display(), history.len());
|
||||
}
|
||||
let mut reader = Reader::new(base);
|
||||
show(&s);
|
||||
@@ -281,12 +282,12 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
|
||||
"/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, &mut history).await; save_runs(base, &history); }
|
||||
"/run" | "/go" => { save_session(&s); run(base, &s, &mut history).await; save_runs(base, &history); }
|
||||
"/runs" | "/history" => list_runs(&history),
|
||||
"/results" => results(&history, arg),
|
||||
"/report" => open_report(&history, arg),
|
||||
"/status" => run_status(&history, arg),
|
||||
"/quit" | "/exit" | "/q" => { println!(" bye."); break; }
|
||||
"/quit" | "/exit" | "/q" => { save_session(&s); println!(" session saved → {} · bye.", proj_dir().display()); break; }
|
||||
other => println!(" unknown command '{other}' — try /help"),
|
||||
}
|
||||
}
|
||||
@@ -423,20 +424,61 @@ async fn run(base: &Path, s: &Session, history: &mut Vec<RunRecord>) {
|
||||
}
|
||||
}
|
||||
|
||||
fn runs_path(base: &Path) -> std::path::PathBuf {
|
||||
base.join("data").join("repl_runs.json")
|
||||
/// Project-local store: `<cwd>/.neurosploit/` so each project keeps its own
|
||||
/// session, run history and command history (resume on reopen). No DB needed —
|
||||
/// it's structured state, not semantic search.
|
||||
pub(crate) fn proj_dir() -> std::path::PathBuf {
|
||||
let d = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")).join(".neurosploit");
|
||||
std::fs::create_dir_all(&d).ok();
|
||||
d
|
||||
}
|
||||
fn load_runs(base: &Path) -> Vec<RunRecord> {
|
||||
std::fs::read_to_string(runs_path(base)).ok()
|
||||
fn runs_path(_base: &Path) -> std::path::PathBuf { proj_dir().join("runs.json") }
|
||||
fn load_runs(_base: &Path) -> Vec<RunRecord> {
|
||||
std::fs::read_to_string(runs_path(_base)).ok()
|
||||
.and_then(|t| serde_json::from_str(&t).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
fn save_runs(base: &Path, history: &[RunRecord]) {
|
||||
let p = runs_path(base);
|
||||
if let Some(dir) = p.parent() { std::fs::create_dir_all(dir).ok(); }
|
||||
fn save_runs(_base: &Path, history: &[RunRecord]) {
|
||||
let p = runs_path(_base);
|
||||
if let Ok(j) = serde_json::to_string_pretty(history) { std::fs::write(p, j).ok(); }
|
||||
}
|
||||
|
||||
/// Persistable snapshot of the session config (resume across restarts).
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
struct Snapshot {
|
||||
models: Vec<String>,
|
||||
subscription: bool,
|
||||
mcp: bool,
|
||||
vote_n: usize,
|
||||
max_agents: usize,
|
||||
target: Option<String>,
|
||||
repo: Option<String>,
|
||||
auth: Option<String>,
|
||||
creds: Option<String>,
|
||||
instructions: Option<String>,
|
||||
}
|
||||
fn session_path() -> std::path::PathBuf { proj_dir().join("session.json") }
|
||||
fn save_session(s: &Session) {
|
||||
let snap = Snapshot {
|
||||
models: s.models.clone(), subscription: s.subscription, mcp: s.mcp,
|
||||
vote_n: s.vote_n, max_agents: s.max_agents, target: s.target.clone(),
|
||||
repo: s.repo.clone(), auth: s.auth.clone(), creds: s.creds.clone(),
|
||||
instructions: s.instructions.clone(),
|
||||
};
|
||||
if let Ok(j) = serde_json::to_string_pretty(&snap) { std::fs::write(session_path(), j).ok(); }
|
||||
}
|
||||
fn load_session(s: &mut Session) -> bool {
|
||||
let Ok(txt) = std::fs::read_to_string(session_path()) else { return false };
|
||||
let Ok(snap) = serde_json::from_str::<Snapshot>(&txt) else { return false };
|
||||
if !snap.models.is_empty() { s.models = snap.models; }
|
||||
s.subscription = snap.subscription; s.mcp = snap.mcp;
|
||||
if snap.vote_n > 0 { s.vote_n = snap.vote_n; }
|
||||
s.max_agents = snap.max_agents;
|
||||
s.target = snap.target; s.repo = snap.repo; s.auth = snap.auth;
|
||||
s.creds = snap.creds; s.instructions = snap.instructions;
|
||||
true
|
||||
}
|
||||
|
||||
fn pick<'a>(history: &'a [RunRecord], arg: &str) -> Option<&'a RunRecord> {
|
||||
if history.is_empty() { println!(" no runs yet — /run first."); return None; }
|
||||
if arg.trim().is_empty() { return history.last(); }
|
||||
|
||||
@@ -24,11 +24,13 @@ pub struct Library {
|
||||
pub recon: Vec<Agent>,
|
||||
pub code: Vec<Agent>,
|
||||
pub infra: Vec<Agent>,
|
||||
pub chains: Vec<Agent>,
|
||||
}
|
||||
|
||||
impl Library {
|
||||
pub fn total(&self) -> usize {
|
||||
self.vulns.len() + self.meta.len() + self.recon.len() + self.code.len() + self.infra.len()
|
||||
self.vulns.len() + self.meta.len() + self.recon.len() + self.code.len()
|
||||
+ self.infra.len() + self.chains.len()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +43,7 @@ pub fn load(base: &Path) -> Library {
|
||||
recon: load_dir(&root.join("recon"), "recon"),
|
||||
code: load_dir(&root.join("code"), "code"),
|
||||
infra: load_dir(&root.join("infra"), "infra"),
|
||||
chains: load_dir(&root.join("chains"), "chain"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
|
||||
let mut findings = validate(candidates, pool, VOTE_SYS, cfg.vote_n, &tx).await;
|
||||
|
||||
// ---- 5. Chain confirmed findings into deeper impact ----------------
|
||||
let chained = chain_round(pool, &cfg.target, &recon, &operator_directives(&cfg), &findings, &tx).await;
|
||||
let chained = chain_round(pool, &cfg.target, &recon, &operator_directives(&cfg), &findings, &lib.chains, &tx).await;
|
||||
if !chained.is_empty() {
|
||||
let extra = validate(dedup_findings(chained), pool, VOTE_SYS, cfg.vote_n, &tx).await;
|
||||
let _ = tx.send(format!("chaining added {} validated finding(s)", extra.len())).await;
|
||||
@@ -409,7 +409,7 @@ pub async fn run_greybox(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Se
|
||||
let candidates = dedup_findings(raw.iter().flat_map(|(_, _, f)| f.clone()).collect());
|
||||
let _ = tx.send(format!("{} candidate finding(s) (deduped) — validating", candidates.len())).await;
|
||||
let mut findings = validate(candidates, pool, VOTE_SYS, cfg.vote_n, &tx).await;
|
||||
let chained = chain_round(pool, &cfg.target, &recon, &operator_directives(&cfg), &findings, &tx).await;
|
||||
let chained = chain_round(pool, &cfg.target, &recon, &operator_directives(&cfg), &findings, &lib.chains, &tx).await;
|
||||
if !chained.is_empty() {
|
||||
let extra = validate(dedup_findings(chained), pool, VOTE_SYS, cfg.vote_n, &tx).await;
|
||||
let _ = tx.send(format!("chaining added {} validated finding(s)", extra.len())).await;
|
||||
@@ -425,19 +425,23 @@ const CHAIN_SYS: &str = "You are an exploit-chaining specialist. Given already-C
|
||||
/// into higher-impact follow-ups, reusing the recon/auth context. Returns the
|
||||
/// (unvalidated) new candidate findings produced by chaining.
|
||||
async fn chain_round(pool: &ModelPool, target: &str, recon: &str, directives: &str,
|
||||
confirmed: &[Finding], tx: &Sender<String>) -> Vec<Finding> {
|
||||
confirmed: &[Finding], chains: &[Agent], tx: &Sender<String>) -> Vec<Finding> {
|
||||
if confirmed.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
let summary: String = confirmed.iter().take(20)
|
||||
.map(|f| format!("- [{}] {} @ {} ({})", f.severity, f.title, f.endpoint, f.cwe))
|
||||
.collect::<Vec<_>>().join("\n");
|
||||
// Offer the known chain recipes as a menu so the LLM applies proven multi-stage paths.
|
||||
let recipes: String = chains.iter().map(|a| format!("- {}", a.title.replace(" Agent", ""))).collect::<Vec<_>>().join("\n");
|
||||
let recipe_block = if recipes.is_empty() { String::new() } else { format!("KNOWN CHAIN RECIPES (apply any that fit):\n{recipes}\n\n") };
|
||||
let _ = tx.send(format!("chaining {} confirmed finding(s) for deeper impact…", confirmed.len())).await;
|
||||
let recon_ctx: String = recon.chars().take(2500).collect();
|
||||
let user = format!(
|
||||
"AUTHORIZED engagement on {target}.\n\n{directives}{react}{doctrine}\
|
||||
"AUTHORIZED engagement on {target}.\n\n{directives}{react}{doctrine}{recipe_block}\
|
||||
CONFIRMED FINDINGS TO CHAIN:\n{summary}\n\nRecon:\n{recon_ctx}\n\n\
|
||||
Chain these into deeper impact and PROVE it. Reply ONLY a JSON array of NEW findings \
|
||||
Chain these into deeper impact (e.g. SQLi→RCE→LPE, SSRF→cloud creds, upload→LFI→RCE) and PROVE each stage. \
|
||||
Reply ONLY a JSON array of NEW findings \
|
||||
(may be []): {{id,title,severity,cwe,endpoint,payload,evidence,impact,remediation,confidence}}.",
|
||||
react = REACT_DOCTRINE, doctrine = tool_doctrine(pool.mcp_config.is_some()),
|
||||
);
|
||||
@@ -951,7 +955,7 @@ pub async fn run_host(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sende
|
||||
let candidates = dedup_findings(raw.iter().flat_map(|(_, _, f)| f.clone()).collect());
|
||||
let _ = tx.send(format!("{} candidate finding(s) (deduped) — validating", candidates.len())).await;
|
||||
let mut findings = validate(candidates, pool, VOTE_SYS, cfg.vote_n, &tx).await;
|
||||
let chained = chain_round(pool, &cfg.target, &recon, &operator_directives(&cfg), &findings, &tx).await;
|
||||
let chained = chain_round(pool, &cfg.target, &recon, &operator_directives(&cfg), &findings, &lib.chains, &tx).await;
|
||||
if !chained.is_empty() {
|
||||
let extra = validate(dedup_findings(chained), pool, VOTE_SYS, cfg.vote_n, &tx).await;
|
||||
findings.extend(extra);
|
||||
|
||||
Reference in New Issue
Block a user