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:
CyberSecurityUP
2026-06-24 22:17:14 -03:00
co-authored by Claude Opus 4.8
parent 969af20a8e
commit f8d70ce9c5
23 changed files with 893 additions and 32 deletions
+50 -4
View File
@@ -130,6 +130,29 @@ enum Cmd {
#[arg(long)]
mcp: bool,
},
/// Infra/host: scan an IP/host and run Linux/Windows/AD agents. SSH/Windows
/// credentials come from --creds (creds.yaml ssh:/windows: blocks).
Host {
/// Target host or IP.
target: String,
#[arg(long = "model")]
models: Vec<String>,
/// Credentials YAML (ssh / windows / ad blocks).
#[arg(long)]
creds: Option<String>,
#[arg(long)]
focus: Option<String>,
#[arg(long, default_value_t = 0)]
max_agents: usize,
#[arg(long, default_value_t = 3)]
vote_n: usize,
#[arg(long)]
offline: bool,
#[arg(long)]
subscription: bool,
#[arg(short, long)]
verbose: bool,
},
/// Show agent library counts.
Agents,
/// List providers and models.
@@ -178,8 +201,8 @@ async fn main() -> anyhow::Result<()> {
Cmd::Agents => {
let lib = agents::load(&base);
println!(
"{{\"vulns\":{},\"recon\":{},\"code\":{},\"meta\":{},\"total\":{}}}",
lib.vulns.len(), lib.recon.len(), lib.code.len(), lib.meta.len(), lib.total()
"{{\"vulns\":{},\"recon\":{},\"code\":{},\"infra\":{},\"meta\":{},\"total\":{}}}",
lib.vulns.len(), lib.recon.len(), lib.code.len(), lib.infra.len(), lib.meta.len(), lib.total()
);
}
Cmd::Models => {
@@ -251,6 +274,21 @@ async fn main() -> anyhow::Result<()> {
let mode = if repo.is_some() { Mode::Grey } else { Mode::Black };
tui::run(&base, cfg, mcp, mode).await?;
}
Cmd::Host { target, models, creds, focus, max_agents, vote_n, offline, subscription, verbose } => {
let mut cfg = RunConfig::new(&target);
cfg.max_agents = max_agents;
cfg.vote_n = vote_n;
cfg.offline = offline;
cfg.subscription = subscription;
cfg.verbose = verbose;
cfg.instructions = focus;
if !models.is_empty() {
cfg.models = models;
}
apply_creds(&mut cfg, creds.as_deref()).await;
let out = run_mode(&base, cfg, false, Mode::Host).await?;
print_findings(&out);
}
}
Ok(())
}
@@ -274,6 +312,13 @@ pub(crate) async fn apply_creds(cfg: &mut RunConfig, path: Option<&str>) {
if cfg.auth.is_none() {
cfg.auth = c.auth_header();
}
// Host credentials (SSH / Windows-AD) → tell the agents how to authenticate
// to the host so they can run on-host enumeration / privesc / AD checks.
if let Some(hi) = c.host_instruction() {
let base = cfg.instructions.clone().unwrap_or_default();
cfg.instructions = Some(format!("{hi}\n{base}"));
println!(" [*] host credentials loaded (SSH/Windows-AD)");
}
// No direct material but a login flow → perform it now.
if cfg.auth.is_none() {
if let Some(login) = &c.login {
@@ -296,7 +341,7 @@ pub(crate) async fn apply_creds(cfg: &mut RunConfig, path: Option<&str>) {
}
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum Mode { Black, White, Grey }
pub(crate) enum Mode { Black, White, Grey, Host }
pub(crate) async fn run_greybox_engagement(base: &Path, cfg: RunConfig, mcp: bool) -> anyhow::Result<RunOutput> {
run_mode(base, cfg, mcp, Mode::Grey).await
@@ -327,7 +372,7 @@ async fn run_mode(base: &Path, mut cfg: RunConfig, mcp: bool, mode: Mode) -> any
println!(" │ repo : {}", cfg.repo.clone().unwrap_or_default());
}
println!(" └─ mode : {}{}{}",
match mode { Mode::White => "white-box", Mode::Grey => "greybox", Mode::Black => "black-box" },
match mode { Mode::White => "white-box", Mode::Grey => "greybox", Mode::Host => "host/infra", Mode::Black => "black-box" },
if cfg.subscription { " · subscription" } else { " · api" },
if mcp { " · mcp" } else { "" });
@@ -376,6 +421,7 @@ async fn run_mode(base: &Path, mut cfg: RunConfig, mcp: bool, mode: Mode) -> any
let out = match mode {
Mode::White => harness::run_whitebox(cfg, &lib, &pool, tx).await,
Mode::Grey => harness::run_greybox(cfg, &lib, &pool, tx).await,
Mode::Host => harness::run_host(cfg, &lib, &pool, tx).await,
Mode::Black => harness::run(cfg, &lib, &pool, tx).await,
};
out
+26 -6
View File
@@ -131,8 +131,6 @@ impl Default for Session {
}
}
const PROMPT: &str = "\x1b[35mneurosploit\x1b[0m ";
/// Line reader: full rustyline editing (Tab-complete, history, multiline) when
/// interactive, plain stdin when piped.
enum Reader {
@@ -157,9 +155,10 @@ impl Reader {
/// Returns None to exit (EOF / Ctrl-D), Some(line) otherwise. Ctrl-C cancels
/// the current line (returns an empty string) instead of exiting.
fn read(&mut self) -> Option<String> {
/// `prompt` is the dynamic context bar + prompt to show.
fn read(&mut self, prompt: &str) -> Option<String> {
match self {
Reader::Rl(ed, hist) => match ed.readline(PROMPT) {
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', " ");
@@ -174,7 +173,7 @@ impl Reader {
},
Reader::Plain(stdin) => {
use std::io::Write;
print!("{PROMPT}");
print!("{prompt}");
std::io::stdout().flush().ok();
let mut s = String::new();
match stdin.read_line(&mut s) {
@@ -209,7 +208,7 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
show(&s);
loop {
let Some(line) = reader.read() else { println!("\n bye."); break };
let Some(line) = reader.read(&context_prompt(&s)) else { println!("\n bye."); break };
let line = line.trim();
if line.is_empty() {
continue;
@@ -590,5 +589,26 @@ fn parse_range(r: &str) -> Option<(usize, usize)> {
}
}
/// Context/status bar shown above the prompt — model · cwd · mode/target,
/// e.g. "claude-opus-4-8 · /opt/projeto · black-box▸target".
fn context_prompt(s: &Session) -> String {
let model = s.models.first().map(|m| m.split(':').next_back().unwrap_or(m)).unwrap_or("?");
let auth = if s.subscription { "sub" } else { "api" };
let cwd = std::env::current_dir().ok()
.map(|p| p.display().to_string())
.unwrap_or_else(|| ".".into());
let mode = match (&s.repo, &s.target) {
(Some(_), Some(_)) => "greybox",
(Some(_), None) => "white-box",
(None, Some(_)) => "black-box",
_ => "idle",
};
let tgt = s.target.clone().or_else(|| s.repo.clone()).unwrap_or_default();
let tgt = if tgt.is_empty() { String::new() } else { format!("{}", tgt.replace("https://", "").replace("http://", "")) };
format!(
"\x1b[2m{model} {auth} · {cwd} · {mode}{tgt}\x1b[0m\n\x1b[35mneurosploit\x1b[0m "
)
}
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)]) } }
+2 -1
View File
@@ -148,7 +148,7 @@ pub async fn run(base: &Path, mut cfg: RunConfig, mcp: bool, mode: Mode) -> anyh
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(512);
let models = cfg.models.join(", ");
let mode_s = match mode { Mode::White => "white-box", Mode::Grey => "greybox", Mode::Black => "black-box" };
let mode_s = match mode { Mode::White => "white-box", Mode::Grey => "greybox", Mode::Host => "host/infra", Mode::Black => "black-box" };
let target_s = cfg.target.clone();
// ---- terminal setup FIRST: on a non-TTY this errors before we spawn any
@@ -162,6 +162,7 @@ pub async fn run(base: &Path, mut cfg: RunConfig, mcp: bool, mode: Mode) -> anyh
match mode {
Mode::White => harness::run_whitebox(cfg, &lib, &pool, tx).await,
Mode::Grey => harness::run_greybox(cfg, &lib, &pool, tx).await,
Mode::Host => harness::run_host(cfg, &lib, &pool, tx).await,
Mode::Black => harness::run(cfg, &lib, &pool, tx).await,
}
});