diff --git a/examples/scopes/nasa.yaml b/examples/scopes/nasa.yaml new file mode 100644 index 0000000..3f24428 --- /dev/null +++ b/examples/scopes/nasa.yaml @@ -0,0 +1,63 @@ +# =========================================================================== +# NeuroSploit scope config — NASA (TEMPLATE) +# --------------------------------------------------------------------------- +# ⚠ BEFORE YOU RUN: confirm this matches NASA's CURRENT VDP scope. +# NASA Vulnerability Disclosure Policy: https://www.nasa.gov/nasa-vulnerability-disclosure-policy/ +# (coordinated via https://bugcrowd.com/nasa-vdp). Open the policy/program +# page and align `hard` / `exclude` with the EXACT in- and out-of-scope assets +# it lists today. A VDP is for good-faith disclosure — follow its rules. +# +# `*.nasa.gov` authorizes the apex AND every subdomain, so NeuroSploit's recon +# will enumerate subdomains and test within this boundary. NASA runs MANY +# subdomains/mission sites; several are explicitly out of scope and some are +# third-party hosted — verify before testing. +# +# Import it: +# neurosploit run "*.nasa.gov" --scope-file examples/scopes/nasa.yaml --subscription +# or in the REPL: +# /scope-file examples/scopes/nasa.yaml +# /authorization https://www.nasa.gov/nasa-vulnerability-disclosure-policy/ +# /target *.nasa.gov +# /run +# =========================================================================== + +# --- HARD: the allowlist. Only these are testable. ------------------------ +hard: + - "*.nasa.gov" # apex + every subdomain (VERIFY against the VDP) + - nasa.gov + +# --- EXCLUDE: carve-outs that always beat the allowlist. ------------------ +# Fill from the VDP's OUT-OF-SCOPE list. Typical for a large gov org: auth/SSO +# providers, third-party-hosted services, APIs with their own terms, and any +# system the policy names as excluded. Examples are PLACEHOLDERS — verify. +exclude: + # - auth.launchpad.nasa.gov + # - "*.ndc.nasa.gov" + # - api.nasa.gov # has its own API terms / key system — check first + +# --- SOFT: guardrails inside the boundary (VDP-safe, conservative) --------- +soft: + observe_only: [] + + # No state-mutating verbs, no account creation — a government VDP expects + # minimal-impact, good-faith testing. + allow_destructive_methods: false + allow_account_creation: false + max_accounts: 0 + + # Low rate: these are production government systems. + max_requests_per_minute: 60 + + forbidden_payloads: + - "drop table" + - "truncate table" + - "delete from" + - "rm -rf /" + - "shutdown" + - "while(true)" + + notes: + - "Authorized under NASA's Vulnerability Disclosure Policy (good-faith research only)." + - "No DoS, no social engineering, no physical testing, no disruption of operations or spacecraft/mission systems." + - "Access only the minimum data needed to demonstrate a vulnerability; never exfiltrate or retain PII/ITAR/sensitive data; stop and report if you encounter it." + - "Verify in/out-of-scope on the VDP page before each run — scope changes." diff --git a/examples/scopes/rockstargames.yaml b/examples/scopes/rockstargames.yaml new file mode 100644 index 0000000..3bdae41 --- /dev/null +++ b/examples/scopes/rockstargames.yaml @@ -0,0 +1,74 @@ +# =========================================================================== +# NeuroSploit scope config — Rockstar Games (TEMPLATE) +# --------------------------------------------------------------------------- +# ⚠ BEFORE YOU RUN: confirm this matches the program's CURRENT scope. +# Rockstar Games bug bounty: https://hackerone.com/rockstargames +# Open the program page and align the `hard` allowlist and `exclude` list +# below with the EXACT in-scope / out-of-scope assets it lists today. Scope +# on a bounty program changes; this file is a starting point, not authority. +# +# HARD scope is enforced in code: a request whose host is not covered by `hard` +# (or hit by `exclude`) is REFUSED before it leaves. `*.rockstargames.com` +# authorizes the apex AND every subdomain, so NeuroSploit's recon will +# enumerate subdomains and test them within this boundary. +# +# Import it: +# neurosploit run "*.rockstargames.com" --scope-file examples/scopes/rockstargames.yaml --subscription +# or in the REPL: +# /scope-file examples/scopes/rockstargames.yaml +# /authorization https://hackerone.com/rockstargames +# /target *.rockstargames.com +# /run +# =========================================================================== + +# --- HARD: the allowlist. Only these are testable. ------------------------ +# Start with the apex + all subdomains the user named. ADD the specific extra +# roots the program lists (and REMOVE this wildcard if the program only allows +# named subdomains — check first). +hard: + - "*.rockstargames.com" # apex + every subdomain + - rockstargames.com # the apex itself + +# --- EXCLUDE: carve-outs that always beat the allowlist. ------------------ +# Fill these in from the program's OUT-OF-SCOPE list. Common exclusions on a +# gaming publisher: live game servers, payment/billing, support/helpdesk, +# status pages, third-party-hosted marketing. Examples below are PLACEHOLDERS — +# verify the real ones on the program page before relying on them. +exclude: + # - support.rockstargames.com + # - "*.status.rockstargames.com" + # - https://www.rockstargames.com/billing + +# --- SOFT: guardrails inside the boundary (bounty-safe defaults) ----------- +soft: + # Hosts you may LOOK at but never send payloads to. + observe_only: [] + + # State-mutating verbs (DELETE/PUT/PATCH) stay OFF — a scan must not change + # the target's state to "prove" a bug on someone's production. + allow_destructive_methods: false + + # No account creation by default. Most programs forbid mass registration; + # flip to true only if the program allows it AND keep it to a couple accounts. + allow_account_creation: false + max_accounts: 0 + + # Conservative rate: a bounty target is production. Raise only within the + # program's stated limit. + max_requests_per_minute: 120 + + # Classes that damage production rather than demonstrate a bug — never run. + forbidden_payloads: + - "drop table" + - "truncate table" + - "delete from" + - "rm -rf /" + - "shutdown" + - "while(true)" + + # Free-text context for the agents (NOT enforced — prose, not a control). + notes: + - "Authorized under the Rockstar Games bug bounty program (https://hackerone.com/rockstargames)." + - "Stay within the program's rules of engagement: no DoS, no social engineering, no spam/mass-account creation, no disruption of live game services." + - "Prove data access with a benign canary, never pull real player PII." + - "Verify in/out-of-scope on the program page before each run — scope changes." diff --git a/neurosploit-rs/app/src/repl.rs b/neurosploit-rs/app/src/repl.rs index 75b7b9e..abac0b3 100644 --- a/neurosploit-rs/app/src/repl.rs +++ b/neurosploit-rs/app/src/repl.rs @@ -150,7 +150,7 @@ pub(crate) const ACCEPTED: &[&str] = &[ "/context", "/continue", "/creds", "/diff", "/exclude", "/exit", "/expand", "/feed", "/finding", "/findings", "/focus", "/forget", "/full", "/go", "/goal", "/graph", "/guardrail", "/guardrails", "/help", "/history", "/idle", "/inscope", "/instructions", "/integration", "/integrations", "/key", "/log", - "/authorization", "/authz", "/program", "/logs", "/mcp", "/memory", "/model", "/models", "/objective", "/objectives", "/observe", + "/scope-file", "/scopefile", "/import-scope", "/authorization", "/authz", "/program", "/logs", "/mcp", "/memory", "/model", "/models", "/objective", "/objectives", "/observe", "/observe-only", "/offline", "/onboard", "/only", "/oos", "/outofscope", "/policy", "/providers", "/proxy", "/class", "/classes", "/focus-class", "/research", "/quick", "/economy", "/eco", "/q", "/quit", "/recon", "/pause", "/repo", "/report", "/results", "/resume", "/retest", "/revalidate", "/run", "/runs", @@ -162,7 +162,7 @@ pub(crate) const ACCEPTED: &[&str] = &[ /// All slash-commands, for Tab completion. const COMMANDS: &[&str] = &[ "/help", "/onboard", "/show", "/config", "/providers", "/model", "/key", "/sub", "/target", - "/authorization", "/class", "/repo", "/auth", "/creds", "/focus", "/objective", "/scope-out", "/attach", "/context", "/mcp", "/offline", + "/scope-file", "/authorization", "/class", "/repo", "/auth", "/creds", "/focus", "/objective", "/scope-out", "/attach", "/context", "/mcp", "/offline", "/class", "/research", "/quick", "/economy", "/eco", "/votes", "/chain", "/recon", "/tempmail", "/timeout", "/proxy", "/burp", "/ua", "/agents", "/only", "/theme", "/clear", "/run", "/stop", "/pause", "/continue", "/runs", "/results", "/report", "/status", "/logs", "/diff", "/retest", "/validate", "/finding", "/expand", "/integrations", "/memory", "/forget", "/graph", "/inscope", "/observe", "/guardrail", "/policy", @@ -289,6 +289,10 @@ struct Session { /// Engagement objective / rules-of-engagement context (why + what matters). objective: Option, authorization: Option, + /// Set once the operator defines scope explicitly (scope-file, /inscope, or + /// a capability). While pinned, `/target` stops re-deriving the scope from + /// the target, so an imported allowlist is not clobbered by picking a target. + scope_pinned: bool, /// Explicit out-of-scope exclusions the agents must not touch. out_of_scope: Option, /// Authorization boundary + guardrails, enforced by the harness. @@ -336,6 +340,7 @@ impl Default for Session { instructions: None, objective: None, authorization: None, + scope_pinned: false, out_of_scope: None, policy: Default::default(), capability: None, @@ -720,7 +725,11 @@ pub async fn repl(base: &Path, auth: SessionAuth) -> anyhow::Result<()> { // no capability, the target the operator picks IS the grant // (same model as `neurosploit run `); explicit excludes // and guardrails are preserved. - if s.capability.is_none() { + // Only re-derive when scope was NOT set explicitly (no + // imported scope-file, no /inscope, no capability) — otherwise + // picking a target would clobber the operator's allowlist. + let rederive = s.capability.is_none() && !s.scope_pinned; + if rederive { let keep_exclude = s.policy.exclude.clone(); let keep_soft = s.policy.soft.clone(); let mut np = harness::scope::ScopePolicy::for_target(&seeds[0]); @@ -736,7 +745,9 @@ pub async fn repl(base: &Path, auth: SessionAuth) -> anyhow::Result<()> { } if ts.len() > 1 { println!(" targets ({}): {}", ts.len(), seeds.join(", ")); println!(" \x1b[2m/run tests them sequentially, one report each\x1b[0m"); } else { println!(" target: {}", seeds.first().cloned().unwrap_or_default()); } - if let Some(d) = &wildcard_domain { + if s.scope_pinned { + println!(" \x1b[2mscope: using the imported scope ({}) — target must fall inside it\x1b[0m", s.policy.summary()); + } else if let Some(d) = &wildcard_domain { println!(" \x1b[2mscope: *.{d} — apex + all subdomains authorized; recon will enumerate subdomains\x1b[0m"); // Nudge recon toward active subdomain discovery for a domain-wide engagement. if s.recon_intensity < 3 { s.recon_intensity = 3; } @@ -832,6 +843,28 @@ pub async fn repl(base: &Path, auth: SessionAuth) -> anyhow::Result<()> { s.objective = Some(arg.to_string()); println!(" objective set — steers what agents prioritise and what counts as impact"); } + "/scope-file" | "/scopefile" | "/import-scope" => { + 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!(" current scope: {}", s.policy.summary()); + continue; + } + match harness::scope::ScopePolicy::from_file(std::path::Path::new(path)) { + Ok(sp) => { + if sp.hard.is_empty() { + println!(" \x1b[33m⚠ {path} sets no hard scope — nothing would be authorized; not applied.\x1b[0m"); + } else { + 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"); + } + } + Err(e) => println!(" \x1b[31m⛔ could not read {path}: {e}\x1b[0m"), + } + } "/authorization" | "/authz" | "/program" => { if arg == "clear" { s.authorization = None; println!(" authorization reference cleared"); continue; } if arg.is_empty() { @@ -1263,6 +1296,7 @@ pub async fn repl(base: &Path, auth: SessionAuth) -> anyhow::Result<()> { // dropped it is the same lie the ceiling exists to prevent. let before = s.policy.hard.len(); s.policy.allow(arg); + s.scope_pinned = true; let refused = reapply_grant(&mut s); if !refused.is_empty() { println!(" \x1b[33m⛔ outside the capability grant, not authorized:\x1b[0m {}", refused.join(", ")); @@ -2387,6 +2421,7 @@ fn help() { h("/chain ", "attack-chain depth (post-exploitation pivots; 0 = off)"); 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("/scope-file ", "import a ready scope config (hard allowlist + exclusions + guardrails) — e.g. examples/scopes/rockstargames.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)");