feat: one-file engagement configs; scope-file reseeds stale target; English UI + any-language input; docs

- /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 <noreply@anthropic.com>
This commit is contained in:
CyberSecurityUPandClaude Opus 4.8 committed 2026-10-04 00:03:50 -03:00
1 parent c233da8b17
commit 3a820f09e6
4 files changed
+218 -14

No files matched your search

+1 -1
View File
@@ -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` /
+92 -3
View File
@@ -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 <url> black-box target /repo <path> add a repo (repo+target = greybox)
/auth <value> send an auth header /creds <file> load creds.yaml
/focus <text> steer the tests (or just type the instruction)
/class <a,b,..> focus on vuln CLASSES (idor,sqli,xss,ssrf,…) — pins the matching agents
/only <agents> 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 <n> /agents <n> /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 <a b c> direct engagement: declare the whole authorized scope in one line
(hosts / *.domains / CIDRs / URL-prefixes) — no program needed
/scope-file <path> import a ready scope/engagement config (see §6.1)
/inscope <entry> add one more host/*.domain/CIDR to scope · /scope-out <entry> exclude
/guardrail <k> <v> tune limits: destructive on|off · accounts <n|off> · rate <req/min>
/authorization <url> 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: "<SOW / contract reference, or a program URL>"
```
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
+10
View File
@@ -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)
+115 -10
View File
@@ -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 <program-url> 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<String> = 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 <program-url> 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: `<cwd>/.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<String>,
models: Vec<String>,
focus: Option<String>,
objective: Option<String>,
authorization: Option<String>,
classes: Vec<String>,
}
fn read_engagement_meta(text: &str) -> EngagementMeta {
let mut m = EngagementMeta::default();
let mut list_key: Option<String> = 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<String> = 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<String> {
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 <a,b>", "focus a run on vuln classes (idor,sqli,xss,ssrf,…) — pins the matching agents");
h("/authorize <a b c>", "direct engagement: declare the whole authorized scope in one line (hosts/*.domains/CIDRs/URLs) — no program needed");
h("/scope-file <path>", "import a ready scope config (hard allowlist + exclusions + guardrails) — e.g. examples/scopes/rockstargames.yaml");
h("/scope-file <path>", "import a ready scope config (hard allowlist + exclusions + guardrails) — e.g. examples/scopes/engagement.example.yaml");
h("/authorization <url>", "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)");