v3.5.5 — cloud infrastructure testing + REPL polish

Cloud testing:
- +17 cloud agents (agents_md/infra/) for AWS/GCP/Azure: IAM/RBAC privesc,
  storage exposure (S3/GCS/Blob), compute & network exposure + IMDS, secrets
  (Secrets Manager / Secret Manager / Key Vault), SA/SP key abuse, Entra ID
  enum, and a multi-cloud footprint/identity recon agent. Library 348 -> 365.
- creds.yaml gains aws:/gcp:/azure: blocks (Creds::cloud). The harness exports
  provider env vars (AWS_*, GOOGLE_APPLICATION_CREDENTIALS, AZURE_* SP) so
  aws/gcloud/az authenticate automatically, and injects a cloud directive. GCP
  inline JSON is written to a temp file. Best-practice auth per provider.

REPL polish:
- /chain <n> (attack-chain depth, wired to Session.chain_depth), /agents list
  (library category counts incl. infra/cloud); /show now shows chain-depth and
  enabled integrations. Tab-completion + help updated.

Docs: README badges (365 agents / 14 providers), new "Cloud credentials" section;
RELEASE notes. Version 3.5.4 -> 3.5.5.
This commit is contained in:
CyberSecurityUP
2026-07-01 22:38:27 -03:00
parent e5c607f467
commit 2e25809a93
34 changed files with 1123 additions and 38 deletions
+2 -2
View File
@@ -871,7 +871,7 @@ dependencies = [
[[package]]
name = "neurosploit"
version = "3.5.4"
version = "3.5.5"
dependencies = [
"anyhow",
"clap",
@@ -888,7 +888,7 @@ dependencies = [
[[package]]
name = "neurosploit-harness"
version = "3.5.4"
version = "3.5.5"
dependencies = [
"anyhow",
"futures",
+1 -1
View File
@@ -3,7 +3,7 @@ members = ["crates/harness", "app"]
resolver = "2"
[workspace.package]
version = "3.5.4"
version = "3.5.5"
edition = "2021"
license = "MIT"
repository = "https://github.com/JoasASantos/NeuroSploit"
+22 -4
View File
@@ -1,4 +1,4 @@
//! NeuroSploit v3.5.4 — interactive harness + CLI (`run` / `whitebox` / `agents` / `models`).
//! NeuroSploit v3.5.5 — interactive harness + CLI (`run` / `whitebox` / `agents` / `models`).
mod repl;
mod tui;
@@ -11,8 +11,8 @@ use std::path::{Path, PathBuf};
#[command(
name = "neurosploit",
version,
about = "NeuroSploit v3.5.4 — multi-model autonomous pentest harness",
long_about = "NeuroSploit v3.5.4 — a Rust multi-model harness that drives a pool of LLMs \
about = "NeuroSploit v3.5.5 — multi-model autonomous pentest harness",
long_about = "NeuroSploit v3.5.5 — a Rust multi-model harness that drives a pool of LLMs \
(API key or local subscription: Claude/Codex/Gemini/Grok) to autonomously test a target. \
After recon it INTELLIGENTLY selects only the agents matching the discovered surface, runs \
them in parallel, then validates every finding by cross-model voting before reporting.\n\n\
@@ -474,6 +474,24 @@ pub(crate) async fn apply_creds(cfg: &mut RunConfig, path: Option<&str>) {
cfg.instructions = Some(format!("{hi}\n{base}"));
println!(" [*] host credentials loaded (SSH/Windows-AD)");
}
// Cloud credentials (AWS / GCP / Azure) → export env for the provider CLIs
// and tell the agents how to authenticate & what to enumerate.
let cloud_env = c.cloud_env();
if !cloud_env.is_empty() {
for (k, v) in &cloud_env {
std::env::set_var(k, v);
}
let names: Vec<&str> = [
(!c.cloud.as_ref().map(|x| x.aws_access_key_id.is_empty() && x.aws_profile.is_empty()).unwrap_or(true), "AWS"),
(!c.cloud.as_ref().map(|x| x.gcp_sa_json.is_empty()).unwrap_or(true), "GCP"),
(!c.cloud.as_ref().map(|x| x.azure_client_id.is_empty()).unwrap_or(true), "Azure"),
].iter().filter(|(on, _)| *on).map(|(_, n)| *n).collect();
println!(" [*] cloud credentials loaded ({}) — {} env var(s) exported", names.join("/"), cloud_env.len());
if let Some(ci) = c.cloud_instruction() {
let base = cfg.instructions.clone().unwrap_or_default();
cfg.instructions = Some(format!("{ci}\n{base}"));
}
}
// No direct material but a login flow → perform it now.
if cfg.auth.is_none() {
if let Some(login) = &c.login {
@@ -534,7 +552,7 @@ pub(crate) fn spawn_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, mode:
cfg.rl_path = Some(base.join("data").join("rl_state_rs.json").display().to_string());
write_status(&workdir, "running", &format!("\"target\":{:?}", cfg.target));
println!(" ┌─ NeuroSploit v3.5.4 · by Joas A Santos & Red Team Leaders");
println!(" ┌─ NeuroSploit v3.5.5 · by Joas A Santos & Red Team Leaders");
println!(" │ run id : {run_id}");
println!(" │ target : {}", cfg.target);
println!(" │ models : {}", cfg.models.join(", "));
+34 -7
View File
@@ -1,4 +1,4 @@
//! NeuroSploit v3.5.4 — interactive session (Claude-Code / Codex / Cursor-CLI style).
//! NeuroSploit v3.5.5 — interactive session (Claude-Code / Codex / Cursor-CLI style).
//!
//! Launched when `neurosploit` runs with no subcommand. A persistent REPL with
//! real line editing (arrow-key history recall, Ctrl-A/E/K, paste), model
@@ -119,7 +119,7 @@ struct LiveCheckpoint {
const COMMANDS: &[&str] = &[
"/help", "/show", "/config", "/providers", "/model", "/key", "/sub", "/target",
"/repo", "/auth", "/creds", "/focus", "/attach", "/context", "/mcp", "/offline",
"/votes", "/agents", "/theme", "/clear", "/run", "/stop", "/continue", "/runs", "/results", "/report",
"/votes", "/chain", "/agents", "/theme", "/clear", "/run", "/stop", "/continue", "/runs", "/results", "/report",
"/status", "/diff", "/retest", "/integrations", "/quit",
];
@@ -198,6 +198,7 @@ struct Session {
mcp: bool,
vote_n: usize,
max_agents: usize,
chain_depth: usize,
offline: bool,
target: Option<String>,
repo: Option<String>,
@@ -216,6 +217,7 @@ impl Default for Session {
mcp: false,
vote_n: 3,
max_agents: 0,
chain_depth: 2,
offline: false,
target: None,
repo: None,
@@ -299,7 +301,7 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
let backends = harness::installed_cli_backends();
println!("\x1b[1m");
println!(" ███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗");
println!(" ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗ NeuroSploit v3.5.4");
println!(" ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗ NeuroSploit v3.5.5");
println!(" ██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║ interactive harness");
println!(" ██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║ by Joas A Santos");
println!(" ██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝ & Red Team Leaders");
@@ -432,7 +434,22 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
"/offline" => { s.offline = !matches!(arg, "off" | "false" | "0" | "no"); println!(" offline: {}", onoff(s.offline)); }
"/integrations" | "/integration" => integrations_cmd(arg),
"/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); }
"/chain" => {
if arg.is_empty() { println!(" attack-chain depth: {} (0 disables) — set with /chain <n>", s.chain_depth); }
else { s.chain_depth = arg.parse().unwrap_or(s.chain_depth); println!(" attack-chain depth: {}", s.chain_depth); }
}
"/agents" => {
if arg == "list" || arg == "ls" {
let lib = agents::load(base);
println!(" agent library ({} total):", lib.total());
println!(" vulns {} · code {} · infra/cloud {} · recon {} · chains {} · meta {}",
lib.vulns.len(), lib.code.len(), lib.infra.len(), lib.recon.len(), lib.chains.len(), lib.meta.len());
} else if arg.is_empty() {
println!(" max agents: {} (0 = all) — set with /agents <n>, or /agents list for counts", s.max_agents);
} else {
s.max_agents = arg.parse().unwrap_or(s.max_agents); println!(" max agents: {}", s.max_agents);
}
}
"/clear" => { print!("\x1b[2J\x1b[H"); }
"/run" | "/go" => {
if active.as_ref().map(|a| !a.done.load(Ordering::Relaxed)).unwrap_or(false) {
@@ -667,6 +684,7 @@ async fn run(base: &Path, s: &Session, history: &mut Vec<RunRecord>) {
cfg.models = s.models.clone();
cfg.subscription = s.subscription;
cfg.vote_n = s.vote_n;
cfg.chain_depth = s.chain_depth;
cfg.max_agents = s.max_agents;
cfg.verbose = true;
cfg.offline = s.offline;
@@ -716,6 +734,7 @@ async fn start_background(base: &Path, s: &Session, reader: &mut Reader,
cfg.models = s.models.clone();
cfg.subscription = s.subscription;
cfg.vote_n = s.vote_n;
cfg.chain_depth = s.chain_depth;
cfg.max_agents = s.max_agents;
cfg.verbose = true;
cfg.offline = s.offline;
@@ -1062,7 +1081,14 @@ fn show(s: &Session) {
println!(" │ auth : {}", s.auth.clone().unwrap_or_else(|| "(none)".into()));
println!(" │ creds : {}", s.creds.clone().unwrap_or_else(|| "(none)".into()));
println!(" │ focus : {}", s.instructions.clone().unwrap_or_else(|| "(none — tests everything)".into()));
println!(" │ opts : mcp={} offline={} votes={} max-agents={}", onoff(s.mcp), onoff(s.offline), s.vote_n, s.max_agents);
println!(" │ opts : mcp={} offline={} votes={} chain-depth={} max-agents={}", onoff(s.mcp), onoff(s.offline), s.vote_n, s.chain_depth, s.max_agents);
// Integrations at a glance (see /integrations for detail).
{
let ig = harness::integrations::Integrations::load(&proj_dir());
let on: Vec<&str> = [(ig.github.enabled, "github"), (ig.gitlab.enabled, "gitlab"), (ig.jira.enabled, "jira")]
.iter().filter(|(e, _)| *e).map(|(_, n)| *n).collect();
println!(" │ integr. : {}", if on.is_empty() { "(none — /integrations)".into() } else { on.join(", ") });
}
// API-key status for the providers your selected models need.
if !s.subscription {
let provs: std::collections::BTreeSet<String> = s.models.iter()
@@ -1111,8 +1137,9 @@ fn help() {
println!("\n \x1b[2mOPTIONS\x1b[0m");
h("/mcp on|off", "Playwright MCP browser /offline on|off self-test");
h("/votes <n>", "validator votes /agents <n> cap agents");
h("/theme color|mono", "/show (config) /clear /quit");
h("/votes <n>", "validator votes /chain <n> attack-chain depth");
h("/agents <n>|list", "cap agents · list counts /theme color|mono");
h("/show (config)", "/clear /quit");
println!("\n \x1b[2mMODES — black-box: set /target · white-box: set /repo · grey-box: set BOTH /repo + /target · host: /target <ip> + /creds\x1b[0m");
println!(" \x1b[2mFindings are checkpointed live to .neurosploit/ — quit/crash mid-run and they're recovered into /runs next launch.\x1b[0m");
+1 -1
View File
@@ -1,4 +1,4 @@
//! NeuroSploit v3.5.4 — TUI "Mission Control" mode.
//! NeuroSploit v3.5.5 — TUI "Mission Control" mode.
//!
//! Concurrent panels that update live while the engagement runs in the
//! background, with a composer input that stays active during execution:
+1 -1
View File
@@ -1,4 +1,4 @@
//! POMDP belief-state world model (v3.5.4).
//! POMDP belief-state world model (v3.5.5).
//!
//! The target is only partially observable, so we don't track booleans — we
//! track a **belief**: a property graph whose nodes (host / service / vuln /
+124 -2
View File
@@ -51,6 +51,35 @@ pub struct Win {
pub hash: String, // NTLM hash for pass-the-hash (LM:NT or NT)
}
/// Cloud provider credentials for cloud-infra testing (AWS / GCP / Azure).
/// Secrets are read from `creds.yaml` and exported to the process environment so
/// the `aws` / `gcloud` / `az` CLIs the agents use pick them up automatically.
#[derive(Default, Debug, Clone)]
pub struct Cloud {
// AWS — static keys (access key + secret [+ session token]) OR a named profile.
pub aws_access_key_id: String,
pub aws_secret_access_key: String,
pub aws_session_token: String,
pub aws_region: String,
pub aws_profile: String,
// GCP — a service-account JSON (path, recommended) or inline single-line JSON.
pub gcp_sa_json: String,
pub gcp_project: String,
// Azure — a service principal (recommended for non-interactive automation).
pub azure_tenant_id: String,
pub azure_client_id: String,
pub azure_client_secret: String,
pub azure_subscription_id: String,
}
impl Cloud {
fn is_empty(&self) -> bool {
self.aws_access_key_id.is_empty() && self.aws_profile.is_empty()
&& self.gcp_sa_json.is_empty()
&& self.azure_client_id.is_empty()
}
}
#[derive(Default, Debug, Clone)]
pub struct Creds {
pub jwt: Option<String>,
@@ -59,6 +88,7 @@ pub struct Creds {
pub login: Option<Login>,
pub ssh: Option<Ssh>,
pub win: Option<Win>,
pub cloud: Option<Cloud>,
}
impl Creds {
@@ -68,8 +98,9 @@ impl Creds {
let mut login = Login { method: "POST".into(), ..Default::default() };
let mut ssh = Ssh { port: "22".into(), ..Default::default() };
let mut win = Win::default();
let mut cloud = Cloud::default();
let (mut have_login, mut have_ssh, mut have_win) = (false, false, false);
let mut block = ""; // "", "login", "ssh", "windows"
let mut block = ""; // "", "login", "ssh", "windows", "aws", "gcp", "azure"
for raw in text.lines() {
let line = raw.split('#').next().unwrap_or("");
if line.trim().is_empty() {
@@ -86,6 +117,9 @@ impl Creds {
"login" => { have_login = true; "login" }
"ssh" => { have_ssh = true; "ssh" }
"windows" | "win" | "ad" => { have_win = true; "windows" }
"aws" => "aws",
"gcp" | "google" | "gcloud" => "gcp",
"azure" | "az" => "azure",
_ => "",
};
continue;
@@ -118,6 +152,26 @@ impl Creds {
"hash" | "ntlm" => win.hash = v,
_ => {}
},
"aws" => match k.as_str() {
"access_key_id" | "access_key" | "key" => cloud.aws_access_key_id = v,
"secret_access_key" | "secret" => cloud.aws_secret_access_key = v,
"session_token" | "token" => cloud.aws_session_token = v,
"region" => cloud.aws_region = v,
"profile" => cloud.aws_profile = v,
_ => {}
},
"gcp" => match k.as_str() {
"service_account_json" | "sa_json" | "key" | "keyfile" | "credentials" => cloud.gcp_sa_json = v,
"project" | "project_id" => cloud.gcp_project = v,
_ => {}
},
"azure" => match k.as_str() {
"tenant_id" | "tenant" => cloud.azure_tenant_id = v,
"client_id" | "app_id" => cloud.azure_client_id = v,
"client_secret" | "secret" | "password" => cloud.azure_client_secret = v,
"subscription_id" | "subscription" => cloud.azure_subscription_id = v,
_ => {}
},
_ => {}
}
continue;
@@ -133,13 +187,81 @@ impl Creds {
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 !cloud.is_empty() { c.cloud = Some(cloud); }
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() {
&& c.login.is_none() && c.ssh.is_none() && c.win.is_none() && c.cloud.is_none() {
return None;
}
Some(c)
}
/// Environment variables to export so the `aws`/`gcloud`/`az` CLIs the agents
/// run pick up the cloud credentials automatically. For inline GCP JSON the
/// content is written to a temp file and that path is returned.
pub fn cloud_env(&self) -> Vec<(String, String)> {
let mut e: Vec<(String, String)> = Vec::new();
let Some(c) = &self.cloud else { return e };
// AWS
if !c.aws_access_key_id.is_empty() {
e.push(("AWS_ACCESS_KEY_ID".into(), c.aws_access_key_id.clone()));
e.push(("AWS_SECRET_ACCESS_KEY".into(), c.aws_secret_access_key.clone()));
if !c.aws_session_token.is_empty() {
e.push(("AWS_SESSION_TOKEN".into(), c.aws_session_token.clone()));
}
}
if !c.aws_profile.is_empty() { e.push(("AWS_PROFILE".into(), c.aws_profile.clone())); }
if !c.aws_region.is_empty() {
e.push(("AWS_DEFAULT_REGION".into(), c.aws_region.clone()));
e.push(("AWS_REGION".into(), c.aws_region.clone()));
}
// GCP — path (recommended) or inline JSON written to a temp file.
if !c.gcp_sa_json.is_empty() {
let path = if c.gcp_sa_json.trim_start().starts_with('{') {
let p = std::env::temp_dir().join("neurosploit-gcp-sa.json");
let _ = std::fs::write(&p, c.gcp_sa_json.as_bytes());
p.display().to_string()
} else {
c.gcp_sa_json.clone()
};
e.push(("GOOGLE_APPLICATION_CREDENTIALS".into(), path));
}
if !c.gcp_project.is_empty() {
e.push(("GOOGLE_CLOUD_PROJECT".into(), c.gcp_project.clone()));
e.push(("CLOUDSDK_CORE_PROJECT".into(), c.gcp_project.clone()));
}
// Azure — service principal env (consumed by `az login --service-principal`).
if !c.azure_tenant_id.is_empty() { e.push(("AZURE_TENANT_ID".into(), c.azure_tenant_id.clone())); }
if !c.azure_client_id.is_empty() { e.push(("AZURE_CLIENT_ID".into(), c.azure_client_id.clone())); }
if !c.azure_client_secret.is_empty() { e.push(("AZURE_CLIENT_SECRET".into(), c.azure_client_secret.clone())); }
if !c.azure_subscription_id.is_empty() {
e.push(("AZURE_SUBSCRIPTION_ID".into(), c.azure_subscription_id.clone()));
e.push(("ARM_SUBSCRIPTION_ID".into(), c.azure_subscription_id.clone()));
}
e
}
/// A directive telling the agents which cloud creds are available and how to
/// authenticate the provider CLI, so they enumerate/test the cloud account.
pub fn cloud_instruction(&self) -> Option<String> {
let c = self.cloud.as_ref()?;
let mut s = String::new();
if !c.aws_access_key_id.is_empty() || !c.aws_profile.is_empty() {
s.push_str(&format!(
"AWS ACCESS: credentials are set in the environment{}. Use the `aws` CLI to enumerate and test the account — start with `aws sts get-caller-identity`, then IAM (users/roles/policies, privilege escalation paths), S3 (public/misconfigured buckets), EC2/SG, Lambda, Secrets Manager. Read-only enumeration first; never destructive.\n",
if c.aws_region.is_empty() { String::new() } else { format!(" (region {})", c.aws_region) }));
}
if !c.gcp_sa_json.is_empty() {
s.push_str(&format!(
"GCP ACCESS: a service account is available via $GOOGLE_APPLICATION_CREDENTIALS{}. Run `gcloud auth activate-service-account --key-file=$GOOGLE_APPLICATION_CREDENTIALS` first, then enumerate with `gcloud`/`gsutil` — IAM bindings & privilege escalation, buckets, compute, service accounts/keys, Cloud Functions.\n",
if c.gcp_project.is_empty() { String::new() } else { format!(" (project {})", c.gcp_project) }));
}
if !c.azure_client_id.is_empty() {
s.push_str(
"AZURE ACCESS: a service principal is set in the environment. Authenticate with `az login --service-principal -u $AZURE_CLIENT_ID -p $AZURE_CLIENT_SECRET --tenant $AZURE_TENANT_ID`, then enumerate with `az` — role assignments (RBAC) & escalation, storage accounts/containers, VMs, Key Vaults, managed identities.\n");
}
if s.is_empty() { None } else { Some(s) }
}
/// 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> {
@@ -1,4 +1,4 @@
//! Verification / grounding engine (v3.5.4).
//! Verification / grounding engine (v3.5.5).
//!
//! Hard rule: **no claim enters the world model without a tool receipt** — raw
//! tool output, not the LLM's paraphrase. This is the empirical anti-hallucination
+1 -1
View File
@@ -1,4 +1,4 @@
//! NeuroSploit v3.5.4 harness — a robust multi-model runtime for the
//! NeuroSploit v3.5.5 harness — a robust multi-model runtime for the
//! markdown-driven autonomous pentest engine.
//!
//! The harness loads the `agents_md/` library, drives a *pool* of LLM models
+1 -1
View File
@@ -1,4 +1,4 @@
//! POMDP decision layer (v3.5.4): value-of-information planning + the
//! POMDP decision layer (v3.5.5): value-of-information planning + the
//! anti-hallucination gate.
//!
//! The choice "scan more vs exploit now" is **not** a heuristic here — it falls
+3 -3
View File
@@ -97,9 +97,9 @@ pub fn html(target: &str, findings: &[Finding]) -> String {
h4{{margin:12px 0 3px;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:#8b5cf6}}\
.b{{color:#8b5cf6;font-weight:800}}</style></head><body>\
<h1><span class=b>NeuroSploit</span> Penetration Test Report</h1>\
<div class=meta>Target: <b>{t}</b> · v3.5.4 Rust harness · multi-model validated</div>\
<div class=meta>Target: <b>{t}</b> · v3.5.5 Rust harness · multi-model validated</div>\
<div>{chips}</div>{graph_block}<h2>Findings ({n})</h2>{body}\
<p class=meta>Authorized testing only. Findings confirmed by multi-model adversarial voting.<br>NeuroSploit v3.5.4 · by <b>Joas A Santos</b> &amp; <b>Red Team Leaders</b></p></body></html>",
<p class=meta>Authorized testing only. Findings confirmed by multi-model adversarial voting.<br>NeuroSploit v3.5.5 · by <b>Joas A Santos</b> &amp; <b>Red Team Leaders</b></p></body></html>",
t = esc(target), chips = chips, n = sorted.len(), body = body, graph_block = graph_block,
)
}
@@ -135,7 +135,7 @@ pub fn typst_report(target: &str, findings: &[Finding], dir: &Path) -> std::io::
let mut data = String::new();
data.push_str(&format!(
"#let meta = (target: {}, run_id: {}, generated: {}, model: {})\n",
tq(target), tq(&run_id), tq("NeuroSploit v3.5.4"), tq("multi-model")
tq(target), tq(&run_id), tq("NeuroSploit v3.5.5"), tq("multi-model")
));
data.push_str("#let findings = (\n");
for f in sorted_findings(findings) {