mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-06-29 23:05:30 +02:00
feat: whitebox/greybox/repl accept a GitHub URL (auto-clone)
`whitebox <arg>`, `greybox --repo <arg>`, `tui --repo`, and the REPL `/repo` now accept a git URL (https://github.com/owner/repo[.git], git@…, ssh://, *.git) or an `owner/repo` shorthand. A new resolve_source() shallow-clones it into <base>/repos/<name> (cached, .gitignored) and reviews it; existing local paths are used unchanged. Works identically with API-key (--model) and --subscription. Verified: `neurosploit whitebox https://github.com/digininja/DVWA --offline` clones DVWA and runs the 78 code agents over 120KB of source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -104,3 +104,7 @@ data/repl_runs.json
|
||||
data/repl_history.txt
|
||||
.neurosploit/
|
||||
/tmp/*
|
||||
|
||||
# Cloned source repos (whitebox/greybox from a git URL)
|
||||
repos/
|
||||
neurosploit-rs/repos/
|
||||
|
||||
@@ -47,6 +47,14 @@ and severity-calibrated).
|
||||
- **5 new doctrine meta-agents** (`agents_md/meta/`): `exploit_depth_doctrine`,
|
||||
`finding_chainer`, `artifact_decoder`, `token_auditor`, `report_calibrator`
|
||||
(meta agents 17 → 22; total library 343 → 348).
|
||||
- **Source from a GitHub URL.** `whitebox` / `greybox --repo` (and the REPL
|
||||
`/repo`) now accept a **git URL** (`https://github.com/owner/repo[.git]`) or an
|
||||
`owner/repo` shorthand — the repo is cloned (shallow) into `<base>/repos/` and
|
||||
reviewed automatically, no manual `git clone` needed:
|
||||
```bash
|
||||
neurosploit whitebox https://github.com/digininja/DVWA \
|
||||
--subscription --model anthropic:claude-opus-4-8 -v
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@@ -65,8 +65,10 @@ enum Cmd {
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
},
|
||||
/// White-box: analyse a local repository's source code for vulnerabilities.
|
||||
/// White-box: analyse a repository's source code for vulnerabilities.
|
||||
Whitebox {
|
||||
/// Local path, a GitHub URL (https://github.com/owner/repo[.git]) or an
|
||||
/// `owner/repo` shorthand — git URLs are cloned automatically.
|
||||
path: String,
|
||||
#[arg(long = "model")]
|
||||
models: Vec<String>,
|
||||
@@ -83,7 +85,7 @@ enum Cmd {
|
||||
},
|
||||
/// Greybox: review a repo's source AND exploit the running app together.
|
||||
Greybox {
|
||||
/// Path to the source repository.
|
||||
/// Source repo: local path, a GitHub URL, or `owner/repo` (cloned if a URL).
|
||||
repo: String,
|
||||
/// URL of the running application.
|
||||
#[arg(long)]
|
||||
@@ -230,6 +232,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
print_findings(&out);
|
||||
}
|
||||
Cmd::Whitebox { path, models, max_agents, vote_n, offline, subscription, verbose } => {
|
||||
let path = resolve_source(&base, &path)?; // local path OR github URL/owner/repo
|
||||
let mut cfg = RunConfig::new(&path);
|
||||
cfg.max_agents = max_agents;
|
||||
cfg.vote_n = vote_n;
|
||||
@@ -243,6 +246,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
print_findings(&out);
|
||||
}
|
||||
Cmd::Greybox { repo, url, models, creds, focus, max_agents, vote_n, offline, subscription, mcp, verbose } => {
|
||||
let repo = resolve_source(&base, &repo)?; // local path OR github URL/owner/repo
|
||||
let url = if url.starts_with("http") { url } else { format!("https://{url}") };
|
||||
let mut cfg = RunConfig::new(&url);
|
||||
cfg.repo = Some(repo);
|
||||
@@ -260,6 +264,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
print_findings(&out);
|
||||
}
|
||||
Cmd::Tui { url, models, repo, creds, focus, max_agents, vote_n, subscription, mcp } => {
|
||||
let repo = match repo { Some(r) => Some(resolve_source(&base, &r)?), None => None }; // github URL ok
|
||||
let url = if url.starts_with("http") { url } else { format!("https://{url}") };
|
||||
let mut cfg = RunConfig::new(&url);
|
||||
cfg.max_agents = max_agents;
|
||||
@@ -532,6 +537,45 @@ fn now_ts() -> u64 {
|
||||
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Resolve a source argument (white-box `path` / grey-box `--repo`) to a local
|
||||
/// directory. A git URL (`https://…`, `git@…`, `ssh://…`, `*.git`) or a GitHub
|
||||
/// `owner/repo` shorthand is **cloned** (shallow) into `<base>/repos/<name>` and
|
||||
/// that path is returned; an existing local path is returned unchanged.
|
||||
pub(crate) fn resolve_source(base: &Path, arg: &str) -> anyhow::Result<String> {
|
||||
let is_url = arg.starts_with("http://") || arg.starts_with("https://")
|
||||
|| arg.starts_with("git@") || arg.starts_with("ssh://") || arg.ends_with(".git");
|
||||
// `owner/repo` GitHub shorthand: no scheme, exactly one slash, not a real path.
|
||||
let is_shorthand = !is_url
|
||||
&& !Path::new(arg).exists()
|
||||
&& arg.matches('/').count() == 1
|
||||
&& !arg.starts_with('.') && !arg.starts_with('/') && !arg.starts_with('~')
|
||||
&& arg.chars().all(|c| c.is_ascii_alphanumeric() || "._-/".contains(c));
|
||||
if !is_url && !is_shorthand {
|
||||
return Ok(arg.to_string()); // already a local path
|
||||
}
|
||||
|
||||
let url = if is_shorthand { format!("https://github.com/{arg}") } else { arg.to_string() };
|
||||
let name = sanitize(url.trim_end_matches('/').trim_end_matches(".git").rsplit('/').next().unwrap_or("repo"));
|
||||
let repos_dir = base.join("repos");
|
||||
std::fs::create_dir_all(&repos_dir).ok();
|
||||
let dest = repos_dir.join(&name);
|
||||
|
||||
if dest.join(".git").is_dir() {
|
||||
println!(" [*] repo cache hit → {} (delete it to re-clone)", dest.display());
|
||||
return Ok(dest.display().to_string());
|
||||
}
|
||||
println!(" [*] cloning {url} → {}", dest.display());
|
||||
let status = std::process::Command::new("git")
|
||||
.args(["clone", "--depth", "1", &url, &dest.display().to_string()])
|
||||
.status()
|
||||
.map_err(|e| anyhow::anyhow!("could not start `git clone` (is git installed?): {e}"))?;
|
||||
if !status.success() {
|
||||
std::fs::remove_dir_all(&dest).ok();
|
||||
anyhow::bail!("git clone failed for {url}");
|
||||
}
|
||||
Ok(dest.display().to_string())
|
||||
}
|
||||
|
||||
/// Blocking yes/no prompt (default yes). Used after a graceful Ctrl-C.
|
||||
fn ask_yes_no(q: &str) -> bool {
|
||||
use std::io::Write;
|
||||
|
||||
@@ -392,9 +392,15 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
|
||||
s.target = Some(t.clone()); println!(" target: {t}"); }
|
||||
}
|
||||
"/repo" => {
|
||||
if arg.is_empty() { println!(" repo: {}", s.repo.clone().unwrap_or_else(|| "(none) — set with /repo <path>, clear with /repo clear".into())); }
|
||||
if arg.is_empty() { println!(" repo: {}", s.repo.clone().unwrap_or_else(|| "(none) — set with /repo <path | github-url | owner/repo>, clear with /repo clear".into())); }
|
||||
else if arg == "clear" { s.repo = None; println!(" repo cleared"); }
|
||||
else { s.repo = Some(arg.to_string()); println!(" repo: {arg}"); }
|
||||
else {
|
||||
// Accept a local path OR a GitHub URL / owner-repo shorthand (cloned on set).
|
||||
match crate::resolve_source(base, arg) {
|
||||
Ok(p) => { s.repo = Some(p.clone()); println!(" repo: {p}"); }
|
||||
Err(e) => println!(" \x1b[31mcould not resolve repo: {e}\x1b[0m"),
|
||||
}
|
||||
}
|
||||
}
|
||||
"/auth" => {
|
||||
if arg.is_empty() { println!(" auth: {}", s.auth.clone().unwrap_or_else(|| "(none) — set with /auth <header>, clear with /auth clear".into())); }
|
||||
|
||||
Reference in New Issue
Block a user