From bb659412fc3e370f9f259b589ea81ca475f50dcd Mon Sep 17 00:00:00 2001 From: CyberSecurityUP Date: Sun, 23 Aug 2026 14:16:46 -0300 Subject: [PATCH] feat(web): engagement wizard, model/auth picker, Auth & Keys menu, attack-path graph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_0129WdYHccPsH27k5GGuwijd --- web/API.md | 46 +++- web/README.md | 20 +- web/public/app.js | 555 +++++++++++++++++++++++++++++++----------- web/public/index.html | 383 +++++++++++++++++++---------- web/public/style.css | 523 +++++++++++++++++++++++++-------------- web/server.js | 118 ++++++++- 6 files changed, 1172 insertions(+), 473 deletions(-) diff --git a/web/API.md b/web/API.md index d008bf9..6728c96 100644 --- a/web/API.md +++ b/web/API.md @@ -52,6 +52,40 @@ An agent's `id`/`name` is exactly what the CLI's `--only ` flag expects (s --- +## Providers / models / API keys + +### `GET /api/providers` + +Static mirror of `crates/harness/src/models.rs` `providers()` — every provider the harness +supports, its models, and whether it's usable via a local CLI subscription login (`kind: "cli"`) +or API key only (`kind: "api"`). + +```json +[ { "key": "anthropic", "label": "Anthropic Claude", "kind": "cli", "models": ["claude-opus-5", "..."] } ] +``` + +### `GET /api/keys` + +Which providers currently have an API key set **in this server process's memory** (booleans only +— never the value): + +```json +[ { "provider": "anthropic", "set": true }, { "provider": "openai", "set": false } ] +``` + +### `POST /api/keys` + +Body `{ "provider": "anthropic", "key": "sk-..." }`. Stores the key in an in-memory `Map` — +**never written to disk**, lost on server restart. Every subsequent `/api/exploit` and `/api/repl` +child process is spawned with `.envKey` set from this store (merged over `process.env`). +Omitting `key` (or passing an empty string) clears it. 400 on an unknown provider. + +### `DELETE /api/keys/:provider` + +Clears one provider's key. + +--- + ## Runs (history) ### `GET /api/runs` @@ -106,10 +140,20 @@ Body: "focus": "injection and business logic", // --focus "objective": "pre-launch review of checkout", // --objective "outOfScope": "staging.example.com", // --out-of-scope - "agents": ["sqli_error", "idor"] // --only , repeated — the lead-board selection + "agents": ["sqli_error", "idor"], // --only , repeated — the lead-board selection + "auth": "Authorization: Bearer ", // target auth header — see Target auth below + "roles": [{ "name": "admin", "header": "Authorization: Bearer ..." }], // multi-identity access-control testing } ``` +### Target auth (`auth` / `roles`) + +If `creds` is omitted and either `auth` or `roles` is set, the server writes a minimal +`creds.yaml`-compatible file (matching `neurosploit-rs/creds.example.yaml`'s schema) to +`os.tmpdir()/neurosploit-web/.creds.yaml` and passes it via `--creds`. An explicit `creds` +path always wins over `auth`/`roles`. These ephemeral files are not cleaned up automatically — +they live in the OS temp dir, never in the repo. + Response: `{ "id": "" }`. This `id` is the **web job id**, not the run id — the CLI's own `ns--` run id is discovered from its own log line and exposed as `runId` in the job snapshot once the engagement starts writing to `runs/`. diff --git a/web/README.md b/web/README.md index 0c2df8d..69daa31 100644 --- a/web/README.md +++ b/web/README.md @@ -1,8 +1,22 @@ # NeuroSploit v4.0.0 — web console -A browser UI for the `neurosploit` CLI harness: a lead board (categorized agent picker + custom -leads → `Start Exploitation`), a live structured findings view, run history, and a real REPL — -all driven by spawning the actual CLI binary, never a reimplementation of harness logic. +A browser UI for the `neurosploit` CLI harness: a 5-step engagement wizard (Asset → Scope & Auth +→ Leads → Model & Run → Review), a live structured findings view with a generative attack-path +graph, run history, an Auth & Keys menu, and a real REPL — all driven by spawning the actual CLI +binary, never a reimplementation of harness logic. + +- **Asset** — pick black/white/grey-box, host/infra, or AI/LLM, set the target or repo. +- **Scope & Auth** — objective, focus, out-of-scope, and a link into the Auth & Keys menu. +- **Leads** — the categorized agent picker (435 agents auto-classified) + custom leads. +- **Model & Run** — pick a provider/model from the live catalog, API-key vs. subscription auth + mode, votes/chain-depth/recon intensity. +- **Review** — confirm the plan, then `Start Exploitation` spawns the real CLI. +- **Auth & Keys** (one menu, 🔑 in the sidebar) — target auth header + named roles for + IDOR/BOLA/BFLA testing, per-provider API keys (kept in server memory only, never on disk), and + an explicit `creds.yaml` path override. +- **Generative Attack Path Chaining** — findings are grouped into kill-chain columns + (recon → initial-access → execution → privesc → lateral → exfil → impact) with chained findings + linked back to their parent, built live as findings stream in. ```bash cd neurosploit-rs && cargo build --release # build the CLI once diff --git a/web/public/app.js b/web/public/app.js index c2a44eb..ba9cf86 100644 --- a/web/public/app.js +++ b/web/public/app.js @@ -1,20 +1,36 @@ 'use strict'; /* NeuroSploit v4.0.0 — web console frontend. Vanilla JS, no build step. */ -const $ = (sel) => document.querySelector(sel); -const $$ = (sel) => Array.from(document.querySelectorAll(sel)); +const $ = (sel, root = document) => root.querySelector(sel); +const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel)); + +const MODE_LABELS = { + run: { target: 'Target URL', help: 'The application to test.', showRepo: false, placeholder: 'https://target.example.com' }, + whitebox: { target: 'Source repo / path', help: "A GitHub URL, owner/repo shorthand, or a local path — cloned automatically if it's remote.", showRepo: false, placeholder: 'owner/repo' }, + greybox: { target: 'Target URL', help: 'The running application to exploit, alongside the source repo below.', showRepo: true, placeholder: 'https://target.example.com' }, + host: { target: 'Target host / IP', help: 'Runs Linux / Windows / Active Directory agents.', showRepo: false, placeholder: '10.0.0.10' }, + aitest: { target: 'AI endpoint URL', help: 'A live AI agent, LLM chat, or MCP endpoint (OWASP LLM Top 10).', showRepo: false, placeholder: 'https://target.example.com/chat' }, +}; + +const STEP_COUNT = 5; const state = { - categories: [], // from /api/agents - selected: new Set(), // agent ids toggled on - customLeads: [], // free-text custom leads (folded into --focus) - filter: 'all', // all | selected | excluded + theme: localStorage.getItem('ns-theme') || 'light', + step: 0, + mode: 'run', + categories: [], + selected: new Set(), + customLeads: [], + filter: 'all', search: '', - runs: [], // from /api/runs - currentJob: null, // {id, es} for the live view - currentDetailId: null, // run id shown in detail view + providers: [], + auth: { header: '', roles: [] }, + credsPath: '', + keys: [], + runs: [], + currentJob: null, + currentDetailId: null, detailPoll: null, - askFocus: '', askObjective: '', askOutOfScope: '', replId: null, replEs: null, }; @@ -27,21 +43,96 @@ function esc(s) { } async function api(path, opts) { const res = await fetch(path, opts); - if (!res.ok) throw new Error(`${path} → ${res.status}`); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error(body.error || `${path} → ${res.status}`); + } return res.headers.get('content-type')?.includes('json') ? res.json() : res.text(); } -function sevClass(sev) { +function sevRank(sev) { const s = (sev || '').toLowerCase(); - if (s.includes('crit')) return 'sev-critical'; - if (s.includes('high')) return 'sev-high'; - if (s.includes('med')) return 'sev-medium'; - if (s.includes('low')) return 'sev-low'; - return 'sev-info'; + if (s.includes('crit')) return 0; + if (s.includes('high')) return 1; + if (s.includes('med')) return 2; + if (s.includes('low')) return 3; + return 4; } -function show(el, on) { el.hidden = !on; } +function sevClass(sev) { + return ['sev-critical', 'sev-high', 'sev-medium', 'sev-low', 'sev-info'][sevRank(sev)]; +} +function show(el, on) { if (el) el.hidden = !on; } +function toast(msg) { console.log('[ns]', msg); } // --------------------------------------------------------------------------- -// agents / lead board +// theme +// --------------------------------------------------------------------------- + +function applyTheme() { + document.documentElement.setAttribute('data-theme', state.theme); + $('#btnThemeToggle').textContent = state.theme === 'dark' ? '☀' : '☾'; + $('#btnThemeToggle').title = state.theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'; +} +$('#btnThemeToggle').addEventListener('click', () => { + state.theme = state.theme === 'dark' ? 'light' : 'dark'; + localStorage.setItem('ns-theme', state.theme); + applyTheme(); +}); + +// --------------------------------------------------------------------------- +// wizard — step navigation +// --------------------------------------------------------------------------- + +function goToStep(n) { + state.step = Math.max(0, Math.min(STEP_COUNT - 1, n)); + $$('.step-tab').forEach((tab, i) => { + tab.classList.toggle('active', i === state.step); + tab.classList.toggle('done', i < state.step); + }); + $$('.wizard-panel').forEach((panel) => show(panel, Number(panel.dataset.panel) === state.step)); + show($('#btnStepBack'), state.step > 0); + show($('#btnStepNext'), state.step < STEP_COUNT - 1); + show($('#btnLaunch'), state.step === STEP_COUNT - 1); + if (state.step === STEP_COUNT - 1) renderReview(); + updateWizardSummary(); +} + +function validateStep(n) { + if (n === 0) { + const target = $('#fieldTarget').value.trim(); + if (!target) { alert(`${MODE_LABELS[state.mode].target} is required.`); return false; } + if (state.mode === 'greybox' && !$('#fieldRepo').value.trim()) { alert('Source repo is required for grey-box.'); return false; } + } + return true; +} + +$('#btnStepNext').addEventListener('click', () => { if (validateStep(state.step)) goToStep(state.step + 1); }); +$('#btnStepBack').addEventListener('click', () => goToStep(state.step - 1)); +$$('.step-tab').forEach((tab) => tab.addEventListener('click', () => { + const n = Number(tab.dataset.step); + if (n <= state.step || validateStep(state.step)) goToStep(n); +})); + +function updateWizardSummary() { + const target = $('#fieldTarget').value.trim() || '(not set)'; + $('#wizardSummary').innerHTML = `Step ${state.step + 1} of ${STEP_COUNT} · ${esc(state.mode)} · ${esc(target)}`; +} + +// mode tiles +function selectMode(mode) { + state.mode = mode; + $$('.mode-tile').forEach((t) => t.classList.toggle('selected', t.dataset.mode === mode)); + const cfg = MODE_LABELS[mode]; + $('#targetLabel').textContent = cfg.target; + $('#targetHelp').textContent = cfg.help; + $('#fieldTarget').placeholder = cfg.placeholder; + show($('#fieldRepoGroup'), cfg.showRepo); + updateWizardSummary(); +} +$$('.mode-tile').forEach((tile) => tile.addEventListener('click', () => selectMode(tile.dataset.mode))); +$('#fieldTarget').addEventListener('input', updateWizardSummary); + +// --------------------------------------------------------------------------- +// agents / lead board (step 3) // --------------------------------------------------------------------------- async function loadAgents() { @@ -57,7 +148,6 @@ function renderBoard() { const selCount = group.agents.filter((a) => state.selected.has(a.id)).length; const card = document.createElement('div'); card.className = 'cat-card'; - card.dataset.category = group.category; card.innerHTML = `