From a6643968e2e23c7af5c2cf822f8f6b604bb33410 Mon Sep 17 00:00:00 2001 From: CyberSecurityUP Date: Thu, 30 Jul 2026 19:32:25 -0300 Subject: [PATCH] feat: liveness preflight, auto-run registration agent, vault in .neurosploit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Preflight: abort a run early with '✗ target unreachable … is DOWN' when the probe gets no HTTP response, instead of running agents against a dead host; print '✓ target is UP' otherwise. - When no --auth/creds are set on a web run, force account_registration_and_forms to run first so the authenticated surface is always attempted and visible. - Move the credential vault to /.neurosploit/vault/.json (persistent project store) via new RunConfig.vault_dir; header now prints the vault path at launch. engagement_ops + finish() resolve paths through vault_paths(). --- RELEASE.md | 8 ++- TUTORIAL.md | 2 +- neurosploit-rs/app/src/main.rs | 7 +++ neurosploit-rs/crates/harness/src/pipeline.rs | 55 +++++++++++++++---- neurosploit-rs/crates/harness/src/types.rs | 5 ++ 5 files changed, 65 insertions(+), 12 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index f5c255f..3cd2947 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -37,17 +37,23 @@ as a finder or in the validator voting panel, e.g. `--model anthropic:claude-opus-5 --model moonshot:kimi-k3`. +- **Liveness preflight.** Before recon, the run confirms the target actually + answers HTTP; a dead host prints `✗ target unreachable — … is DOWN` and aborts + instead of running agents against nothing. A reachable host prints `✓ target is UP`. + - **Account registration & form analysis (+1 agent → total 430).** A new `account_registration_and_forms` agent lets NeuroSploit reach the authenticated surface on its own: it analyzes the app's forms (the deterministic probe now extracts each `
`'s action/method/fields/kind/CSRF) and creates a benign test account with **curl** or the **Playwright browser** when no creds are given. + When no `--auth`/creds are set on a web run, this agent is **run first + automatically** so the authenticated surface is always attempted (and visible). - **Anti-flood guardrail (hard):** at most **2 accounts per engagement**, never looping/scripting/batching the register endpoint or flooding the database — reuse the account made; a test needing many sign-ups is reported as a lead and stopped. Enforced in `SAFETY_DOCTRINE` (all flows) and the agent. - **Credential vault:** every generated credential is saved to - **`/vault.json`** for the operator to consult; secrets are **masked in + **`.neurosploit/vault/.json`** for the operator to consult; secrets are **masked in the report**. The report adds a **"Test accounts created (DELETE after)"** cleanup section listing each account and how it was created. - **Finding labels:** findings are tagged **`auth_context`** diff --git a/TUTORIAL.md b/TUTORIAL.md index 4e6c12b..5508ee5 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -326,7 +326,7 @@ agents know exactly what to submit. account they made. A test that would need many sign-ups is reported as a lead and stopped. - **Credential vault:** every account/credential the run generates is written to - **`/vault.json`** so you can consult the passwords later. Secrets are + **`.neurosploit/vault/.json`** so you can consult the passwords later. Secrets are **masked in the report** and live only in the vault. - **Cleanup list:** the report includes an Info finding **"Test accounts created (DELETE after)"** listing each account and exactly **how it was created** — so you diff --git a/neurosploit-rs/app/src/main.rs b/neurosploit-rs/app/src/main.rs index f9e5a20..e181e37 100644 --- a/neurosploit-rs/app/src/main.rs +++ b/neurosploit-rs/app/src/main.rs @@ -699,6 +699,12 @@ pub(crate) fn spawn_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, mode: std::fs::create_dir_all(&workdir).ok(); cfg.workdir = Some(workdir.display().to_string()); cfg.rl_path = Some(base.join("data").join("rl_state_rs.json").display().to_string()); + // Credential vault lives in the project's .neurosploit store (persistent), + // NOT the transient run dir, so secrets are kept in one known place. + let vault_dir = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")) + .join(".neurosploit").join("vault"); + std::fs::create_dir_all(&vault_dir).ok(); + cfg.vault_dir = Some(vault_dir.display().to_string()); // PoC scratch dir: agents write custom exploit scripts here (see doctrine). let pocs = workdir.join("pocs"); std::fs::create_dir_all(&pocs).ok(); @@ -726,6 +732,7 @@ pub(crate) fn spawn_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, mode: println!(" │ target : {}", cfg.target); println!(" │ models : {}", cfg.models.join(", ")); println!(" │ output : {}", workdir.display()); + println!(" │ vault : {}/{run_id}.json (created test-account credentials; masked in the report)", vault_dir.display()); if let Mode::Grey = mode { println!(" │ repo : {}", cfg.repo.clone().unwrap_or_default()); } diff --git a/neurosploit-rs/crates/harness/src/pipeline.rs b/neurosploit-rs/crates/harness/src/pipeline.rs index 1813bd9..8dd6216 100644 --- a/neurosploit-rs/crates/harness/src/pipeline.rs +++ b/neurosploit-rs/crates/harness/src/pipeline.rs @@ -195,9 +195,7 @@ many registrations, report it as a LEAD and STOP rather than mass-creating accou /// engagement prompts so created accounts are logged for cleanup and findings /// are labelled authenticated vs unauthenticated. fn engagement_ops(cfg: &RunConfig) -> String { - let vault = cfg.workdir.as_deref() - .map(|d| format!("{}/vault.jsonl", d.trim_end_matches('/'))) - .unwrap_or_else(|| "the run directory's vault.jsonl".into()); + let (vault, _) = vault_paths(cfg); let temp = if cfg.temp_email { "DISPOSABLE EMAIL (enabled): if registration requires an email confirmation code/link, you MAY use the free \ mail.tm API (no key) — `POST https://api.mail.tm/accounts` {address,password} to create an inbox (get a \ @@ -224,6 +222,23 @@ fn engagement_ops(cfg: &RunConfig) -> String { - {temp}\n\n" ) } +/// Resolve the vault directory + this run's file stem. Prefer `.neurosploit/vault` +/// (persistent project store) set by the app; fall back to the run workdir. Returns +/// (jsonl_append_path, json_consolidated_path). +fn vault_paths(cfg: &RunConfig) -> (String, String) { + let dir = cfg.vault_dir.clone() + .or_else(|| cfg.workdir.clone()) + .unwrap_or_else(|| ".".into()); + let dir = dir.trim_end_matches('/').to_string(); + let _ = std::fs::create_dir_all(&dir); + // Per-run file stem = the run id (workdir basename), so vaults don't collide. + let stem = cfg.workdir.as_deref() + .and_then(|w| w.trim_end_matches('/').rsplit('/').next()) + .filter(|s| !s.is_empty()) + .unwrap_or("vault").to_string(); + (format!("{dir}/{stem}.jsonl"), format!("{dir}/{stem}.json")) +} + /// One credential the run generated (a created test account). Stored in the run /// vault so the operator can consult it and later delete the account. #[derive(Serialize, Deserialize, Default, Clone)] @@ -239,10 +254,9 @@ struct VaultEntry { /// Read the agent-appended `vault.jsonl` (one JSON object per created account) /// from the run dir. Best-effort: skips malformed lines. `_findings` reserved for /// future correlation. Deduped by account identity. -fn collect_vault(dir: &str, _findings: &[Finding]) -> Vec { - let path = format!("{}/vault.jsonl", dir.trim_end_matches('/')); +fn collect_vault(path: &str, _findings: &[Finding]) -> Vec { let mut out: Vec = Vec::new(); - if let Ok(txt) = std::fs::read_to_string(&path) { + if let Ok(txt) = std::fs::read_to_string(path) { for line in txt.lines() { let line = line.trim(); if line.is_empty() { continue; } @@ -312,6 +326,16 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender = { + let mut selected: Vec = { let mut seen = std::collections::HashSet::new(); selected.into_iter().filter(|a| seen.insert(a.name.clone())).collect() }; + // No creds given → always run the registration/form agent FIRST so the run + // reaches the authenticated surface (and the operator sees it happen). It + // self-registers one test account under the anti-flood guardrail. + if cfg.auth.as_deref().unwrap_or("").trim().is_empty() { + if let Some(reg) = lib.vulns.iter().find(|a| a.name == "account_registration_and_forms") { + if !selected.iter().any(|a| a.name == reg.name) { + let _ = tx.send("no creds set — running account_registration_and_forms first to reach the authenticated surface".into()).await; + selected.insert(0, reg.clone()); + } + } + } let _ = tx .send(format!("intelligently selected {} agent(s) matching recon: {}", selected.len(), selected.iter().map(|a| a.name.clone()).collect::>().join(", "))) @@ -1022,8 +1057,9 @@ async fn finish(cfg: RunConfig, _lib: &Library, recon: String, transcript: Strin // vault.jsonl AND any finding that carried a generated `secret`) into a single // vault.json the operator can consult, then MASK the secret in the report and // add a cleanup summary listing the accounts to delete. - if let Some(dir) = cfg.workdir.clone() { - let mut vault = collect_vault(&dir, &findings); + { + let (jsonl, path) = vault_paths(&cfg); + let mut vault = collect_vault(&jsonl, &findings); // Fold in secrets captured on findings (dedup by account identity). for f in &findings { if !f.secret.is_empty() && !f.account.is_empty() @@ -1035,7 +1071,6 @@ async fn finish(cfg: RunConfig, _lib: &Library, recon: String, transcript: Strin } } if !vault.is_empty() { - let path = format!("{}/vault.json", dir.trim_end_matches('/')); if let Ok(j) = serde_json::to_string_pretty(&vault) { let _ = std::fs::write(&path, j); } let _ = tx.send(format!( "notify: 🔐 vault: {} test account(s) saved → {} — DELETE these after the engagement", diff --git a/neurosploit-rs/crates/harness/src/types.rs b/neurosploit-rs/crates/harness/src/types.rs index 92a92a2..d11e291 100644 --- a/neurosploit-rs/crates/harness/src/types.rs +++ b/neurosploit-rs/crates/harness/src/types.rs @@ -163,6 +163,10 @@ pub struct RunConfig { /// Off by default. Account creation is still capped by the safety guardrail. #[serde(default)] pub temp_email: bool, + /// Directory for the credential vault (created test-account secrets). Set by + /// the app to `/.neurosploit/vault`; falls back to the run workdir. + #[serde(default)] + pub vault_dir: Option, } fn default_vote() -> usize { @@ -202,6 +206,7 @@ impl RunConfig { user_agent: None, recon_intensity: 3, temp_email: false, + vault_dir: None, } } }