mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-10-08 00:51:09 +02:00
fix: wildcard-target probe, per-run provenance, qwen via Hermes, broad default web objective
- Wildcard target from a scope-file (target: "*.nasa.gov") was probed literally → "builder error" / target unreachable. It's now reduced to the apex (https://nasa.gov) for the seed, while the scope keeps *.nasa.gov so subdomain enumeration stays authorized. (The /target command already did this; the engagement-file meta path didn't.) - Provenance was a OnceLock ("first run wins"), so in the REPL every run after the first minted markers and the provenance line with the FIRST run's id (nasa run showing a rockstargames id). Now a RwLock that rebinds per run — each engagement gets its own id; the build fingerprint stays stable. - Nous/Hermes: qwen3.8-max / qwen3.8-omni-flash added to the provider list so `nous:qwen3.8-max` routes qwen through the Hermes portal (model name passes through `hermes chat -m <model> --provider nous`). - Black-box `run` now gets a broad DEFAULT objective when none is set: a comprehensive WEB assessment grounded in OWASP Top 10 / ASVS / WSTG / CWE that traverses every applicable web vuln class then goes deep — web-only (this path loads only web vuln agents; mobile/binary are separate modes), so it never drifts into mobile/exe. 423 tests passing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
c2c6de1c3f
commit
101eca2700
5 files changed
+63
-21
No files matched your search
@@ -1925,8 +1925,9 @@ fn handle_provenance(cmd: ProvCmd) -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
// A marker carrying this build's fingerprint came from this binary;
|
// A marker carrying this build's fingerprint came from this binary;
|
||||||
// one that does not still came from NeuroSploit, just elsewhere.
|
// one that does not still came from NeuroSploit, just elsewhere.
|
||||||
let mine = marks.iter().filter(|m| m.contains(&Provenance::process().build[..6])).count();
|
let bld = Provenance::process().build;
|
||||||
println!(" \x1b[2m{mine} of them minted by this build ({}), the rest by another\x1b[0m", Provenance::process().build);
|
let mine = marks.iter().filter(|m| m.contains(&bld[..6.min(bld.len())])).count();
|
||||||
|
println!(" \x1b[2m{mine} of them minted by this build ({bld}), the rest by another\x1b[0m");
|
||||||
}
|
}
|
||||||
ProvCmd::Verify { dir } => {
|
ProvCmd::Verify { dir } => {
|
||||||
let dir = std::path::Path::new(&dir);
|
let dir = std::path::Path::new(&dir);
|
||||||
|
|||||||
@@ -927,7 +927,18 @@ pub async fn repl(base: &Path, auth: SessionAuth) -> anyhow::Result<()> {
|
|||||||
// classes — so one YAML defines the whole engagement.
|
// classes — so one YAML defines the whole engagement.
|
||||||
if let Ok(text) = std::fs::read_to_string(path) {
|
if let Ok(text) = std::fs::read_to_string(path) {
|
||||||
let meta = read_engagement_meta(&text);
|
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 let Some(t) = meta.target {
|
||||||
|
if s.policy.in_hard_scope(&t) {
|
||||||
|
// A wildcard target (`*.nasa.gov`) is not a
|
||||||
|
// host to probe — seed with the apex, since
|
||||||
|
// the scope already authorizes the subdomains.
|
||||||
|
let host = harness::scope::host_of(&t);
|
||||||
|
let seed = if let Some(apex) = host.strip_prefix("*.") { format!("https://{apex}") }
|
||||||
|
else if t.contains("://") { t.clone() } else { format!("https://{host}") };
|
||||||
|
s.target = Some(seed.clone());
|
||||||
|
println!(" \x1b[2m· target: {seed}\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 !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(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(o) = meta.objective { s.objective = Some(o.clone()); println!(" \x1b[2m· objective: {o}\x1b[0m"); }
|
||||||
|
|||||||
@@ -68,8 +68,11 @@ pub fn providers() -> Vec<Provider> {
|
|||||||
// or (with --subscription) driven through the `hermes` CLI
|
// or (with --subscription) driven through the `hermes` CLI
|
||||||
// (NousResearch/hermes-agent) on the user's OAuth Portal login
|
// (NousResearch/hermes-agent) on the user's OAuth Portal login
|
||||||
// (`hermes setup --portal`) — 300+ routed frontier models, no key.
|
// (`hermes setup --portal`) — 300+ routed frontier models, no key.
|
||||||
|
// The Hermes portal routes 300+ models, so the model name passes through
|
||||||
|
// (`hermes chat -m <model> --provider nous`): `nous:qwen3.8-max` and other
|
||||||
|
// routed models work even though only the Hermes-family defaults are listed.
|
||||||
Provider { key: "nous", label: "Nous Research (Hermes)", base_url: "https://inference-api.nousresearch.com/v1", env_key: "NOUS_API_KEY", kind: "cli",
|
Provider { key: "nous", label: "Nous Research (Hermes)", base_url: "https://inference-api.nousresearch.com/v1", env_key: "NOUS_API_KEY", kind: "cli",
|
||||||
models: vec!["Hermes-4-405B", "Hermes-4-70B", "DeepHermes-3-Mistral-24B-Preview"] },
|
models: vec!["Hermes-4-405B", "Hermes-4-70B", "DeepHermes-3-Mistral-24B-Preview", "qwen3.8-max", "qwen3.8-omni-flash"] },
|
||||||
// Azure OpenAI (OpenAI-compatible). Set AZURE_OPENAI_ENDPOINT (e.g.
|
// Azure OpenAI (OpenAI-compatible). Set AZURE_OPENAI_ENDPOINT (e.g.
|
||||||
// https://<resource>.openai.azure.com), optionally AZURE_OPENAI_API_VERSION
|
// https://<resource>.openai.azure.com), optionally AZURE_OPENAI_API_VERSION
|
||||||
// (default 2024-10-21), and use `azure:<your-deployment-name>` as the model.
|
// (default 2024-10-21), and use `azure:<your-deployment-name>` as the model.
|
||||||
|
|||||||
@@ -614,6 +614,16 @@ const DECISION_DOCTRINE: &str = "DECIDE WHERE TO ATTACK (analyse, then act):\n\
|
|||||||
- Build PoCs when needed: for issues that need an artifact to prove (clickjacking → an HTML page that frames the target; CSRF → an auto-submitting HTML form; a multi-step or timing exploit → a script), WRITE the PoC to the run's PoC dir, run/validate it, and cite the file in the evidence.\n\
|
- Build PoCs when needed: for issues that need an artifact to prove (clickjacking → an HTML page that frames the target; CSRF → an auto-submitting HTML form; a multi-step or timing exploit → a script), WRITE the PoC to the run's PoC dir, run/validate it, and cite the file in the evidence.\n\
|
||||||
- Test control BYPASSES: when something returns 401/403/redirect or is 'blocked', try to bypass it (verb tampering, path/case/encoding normalization, X-Original-URL / X-Rewrite-URL / X-Forwarded-* headers, missing-vs-invalid token, direct object/API access) and confirm the bypass with the two requests.\n\n";
|
- Test control BYPASSES: when something returns 401/403/redirect or is 'blocked', try to bypass it (verb tampering, path/case/encoding normalization, X-Original-URL / X-Rewrite-URL / X-Forwarded-* headers, missing-vs-invalid token, direct object/API access) and confirm the bypass with the two requests.\n\n";
|
||||||
|
|
||||||
|
/// Broad default objective for a black-box WEB engagement (used when the
|
||||||
|
/// operator set none). Grounds the run in the recognised web standards and asks
|
||||||
|
/// for full-breadth coverage so it traverses every applicable class, then goes
|
||||||
|
/// deep where signal is strong — without drifting off web (mobile/binary are
|
||||||
|
/// separate modes).
|
||||||
|
const DEFAULT_WEB_OBJECTIVE: &str = "Comprehensive black-box WEB application penetration test. \
|
||||||
|
Methodology: OWASP Top 10 (2021), OWASP ASVS verification requirements, the OWASP Web Security Testing Guide, and CWE for classification. \
|
||||||
|
COVER THE WHOLE SURFACE — traverse every web vulnerability class that the recon makes applicable (injection: SQL/NoSQL/command/SSTI/LDAP/XPath; XSS reflected/stored/DOM; access control: IDOR/BOLA/BFLA/privilege escalation/forced browsing; authentication & session: login, signup, password reset, MFA, OAuth/OIDC/SAML, JWT; SSRF; XXE; insecure deserialization; CSRF; open redirect; CORS; file upload/download & path traversal; business-logic & multi-step flow abuse; mass assignment; request smuggling; info disclosure & security misconfiguration; cryptographic failures; known-CVE components) — do not stop at the first class that yields something. \
|
||||||
|
Then go DEEP where the signal is strong: prove impact with a real receipt, chain footholds into higher impact, and prioritise the authenticated surface and less-hardened subdomains. This is a WEB engagement only — do not attempt mobile/binary analysis.";
|
||||||
|
|
||||||
/// FREE EXPLORATION doctrine: the agent is NOT limited to its named vuln class.
|
/// FREE EXPLORATION doctrine: the agent is NOT limited to its named vuln class.
|
||||||
/// This is what stops a run collapsing into "only SQLi": every exploit agent is
|
/// This is what stops a run collapsing into "only SQLi": every exploit agent is
|
||||||
/// told to hunt the whole application with its own judgment and report ANY class
|
/// told to hunt the whole application with its own judgment and report ANY class
|
||||||
@@ -777,9 +787,19 @@ fn write_meta(cfg: &RunConfig, p: &crate::probe::Probe, asset: &str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Black-box web engagement: recon → parallel exploit → N-model vote → report.
|
/// Black-box web engagement: recon → parallel exploit → N-model vote → report.
|
||||||
pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<String>) -> RunOutput {
|
pub async fn run(mut cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<String>) -> RunOutput {
|
||||||
pool.set_progress(tx.clone());
|
pool.set_progress(tx.clone());
|
||||||
|
|
||||||
|
// A broad DEFAULT objective for a black-box web engagement when the operator
|
||||||
|
// set none — so a plain `/run` already does a comprehensive web assessment
|
||||||
|
// grounded in OWASP Top 10 / ASVS / CWE and traverses every applicable web
|
||||||
|
// vuln class, instead of stopping at the first thing it finds. Web-only by
|
||||||
|
// construction: this `run` path loads only the web vuln agents (mobile/APK
|
||||||
|
// and container are separate modes), so it never drifts into mobile/binary.
|
||||||
|
if cfg.objective.as_deref().map(|o| o.trim().is_empty()).unwrap_or(true) {
|
||||||
|
cfg.objective = Some(DEFAULT_WEB_OBJECTIVE.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
// Authorization first. A supplied token that does not verify ends the run
|
// Authorization first. A supplied token that does not verify ends the run
|
||||||
// here: proceeding would mean acting on a grant nobody can prove was
|
// here: proceeding would mean acting on a grant nobody can prove was
|
||||||
// issued, which is the one failure this whole layer exists to prevent.
|
// issued, which is the one failure this whole layer exists to prevent.
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ fn now_secs() -> u64 {
|
|||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
static PROCESS: std::sync::OnceLock<Provenance> = std::sync::OnceLock::new();
|
static PROCESS: std::sync::RwLock<Option<Provenance>> = std::sync::RwLock::new(None);
|
||||||
|
|
||||||
/// Identity of one build of the engine, plus the run currently using it.
|
/// Identity of one build of the engine, plus the run currently using it.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -121,15 +121,18 @@ impl Provenance {
|
|||||||
/// made by that process agrees on which engagement it belongs to. Reading
|
/// made by that process agrees on which engagement it belongs to. Reading
|
||||||
/// it before it is bound is fine — it mints an ad-hoc identity rather than
|
/// it before it is bound is fine — it mints an ad-hoc identity rather than
|
||||||
/// failing, because an unattributed artifact is worse than a vague one.
|
/// failing, because an unattributed artifact is worse than a vague one.
|
||||||
pub fn process() -> &'static Provenance {
|
pub fn process() -> Provenance {
|
||||||
PROCESS.get_or_init(|| Provenance::for_run(""))
|
PROCESS.read().ok().and_then(|g| g.clone()).unwrap_or_else(|| Provenance::for_run(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bind this process to a run. First call wins: a run's identity must not
|
/// Bind this process to a run. In the REPL many engagements run in one
|
||||||
/// change underneath the markers already minted against it.
|
/// process, so this REPLACES the binding each run — otherwise every run
|
||||||
pub fn bind_run(run_id: &str) -> &'static Provenance {
|
/// after the first would mint markers (and the provenance line) with the
|
||||||
let _ = PROCESS.set(Provenance::for_run(run_id));
|
/// FIRST run's id. Markers minted within a run use that run's provenance.
|
||||||
Provenance::process()
|
pub fn bind_run(run_id: &str) -> Provenance {
|
||||||
|
let p = Provenance::for_run(run_id);
|
||||||
|
if let Ok(mut g) = PROCESS.write() { *g = Some(p.clone()); }
|
||||||
|
p
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Short identity string: what goes in a footer or a log line.
|
/// Short identity string: what goes in a footer or a log line.
|
||||||
@@ -393,14 +396,18 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn process_provenance_is_stable_once_bound() {
|
fn bind_run_rebinds_per_run_so_the_repl_gets_each_runs_id() {
|
||||||
let a = Provenance::process().run.clone();
|
// The REPL runs many engagements in one process; each run must bind its
|
||||||
let b = Provenance::process().run.clone();
|
// OWN id, or every run after the first mints markers/the provenance line
|
||||||
assert_eq!(a, b, "markers minted in one process must agree on the run");
|
// with the FIRST run's id (the stale-provenance bug).
|
||||||
// Binding after the fact must not move the ground under markers that
|
let a = Provenance::bind_run("ns-1-alpha").run.clone();
|
||||||
// already went out.
|
assert_eq!(a, "ns-1-alpha");
|
||||||
let c = Provenance::bind_run("ns-9-other").run.clone();
|
assert_eq!(Provenance::process().run, "ns-1-alpha", "process() reflects the bound run");
|
||||||
assert_eq!(a, c, "first identity wins");
|
let b = Provenance::bind_run("ns-2-bravo").run.clone();
|
||||||
|
assert_eq!(b, "ns-2-bravo");
|
||||||
|
assert_eq!(Provenance::process().run, "ns-2-bravo", "a later run rebinds");
|
||||||
|
// The build fingerprint stays stable across rebinds.
|
||||||
|
assert_eq!(Provenance::process().build, Provenance::build_fingerprint());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in new issue
Block a user