mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-07-06 03:17:52 +02:00
v3.5.0: greybox (code + live) pipeline + credentials (creds.yaml / JWT / auth)
- New GREYBOX mode: review a repo's source AND exploit the running app in one pipeline — code-review findings become LEADS injected into live exploitation. CLI: `neurosploit greybox <repo> --url <app> [--creds creds.yaml] [--focus ...]` REPL: set both /repo and /target → greybox auto-selected. - Credentials (harness/src/creds.rs, dependency-free YAML subset): jwt / header / cookie, or an automated `login:` flow. Derives an auth header and/or a "authenticate first via curl" directive injected into prompts so agents test authenticated. --creds flag + /creds command + creds.example.yaml. - RunConfig gains `repo`; run_engagement refactored to a Mode enum (Black/White/Grey). - Verified offline: greybox loads creds, combines repo+URL, runs pipeline, writes report. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -74,6 +74,34 @@ enum Cmd {
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
},
|
||||
/// Greybox: review a repo's source AND exploit the running app together.
|
||||
Greybox {
|
||||
/// Path to the source repository.
|
||||
repo: String,
|
||||
/// URL of the running application.
|
||||
#[arg(long)]
|
||||
url: String,
|
||||
#[arg(long = "model")]
|
||||
models: Vec<String>,
|
||||
/// Credentials YAML for authenticated testing (jwt/header/cookie/login).
|
||||
#[arg(long)]
|
||||
creds: Option<String>,
|
||||
/// Free-text focus, e.g. "injection and broken access control".
|
||||
#[arg(long)]
|
||||
focus: Option<String>,
|
||||
#[arg(long, default_value_t = 0)]
|
||||
max_agents: usize,
|
||||
#[arg(long, default_value_t = 3)]
|
||||
vote_n: usize,
|
||||
#[arg(long)]
|
||||
offline: bool,
|
||||
#[arg(long)]
|
||||
subscription: bool,
|
||||
#[arg(long)]
|
||||
mcp: bool,
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
},
|
||||
/// Show agent library counts.
|
||||
Agents,
|
||||
/// List providers and models.
|
||||
@@ -161,12 +189,59 @@ async fn main() -> anyhow::Result<()> {
|
||||
let out = run_engagement(&base, cfg, false, true).await?;
|
||||
print_findings(&out);
|
||||
}
|
||||
Cmd::Greybox { repo, url, models, creds, focus, max_agents, vote_n, offline, subscription, mcp, verbose } => {
|
||||
let url = if url.starts_with("http") { url } else { format!("https://{url}") };
|
||||
let mut cfg = RunConfig::new(&url);
|
||||
cfg.repo = Some(repo);
|
||||
cfg.max_agents = max_agents;
|
||||
cfg.vote_n = vote_n;
|
||||
cfg.offline = offline;
|
||||
cfg.subscription = subscription;
|
||||
cfg.verbose = verbose;
|
||||
cfg.instructions = focus;
|
||||
if !models.is_empty() {
|
||||
cfg.models = models;
|
||||
}
|
||||
apply_creds(&mut cfg, creds.as_deref());
|
||||
let out = run_greybox_engagement(&base, cfg, mcp).await?;
|
||||
print_findings(&out);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load a creds.yaml into the run config: derive the auth header and prepend any
|
||||
/// login flow to the operator instructions.
|
||||
pub(crate) fn apply_creds(cfg: &mut RunConfig, path: Option<&str>) {
|
||||
let Some(p) = path else { return };
|
||||
match harness::creds::Creds::load(Path::new(p)) {
|
||||
Some(c) => {
|
||||
if cfg.auth.is_none() {
|
||||
cfg.auth = c.auth_header();
|
||||
}
|
||||
if let Some(login) = c.login_instruction() {
|
||||
let base = cfg.instructions.clone().unwrap_or_default();
|
||||
cfg.instructions = Some(format!("{login}\n{base}"));
|
||||
}
|
||||
println!(" [*] loaded credentials from {p}");
|
||||
}
|
||||
None => eprintln!(" [!] no usable credentials in {p}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub(crate) enum Mode { Black, White, Grey }
|
||||
|
||||
pub(crate) async fn run_greybox_engagement(base: &Path, cfg: RunConfig, mcp: bool) -> anyhow::Result<RunOutput> {
|
||||
run_mode(base, cfg, mcp, Mode::Grey).await
|
||||
}
|
||||
|
||||
/// Shared engagement runner for `run` / `whitebox` / the interactive session.
|
||||
pub(crate) async fn run_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, whitebox: bool) -> anyhow::Result<RunOutput> {
|
||||
pub(crate) async fn run_engagement(base: &Path, cfg: RunConfig, mcp: bool, whitebox: bool) -> anyhow::Result<RunOutput> {
|
||||
run_mode(base, cfg, mcp, if whitebox { Mode::White } else { Mode::Black }).await
|
||||
}
|
||||
|
||||
async fn run_mode(base: &Path, mut cfg: RunConfig, mcp: bool, mode: Mode) -> anyhow::Result<RunOutput> {
|
||||
let lib = agents::load(base);
|
||||
|
||||
// Unique, sortable run id → runs/<id>/
|
||||
@@ -182,8 +257,11 @@ pub(crate) async fn run_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, w
|
||||
println!(" │ target : {}", cfg.target);
|
||||
println!(" │ models : {}", cfg.models.join(", "));
|
||||
println!(" │ output : {}", workdir.display());
|
||||
if let Mode::Grey = mode {
|
||||
println!(" │ repo : {}", cfg.repo.clone().unwrap_or_default());
|
||||
}
|
||||
println!(" └─ mode : {}{}{}",
|
||||
if whitebox { "white-box" } else { "black-box" },
|
||||
match mode { Mode::White => "white-box", Mode::Grey => "greybox", Mode::Black => "black-box" },
|
||||
if cfg.subscription { " · subscription" } else { " · api" },
|
||||
if mcp { " · mcp" } else { "" });
|
||||
|
||||
@@ -224,10 +302,10 @@ pub(crate) async fn run_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, w
|
||||
println!(" [*] {line}");
|
||||
}
|
||||
});
|
||||
let out = if whitebox {
|
||||
harness::run_whitebox(cfg, &lib, &pool, tx).await
|
||||
} else {
|
||||
harness::run(cfg, &lib, &pool, tx).await
|
||||
let out = match mode {
|
||||
Mode::White => harness::run_whitebox(cfg, &lib, &pool, tx).await,
|
||||
Mode::Grey => harness::run_greybox(cfg, &lib, &pool, tx).await,
|
||||
Mode::Black => harness::run(cfg, &lib, &pool, tx).await,
|
||||
};
|
||||
let _ = printer.await;
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ struct Session {
|
||||
target: Option<String>,
|
||||
repo: Option<String>,
|
||||
auth: Option<String>,
|
||||
creds: Option<String>,
|
||||
instructions: Option<String>,
|
||||
}
|
||||
|
||||
@@ -34,6 +35,7 @@ impl Default for Session {
|
||||
target: None,
|
||||
repo: None,
|
||||
auth: None,
|
||||
creds: None,
|
||||
instructions: None,
|
||||
}
|
||||
}
|
||||
@@ -119,20 +121,23 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
|
||||
println!(" subscription: {}", onoff(s.subscription));
|
||||
}
|
||||
"/target" | "/url" => {
|
||||
// target + repo can coexist → greybox.
|
||||
let t = if arg.starts_with("http") || arg.is_empty() { arg.to_string() } else { format!("https://{arg}") };
|
||||
s.target = if t.is_empty() { None } else { Some(t) };
|
||||
s.repo = None;
|
||||
println!(" target: {}", s.target.clone().unwrap_or_else(|| "(none)".into()));
|
||||
}
|
||||
"/repo" => {
|
||||
s.repo = if arg.is_empty() { None } else { Some(arg.to_string()) };
|
||||
s.target = None;
|
||||
println!(" repo: {}", s.repo.clone().unwrap_or_else(|| "(none)".into()));
|
||||
}
|
||||
"/auth" => {
|
||||
s.auth = if arg.is_empty() { None } else { Some(arg.to_string()) };
|
||||
println!(" auth: {}", s.auth.clone().unwrap_or_else(|| "(none)".into()));
|
||||
}
|
||||
"/creds" => {
|
||||
s.creds = if arg.is_empty() { None } else { Some(arg.to_string()) };
|
||||
println!(" creds file: {}", s.creds.clone().unwrap_or_else(|| "(none)".into()));
|
||||
}
|
||||
"/focus" | "/instructions" => {
|
||||
s.instructions = if arg.is_empty() { None } else { Some(arg.to_string()) };
|
||||
println!(" focus: {}", s.instructions.clone().unwrap_or_else(|| "(none)".into()));
|
||||
@@ -153,15 +158,22 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
async fn run(base: &Path, s: &Session) {
|
||||
let (target, whitebox) = match (&s.repo, &s.target) {
|
||||
(Some(r), _) => (r.clone(), true),
|
||||
(_, Some(t)) => (t.clone(), false),
|
||||
// repo + target → greybox; repo only → whitebox; target only → black-box.
|
||||
enum M { Black(String), White(String), Grey { url: String, repo: String } }
|
||||
let m = match (&s.repo, &s.target) {
|
||||
(Some(r), Some(t)) => M::Grey { url: t.clone(), repo: r.clone() },
|
||||
(Some(r), None) => M::White(r.clone()),
|
||||
(None, Some(t)) => M::Black(t.clone()),
|
||||
_ => {
|
||||
println!(" \x1b[31m✗ set a /target <url> or /repo <path> first.\x1b[0m");
|
||||
println!(" \x1b[31m✗ set a /target <url> and/or /repo <path> first.\x1b[0m");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut cfg = RunConfig::new(&target);
|
||||
let primary = match &m {
|
||||
M::Black(t) | M::White(t) => t.clone(),
|
||||
M::Grey { url, .. } => url.clone(),
|
||||
};
|
||||
let mut cfg = RunConfig::new(&primary);
|
||||
cfg.models = s.models.clone();
|
||||
cfg.subscription = s.subscription;
|
||||
cfg.vote_n = s.vote_n;
|
||||
@@ -169,8 +181,17 @@ async fn run(base: &Path, s: &Session) {
|
||||
cfg.verbose = true;
|
||||
cfg.instructions = s.instructions.clone();
|
||||
cfg.auth = s.auth.clone();
|
||||
if let M::Grey { repo, .. } = &m {
|
||||
cfg.repo = Some(repo.clone());
|
||||
}
|
||||
crate::apply_creds(&mut cfg, s.creds.as_deref());
|
||||
|
||||
match crate::run_engagement(base, cfg, s.mcp && !whitebox, whitebox).await {
|
||||
let result = match m {
|
||||
M::Grey { .. } => crate::run_greybox_engagement(base, cfg, s.mcp).await,
|
||||
M::White(_) => crate::run_engagement(base, cfg, false, true).await,
|
||||
M::Black(_) => crate::run_engagement(base, cfg, s.mcp, false).await,
|
||||
};
|
||||
match result {
|
||||
Ok(out) => crate::print_findings(&out),
|
||||
Err(e) => println!(" \x1b[31m✗ run failed: {e}\x1b[0m"),
|
||||
}
|
||||
@@ -180,9 +201,17 @@ fn show(s: &Session) {
|
||||
println!(" ┌─ session");
|
||||
println!(" │ models : {}", s.models.join(", "));
|
||||
println!(" │ auth mode: {}", if s.subscription { "subscription (CLI login)" } else { "API key" });
|
||||
let mode = match (&s.repo, &s.target) {
|
||||
(Some(_), Some(_)) => "greybox (code + live)",
|
||||
(Some(_), None) => "white-box (code)",
|
||||
(None, Some(_)) => "black-box (live)",
|
||||
_ => "(set /target and/or /repo)",
|
||||
};
|
||||
println!(" │ mode : {mode}");
|
||||
println!(" │ target : {}", s.target.clone().unwrap_or_else(|| "(none)".into()));
|
||||
println!(" │ repo : {}", s.repo.clone().unwrap_or_else(|| "(none)".into()));
|
||||
println!(" │ auth : {}", s.auth.clone().unwrap_or_else(|| "(none)".into()));
|
||||
println!(" │ creds : {}", s.creds.clone().unwrap_or_else(|| "(none)".into()));
|
||||
println!(" │ focus : {}", s.instructions.clone().unwrap_or_else(|| "(none — tests everything)".into()));
|
||||
println!(" │ mcp : {} votes: {} max-agents: {}", onoff(s.mcp), s.vote_n, s.max_agents);
|
||||
println!(" └─ /run to launch");
|
||||
@@ -195,8 +224,9 @@ fn help() {
|
||||
println!(" /key <prov> <key> set a provider API key (switches to API mode)");
|
||||
println!(" /sub on|off use local subscription login instead of API key");
|
||||
println!(" /target <url> black-box target URL");
|
||||
println!(" /repo <path> white-box: analyse a local repository");
|
||||
println!(" /repo <path> analyse a local repo (repo+target = greybox: code + live)");
|
||||
println!(" /auth <value> auth to send (e.g. 'Authorization: Bearer <jwt>' or 'Cookie: s=..')");
|
||||
println!(" /creds <file.yaml> load credentials (jwt/header/cookie/login) for authenticated tests");
|
||||
println!(" /focus <text> steer the tests, e.g. 'injection and broken access control'");
|
||||
println!(" (or just type the instruction with no slash)");
|
||||
println!(" /mcp on|off enable Playwright MCP browser (subscription path)");
|
||||
|
||||
Reference in New Issue
Block a user