mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-09-30 21:19:49 +02:00
v3.5.1: infra/host engagements — IP + SSH/Windows-AD creds + Linux/Win/AD agents + REPL context bar
Infra: - creds.yaml gains `ssh:` (host/port/user/password/key) and `windows:`/`ad:` (host/user/password/domain/ntlm-hash) blocks; multi-block YAML parser. host_instruction() tells agents how to authenticate to the host. - 14 infra agents (agents_md/infra/): port/service scan, SMB enum, Linux privesc/ sudo/cron/SSH, Windows privesc/SMB-signing/WinRM, AD kerberoast/asreproast/ACL/ DCSync/default-creds. Loader gains `infra` category → 317 agents total. - run_host pipeline + `neurosploit host <ip> --creds creds.yaml` (and Mode::Host in run_mode/TUI): host recon (nmap/netexec) → infra agent selection → test → validate → chain → report, with host tooling doctrine + supplied creds. REPL: - Context/status bar above the prompt: "model auth · cwd · mode▸target" (e.g. claude-opus-4-8 sub · /opt/projeto · black-box▸app.acme.com). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
969af20a8e
commit
f8d70ce9c5
23 files changed
+893
-32
No files matched your search
@@ -23,11 +23,12 @@ pub struct Library {
|
||||
pub meta: Vec<Agent>,
|
||||
pub recon: Vec<Agent>,
|
||||
pub code: Vec<Agent>,
|
||||
pub infra: Vec<Agent>,
|
||||
}
|
||||
|
||||
impl Library {
|
||||
pub fn total(&self) -> usize {
|
||||
self.vulns.len() + self.meta.len() + self.recon.len() + self.code.len()
|
||||
self.vulns.len() + self.meta.len() + self.recon.len() + self.code.len() + self.infra.len()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +40,7 @@ pub fn load(base: &Path) -> Library {
|
||||
meta: load_dir(&root.join("meta"), "meta"),
|
||||
recon: load_dir(&root.join("recon"), "recon"),
|
||||
code: load_dir(&root.join("code"), "code"),
|
||||
infra: load_dir(&root.join("infra"), "infra"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,12 +31,34 @@ pub struct Login {
|
||||
pub success: String,
|
||||
}
|
||||
|
||||
/// SSH credentials for Linux host testing.
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct Ssh {
|
||||
pub host: String,
|
||||
pub port: String, // default 22
|
||||
pub user: String,
|
||||
pub password: String,
|
||||
pub key: String, // path to a private key
|
||||
}
|
||||
|
||||
/// Windows / Active Directory credentials.
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct Win {
|
||||
pub host: String,
|
||||
pub user: String,
|
||||
pub password: String,
|
||||
pub domain: String,
|
||||
pub hash: String, // NTLM hash for pass-the-hash (LM:NT or NT)
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct Creds {
|
||||
pub jwt: Option<String>,
|
||||
pub header: Option<String>,
|
||||
pub cookie: Option<String>,
|
||||
pub login: Option<Login>,
|
||||
pub ssh: Option<Ssh>,
|
||||
pub win: Option<Win>,
|
||||
}
|
||||
|
||||
impl Creds {
|
||||
@@ -44,8 +66,10 @@ impl Creds {
|
||||
let text = std::fs::read_to_string(path).ok()?;
|
||||
let mut c = Creds::default();
|
||||
let mut login = Login { method: "POST".into(), ..Default::default() };
|
||||
let mut in_login = false;
|
||||
let mut have_login = false;
|
||||
let mut ssh = Ssh { port: "22".into(), ..Default::default() };
|
||||
let mut win = Win::default();
|
||||
let (mut have_login, mut have_ssh, mut have_win) = (false, false, false);
|
||||
let mut block = ""; // "", "login", "ssh", "windows"
|
||||
for raw in text.lines() {
|
||||
let line = raw.split('#').next().unwrap_or("");
|
||||
if line.trim().is_empty() {
|
||||
@@ -56,25 +80,49 @@ impl Creds {
|
||||
Some((k, v)) => (k.trim().to_string(), unquote(v.trim())),
|
||||
None => continue,
|
||||
};
|
||||
if k == "login" && v.is_empty() {
|
||||
in_login = true;
|
||||
have_login = true;
|
||||
// Enter a nested block (header line with empty value).
|
||||
if v.is_empty() && !indented {
|
||||
block = match k.as_str() {
|
||||
"login" => { have_login = true; "login" }
|
||||
"ssh" => { have_ssh = true; "ssh" }
|
||||
"windows" | "win" | "ad" => { have_win = true; "windows" }
|
||||
_ => "",
|
||||
};
|
||||
continue;
|
||||
}
|
||||
if in_login && indented {
|
||||
match k.as_str() {
|
||||
"url" => login.url = v,
|
||||
"method" => login.method = v.to_uppercase(),
|
||||
"username_field" => login.username_field = v,
|
||||
"password_field" => login.password_field = v,
|
||||
"username" | "user" => login.username = v,
|
||||
"password" | "pass" => login.password = v,
|
||||
"success" => login.success = v,
|
||||
if indented {
|
||||
match block {
|
||||
"login" => match k.as_str() {
|
||||
"url" => login.url = v,
|
||||
"method" => login.method = v.to_uppercase(),
|
||||
"username_field" => login.username_field = v,
|
||||
"password_field" => login.password_field = v,
|
||||
"username" | "user" => login.username = v,
|
||||
"password" | "pass" => login.password = v,
|
||||
"success" => login.success = v,
|
||||
_ => {}
|
||||
},
|
||||
"ssh" => match k.as_str() {
|
||||
"host" | "ip" => ssh.host = v,
|
||||
"port" => ssh.port = v,
|
||||
"user" | "username" => ssh.user = v,
|
||||
"password" | "pass" => ssh.password = v,
|
||||
"key" | "keyfile" | "identity" => ssh.key = v,
|
||||
_ => {}
|
||||
},
|
||||
"windows" => match k.as_str() {
|
||||
"host" | "ip" => win.host = v,
|
||||
"user" | "username" => win.user = v,
|
||||
"password" | "pass" => win.password = v,
|
||||
"domain" => win.domain = v,
|
||||
"hash" | "ntlm" => win.hash = v,
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
in_login = false;
|
||||
block = "";
|
||||
match k.as_str() {
|
||||
"jwt" | "token" => c.jwt = Some(v),
|
||||
"header" => c.header = Some(v),
|
||||
@@ -82,15 +130,37 @@ impl Creds {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if have_login && !login.url.is_empty() {
|
||||
c.login = Some(login);
|
||||
}
|
||||
if c.jwt.is_none() && c.header.is_none() && c.cookie.is_none() && c.login.is_none() {
|
||||
if have_login && !login.url.is_empty() { c.login = Some(login); }
|
||||
if have_ssh && !ssh.host.is_empty() { c.ssh = Some(ssh); }
|
||||
if have_win && !win.host.is_empty() { c.win = Some(win); }
|
||||
if c.jwt.is_none() && c.header.is_none() && c.cookie.is_none()
|
||||
&& c.login.is_none() && c.ssh.is_none() && c.win.is_none() {
|
||||
return None;
|
||||
}
|
||||
Some(c)
|
||||
}
|
||||
|
||||
/// A directive describing the host credentials available to the agents, so
|
||||
/// they can authenticate to Linux (SSH) / Windows (AD) hosts.
|
||||
pub fn host_instruction(&self) -> Option<String> {
|
||||
let mut s = String::new();
|
||||
if let Some(h) = &self.ssh {
|
||||
let auth = if !h.key.is_empty() { format!("private key {}", h.key) } else { "password (provided)".into() };
|
||||
s.push_str(&format!(
|
||||
"SSH ACCESS (Linux): host {}:{} as user '{}' via {}. Use `ssh`/`sshpass` to run \
|
||||
enumeration and privilege-escalation checks on the host.\n",
|
||||
h.host, h.port, h.user, auth));
|
||||
}
|
||||
if let Some(w) = &self.win {
|
||||
let auth = if !w.hash.is_empty() { "NTLM hash (pass-the-hash)".to_string() } else { "password".into() };
|
||||
s.push_str(&format!(
|
||||
"WINDOWS/AD ACCESS: host {} domain '{}' as user '{}' via {}. Use tools like \
|
||||
crackmapexec/netexec, impacket, evil-winrm, bloodhound-python for host and AD checks.\n",
|
||||
w.host, if w.domain.is_empty() { "(workgroup)" } else { &w.domain }, w.user, auth));
|
||||
}
|
||||
if s.is_empty() { None } else { Some(s) }
|
||||
}
|
||||
|
||||
/// The auth material to send with each request, as a header line.
|
||||
pub fn auth_header(&self) -> Option<String> {
|
||||
if let Some(h) = &self.header {
|
||||
|
||||
@@ -24,7 +24,7 @@ pub use models::{
|
||||
cli_binary_for, ensure_playwright_mcp, installed_cli_backends, mcp_supported, provider_for,
|
||||
providers, write_mcp_config, ChatClient, ModelRef, Provider,
|
||||
};
|
||||
pub use pipeline::{run_greybox, run_whitebox, RunOutput};
|
||||
pub use pipeline::{run_greybox, run_host, run_whitebox, RunOutput};
|
||||
pub use pipeline::run;
|
||||
pub use pool::{ModelPool, Task};
|
||||
pub use types::{Finding, RunConfig};
|
||||
@@ -864,3 +864,98 @@ fn collect_repo_context(root: &Path, max_files: usize, max_bytes: usize) -> Stri
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
const HOST_RECON_SYS: &str = "You are an infrastructure recon specialist on an AUTHORIZED engagement against a HOST/IP. Actively scan with rustscan/nmap (and netexec/smbclient where relevant) to map open ports, services, versions and auth surfaces. Use any provided SSH/Windows credentials to enumerate from inside. Do not ask permission; proceed. Reply with a compact JSON object (host, os, ports, services, auth, ad). No prose.";
|
||||
|
||||
const HOST_TOOLING: &str = "TOOLING (best on Kali): nmap/rustscan (ports), netexec/crackmapexec + smbclient (SMB/AD), ssh/sshpass + linpeas (Linux), evil-winrm + winPEAS + impacket (Windows), bloodhound-python/SharpHound (AD), hashcat (offline cracking). Use only supplied credentials; never brute force or run destructive/DoS actions.\n\n";
|
||||
|
||||
/// Infrastructure engagement: scan/enumerate an IP/host and run Linux/Windows/AD
|
||||
/// agents. Mirrors the web pipeline but selects from the `infra` agent set.
|
||||
pub async fn run_host(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<String>) -> RunOutput {
|
||||
pool.set_progress(tx.clone());
|
||||
let _ = tx.send(format!("HOST · target: {} · {} infra agents · models: {}", cfg.target, lib.infra.len(),
|
||||
pool.candidates.iter().map(|m| m.label()).collect::<Vec<_>>().join(", "))).await;
|
||||
|
||||
let recon = if cfg.offline {
|
||||
"{}".to_string()
|
||||
} else {
|
||||
let user = format!("{}{}Target host: {}", operator_directives(&cfg), HOST_TOOLING, cfg.target);
|
||||
match pool.complete_routed(Task::Recon, "recon", HOST_RECON_SYS, &user).await {
|
||||
Ok((m, t)) => { let _ = tx.send(format!("recon complete via {}", m.label())).await; t }
|
||||
Err(e) => { let _ = tx.send(format!("recon failed ({e})")).await; "{}".to_string() }
|
||||
}
|
||||
};
|
||||
|
||||
let mut rl = cfg.rl_path.as_ref().map(|p| RlState::load(Path::new(p))).unwrap_or_default();
|
||||
let mut ranked: Vec<Agent> = lib.infra.clone();
|
||||
ranked.sort_by(|a, b| rl.weight(&b.name).partial_cmp(&rl.weight(&a.name)).unwrap_or(std::cmp::Ordering::Equal));
|
||||
let cap = if cfg.max_agents > 0 { cfg.max_agents.min(ranked.len()) } else { ranked.len() };
|
||||
let focus = cfg.instructions.clone().unwrap_or_default();
|
||||
|
||||
if cfg.offline {
|
||||
let selected: Vec<Agent> = ranked.into_iter().take(cap).collect();
|
||||
let _ = tx.send(format!("offline: selected {} infra agent(s); no live testing", selected.len())).await;
|
||||
let artifacts = persist(&cfg, &recon, "", &[]);
|
||||
return RunOutput { target: cfg.target.clone(), workdir: cfg.workdir.clone().unwrap_or_default(), findings: vec![],
|
||||
agents_ran: selected.iter().map(|a| a.name.clone()).collect(), candidates: 0, recon, artifacts };
|
||||
}
|
||||
|
||||
let chosen = select_agents(pool, &recon, &focus, &ranked, &tx).await;
|
||||
let selected: Vec<Agent> = if !chosen.is_empty() {
|
||||
let sel: Vec<Agent> = ranked.iter().filter(|a| chosen.iter().any(|c| c == &a.name)).cloned().collect();
|
||||
if sel.is_empty() { ranked.iter().take(cap).cloned().collect() } else { sel.into_iter().take(cap).collect() }
|
||||
} else {
|
||||
ranked.iter().take(cap).cloned().collect()
|
||||
};
|
||||
let selected: Vec<Agent> = { let mut seen = std::collections::HashSet::new();
|
||||
selected.into_iter().filter(|a| seen.insert(a.name.clone())).collect() };
|
||||
let _ = tx.send(format!("selected {} infra agent(s): {}", selected.len(),
|
||||
selected.iter().map(|a| a.name.clone()).collect::<Vec<_>>().join(", "))).await;
|
||||
|
||||
let target = cfg.target.clone();
|
||||
let verbose = cfg.verbose;
|
||||
let directives = operator_directives(&cfg);
|
||||
let recon_ctx: String = recon.chars().take(3000).collect();
|
||||
let raw: Vec<(String, String, Vec<Finding>)> = stream::iter(selected.iter().cloned())
|
||||
.map(|ag| {
|
||||
let target = target.clone();
|
||||
let recon = recon_ctx.clone();
|
||||
let directives = directives.clone();
|
||||
let txc = tx.clone();
|
||||
async move {
|
||||
if pool.is_cancelled() { return (ag.name.clone(), String::new(), vec![]); }
|
||||
if verbose {
|
||||
let _ = txc.send(format!(" ▶ launching agent: {} ({})", ag.name, ag.title.replace(" Agent", ""))).await;
|
||||
}
|
||||
let user = format!(
|
||||
"AUTHORIZED host engagement on {target}. Proceed and PROVE each issue with raw tool output.\n\n{directives}{tooling}{react}{body}\n\nReply ONLY a JSON array of confirmed findings (may be []): {{id,title,severity,cwe,endpoint,payload,evidence,impact,remediation,confidence}}.",
|
||||
target = target, directives = directives, tooling = HOST_TOOLING, react = REACT_DOCTRINE,
|
||||
body = ag.user.replace("{target}", &target).replace("{recon_json}", &recon),
|
||||
);
|
||||
match pool.complete_routed(Task::Exploit, &ag.name, &ag.system, &user).await {
|
||||
Ok((m, text)) => {
|
||||
let f = extract_findings(&text, &ag.name);
|
||||
let _ = txc.send(format!("test {} via {} → {} candidate(s)", ag.name, m.label(), f.len())).await;
|
||||
for c in &f { let _ = txc.send(format!("finding: [{}] {} @ {}", c.severity, c.title, c.endpoint)).await; }
|
||||
(ag.name.clone(), text, f)
|
||||
}
|
||||
Err(e) => { let _ = txc.send(format!("test {} failed: {e}", ag.name)).await;
|
||||
(ag.name.clone(), format!("ERROR: {e}"), vec![]) }
|
||||
}
|
||||
}
|
||||
})
|
||||
.buffer_unordered(cfg.concurrency)
|
||||
.collect::<Vec<_>>().await;
|
||||
|
||||
let transcript = transcript_of(&raw);
|
||||
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;
|
||||
if !chained.is_empty() {
|
||||
let extra = validate(dedup_findings(chained), pool, VOTE_SYS, cfg.vote_n, &tx).await;
|
||||
findings.extend(extra);
|
||||
findings = dedup_findings(findings);
|
||||
}
|
||||
finish(cfg, lib, recon, transcript, findings, selected, &mut rl, tx).await
|
||||
}
|
||||
Reference in new issue
Block a user