mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-06-30 07:15:30 +02:00
v3.4.0: subscription backend (Claude Code / Codex / Grok logins)
The Rust harness can now use models two ways: - API: provider API key (OpenAI-compatible HTTP) — existing path - Subscription: drive the locally-installed agentic CLI login directly, no API key (anthropic→claude, openai→codex, xai→grok) - models.rs: ChatClient::chat_cli spawns the CLI (stdin prompt), cli_binary_for + installed_cli_backends + binary_in_path PATH detection - pool.rs: ModelPool::with_auth(subscription); one() routes per model - types/CLI: RunConfig.subscription + `run --subscription` flag - web: /api/run honors "subscription"; /api/info reports detected cli_backends; SPA gets a "Use subscription" toggle Verified live: `run --subscription --model anthropic:claude-haiku-4-5` drove the Claude subscription end-to-end (recon + agent + vote) with no API key set. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,10 @@ and a **reinforcement-learning** loop that gets smarter every run.
|
||||
> ./target/release/neurosploit serve # web dashboard → :8788
|
||||
> ./target/release/neurosploit run https://target.example --model anthropic:claude-opus-4-8 --model openai:gpt-5.1
|
||||
> ./target/release/neurosploit run https://t.example --offline # pipeline self-test, no API keys
|
||||
> ./target/release/neurosploit run https://t.example --subscription --model anthropic:claude-opus-4-8 # uses Claude Code login, no API key
|
||||
> ```
|
||||
> Two auth paths: **model APIs** (provider key) or **subscription** — drive your
|
||||
> local **Claude Code** / **Codex** / **Grok** logins directly (no API key).
|
||||
> 11 OpenAI-compatible providers / 31 models (Claude, GPT, Grok, NVIDIA NIM,
|
||||
> DeepSeek, Mistral, Qwen, Groq, Together, OpenRouter, Ollama). Reads the same
|
||||
> `agents_md/` library (213 agents).
|
||||
|
||||
@@ -33,6 +33,10 @@ enum Cmd {
|
||||
/// Exercise the pipeline without calling any model API.
|
||||
#[arg(long)]
|
||||
offline: bool,
|
||||
/// Use local agentic CLI subscriptions (Claude Code / Codex / Grok)
|
||||
/// instead of HTTP API keys.
|
||||
#[arg(long)]
|
||||
subscription: bool,
|
||||
},
|
||||
/// Show agent library counts.
|
||||
Agents,
|
||||
@@ -84,18 +88,19 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Cmd::Run { url, models, max_agents, vote_n, offline } => {
|
||||
Cmd::Run { url, models, max_agents, vote_n, offline, subscription } => {
|
||||
let url = if url.starts_with("http") { url } else { format!("https://{url}") };
|
||||
let mut cfg = RunConfig::new(&url);
|
||||
cfg.max_agents = max_agents;
|
||||
cfg.vote_n = vote_n;
|
||||
cfg.offline = offline;
|
||||
cfg.subscription = subscription;
|
||||
if !models.is_empty() {
|
||||
cfg.models = models;
|
||||
}
|
||||
let lib = agents::load(&base);
|
||||
let refs: Vec<ModelRef> = cfg.models.iter().map(|s| ModelRef::parse(s)).collect();
|
||||
let pool = ModelPool::new(refs, cfg.concurrency);
|
||||
let pool = ModelPool::with_auth(refs, cfg.concurrency, cfg.subscription);
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(256);
|
||||
let printer = tokio::spawn(async move {
|
||||
|
||||
@@ -59,6 +59,7 @@ async fn info(State(st): State<Arc<AppState>>) -> Json<Value> {
|
||||
"version": "3.4.0",
|
||||
"agents": {"vulns": lib.vulns.len(), "meta": lib.meta.len(), "total": lib.total()},
|
||||
"providers": provs,
|
||||
"cli_backends": harness::installed_cli_backends(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -126,6 +127,7 @@ async fn run(State(st): State<Arc<AppState>>, Json(body): Json<Value>) -> Json<V
|
||||
let vote_n = body.get("vote_n").and_then(|v| v.as_u64()).unwrap_or(3) as usize;
|
||||
let max_agents = body.get("max_agents").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
|
||||
let offline = body.get("offline").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
let subscription = body.get("subscription").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
|
||||
let lib = agents::load(&base);
|
||||
let refs: Vec<ModelRef> = if models.is_empty() {
|
||||
@@ -133,7 +135,7 @@ async fn run(State(st): State<Arc<AppState>>, Json(body): Json<Value>) -> Json<V
|
||||
} else {
|
||||
models.iter().map(|s| ModelRef::parse(s)).collect()
|
||||
};
|
||||
let pool = ModelPool::new(refs, 8);
|
||||
let pool = ModelPool::with_auth(refs, 8, subscription);
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(256);
|
||||
let stf = st2.clone();
|
||||
@@ -160,6 +162,7 @@ async fn run(State(st): State<Arc<AppState>>, Json(body): Json<Value>) -> Json<V
|
||||
cfg.vote_n = vote_n;
|
||||
cfg.max_agents = max_agents;
|
||||
cfg.offline = offline;
|
||||
cfg.subscription = subscription;
|
||||
let _ = tx.send(format!("=== target: {url} ===")).await;
|
||||
let out = harness::run(cfg, &lib, &pool, tx.clone()).await;
|
||||
all_findings.extend(out.findings);
|
||||
|
||||
@@ -86,7 +86,10 @@
|
||||
<div class="field"><label>Validator votes (N)</label><input id="voten" type="number" value="3" min="1" max="9"/></div>
|
||||
<div class="field"><label>Max agents (0 = all)</label><input id="maxa" type="number" value="8" min="0"/></div>
|
||||
</div>
|
||||
<div class="toggles"><label class="toggle on" id="t-off"><input type="checkbox" id="offline" checked/> Offline (no API calls — pipeline self-test)</label></div>
|
||||
<div class="toggles">
|
||||
<label class="toggle on" id="t-off"><input type="checkbox" id="offline" checked/> Offline (pipeline self-test)</label>
|
||||
<label class="toggle" id="t-sub"><input type="checkbox" id="subscription"/> Use subscription (Claude/Codex login)</label>
|
||||
</div>
|
||||
<div class="btns"><button class="run" id="go">▶ Run harness</button></div>
|
||||
<div class="term" id="term"></div>
|
||||
<div class="sevrow" id="sevrow"></div>
|
||||
@@ -112,6 +115,7 @@ let INFO=null,AGENTS=[],lastRun=null;
|
||||
$$('.nav').forEach(n=>n.onclick=()=>{$$('.nav').forEach(x=>x.classList.remove('on'));n.classList.add('on');
|
||||
$$('.view').forEach(v=>v.classList.remove('on'));$('#v-'+n.dataset.v).classList.add('on');});
|
||||
$('#t-off').querySelector('input').onchange=e=>$('#t-off').classList.toggle('on',e.target.checked);
|
||||
$('#t-sub').querySelector('input').onchange=e=>$('#t-sub').classList.toggle('on',e.target.checked);
|
||||
|
||||
async function init(){
|
||||
INFO=await (await fetch('/api/info')).json();
|
||||
@@ -136,7 +140,7 @@ async function run(){
|
||||
const targets=$('#targets').value.split('\n').map(s=>s.trim()).filter(Boolean);
|
||||
if(!targets.length){$('#targets').focus();$('#targets').style.borderColor='var(--crit)';return;}
|
||||
$('#go').disabled=true;$('#term').innerHTML='';$('#sevrow').style.display='none';$('#findings').innerHTML='';
|
||||
const body={targets,models:selectedModels(),vote_n:+$('#voten').value,max_agents:+$('#maxa').value,offline:$('#offline').checked};
|
||||
const body={targets,models:selectedModels(),vote_n:+$('#voten').value,max_agents:+$('#maxa').value,offline:$('#offline').checked,subscription:$('#subscription').checked};
|
||||
logLine('Queued '+targets.length+' target(s) · panel: '+(body.models.join(', ')||'default'));
|
||||
const r=await (await fetch('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})).json();
|
||||
if(r.error){logLine('ERROR: '+r.error);$('#go').disabled=false;return;}
|
||||
|
||||
@@ -14,7 +14,9 @@ pub mod report;
|
||||
pub mod types;
|
||||
|
||||
pub use agents::{Agent, Library};
|
||||
pub use models::{provider_for, providers, ChatClient, ModelRef, Provider};
|
||||
pub use models::{
|
||||
cli_binary_for, installed_cli_backends, provider_for, providers, ChatClient, ModelRef, Provider,
|
||||
};
|
||||
pub use pipeline::run;
|
||||
pub use pool::ModelPool;
|
||||
pub use types::{Finding, RunConfig};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use serde::Serialize;
|
||||
use std::process::Stdio;
|
||||
use std::time::Duration;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::process::Command;
|
||||
|
||||
/// A model provider exposing an OpenAI-compatible `/chat/completions` endpoint.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
@@ -119,6 +122,66 @@ impl ChatClient {
|
||||
}
|
||||
}
|
||||
|
||||
impl ChatClient {
|
||||
/// Complete via a locally-installed **agentic CLI subscription** (Claude
|
||||
/// Code / Codex / Grok) instead of an API key. This uses the user's logged-in
|
||||
/// subscription, so no provider key is required.
|
||||
pub async fn chat_cli(&self, provider: &str, model: &str, system: &str, user: &str) -> Result<String> {
|
||||
let bin = cli_binary_for(provider)
|
||||
.ok_or_else(|| anyhow!("no CLI/subscription backend for provider '{}'", provider))?;
|
||||
let prompt = format!("{system}\n\n{user}");
|
||||
let mut cmd = Command::new(bin);
|
||||
match bin {
|
||||
// Claude Code headless print mode (uses the Claude subscription login).
|
||||
"claude" => {
|
||||
cmd.arg("-p").arg("--model").arg(model);
|
||||
}
|
||||
// Codex non-interactive exec (uses the ChatGPT/Codex login), prompt on stdin.
|
||||
"codex" => {
|
||||
cmd.arg("exec").arg("--model").arg(model).arg("-");
|
||||
}
|
||||
// Grok CLI, prompt on stdin (best-effort flags).
|
||||
"grok" => {
|
||||
cmd.arg("--model").arg(model);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
let mut child = cmd.spawn().map_err(|e| anyhow!("spawn {} failed: {}", bin, e))?;
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
stdin.write_all(prompt.as_bytes()).await?;
|
||||
// Drop closes stdin so the CLI processes the prompt and exits.
|
||||
}
|
||||
let out = child.wait_with_output().await?;
|
||||
if !out.status.success() {
|
||||
return Err(anyhow!("{} subscription CLI failed: {}", bin, truncate(&String::from_utf8_lossy(&out.stderr), 200)));
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a provider to its local agentic CLI binary (subscription backend).
|
||||
pub fn cli_binary_for(provider: &str) -> Option<&'static str> {
|
||||
match provider {
|
||||
"anthropic" => Some("claude"),
|
||||
"openai" => Some("codex"),
|
||||
"xai" => Some("grok"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Is `name` an executable found on PATH?
|
||||
pub fn binary_in_path(name: &str) -> bool {
|
||||
std::env::var_os("PATH")
|
||||
.map(|path| std::env::split_paths(&path).any(|dir| dir.join(name).is_file()))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Which subscription CLI backends are installed locally.
|
||||
pub fn installed_cli_backends() -> Vec<&'static str> {
|
||||
["claude", "codex", "grok"].into_iter().filter(|b| binary_in_path(b)).collect()
|
||||
}
|
||||
|
||||
impl Default for ChatClient {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
use crate::models::{ChatClient, ModelRef};
|
||||
use crate::models::{cli_binary_for, ChatClient, ModelRef};
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
/// A pool of candidate models with a global concurrency cap and provider
|
||||
/// failover. The same panel of models is reused for validator voting.
|
||||
///
|
||||
/// `subscription = true` routes each model through its local agentic CLI
|
||||
/// (Claude Code / Codex / Grok login) instead of an HTTP API key.
|
||||
pub struct ModelPool {
|
||||
client: ChatClient,
|
||||
sem: Arc<Semaphore>,
|
||||
pub candidates: Vec<ModelRef>,
|
||||
pub subscription: bool,
|
||||
}
|
||||
|
||||
impl ModelPool {
|
||||
pub fn new(models: Vec<ModelRef>, concurrency: usize) -> Self {
|
||||
Self::with_auth(models, concurrency, false)
|
||||
}
|
||||
|
||||
pub fn with_auth(models: Vec<ModelRef>, concurrency: usize, subscription: bool) -> Self {
|
||||
let concurrency = concurrency.max(1);
|
||||
ModelPool {
|
||||
client: ChatClient::new(),
|
||||
@@ -22,16 +30,25 @@ impl ModelPool {
|
||||
} else {
|
||||
models
|
||||
},
|
||||
subscription,
|
||||
}
|
||||
}
|
||||
|
||||
/// One completion for a model, via subscription CLI or HTTP API.
|
||||
async fn one(&self, m: &ModelRef, system: &str, user: &str) -> Result<String> {
|
||||
if self.subscription && cli_binary_for(&m.provider).is_some() {
|
||||
return self.client.chat_cli(&m.provider, &m.model, system, user).await;
|
||||
}
|
||||
self.client.chat(m, system, user).await
|
||||
}
|
||||
|
||||
/// Complete a prompt, trying each candidate model until one succeeds.
|
||||
/// Returns the model that answered and its text.
|
||||
pub async fn complete(&self, system: &str, user: &str) -> Result<(ModelRef, String)> {
|
||||
let _permit = self.sem.acquire().await.expect("semaphore closed");
|
||||
let mut last = anyhow!("no candidate models");
|
||||
for m in &self.candidates {
|
||||
match self.client.chat(m, system, user).await {
|
||||
match self.one(m, system, user).await {
|
||||
Ok(text) => return Ok((m.clone(), text)),
|
||||
Err(e) => last = e,
|
||||
}
|
||||
@@ -51,7 +68,7 @@ impl ModelPool {
|
||||
Ok(p) => p,
|
||||
Err(_) => break,
|
||||
};
|
||||
if let Ok(text) = self.client.chat(m, system, user).await {
|
||||
if let Ok(text) = self.one(m, system, user).await {
|
||||
total += 1;
|
||||
let t = text.to_lowercase();
|
||||
if t.contains("\"verdict\": \"confirmed\"")
|
||||
|
||||
@@ -70,6 +70,10 @@ pub struct RunConfig {
|
||||
/// Offline mode: exercise the full pipeline without calling any model API.
|
||||
#[serde(default)]
|
||||
pub offline: bool,
|
||||
/// Use local agentic CLI subscriptions (Claude Code / Codex / Grok) instead
|
||||
/// of HTTP API keys.
|
||||
#[serde(default)]
|
||||
pub subscription: bool,
|
||||
}
|
||||
|
||||
fn default_vote() -> usize {
|
||||
@@ -88,6 +92,7 @@ impl RunConfig {
|
||||
concurrency: 8,
|
||||
max_agents: 0,
|
||||
offline: false,
|
||||
subscription: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user