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:
CyberSecurityUP
2026-06-26 13:50:47 -03:00
parent 489b3abd3f
commit 761d3df444
4 changed files with 66 additions and 4 deletions
+4
View File
@@ -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/
+8
View File
@@ -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
+46 -2
View File
@@ -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;
+8 -2
View File
@@ -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())); }