From 3a820f09e64f624f18f4d082857dd7e3c3318dc2 Mon Sep 17 00:00:00 2001 From: CyberSecurityUP Date: Sun, 4 Oct 2026 00:03:50 -0300 Subject: [PATCH] feat: one-file engagement configs; scope-file reseeds stale target; English UI + any-language input; docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /scope-file now reads optional engagement keys from the SAME YAML (target, models, focus, objective, authorization, classes) so one file defines the whole engagement, not just scope. examples/scopes/nasa.yaml and engagement.example.yaml show the keys. - When importing a scope (/scope-file) or declaring one (/authorize), a target left over from a previous session that falls OUTSIDE the new scope is reset to a host inside it (was: silently kept, then denied on /run — the "nothing changed" confusion). Added scope_seed_target() + in_hard_scope() check. - UI/help strings are English; the natural-language REPL still accepts input in any language (the two example lines are now English). - README + TUTORIAL updated: new REPL commands (/authorize, /scope-file, /class, /research, /quick, /authorization, /guardrail), a "Scope — three ways" section with the one-file YAML, version/counts refreshed to 4.2.1 / 480 agents. 422 tests passing. Co-Authored-By: Claude Opus 4.8 --- README.md | 2 +- TUTORIAL.md | 95 ++++++++++++++++++++++++- examples/scopes/nasa.yaml | 10 +++ neurosploit-rs/app/src/repl.rs | 125 ++++++++++++++++++++++++++++++--- 4 files changed, 218 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 1d56e2d..b4d48fe 100755 --- a/README.md +++ b/README.md @@ -257,7 +257,7 @@ Zero npm dependencies (Node built-ins only). flat list — click any node or row for the full finding detail, including any PoC script the exploiting agent wrote to `pocs/`. - **Real REPL underneath `run`/`whitebox`/`greybox`** — the wizard scripts an actual interactive - `neurosploit` session (`/target`, `/model`, `/only`, `/run`, …) instead of a one-shot CLI + `neurosploit` session (`/target`, `/authorize`, `/scope-file`, `/class`, `/model`, `/only`, `/research`, `/quick`, `/run`, …) instead of a one-shot CLI invocation, so the session **keeps reading stdin while the engagement streams**. The Activity log tab grows a prompt box (`❭`) to send `/status`, `/stop`, `/continue`, or a plain-language instruction mid-run — same REPL described in [§6](TUTORIAL.md#6-the-interactive-repl). `host` / diff --git a/TUTORIAL.md b/TUTORIAL.md index 5082274..c1e2dd9 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -1,4 +1,4 @@ -# NeuroSploit — Tutorial & User Guide (v4.1.0) +# NeuroSploit — Tutorial & User Guide (v4.2.1) A complete, hands-on guide to installing, configuring and running NeuroSploit — the autonomous, multi-model penetration-testing harness. @@ -100,8 +100,8 @@ Agents **degrade gracefully**: if `rustscan` is absent they use `nmap`; if neith ### Verify ```bash -neurosploit --version # neurosploit 4.1.0 -neurosploit agents # {"vulns":241,...,"ai":30,...,"total":430} +neurosploit --version # neurosploit 4.2.1 +neurosploit agents # {"vulns":255,...,"ai":30,...,"total":480} neurosploit models # all providers & models ``` @@ -368,9 +368,20 @@ A context bar shows `model auth · cwd · mode▸target`. Key commands: /target black-box target /repo add a repo (repo+target = greybox) /auth send an auth header /creds load creds.yaml /focus steer the tests (or just type the instruction) +/class focus on vuln CLASSES (idor,sqli,xss,ssrf,…) — pins the matching agents +/only run EXACTLY these named agents (skips recon-based selection) @path @dir @f:1-20 attach a file/folder/line-range to context (Tab → menu) /mcp on|off /offline on|off /votes /agents /theme color|mono +/quick economy preset (1 voter · 1 chain round · light recon · ≤6 agents) +/research on|off whitebox/greybox: hunt a NOVEL, CVE-reportable bug (dedup + patch-diff) /tempmail on|off opt-in disposable inbox (mail.tm) for a register confirmation code +── scope & authorization ── +/authorize direct engagement: declare the whole authorized scope in one line + (hosts / *.domains / CIDRs / URL-prefixes) — no program needed +/scope-file import a ready scope/engagement config (see §6.1) +/inscope add one more host/*.domain/CIDR to scope · /scope-out exclude +/guardrail tune limits: destructive on|off · accounts · rate +/authorization record the program/authorization (e.g. a bug-bounty URL) — context only /run launch the engagement /runs /results [n] /report [n] /status [n] /diff what changed vs the previous run @@ -378,6 +389,11 @@ A context bar shows `model auth · cwd · mode▸target`. Key commands: /quit ``` +> **Any language.** You don't have to use slash-commands — just **describe the +> engagement in plain text, in any language** (the REPL interprets it): +> `test https://shop.com with opus, focus on SQLi, out of scope /admin, run`. +> The output and help are English; your input can be Portuguese, Spanish, etc. + Line editing: **↑/↓** history, **Tab** completes commands & `@paths`, **Ctrl-A/E/K**, end a line with **`\`** for multiline. @@ -411,6 +427,79 @@ state) and prints `⏸ token/quota exhausted … PAUSED`. Then either: (When stdin is piped/non-interactive, `/run` falls back to blocking mode.) +### 6.1 Scope — three ways, from quick to a full config + +Hard scope is the **safety boundary**: a request whose host isn't authorized is +*refused before it leaves*. You must declare what you're allowed to test — but +that's frictionless, and **no bug-bounty program or capability token is required** +for a normal client engagement with written authorization. + +**1. Just point at the target** (the target *is* the grant): + +``` +/target app.client.com → authorized against app.client.com +/target *.client.com → apex + ALL subdomains (recon enumerates them) +/run +``` + +**2. Declare the whole scope in one line** (direct engagement, multiple assets): + +``` +/authorize app.client.com *.client.com 10.0.0.0/24 https://api.client.com/v2 +/run +``` + +You assert written authorization for those assets. Tune limits any time with +`/guardrail` (e.g. `/guardrail destructive on`, `/guardrail rate 60`, +`/guardrail accounts 2`), exclude with `/scope-out`. + +**3. Import a ready config file** (large scope, version-controlled, or reusable): + +``` +/scope-file examples/scopes/engagement.example.yaml +/run +``` + +A scope config is a **single YAML** that can define the *whole* engagement — +scope **plus** target, models, focus, objective, authorization and vuln classes: + +```yaml +# scope (the safety boundary — the one thing you must set) +hard: + - "*.client.com" # apex + every subdomain + - 10.0.0.0/24 # an internal range +exclude: + - billing.client.com # carve-outs always beat the allowlist +soft: + allow_destructive_methods: false # true only if the authorization covers it + allow_account_creation: true + max_accounts: 3 + max_requests_per_minute: 240 # raise for a lab, lower for fragile prod + forbidden_payloads: ["drop table", "rm -rf /", "shutdown"] + +# optional — define the rest of the engagement in the same file: +target: "*.client.com" +models: + - anthropic:claude-opus-5-5 +classes: idor, sqli, ssrf # focus the run on these classes +focus: "prioritize access control and SSRF" +authorization: "" +``` + +Importing it sets everything in one step; if a stale target left over from a +previous session falls outside the new scope, it's reset to a host inside it. +Ready templates live in **`examples/scopes/`** (`engagement.example.yaml` for a +direct client test; `nasa.yaml` as a VDP example). On the CLI the same file works +with `--scope-file` (which reads the scope portion): + +```bash +neurosploit run "*.client.com" --scope-file examples/scopes/engagement.example.yaml --subscription +``` + +> A capability **token** (`--capability-token`, verified with +> `NEUROSPLOIT_CAPABILITY_KEY`) is a separate, *optional* layer for when a lead +> must hand a tester a scope they cannot widen. Everyday engagements don't need it. + --- ## 7. Mission Control TUI diff --git a/examples/scopes/nasa.yaml b/examples/scopes/nasa.yaml index 3f24428..edb5d27 100644 --- a/examples/scopes/nasa.yaml +++ b/examples/scopes/nasa.yaml @@ -21,6 +21,16 @@ # /run # =========================================================================== +# --- Optional: define the whole engagement in this one file ----------------- +# These top-level keys are read by `/scope-file` (and ignored by the CLI's +# --scope-file, which only reads scope). All optional. +target: "*.nasa.gov" # seed; must fall inside `hard` below +# models: # provider:model list (uncomment to pin) +# - anthropic:claude-opus-5-5 +# classes: idor, ssrf, xss # focus the run on these vuln classes +focus: "prioritize auth, access control and SSRF on in-scope subdomains" +authorization: "https://www.nasa.gov/nasa-vulnerability-disclosure-policy/" + # --- HARD: the allowlist. Only these are testable. ------------------------ hard: - "*.nasa.gov" # apex + every subdomain (VERIFY against the VDP) diff --git a/neurosploit-rs/app/src/repl.rs b/neurosploit-rs/app/src/repl.rs index 850b2e2..0634012 100644 --- a/neurosploit-rs/app/src/repl.rs +++ b/neurosploit-rs/app/src/repl.rs @@ -457,7 +457,7 @@ pub async fn repl(base: &Path, auth: SessionAuth) -> anyhow::Result<()> { println!(" {} agents loaded · detected logins: {}", lib.total(), if backends.is_empty() { "none (use API keys)".into() } else { backends.join(", ") }); 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)"); - println!(" \x1b[2mOr just describe it in any language:\x1b[0m \x1b[36mtesta https://loja.com com opus, foco em SQLi, roda\x1b[0m\n"); + println!(" \x1b[2mOr just describe it in any language:\x1b[0m \x1b[36mtest https://shop.com with opus, focus on SQLi, run\x1b[0m\n"); let mut s = Session::default(); let resumed = load_session(&mut s); @@ -870,12 +870,15 @@ pub async fn repl(base: &Path, auth: SessionAuth) -> anyhow::Result<()> { } else { s.policy = np; s.scope_pinned = true; - // Seed the target from the first entry if none set, so /run works immediately. - if s.target.is_none() { - let first = entries[0]; - let h = harness::scope::host_of(first); - let seed = h.strip_prefix("*.").map(|a| format!("https://{a}")).unwrap_or_else(|| if first.contains("://") { first.to_string() } else { format!("https://{}", harness::scope::host_of(first)) }); - s.target = Some(seed); + // Seed the target so /run works immediately — whenever none + // is set OR the current one falls outside the new scope. + let cur_ok = s.target.as_deref().map(|t| s.policy.in_hard_scope(t)).unwrap_or(false); + if !cur_ok { + let had = s.target.is_some(); + if let Some(seed) = scope_seed_target(&s.policy) { + if had { println!(" \x1b[33m⚠ previous target was outside this scope — target reset to {seed}\x1b[0m"); } + s.target = Some(seed); + } } println!(" \x1b[1;32m🔓 authorized scope set\x1b[0m ({} entr{}) — {}", added, if added == 1 { "y" } else { "ies" }, s.policy.summary()); println!(" \x1b[2mdirect engagement — you assert written authorization for these assets. Tune limits with /guardrail · exclude with /scope-out · /run to start.\x1b[0m"); @@ -885,7 +888,7 @@ pub async fn repl(base: &Path, auth: SessionAuth) -> anyhow::Result<()> { let path = arg.trim().trim_start_matches('@'); if path.is_empty() { println!(" import a scope config (hard allowlist + exclusions + guardrails):"); - println!(" /scope-file examples/scopes/rockstargames.yaml"); + println!(" /scope-file examples/scopes/engagement.example.yaml"); println!(" current scope: {}", s.policy.summary()); continue; } @@ -897,7 +900,37 @@ pub async fn repl(base: &Path, auth: SessionAuth) -> anyhow::Result<()> { s.scope_pinned = true; s.policy = sp; println!(" \x1b[32m📋 scope imported\x1b[0m from {path} — {}", s.policy.summary()); - println!(" \x1b[2m/target a host inside this scope, then /run. Add /authorization to record the authorization.\x1b[0m"); + // A target left over from a previous session may be + // outside the imported scope (it would be denied on + // /run). Reseed it from the scope's first host so the + // run is ready and consistent with what was imported. + let cur_ok = s.target.as_deref().map(|t| s.policy.in_hard_scope(t)).unwrap_or(false); + if !cur_ok { + let had_target = s.target.is_some(); + if let Some(seed) = scope_seed_target(&s.policy) { + if had_target { println!(" \x1b[33m⚠ previous target was outside this scope — target reset to {seed}\x1b[0m"); } + s.target = Some(seed); + } + } + // Optional engagement settings in the SAME file: + // target, models, focus, objective, authorization, + // classes — so one YAML defines the whole engagement. + if let Ok(text) = std::fs::read_to_string(path) { + let meta = read_engagement_meta(&text); + if let Some(t) = meta.target { if s.policy.in_hard_scope(&t) { s.target = Some(t.clone()); println!(" \x1b[2m· target: {t}\x1b[0m"); } else { println!(" \x1b[33m⚠ file's target {t} is outside its own scope — ignored\x1b[0m"); } } + if !meta.models.is_empty() { s.models = meta.models.clone(); println!(" \x1b[2m· models: {}\x1b[0m", meta.models.join(", ")); } + if let Some(f) = meta.focus { s.instructions = Some(f.clone()); println!(" \x1b[2m· focus: {f}\x1b[0m"); } + if let Some(o) = meta.objective { s.objective = Some(o.clone()); println!(" \x1b[2m· objective: {o}\x1b[0m"); } + if let Some(a) = meta.authorization { s.authorization = Some(a.clone()); println!(" \x1b[2m· authorization: {a}\x1b[0m"); } + if !meta.classes.is_empty() { + let lib = agents::load(base); + let mut pinned: Vec = Vec::new(); + for c in &meta.classes { for n in agents_for_class(&lib, &c.to_lowercase()) { if !pinned.contains(&n) { pinned.push(n); } } } + if !pinned.is_empty() { s.pinned = pinned; println!(" \x1b[2m· classes: {} → {} agent(s)\x1b[0m", meta.classes.join(", "), s.pinned.len()); } + } + } + println!(" \x1b[2mtarget: {} · /run to start · /authorization to record the authorization\x1b[0m", + s.target.clone().unwrap_or_else(|| "(set one inside this scope with /target)".into())); } } Err(e) => println!(" \x1b[31m⛔ could not read {path}: {e}\x1b[0m"), @@ -1961,6 +1994,78 @@ fn memory_cmd(s: &Session, arg: &str) { /// Project-local store: `/.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. +/// Optional engagement settings read from the SAME YAML a scope file lives in, +/// so one file can define the whole engagement (scope + target + models + focus +/// + classes). These are top-level keys alongside `hard:`/`exclude:`/`soft:`; +/// all are optional. Minimal reader: scalar `key: value` and simple `- item` +/// lists at indent 0, which is all these need. +#[derive(Default)] +struct EngagementMeta { + target: Option, + models: Vec, + focus: Option, + objective: Option, + authorization: Option, + classes: Vec, +} + +fn read_engagement_meta(text: &str) -> EngagementMeta { + let mut m = EngagementMeta::default(); + let mut list_key: Option = None; // which top-level list we're in + let unq = |s: &str| s.trim().trim_matches('"').trim_matches('\'').to_string(); + for raw in text.lines() { + let line = raw.split('#').next().unwrap_or(raw); // strip comments + if line.trim().is_empty() { continue; } + let indent = line.len() - line.trim_start().len(); + let t = line.trim(); + // A list item under a top-level engagement list key. + if let Some(item) = t.strip_prefix("- ") { + if indent > 0 { if let Some(k) = &list_key { + let v = unq(item); + if !v.is_empty() { match k.as_str() { + "models" => m.models.push(v), + "classes" => m.classes.push(v), + _ => {} + }} + }} + continue; + } + let (key, val) = match t.split_once(':') { Some((k, v)) => (k.trim(), unq(v)), None => continue }; + if indent != 0 { list_key = None; continue; } // only top-level keys here + list_key = None; + match key { + "target" if !val.is_empty() => m.target = Some(val), + "focus" if !val.is_empty() => m.focus = Some(val), + "objective" if !val.is_empty() => m.objective = Some(val), + "authorization" if !val.is_empty() => m.authorization = Some(val), + "models" | "classes" => { + if val.is_empty() { list_key = Some(key.to_string()); } + else { // inline comma list: `models: a, b` + let items: Vec = val.split([',', ';']).map(|x| x.trim().to_string()).filter(|x| !x.is_empty()).collect(); + if key == "models" { m.models = items; } else { m.classes = items; } + } + } + _ => {} + } + } + m +} + +/// A target URL to seed a run from a scope policy: the first hard entry, with a +/// wildcard (`*.dom`) reduced to its apex (a literal `*.dom` has no DNS record). +fn scope_seed_target(p: &harness::scope::ScopePolicy) -> Option { + let first = p.hard.first()?.as_text(); + if let Some(url) = first.strip_prefix("https://").or_else(|| first.strip_prefix("http://")) { + return Some(format!("https://{}", url.split('/').next().unwrap_or(url))); + } + if first.contains('/') { // CIDR or path — not a clean host to seed; skip. + if first.contains("://") { return Some(first); } + return None; + } + let host = first.strip_prefix("*.").unwrap_or(&first); + Some(format!("https://{host}")) +} + /// Expand a vuln-class keyword (idor, sqli, xss, ssrf, …) to the agent names in /// the library that implement it. Matches on the agent's name, title and CWE by /// a set of substrings per class, so `/class sqli` pins every SQLi agent @@ -2460,7 +2565,7 @@ fn help() { h("/recon <1-4>", "recon intensity: 1 quick · 2 standard · 3 deep · 4 exhaustive (installs tools)"); h("/class ", "focus a run on vuln classes (idor,sqli,xss,ssrf,…) — pins the matching agents"); h("/authorize ", "direct engagement: declare the whole authorized scope in one line (hosts/*.domains/CIDRs/URLs) — no program needed"); - h("/scope-file ", "import a ready scope config (hard allowlist + exclusions + guardrails) — e.g. examples/scopes/rockstargames.yaml"); + h("/scope-file ", "import a ready scope config (hard allowlist + exclusions + guardrails) — e.g. examples/scopes/engagement.example.yaml"); h("/authorization ", "declare the program/authorization (e.g. a bug-bounty URL) — recorded; does NOT widen scope"); h("/research", "whitebox/greybox: hunt a NOVEL, CVE-reportable bug (known-CVE dedup + patch-diff variant analysis)"); h("/quick", "economy preset: short, low-cost run (1 voter · 1 chain round · light recon · ≤6 agents)");