mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-08-26 03:12:30 +02:00
feat(web): engagement wizard, model/auth picker, Auth & Keys menu, attack-path graph
Full frontend rewrite following a deliberate visual direction (dense security-operations console — borders over shadows, two radii, one accent, no gradients/glassmorphism) and fixing real bugs found in review: - EventSource on the exploit stream never called es.close() on 'done', so the browser silently reconnected and re-streamed the whole job (duplicate log lines/findings). Fixed. - Sidebar 'running' step indicator and openRun() matched ANY running run instead of the one belonging to the current job (by runId). Fixed. New: - 5-step engagement wizard (Asset -> Scope & Auth -> Leads -> Model & Run -> Review) replacing the single flat board — inspired by the Discovery/Plan/Exploit/Remediate stage model both a.security and terra.security use publicly. - Model is now a real dropdown sourced from /api/providers (mirrors harness::models::providers()), with an API-key vs. subscription toggle that disables subscription for API-only providers. - One Auth & Keys menu: target auth header + named roles (IDOR/BOLA/BFLA multi-identity testing) materialize into an ephemeral creds.yaml passed via --creds; per-provider API keys live in server memory only (never on disk) and are merged into every spawned child's env. - Generative Attack Path Chaining: findings rendered as kill-chain columns (recon -> initial-access -> ... -> impact) with chains_from resolved to parent titles, live in the run view and static in run detail. - Findings are now a proper table (severity/title/endpoint/CWE/agent/ confidence) instead of stacked cards. - Explicit light/dark theme toggle persisted in localStorage, defaulting to light (previously light only won when the OS wasn't in dark mode). - All UI strings in English. Backend additions: GET /api/providers, GET/POST/DELETE /api/keys, ephemeral creds.yaml generation for auth/roles, env override merged into every exploit-job and REPL child spawn. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0129WdYHccPsH27k5GGuwijd
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d1d1c71e24
commit
bb659412fc
+113
-5
@@ -17,6 +17,7 @@
|
||||
const http = require('node:http');
|
||||
const fs = require('node:fs');
|
||||
const fsp = fs.promises;
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawn } = require('node:child_process');
|
||||
const crypto = require('node:crypto');
|
||||
@@ -46,6 +47,90 @@ const BIN = findBinary();
|
||||
|
||||
const PORT = Number(process.env.NEUROSPLOIT_WEB_PORT || process.env.PORT || 4173);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Providers — mirrors crates/harness/src/models.rs `providers()`. Kept as a
|
||||
// literal table (not parsed from CLI output) so `kind` ("cli" = usable via a
|
||||
// locally-installed agentic CLI subscription login, "api" = key-only) and
|
||||
// `envKey` (the environment variable the harness reads for that provider)
|
||||
// are available without shelling out. Keep this in sync when models.rs adds
|
||||
// a provider.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const PROVIDERS = [
|
||||
{ key: 'anthropic', label: 'Anthropic Claude', kind: 'cli', envKey: 'ANTHROPIC_API_KEY',
|
||||
models: ['claude-opus-5', 'claude-sonnet-5', 'claude-opus-4-8', 'claude-sonnet-4-6', 'claude-haiku-4-5'] },
|
||||
{ key: 'openai', label: 'OpenAI (ChatGPT)', kind: 'cli', envKey: 'OPENAI_API_KEY',
|
||||
models: ['gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna', 'gpt-5.5', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex', 'gpt-5.2', 'gpt-5.1', 'gpt-5.1-codex', 'o4'] },
|
||||
{ key: 'xai', label: 'xAI Grok', kind: 'cli', envKey: 'XAI_API_KEY',
|
||||
models: ['grok-4.5', 'grok-4', 'grok-4-fast'] },
|
||||
{ key: 'gemini', label: 'Google Gemini', kind: 'cli', envKey: 'GEMINI_API_KEY',
|
||||
models: ['gemini-3-pro', 'gemini-2.5-pro', 'gemini-2.5-flash'] },
|
||||
{ key: 'opencode', label: 'OpenCode Zen', kind: 'cli', envKey: 'OPENCODE_API_KEY',
|
||||
models: ['claude-opus-5', 'claude-sonnet-5', 'gpt-5.6-sol', 'gpt-5.5', 'gemini-3-pro', 'grok-4.5', 'deepseek-v4-pro', 'qwen3.7-max', 'kimi-k3'] },
|
||||
{ key: 'nous', label: 'Nous Research (Hermes)', kind: 'cli', envKey: 'NOUS_API_KEY',
|
||||
models: ['Hermes-4-405B', 'Hermes-4-70B', 'DeepHermes-3-Mistral-24B-Preview'] },
|
||||
{ key: 'nvidia_nim', label: 'NVIDIA NIM', kind: 'api', envKey: 'NVIDIA_NIM_API_KEY',
|
||||
models: ['nvidia/llama-3.3-nemotron-super-49b-v1', 'deepseek-ai/deepseek-r1', 'qwen/qwen2.5-coder-32b-instruct'] },
|
||||
{ key: 'deepseek', label: 'DeepSeek', kind: 'api', envKey: 'DEEPSEEK_API_KEY',
|
||||
models: ['deepseek-reasoner', 'deepseek-chat'] },
|
||||
{ key: 'mistral', label: 'Mistral', kind: 'api', envKey: 'MISTRAL_API_KEY',
|
||||
models: ['mistral-large-latest', 'codestral-latest'] },
|
||||
{ key: 'qwen', label: 'Qwen (DashScope)', kind: 'api', envKey: 'DASHSCOPE_API_KEY',
|
||||
models: ['qwen-max', 'qwen2.5-coder-32b-instruct', 'qwq-plus'] },
|
||||
{ key: 'groq', label: 'Groq', kind: 'api', envKey: 'GROQ_API_KEY',
|
||||
models: ['llama-3.3-70b-versatile', 'qwen-2.5-coder-32b'] },
|
||||
{ key: 'together', label: 'Together AI', kind: 'api', envKey: 'TOGETHER_API_KEY',
|
||||
models: ['Qwen/Qwen2.5-Coder-32B-Instruct', 'deepseek-ai/DeepSeek-R1', 'meta-llama/Llama-3.3-70B-Instruct-Turbo'] },
|
||||
{ key: 'moonshot', label: 'Moonshot AI (Kimi)', kind: 'api', envKey: 'MOONSHOT_API_KEY',
|
||||
models: ['kimi-k3', 'kimi-k2', 'moonshot-v1-128k', 'moonshot-v1-32k'] },
|
||||
{ key: 'litellm', label: 'LiteLLM (proxy)', kind: 'api', envKey: 'LITELLM_API_KEY',
|
||||
models: ['gpt-4o', 'claude-3-7-sonnet', 'gemini/gemini-2.5-pro'] },
|
||||
{ key: 'openrouter', label: 'OpenRouter', kind: 'api', envKey: 'OPENROUTER_API_KEY',
|
||||
models: ['anthropic/claude-opus-4-8', 'qwen/qwen-2.5-coder-32b-instruct', 'deepseek/deepseek-r1', 'meta-llama/llama-3.3-70b-instruct'] },
|
||||
{ key: 'azure', label: 'Azure OpenAI', kind: 'api', envKey: 'AZURE_OPENAI_API_KEY',
|
||||
models: ['gpt-4o', 'gpt-4o-mini', 'gpt-5.1', 'o4-mini'] },
|
||||
{ key: 'ollama', label: 'Ollama (local)', kind: 'api', envKey: 'OLLAMA_API_KEY',
|
||||
models: ['qwen2.5-coder:32b', 'qwq:32b', 'deepseek-r1:32b', 'llama3.3:70b'] },
|
||||
{ key: 'llamacpp', label: 'llama.cpp (local)', kind: 'api', envKey: 'LLAMACPP_API_KEY',
|
||||
models: ['qwen2.5-coder-32b-instruct', 'dolphin-2.9-llama3-70b', 'deepseek-r1-distill-qwen-32b', 'llama-3.3-70b-instruct'] },
|
||||
];
|
||||
|
||||
// In-memory only — never written to disk. Cleared on server restart.
|
||||
const apiKeys = new Map(); // provider key -> secret
|
||||
|
||||
function envOverrides() {
|
||||
const env = {};
|
||||
for (const [key, secret] of apiKeys) {
|
||||
const p = PROVIDERS.find((x) => x.key === key);
|
||||
if (p && secret) env[p.envKey] = secret;
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
/// Build a minimal creds.yaml-compatible file (see neurosploit-rs/creds.example.yaml)
|
||||
/// from a raw auth header and/or named roles, so the CLI's --creds flag can be
|
||||
/// used to carry web-entered auth material without a real file on disk.
|
||||
function buildCredsYaml({ auth, roles }) {
|
||||
const lines = ['# generated by the NeuroSploit web console — ephemeral, not committed'];
|
||||
if (auth) lines.push(`header: ${JSON.stringify(auth)}`);
|
||||
for (const r of roles || []) {
|
||||
if (!r?.name || !r?.header) continue;
|
||||
const safe = String(r.name).replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
lines.push(`${safe}:`, ` header: ${JSON.stringify(r.header)}`);
|
||||
}
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
async function materializeCreds(body, jobId) {
|
||||
if (body.creds) return body.creds; // explicit file path on disk wins
|
||||
if (!body.auth && !(body.roles || []).length) return undefined;
|
||||
const dir = path.join(os.tmpdir(), 'neurosploit-web');
|
||||
await fsp.mkdir(dir, { recursive: true });
|
||||
const file = path.join(dir, `${jobId}.creds.yaml`);
|
||||
await fsp.writeFile(file, buildCredsYaml(body));
|
||||
return file;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Agent library — read agents_md/{vulns,ai,infra,code,chains,recon,meta}/*.md
|
||||
// and classify each into a lead category the UI can group + toggle.
|
||||
@@ -338,14 +423,15 @@ function buildArgs(body) {
|
||||
return args;
|
||||
}
|
||||
|
||||
function startJob(body) {
|
||||
async function startJob(body) {
|
||||
if (!BIN) throw new Error('neurosploit binary not found — run `cargo build --release` in neurosploit-rs/');
|
||||
const id = crypto.randomUUID();
|
||||
const args = buildArgs(body);
|
||||
const credsPath = await materializeCreds(body, id);
|
||||
const args = buildArgs({ ...body, creds: credsPath });
|
||||
const job = new Job(id, BIN, args, body.repo || body.target || '');
|
||||
jobs.set(id, job);
|
||||
|
||||
const child = spawn(BIN, args, { cwd: ROOT, env: process.env });
|
||||
const child = spawn(BIN, args, { cwd: ROOT, env: { ...process.env, ...envOverrides() } });
|
||||
job.child = child;
|
||||
let buf = '';
|
||||
const onData = (chunk) => {
|
||||
@@ -399,7 +485,7 @@ class ReplSession extends EventEmitter {
|
||||
function startRepl() {
|
||||
if (!BIN) throw new Error('neurosploit binary not found — run `cargo build --release` in neurosploit-rs/');
|
||||
const id = crypto.randomUUID();
|
||||
const child = spawn(BIN, [], { cwd: ROOT, env: process.env });
|
||||
const child = spawn(BIN, [], { cwd: ROOT, env: { ...process.env, ...envOverrides() } });
|
||||
const session = new ReplSession(id, child);
|
||||
replSessions.set(id, session);
|
||||
const onData = (chunk) => session.push(stripAnsi(chunk.toString('utf8')));
|
||||
@@ -535,7 +621,7 @@ const server = http.createServer(async (req, res) => {
|
||||
}
|
||||
if (req.method === 'POST' && p === '/api/exploit') {
|
||||
const body = await readBody(req);
|
||||
const job = startJob(body);
|
||||
const job = await startJob(body);
|
||||
return sendJson(res, 200, { id: job.id });
|
||||
}
|
||||
m = p.match(/^\/api\/exploit\/([^/]+)$/);
|
||||
@@ -607,6 +693,28 @@ const server = http.createServer(async (req, res) => {
|
||||
return sendJson(res, 200, { version: '4.0.0', binary: BIN, root: ROOT });
|
||||
}
|
||||
|
||||
// ---- providers / API keys (in-memory only, never persisted) ----
|
||||
if (req.method === 'GET' && p === '/api/providers') {
|
||||
return sendJson(res, 200, PROVIDERS.map(({ key, label, kind, models }) => ({ key, label, kind, models })));
|
||||
}
|
||||
if (req.method === 'GET' && p === '/api/keys') {
|
||||
return sendJson(res, 200, PROVIDERS.map((pr) => ({ provider: pr.key, set: apiKeys.has(pr.key) && !!apiKeys.get(pr.key) })));
|
||||
}
|
||||
if (req.method === 'POST' && p === '/api/keys') {
|
||||
const body = await readBody(req);
|
||||
if (!body.provider || !PROVIDERS.some((pr) => pr.key === body.provider)) {
|
||||
return sendJson(res, 400, { error: 'unknown provider' });
|
||||
}
|
||||
if (body.key) apiKeys.set(body.provider, body.key);
|
||||
else apiKeys.delete(body.provider);
|
||||
return sendJson(res, 200, { ok: true });
|
||||
}
|
||||
m = p.match(/^\/api\/keys\/([^/]+)$/);
|
||||
if (req.method === 'DELETE' && m) {
|
||||
apiKeys.delete(decodeURIComponent(m[1]));
|
||||
return sendJson(res, 200, { ok: true });
|
||||
}
|
||||
|
||||
sendJson(res, 404, { error: 'not found' });
|
||||
} catch (err) {
|
||||
sendJson(res, 500, { error: err.message });
|
||||
|
||||
Reference in New Issue
Block a user