diff --git a/README.md b/README.md index 4bdf6c5..8f4614e 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

🧠 NeuroSploit v3.6.9

+

🧠 NeuroSploit v4.0.0

JoasASantos%2FNeuroSploit | Trendshift @@ -12,7 +12,7 @@

- + @@ -214,6 +214,21 @@ No login? Use an **API key** instead β€” see [Authentication](#authentication--r --- +## πŸ–₯️ Web console (NEW in v4.0.0) + +A browser UI for the same harness: a categorized lead board (toggle agents by category, add +custom leads, `Start Exploitation`), a live structured findings view, run history, and a real +REPL β€” all driven by spawning the compiled CLI, never a reimplementation of it. + +```bash +cd neurosploit-rs && cargo build --release # once +node web/server.js # β†’ http://localhost:4173 +``` + +Zero npm dependencies. Full API reference: **[web/API.md](web/API.md)**. + +--- + ## πŸ”Œ Integrations (GitHub Β· GitLab Β· Jira) Wire NeuroSploit into your SDLC. Toggle from the REPL (`/integrations`) or the CLI diff --git a/TUTORIAL.md b/TUTORIAL.md index 37dad79..7a33356 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -1,4 +1,4 @@ -# NeuroSploit β€” Tutorial & User Guide (v3.6.9) +# NeuroSploit β€” Tutorial & User Guide (v4.0.0) A complete, hands-on guide to installing, configuring and running NeuroSploit β€” the autonomous, multi-model penetration-testing harness. @@ -98,7 +98,7 @@ Agents **degrade gracefully**: if `rustscan` is absent they use `nmap`; if neith ### Verify ```bash -neurosploit --version # neurosploit 3.6.9 +neurosploit --version # neurosploit 4.0.0 neurosploit agents # {"vulns":241,...,"ai":30,...,"total":430} neurosploit models # all providers & models ``` diff --git a/neurosploit-rs/Cargo.lock b/neurosploit-rs/Cargo.lock index 0a25272..cd42bf8 100644 --- a/neurosploit-rs/Cargo.lock +++ b/neurosploit-rs/Cargo.lock @@ -871,7 +871,7 @@ dependencies = [ [[package]] name = "neurosploit" -version = "3.6.9" +version = "4.0.0" dependencies = [ "anyhow", "clap", @@ -888,7 +888,7 @@ dependencies = [ [[package]] name = "neurosploit-harness" -version = "3.6.9" +version = "4.0.0" dependencies = [ "anyhow", "futures", diff --git a/neurosploit-rs/Cargo.toml b/neurosploit-rs/Cargo.toml index 0005c05..103c30f 100644 --- a/neurosploit-rs/Cargo.toml +++ b/neurosploit-rs/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/harness", "app"] resolver = "2" [workspace.package] -version = "3.6.9" +version = "4.0.0" edition = "2021" license = "MIT" repository = "https://github.com/JoasASantos/NeuroSploit" diff --git a/neurosploit-rs/app/src/main.rs b/neurosploit-rs/app/src/main.rs index fe3a493..5c236b6 100644 --- a/neurosploit-rs/app/src/main.rs +++ b/neurosploit-rs/app/src/main.rs @@ -1,4 +1,4 @@ -//! NeuroSploit v3.6.9 β€” interactive harness + CLI (`run` / `whitebox` / `agents` / `models`). +//! NeuroSploit v4.0.0 β€” interactive harness + CLI (`run` / `whitebox` / `agents` / `models`). mod repl; mod tui; @@ -11,8 +11,8 @@ use std::path::{Path, PathBuf}; #[command( name = "neurosploit", version, - about = "NeuroSploit v3.6.9 β€” multi-model autonomous pentest harness", - long_about = "NeuroSploit v3.6.9 β€” a Rust multi-model harness that drives a pool of LLMs \ + about = "NeuroSploit v4.0.0 β€” multi-model autonomous pentest harness", + long_about = "NeuroSploit v4.0.0 β€” a Rust multi-model harness that drives a pool of LLMs \ (API key or local subscription: Claude/Codex/Gemini/Grok/OpenCode/Hermes) to autonomously test a target. \ After recon it INTELLIGENTLY selects only the agents matching the discovered surface, runs \ them in parallel, then validates every finding by cross-model voting before reporting.\n\n\ @@ -765,7 +765,7 @@ pub(crate) fn spawn_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, mode: println!(" β”‚ ua : {ua}"); write_status(&workdir, "running", &format!("\"target\":{:?}", cfg.target)); - println!(" β”Œβ”€ NeuroSploit v3.6.9 Β· by Joas A Santos & Red Team Leaders"); + println!(" β”Œβ”€ NeuroSploit v4.0.0 Β· by Joas A Santos & Red Team Leaders"); println!(" β”‚ run id : {run_id}"); println!(" β”‚ target : {}", cfg.target); println!(" β”‚ models : {}", cfg.models.join(", ")); diff --git a/neurosploit-rs/app/src/repl.rs b/neurosploit-rs/app/src/repl.rs index 1b1272e..7826b6f 100644 --- a/neurosploit-rs/app/src/repl.rs +++ b/neurosploit-rs/app/src/repl.rs @@ -1,4 +1,4 @@ -//! NeuroSploit v3.6.9 β€” interactive session (Claude-Code / Codex / Cursor-CLI style). +//! NeuroSploit v4.0.0 β€” interactive session (Claude-Code / Codex / Cursor-CLI style). //! //! Launched when `neurosploit` runs with no subcommand. A persistent REPL with //! real line editing (arrow-key history recall, Ctrl-A/E/K, paste), model @@ -371,7 +371,7 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> { let backends = harness::installed_cli_backends(); println!("\x1b[1m"); println!(" β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•— β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—"); - println!(" β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•— NeuroSploit v3.6.9"); + println!(" β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•— NeuroSploit v4.0.0"); println!(" β–ˆβ–ˆβ•”β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ interactive harness"); println!(" β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β• β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ by Joas A Santos"); println!(" β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β• & Red Team Leaders"); diff --git a/neurosploit-rs/app/src/tui.rs b/neurosploit-rs/app/src/tui.rs index 68fe71d..167e28c 100644 --- a/neurosploit-rs/app/src/tui.rs +++ b/neurosploit-rs/app/src/tui.rs @@ -1,4 +1,4 @@ -//! NeuroSploit v3.6.9 β€” TUI "Mission Control" mode. +//! NeuroSploit v4.0.0 β€” TUI "Mission Control" mode. //! //! Concurrent panels that update live while the engagement runs in the //! background, with a composer input that stays active during execution: diff --git a/web/API.md b/web/API.md new file mode 100644 index 0000000..d008bf9 --- /dev/null +++ b/web/API.md @@ -0,0 +1,231 @@ +# NeuroSploit Web Console β€” API reference + +Backend: `web/server.js` (Node, zero external dependencies). It does three things: + +1. Serves the SPA in `web/public/`. +2. Reads `agents_md/` and `runs/` from the repo root to build the lead board and run history. +3. Shells out to the compiled `neurosploit` CLI binary (`neurosploit-rs/target/release/neurosploit`) + for every exploitation run and for the REPL β€” the web UI never reimplements harness logic, + it only drives the real CLI and parses its stdout. + +Base URL: `http://localhost:4173` (override with `PORT` or `NEUROSPLOIT_WEB_PORT`). + +All responses are JSON unless noted. All endpoints are same-origin; there is no auth layer β€” +run this only on a trusted machine/network, same trust model as the CLI itself. + +--- + +## Meta + +### `GET /api/meta` + +Server/version info. + +```json +{ "version": "4.0.0", "binary": "/opt/neurosploit-rs/neurosploit-rs/target/release/neurosploit", "root": "/opt/neurosploit-rs" } +``` + +--- + +## Agents / lead board + +### `GET /api/agents` + +Reads every `agents_md/{vulns,ai,infra,code,chains,recon,meta}/*.md`, extracts `name` (filename +stem), `title` (first `# heading`), `cwe` (first `CWE-\d+` match), `kind` (source directory), and +classifies each into a UI category (`category`) via a keyword taxonomy (Business Logic, Broken +Access Control, Injection, Cross-Site Scripting, LLM Application, Auth & Session, SSRF & Network, +API & GraphQL, Cloud & Infra, Client-Side, Cryptography, Rate Limiting & DoS, Cache & CDN, +Recon & Fingerprint, Linux Host, Windows Host, Attack Chains, Code Review, Recon, Other). +`meta/` (orchestration/doctrine agents) is loaded but excluded from `categories` β€” those aren't +selectable "leads". Cached in-memory for 5s. + +```json +{ + "total": 435, + "agents": [ { "id": "sqli_error", "name": "sqli_error", "title": "SQL Injection (Error-Based) Specialist Agent", "cwe": "CWE-89", "kind": "vuln", "category": "Injection" } ], + "categories": [ { "category": "Business Logic", "agents": [ /* Agent[] */ ] } ] +} +``` + +An agent's `id`/`name` is exactly what the CLI's `--only ` flag expects (see `neurosploit agents`). + +--- + +## Runs (history) + +### `GET /api/runs` + +Lists `runs/ns-*` directories, newest first, with a summary read from each run's +`meta.json` / `status.json` / `findings.json`. + +```json +[ { "id": "ns-1787504238-testphp_vulnweb_com", "ts": 1787504238, "target": "http://testphp.vulnweb.com/", "state": "running", "findings": 3, "severities": { "High": 1, "Medium": 2 }, "hasReport": false } ] +``` + +`state` mirrors the CLI's `status.json`: `running` | `complete` | `stopped-raw` | `discarded` | `unknown`. + +### `GET /api/runs/:id` + +Full detail for one run: `{ id, meta, status, findings, assets }`. `findings` is the raw +`findings.json` array (see [Finding shape](#finding-shape) below). `assets` lists which generated +files exist (`report.html`, `report.pdf`, `report.md`, `recon.md`, `exploitation.md`). + +### `GET /api/runs/:id/asset/:path` + +Serves a file from that run's workdir (e.g. `report.html`, `report.pdf`, `evidence/foo.png`). +Path-traversal-guarded (resolved path must stay under the run dir). Use this to embed/open the +generated report from the browser. + +--- + +## Exploitation jobs (live runs) + +Starting a job spawns `neurosploit [flags...] --verbose` as a child process and +parses its stdout/stderr line-by-line into structured events β€” the same signal the interactive +REPL's status line uses (phase, agent counts, findings, report path). + +### `POST /api/exploit` + +Body: + +```jsonc +{ + "mode": "run", // run | whitebox | greybox | host | aitest | skills + "target": "https://example.com", // required for run/host/aitest/greybox + "repo": "owner/repo", // required for whitebox; source repo for greybox + "models": ["anthropic:claude-opus-4-8"], // optional, repeatable in the CLI + "votes": 3, // --vote-n + "chainDepth": 2, // --chain-depth + "recon": 3, // --recon (1-4) + "maxAgents": 0, // --max-agents (0 = all) + "subscription": false, // --subscription + "offline": false, // --offline + "mcp": false, // --mcp + "creds": "creds.yaml", // --creds + "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 +} +``` + +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/`. + +If `agents` is empty, no `--only` flag is passed and the harness falls back to its normal +recon-driven agent selection (the intelligent default) β€” the lead board's "0 selected" state is a +valid, meaningful choice, not an error. + +### `GET /api/exploit` + +List all jobs known to this server process (in-memory; lost on restart) as snapshots (see below). + +### `GET /api/exploit/:id` + +One job's current snapshot: + +```json +{ + "id": "d98f51ad-...", "target": "http://testphp.vulnweb.com/", + "runId": "ns-1787504238-testphp_vulnweb_com", + "phase": "exploiting", "findings": [ /* Finding[] */ ], + "agents": 245, "agentsDone": 12, "done": false, "exitCode": null, + "reportUrl": null, "startedAt": 1787504238594 +} +``` + +`phase` tracks the same lifecycle the REPL's `/status` shows: `starting β†’ recon β†’ planning β†’ +exploiting β†’ validating β†’ chaining β†’ complete`, or `paused (quota)` / `paused (auth)` if the +harness parks the run (token/quota exhaustion or auth failure β€” findings are preserved either way). + +### `POST /api/exploit/:id/stop` + +Sends `SIGINT` to the child process β€” identical to pressing Ctrl-C in the terminal. The harness's +own graceful-stop logic decides whether to keep partial findings. + +### `GET /api/exploit/:id/events` (Server-Sent Events) + +Live stream. On connect, replays every buffered event so a reconnecting client doesn't miss +history, then streams new ones. Named SSE events: + +| event | data | meaning | +|------------|---------------------------------------|---------| +| `log` | `{ "type": "log", "line": "..." }` | one stdout/stderr line (ANSI stripped) | +| `finding` | `{ "type": "finding", "finding": {…} }` | a `finding_json:` line, parsed | +| `snapshot` | job snapshot (see above) | phase/progress update | +| `done` | job snapshot with `done: true` | process exited; stream closes | + +Client example: + +```js +const es = new EventSource(`/api/exploit/${id}/events`); +es.addEventListener('finding', (e) => console.log(JSON.parse(e.data).finding)); +es.addEventListener('done', () => es.close()); +``` + +--- + +## REPL sessions + +Spawns the CLI with **no subcommand** β€” the same interactive session `neurosploit` launches from +a terminal β€” and pipes stdin/stdout. Because stdin isn't a TTY, the CLI's `Reader::Plain` path +takes over: it prints each prompt to stdout then reads one line at a time from stdin, so it works +perfectly over a plain pipe. This is a real harness process; every `/command` (`/run`, `/status`, +`/stop`, `/model`, `/target`, natural-language input, etc.) behaves exactly as it would in a +terminal. + +### `POST /api/repl` + +Starts a session. Response: `{ "id": "" }`. + +### `POST /api/repl/:id/input` + +Body: `{ "line": "/status" }`. Writes `line + "\n"` to the child's stdin. + +### `POST /api/repl/:id/stop` + +Sends `SIGTERM` to the session's child process. + +### `GET /api/repl/:id/events` (SSE) + +| event | data | meaning | +|---------|-------------------------|---------| +| `data` | `{ "chunk": "..." }` | raw stdout/stderr chunk (ANSI stripped), not line-buffered | +| `close` | `{}` | child process exited | + +Replays the session's buffered output (capped at the last 5000 chunks) on connect, same as the +exploit stream. + +--- + +## Finding shape + +Findings are exactly the harness's `harness::types::Finding` struct (see +`neurosploit-rs/crates/harness/src/types.rs`), serialized as JSON β€” the web UI does not transform +or rename any field: + +```ts +{ + id: string, agent: string, title: string, severity: string, cwe: string, cvss: string, + endpoint: string, payload: string, evidence: string, impact: string, remediation: string, + confidence: number, validated: boolean, votes: string, + owasp: string, mitre: string, stage: string, exploitability: string, business_impact: string, + chains_from: string[], auth_context: string, account: string, secret: string, + review_status: string, review_reason: string, screenshots: string[], +} +``` + +--- + +## Running it + +```bash +cd neurosploit-rs && cargo build --release # once, or after a harness change +node web/server.js # http://localhost:4173 +``` + +`PORT` (or `NEUROSPLOIT_WEB_PORT`) overrides the port. The server auto-locates the compiled binary +under `neurosploit-rs/target/{release,debug}/neurosploit` relative to the repo root; if neither +exists, `/api/exploit` and `/api/repl` return a 500 with a build hint. diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..0c2df8d --- /dev/null +++ b/web/README.md @@ -0,0 +1,27 @@ +# 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. + +```bash +cd neurosploit-rs && cargo build --release # build the CLI once +node web/server.js # β†’ http://localhost:4173 +``` + +Zero npm dependencies (Node β‰₯18, built-ins only: `http`, `child_process`, `events`, `fs`). + +API reference: [`API.md`](./API.md). + +## Layout + +``` +web/ +β”œβ”€β”€ server.js backend: static server + agents_md/runs reader + CLI process manager +β”œβ”€β”€ public/ +β”‚ β”œβ”€β”€ index.html SPA shell +β”‚ β”œβ”€β”€ style.css lead-board / live-run / REPL drawer styling +β”‚ └── app.js client logic (fetch + EventSource, no framework) +β”œβ”€β”€ API.md +└── package.json +``` diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..c42dd73 --- /dev/null +++ b/web/package.json @@ -0,0 +1,13 @@ +{ + "name": "neurosploit-web", + "version": "4.0.0", + "private": true, + "description": "NeuroSploit v4.0.0 web console β€” lead board + REPL, backed by the neurosploit CLI harness.", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "engines": { + "node": ">=18" + } +} diff --git a/web/public/app.js b/web/public/app.js new file mode 100644 index 0000000..c2a44eb --- /dev/null +++ b/web/public/app.js @@ -0,0 +1,441 @@ +'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 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 + search: '', + runs: [], // from /api/runs + currentJob: null, // {id, es} for the live view + currentDetailId: null, // run id shown in detail view + detailPoll: null, + askFocus: '', askObjective: '', askOutOfScope: '', + replId: null, replEs: null, +}; + +// --------------------------------------------------------------------------- +// helpers +// --------------------------------------------------------------------------- + +function esc(s) { + return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); +} +async function api(path, opts) { + const res = await fetch(path, opts); + if (!res.ok) throw new Error(`${path} β†’ ${res.status}`); + return res.headers.get('content-type')?.includes('json') ? res.json() : res.text(); +} +function sevClass(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'; +} +function show(el, on) { el.hidden = !on; } + +// --------------------------------------------------------------------------- +// agents / lead board +// --------------------------------------------------------------------------- + +async function loadAgents() { + const data = await api('/api/agents'); + state.categories = data.categories; + renderBoard(); +} + +function renderBoard() { + const root = $('#categories'); + root.innerHTML = ''; + for (const group of state.categories) { + 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 = ` +

+ + ${esc(group.category)} + ${selCount} / ${group.agents.length} + β–Ύ +
+
+ `; + const rows = card.querySelector('.agent-rows'); + for (const a of group.agents) { + const row = document.createElement('div'); + row.className = 'agent-row'; + row.dataset.id = a.id; + row.dataset.title = (a.title + ' ' + a.name).toLowerCase(); + row.innerHTML = ` + + ${esc(a.title)} + ${a.cwe ? `${esc(a.cwe)}` : ''} + `; + rows.appendChild(row); + } + card.querySelector('.cat-head').addEventListener('click', (e) => { + if (e.target.closest('.switch')) return; + card.classList.toggle('collapsed'); + }); + card.querySelector('.cat-toggle').addEventListener('change', (e) => { + const on = e.target.checked; + for (const a of group.agents) { + if (on) state.selected.add(a.id); else state.selected.delete(a.id); + } + renderBoard(); + updateChips(); + }); + rows.querySelectorAll('.agent-toggle').forEach((input) => { + input.addEventListener('change', (e) => { + const id = e.target.dataset.id; + if (e.target.checked) state.selected.add(id); else state.selected.delete(id); + renderBoard(); + updateChips(); + applyFilters(); + }); + }); + root.appendChild(card); + } + updateChips(); + applyFilters(); +} + +function allAgents() { + return state.categories.flatMap((g) => g.agents); +} + +function updateChips() { + const total = allAgents().length; + $('#chipAll').textContent = total; + $('#chipSelected').textContent = state.selected.size; + $('#chipExcluded').textContent = total - state.selected.size; +} + +function applyFilters() { + const q = state.search.trim().toLowerCase(); + $$('.agent-row').forEach((row) => { + const id = row.dataset.id; + const isSel = state.selected.has(id); + let visible = true; + if (state.filter === 'selected') visible = isSel; + if (state.filter === 'excluded') visible = !isSel; + if (visible && q) visible = row.dataset.title.includes(q); + row.classList.toggle('hidden-by-search', !visible); + }); + $$('.cat-card').forEach((card) => { + const anyVisible = [...card.querySelectorAll('.agent-row')].some((r) => !r.classList.contains('hidden-by-search')); + card.style.display = anyVisible ? '' : 'none'; + }); +} + +// --------------------------------------------------------------------------- +// engagement bar / ask panel +// --------------------------------------------------------------------------- + +function currentMode() { return $('#fieldMode').value; } + +$('#fieldMode').addEventListener('change', () => { + show($('.eb-repo'), currentMode() === 'greybox'); + $('.eb-target label').textContent = currentMode() === 'whitebox' ? 'Repo / path' : 'Target'; +}); + +$('#btnAskSend').addEventListener('click', () => { + const kind = $('#askKind').value; + const text = $('#askInput').value.trim(); + if (!text) return; + if (kind === 'focus') state.askFocus = text; + if (kind === 'objective') state.askObjective = text; + if (kind === 'scope-out') state.askOutOfScope = text; + $('#askHint').textContent = `βœ“ ${kind} definido β€” aplicado no prΓ³ximo "Start Exploitation"`; + $('#askInput').value = ''; +}); + +$('#btnCustomLead').addEventListener('click', () => { + const text = prompt('Descreva a lead customizada (linguagem livre β€” vira contexto de foco para os agentes):'); + if (text && text.trim()) { + state.customLeads.push(text.trim()); + $('#askHint').textContent = `βœ“ custom lead adicionada (${state.customLeads.length} total)`; + } +}); + +$$('.chip').forEach((chip) => { + chip.addEventListener('click', () => { + $$('.chip').forEach((c) => c.classList.remove('chip-active')); + chip.classList.add('chip-active'); + state.filter = chip.dataset.filter; + applyFilters(); + }); +}); + +$('#leadSearch').addEventListener('input', (e) => { state.search = e.target.value; applyFilters(); }); + +// --------------------------------------------------------------------------- +// start exploitation β†’ live run view +// --------------------------------------------------------------------------- + +$('#btnStartExploitation').addEventListener('click', startExploitation); + +async function startExploitation() { + const mode = currentMode(); + const target = $('#fieldTarget').value.trim(); + const repo = $('#fieldRepo').value.trim(); + if (mode !== 'whitebox' && !target) { alert('Defina o target.'); return; } + if (mode === 'whitebox' && !target && !repo) { alert('Defina o repo/path.'); return; } + if (mode === 'greybox' && !repo) { alert('Grey-box precisa de repo + target.'); return; } + + const modelField = $('#fieldModel').value.trim(); + const focusParts = [state.askFocus, ...state.customLeads].filter(Boolean); + + const body = { + mode, + target: mode === 'whitebox' ? undefined : target, + repo: mode === 'whitebox' ? (target || repo) : (repo || undefined), + models: modelField ? [modelField] : [], + votes: Number($('#fieldVotes').value) || 3, + chainDepth: Number($('#fieldChain').value), + recon: Number($('#fieldRecon').value), + subscription: $('#fieldSubscription').checked, + mcp: $('#fieldMcp').checked, + agents: [...state.selected], + focus: focusParts.join('; ') || undefined, + objective: state.askObjective || undefined, + outOfScope: state.askOutOfScope || undefined, + }; + + $('#btnStartExploitation').disabled = true; + try { + const { id } = await api('/api/exploit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); + attachLiveJob(id, body.target || body.repo); + } catch (e) { + alert('Falha ao iniciar: ' + e.message); + } finally { + $('#btnStartExploitation').disabled = false; + } +} + +function attachLiveJob(id, target) { + if (state.currentJob) state.currentJob.es.close(); + state.currentJob = { id, es: null, findings: [], target, phase: 'starting', agents: 0, agentsDone: 0, reportUrl: null }; + + show($('#boardView'), false); + show($('#detailView'), false); + show($('#liveView'), true); + $('#liveTarget').textContent = target || 'β€”'; + $('#livePhase').textContent = 'starting'; + $('#findingsList').innerHTML = ''; + $('#logList').innerHTML = ''; + $('#findingsCount').textContent = '0'; + $('#progressFill').style.width = '0%'; + $('#progressLabel').textContent = '0 / 0 agents'; + show($('#btnOpenReport'), false); + + const es = new EventSource(`/api/exploit/${id}/events`); + state.currentJob.es = es; + es.addEventListener('log', (e) => appendLog(JSON.parse(e.data).line)); + es.addEventListener('finding', (e) => addFinding(JSON.parse(e.data).finding)); + es.addEventListener('snapshot', (e) => applySnapshot(JSON.parse(e.data))); + es.addEventListener('done', (e) => { applySnapshot(JSON.parse(e.data)); refreshRuns(); }); + es.onerror = () => { /* browser auto-retries; fine for a long-running engagement */ }; +} + +function appendLog(line) { + const div = document.createElement('div'); + div.className = 'log-line'; + div.textContent = line; + const list = $('#logList'); + list.appendChild(div); + list.scrollTop = list.scrollHeight; +} + +function addFinding(f) { + state.currentJob.findings.push(f); + const card = document.createElement('div'); + card.className = 'finding-card'; + card.innerHTML = ` +
${esc(f.severity)}${esc(f.title)}
+
${esc(f.cwe || '')} ${f.endpoint ? 'Β· ' + esc(f.endpoint) : ''} ${f.agent ? 'Β· ' + esc(f.agent) : ''}
+ `; + $('#findingsList').appendChild(card); + $('#findingsCount').textContent = state.currentJob.findings.length; +} + +function applySnapshot(snap) { + $('#livePhase').textContent = snap.phase; + $('#progressLabel').textContent = `${snap.agentsDone} / ${snap.agents || '?'} agents`; + if (snap.agents) $('#progressFill').style.width = `${Math.min(100, (snap.agentsDone / snap.agents) * 100)}%`; + if (snap.reportUrl) { + const link = $('#btnOpenReport'); + link.href = `/api/runs/${snap.runId}/asset/report.html`; + show(link, !!snap.runId); + } + if (snap.done) $('#phaseDot').style.background = 'var(--green)'; +} + +$('#btnStopRun').addEventListener('click', async () => { + if (!state.currentJob) return; + await api(`/api/exploit/${state.currentJob.id}/stop`, { method: 'POST' }); +}); +$('#btnBackToBoard').addEventListener('click', () => { show($('#liveView'), false); show($('#boardView'), true); }); + +// --------------------------------------------------------------------------- +// sidebar β€” runs history +// --------------------------------------------------------------------------- + +async function refreshRuns() { + try { + state.runs = await api('/api/runs'); + } catch { state.runs = []; } + renderSidebar(); +} + +function stepClassFor(phase, step) { + const order = ['recon', 'planning', 'exploiting', 'remediation']; + const idx = { recon: 0, starting: 0, planning: 1, exploiting: 2, validating: 2, chaining: 2, complete: 3 }[phase] ?? 0; + const stepIdx = order.indexOf(step); + if (step === 'remediation') return 'pending'; // not automated yet β€” shown for roadmap parity only + if (stepIdx < idx) return 'done'; + if (stepIdx === idx) return 'active'; + return 'pending'; +} + +function renderSidebar() { + const root = $('#sbGroups'); + root.innerHTML = ''; + + const running = state.runs.filter((r) => r.state === 'running'); + const completed = state.runs.filter((r) => r.state !== 'running'); + + const groups = [ + { label: 'Running', items: running, open: true }, + { label: 'Completed', items: completed, open: true }, + ]; + + for (const g of groups) { + const wrap = document.createElement('div'); + wrap.className = 'sb-group'; + wrap.innerHTML = `
β–Ύ${g.label}${g.items.length}
`; + wrap.querySelector('.sb-group-head').addEventListener('click', () => wrap.classList.toggle('collapsed')); + const items = wrap.querySelector('.sb-items'); + for (const r of g.items) { + const btn = document.createElement('button'); + btn.className = 'sb-run' + (state.currentDetailId === r.id ? ' active' : ''); + btn.innerHTML = `${esc(r.target)}${esc(r.id)} Β· ${r.findings} finding(s)`; + btn.addEventListener('click', () => openRun(r)); + items.appendChild(btn); + if (r.state === 'running' && state.currentJob) { + const phase = state.currentJob.target === r.target ? $('#livePhase').textContent : null; + const steps = document.createElement('div'); + steps.className = 'sb-steps'; + steps.innerHTML = ['recon', 'planning', 'exploiting', 'remediation'].map((s) => + `
${s[0].toUpperCase() + s.slice(1)}
`).join(''); + items.appendChild(steps); + } + } + root.appendChild(wrap); + } +} + +function openRun(run) { + state.currentDetailId = run.id; + if (run.state === 'running' && state.currentJob) { + show($('#boardView'), false); show($('#detailView'), false); show($('#liveView'), true); + return; + } + show($('#boardView'), false); show($('#liveView'), false); show($('#detailView'), true); + loadDetail(run.id); + renderSidebar(); +} + +async function loadDetail(id) { + clearInterval(state.detailPoll); + const detail = await api(`/api/runs/${encodeURIComponent(id)}`); + $('#detailTarget').textContent = detail.status?.target || detail.meta?.target || id; + $('#detailState').textContent = detail.status?.state || 'unknown'; + const list = $('#detailFindings'); + list.innerHTML = detail.findings.length + ? detail.findings.map((f) => ` +
+
${esc(f.severity)}${esc(f.title)}
+
${esc(f.cwe || '')} ${f.endpoint ? 'Β· ' + esc(f.endpoint) : ''} ${f.agent ? 'Β· ' + esc(f.agent) : ''}
+
`).join('') + : '
nenhum finding validado.
'; + const reportLink = $('#detailOpenReport'); + if (detail.assets.includes('report.html')) { + reportLink.href = `/api/runs/${encodeURIComponent(id)}/asset/report.html`; + show(reportLink, true); + } else show(reportLink, false); + + if (detail.status?.state === 'running') { + state.detailPoll = setInterval(() => loadDetail(id), 4000); + } +} + +$('#btnDetailBack').addEventListener('click', () => { clearInterval(state.detailPoll); show($('#detailView'), false); show($('#boardView'), true); }); +$('#btnNewEngagement').addEventListener('click', () => { show($('#detailView'), false); show($('#liveView'), false); show($('#boardView'), true); }); + +// --------------------------------------------------------------------------- +// REPL drawer β€” real CLI harness session +// --------------------------------------------------------------------------- + +function openReplDrawer() { show($('#replDrawer'), true); if (!state.replId) startRepl(); } + +async function startRepl() { + $('#replOutput').textContent = ''; + const { id } = await api('/api/repl', { method: 'POST' }); + state.replId = id; + const es = new EventSource(`/api/repl/${id}/events`); + state.replEs = es; + es.addEventListener('data', (e) => { + const { chunk } = JSON.parse(e.data); + const out = $('#replOutput'); + out.textContent += chunk; + out.scrollTop = out.scrollHeight; + }); + es.addEventListener('close', () => { es.close(); }); +} + +$('#fabRepl').addEventListener('click', openReplDrawer); +$('#btnOpenRepl').addEventListener('click', openReplDrawer); +$('#btnReplClose').addEventListener('click', () => show($('#replDrawer'), false)); +$('#btnReplRestart').addEventListener('click', async () => { + if (state.replId) await api(`/api/repl/${state.replId}/stop`, { method: 'POST' }).catch(() => {}); + state.replEs?.close(); + state.replId = null; + startRepl(); +}); +$('#replInput').addEventListener('keydown', async (e) => { + if (e.key !== 'Enter') return; + const line = e.target.value; + e.target.value = ''; + const out = $('#replOutput'); + out.textContent += `❭ ${line}\n`; + out.scrollTop = out.scrollHeight; + if (!state.replId) await startRepl(); + await api(`/api/repl/${state.replId}/input`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ line }) }); +}); + +// --------------------------------------------------------------------------- +// boot +// --------------------------------------------------------------------------- + +async function boot() { + const meta = await api('/api/meta').catch(() => ({})); + $('#sbMeta').textContent = `v${meta.version || '4.0.0'}`; + await loadAgents(); + await refreshRuns(); + setInterval(refreshRuns, 6000); +} +boot(); diff --git a/web/public/index.html b/web/public/index.html new file mode 100644 index 0000000..1aef368 --- /dev/null +++ b/web/public/index.html @@ -0,0 +1,191 @@ + + + + + +NeuroSploit v4.0.0 β€” Console + + + + + +
+ + + + + +
+ + +
+
+ βŒ• + +
+
+ + + +
+
+ + +
+ + + + + +
+
+

Set and modify the action plan

+
+
+ + +
+ + + + + + + +
+
+ + + + + + + + + diff --git a/web/public/style.css b/web/public/style.css new file mode 100644 index 0000000..b528ad6 --- /dev/null +++ b/web/public/style.css @@ -0,0 +1,242 @@ +:root { + --bg: #f3efe9; + --panel: #ffffff; + --panel-2: #faf8f5; + --border: #e6e1d8; + --text: #1c1a17; + --text-dim: #7a746a; + --text-faint: #a39d92; + --accent: #1e2a5e; + --accent-2: #2f3f8f; + --accent-soft: #eef0fa; + --gold: #e8b93f; + --gold-soft: #fdf3d9; + --green: #2e8b57; + --red: #c0392b; + --orange: #d97a1f; + --radius: 12px; + --radius-sm: 8px; + --mono: "SF Mono", "Cascadia Code", "JetBrains Mono", Consolas, monospace; + --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, sans-serif; + --shadow: 0 1px 2px rgba(20, 16, 8, 0.04), 0 8px 24px rgba(20, 16, 8, 0.06); +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --bg: #16151a; + --panel: #1d1c22; + --panel-2: #23222a; + --border: #302f38; + --text: #ecebf0; + --text-dim: #9a97a6; + --text-faint: #6d6a78; + --accent: #6d7fe0; + --accent-2: #8b9af0; + --accent-soft: #262c4a; + --gold: #e8b93f; + --gold-soft: #3a3320; + --green: #4fbf82; + --red: #e0665a; + --orange: #e69a4b; + --shadow: 0 1px 2px rgba(0,0,0,0.3), 0 8px 24px rgba(0,0,0,0.35); + } +} + +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; height: 100%; background: var(--bg); color: var(--text); font-family: var(--sans); } +button { font-family: inherit; cursor: pointer; } +input, select, textarea { font-family: inherit; color: inherit; } + +.app { display: flex; height: 100vh; overflow: hidden; } + +/* ---------------- sidebar ---------------- */ +.sidebar { + width: 260px; flex: none; background: var(--panel); border-right: 1px solid var(--border); + display: flex; flex-direction: column; padding: 14px 12px; +} +.sb-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; } +.brand { width: 32px; height: 32px; border-radius: 9px; background: var(--accent); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 16px; } +.sb-icons { display: flex; gap: 4px; } +.icon-btn { background: transparent; border: 1px solid transparent; color: var(--text-dim); width: 28px; height: 28px; border-radius: var(--radius-sm); font-size: 13px; } +.icon-btn:hover { background: var(--panel-2); border-color: var(--border); } + +.new-engagement { + border: 1px solid var(--border); background: var(--panel-2); color: var(--text); + border-radius: var(--radius-sm); padding: 9px 10px; font-size: 13px; text-align: left; margin-bottom: 12px; +} +.new-engagement:hover { border-color: var(--accent); color: var(--accent); } + +.sb-groups { flex: 1; overflow-y: auto; } +.sb-group { margin-bottom: 6px; } +.sb-group-head { + display: flex; align-items: center; gap: 6px; padding: 6px 6px; font-size: 12px; color: var(--text-dim); + text-transform: none; cursor: pointer; user-select: none; +} +.sb-group-head .count { margin-left: auto; opacity: .7; } +.sb-group-head .caret { font-size: 10px; transition: transform .15s; } +.sb-group.collapsed .caret { transform: rotate(-90deg); } +.sb-group.collapsed .sb-items { display: none; } +.sb-items { padding-left: 4px; } +.sb-run { + display: block; width: 100%; text-align: left; background: transparent; border: none; color: var(--text); + padding: 7px 8px; border-radius: var(--radius-sm); font-size: 13px; margin-bottom: 2px; +} +.sb-run:hover { background: var(--panel-2); } +.sb-run.active { background: var(--gold-soft); color: #7a5a00; font-weight: 600; } +.sb-run .sub { + display: block; font-size: 11px; color: var(--text-faint); font-weight: 400; margin-top: 1px; +} +.sb-steps { padding: 2px 8px 8px 20px; } +.sb-step { font-size: 12px; padding: 3px 0; color: var(--text-faint); display: flex; align-items: center; gap: 6px; } +.sb-step.done { color: var(--text-dim); } +.sb-step.done::before { content: "βœ“"; color: var(--green); } +.sb-step.active { color: var(--gold); font-weight: 600; } +.sb-step.active::before { content: "◐"; } +.sb-step.pending::before { content: "β—‹"; opacity: .5; } + +.sb-footer { display: flex; align-items: center; justify-content: space-between; padding-top: 8px; border-top: 1px solid var(--border); } +.sb-meta { font-size: 11px; color: var(--text-faint); } + +/* ---------------- main / topbar ---------------- */ +.main { flex: 1; display: flex; flex-direction: column; min-width: 0; } + +.topbar { display: flex; align-items: center; gap: 14px; padding: 14px 20px; border-bottom: 1px solid var(--border); background: var(--bg); } +.search-wrap { position: relative; } +.search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--text-faint); font-size: 13px; } +#leadSearch { + width: 220px; padding: 8px 10px 8px 28px; border-radius: 999px; border: 1px solid var(--border); + background: var(--panel); color: var(--text); font-size: 13px; +} +.chips { display: flex; gap: 8px; } +.chip { + border: 1px solid var(--border); background: var(--panel); color: var(--text-dim); border-radius: 999px; + padding: 7px 12px; font-size: 12px; display: flex; gap: 6px; align-items: center; +} +.chip span { color: var(--text-faint); } +.chip-active { background: var(--gold-soft); border-color: var(--gold); color: #7a5a00; } +.chip-active span { color: #7a5a00; } +.topbar-spacer { flex: 1; } + +.btn { border-radius: var(--radius-sm); border: 1px solid var(--border); padding: 9px 14px; font-size: 13px; background: var(--panel); color: var(--text); } +.btn-ghost:hover { border-color: var(--accent); color: var(--accent); } +.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; } +.btn-primary:hover { background: var(--accent-2); } +.btn-danger { background: var(--red); border-color: var(--red); color: #fff; } +.btn-icon { padding: 8px 10px; } +.btn:disabled { opacity: .5; cursor: not-allowed; } + +.engagement-bar { + display: flex; flex-wrap: wrap; gap: 10px 16px; align-items: end; padding: 12px 20px; background: var(--panel); + border-bottom: 1px solid var(--border); +} +.eb-field { display: flex; flex-direction: column; gap: 4px; } +.eb-field label { font-size: 11px; color: var(--text-faint); } +.eb-field input, .eb-field select { + border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 7px 9px; font-size: 13px; + background: var(--panel-2); color: var(--text); min-width: 120px; +} +.eb-target input { min-width: 260px; } +.eb-narrow input, .eb-narrow select { width: 88px; min-width: 0; } +.eb-check { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-dim); margin-bottom: 2px; } + +/* ---------------- board ---------------- */ +.board { flex: 1; display: flex; overflow: hidden; } +.board-scroll { flex: 1; overflow-y: auto; padding: 18px 20px 40px; } +.board-title { font-size: 15px; font-weight: 600; margin: 4px 0 16px; color: var(--text); } + +.categories { display: flex; flex-direction: column; gap: 12px; max-width: 720px; } +.cat-card { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; } +.cat-head { display: flex; align-items: center; gap: 10px; padding: 12px 14px; cursor: pointer; user-select: none; } +.cat-head .swatch { width: 10px; height: 10px; border-radius: 3px; background: var(--accent); } +.cat-head .cat-name { font-weight: 600; font-size: 13.5px; flex: 1; } +.cat-head .cat-count { font-size: 12px; color: var(--text-faint); } +.cat-head .caret { font-size: 10px; color: var(--text-faint); transition: transform .15s; } +.cat-card.collapsed .caret { transform: rotate(-90deg); } +.cat-card.collapsed .agent-rows { display: none; } +.agent-rows { border-top: 1px solid var(--border); } +.agent-row { display: flex; align-items: center; gap: 10px; padding: 9px 14px; border-bottom: 1px solid var(--border); font-size: 13px; } +.agent-row:last-child { border-bottom: none; } +.agent-row.hidden-by-search { display: none; } +.agent-row .agent-title { flex: 1; } +.agent-row .agent-cwe { font-size: 11px; color: var(--text-faint); } +.agent-row .agent-frac { font-size: 11px; color: var(--text-faint); width: 46px; text-align: right; } +.agent-row .chevron { color: var(--text-faint); font-size: 12px; } + +.switch { position: relative; width: 34px; height: 19px; flex: none; } +.switch input { opacity: 0; width: 0; height: 0; } +.switch .track { position: absolute; inset: 0; background: var(--border); border-radius: 999px; transition: background .15s; } +.switch .thumb { position: absolute; top: 2px; left: 2px; width: 15px; height: 15px; border-radius: 50%; background: #fff; transition: transform .15s; box-shadow: 0 1px 2px rgba(0,0,0,.3); } +.switch input:checked + .track { background: var(--accent); } +.switch input:checked + .track + .thumb { transform: translateX(15px); } + +.ask-panel { + width: 300px; flex: none; border-left: 1px solid var(--border); background: var(--panel); padding: 16px; + display: flex; flex-direction: column; gap: 10px; +} +.ask-title { font-size: 12px; color: var(--text-faint); } +#askInput { + flex: 1; min-height: 90px; resize: vertical; border: 1px solid var(--border); border-radius: var(--radius-sm); + padding: 10px; background: var(--panel-2); font-size: 13px; +} +.ask-row { display: flex; gap: 8px; } +.ask-row select { flex: 1; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--panel-2); padding: 8px; font-size: 12px; } +.ask-hint { font-size: 11px; color: var(--text-faint); min-height: 14px; } + +/* ---------------- live run / detail ---------------- */ +.liverun, .rundetail { flex: 1; display: flex; flex-direction: column; overflow: hidden; padding: 18px 20px; } +.liverun-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; } +.liverun-target { font-weight: 600; font-size: 15px; } +.liverun-phase { font-size: 12px; color: var(--text-dim); display: flex; align-items: center; gap: 6px; margin-top: 3px; } +.phase-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--gold); animation: pulse 1.4s infinite; } +@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.35} } +.liverun-actions { display: flex; gap: 8px; } + +.progress-wrap { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; } +.progress-bar { flex: 1; height: 8px; border-radius: 999px; background: var(--panel-2); border: 1px solid var(--border); overflow: hidden; } +.progress-fill { height: 100%; width: 0%; background: var(--accent); transition: width .3s; } +.progress-label { font-size: 12px; color: var(--text-faint); white-space: nowrap; } + +.liverun-body { flex: 1; display: flex; gap: 16px; overflow: hidden; } +.findings-col, .log-col { flex: 1; display: flex; flex-direction: column; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; } +.col-head { padding: 10px 14px; font-size: 12px; font-weight: 600; color: var(--text-dim); border-bottom: 1px solid var(--border); background: var(--panel-2); } +.findings-list, .log-list { flex: 1; overflow-y: auto; padding: 8px 10px; } + +.finding-card { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px 12px; margin-bottom: 8px; background: var(--panel-2); } +.finding-card .f-top { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; } +.sev { font-size: 10px; font-weight: 700; text-transform: uppercase; padding: 2px 7px; border-radius: 999px; letter-spacing: .03em; } +.sev-critical { background: #f6d9d5; color: #8c2a1c; } +.sev-high { background: #fbe1cf; color: #8c4a10; } +.sev-medium { background: #fdf0c8; color: #7a5a00; } +.sev-low { background: #dcecdd; color: #235c34; } +.sev-info { background: #e2e6f5; color: #2c3a7a; } +.finding-card .f-title { font-size: 13px; font-weight: 600; } +.finding-card .f-meta { font-size: 11px; color: var(--text-faint); margin-top: 3px; } + +.log-line { font-family: var(--mono); font-size: 11.5px; color: var(--text-dim); padding: 2px 0; white-space: pre-wrap; word-break: break-word; } + +/* ---------------- REPL drawer ---------------- */ +.repl-drawer { + position: fixed; right: 20px; bottom: 20px; width: 560px; height: 400px; background: #0e0e12; color: #dcdce0; + border-radius: var(--radius); box-shadow: 0 20px 60px rgba(0,0,0,.45); display: flex; flex-direction: column; + overflow: hidden; z-index: 50; border: 1px solid #2a2a33; +} +.repl-head { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; background: #17171d; font-size: 12px; color: #9a97a6; border-bottom: 1px solid #2a2a33; } +.repl-head .icon-btn { color: #9a97a6; } +.repl-head .icon-btn:hover { background: #22222b; color: #fff; } +.repl-output { flex: 1; overflow-y: auto; padding: 10px 12px; font-family: var(--mono); font-size: 12.5px; white-space: pre-wrap; word-break: break-word; } +.repl-input-row { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-top: 1px solid #2a2a33; } +.repl-prompt { color: #6d7fe0; font-family: var(--mono); } +#replInput { flex: 1; background: transparent; border: none; color: #ecebf0; font-family: var(--mono); font-size: 13px; outline: none; } + +.fab { + position: fixed; right: 20px; bottom: 20px; width: 52px; height: 52px; border-radius: 50%; background: var(--accent); + color: #fff; border: none; font-family: var(--mono); font-size: 16px; box-shadow: 0 10px 24px rgba(30,42,94,.35); + z-index: 40; +} +.fab:hover { background: var(--accent-2); } + +@media (max-width: 900px) { + .ask-panel { display: none; } + .sidebar { width: 210px; } + .repl-drawer { width: calc(100vw - 24px); left: 12px; right: 12px; } +} diff --git a/web/server.js b/web/server.js new file mode 100644 index 0000000..67b80e4 --- /dev/null +++ b/web/server.js @@ -0,0 +1,621 @@ +#!/usr/bin/env node +'use strict'; +/** + * NeuroSploit v4.0.0 β€” web console backend. + * + * Zero-dependency Node HTTP server that: + * - serves the static SPA in ./public + * - reads agents_md/ to build the "lead board" (agent/category picker) + * - reads runs/ to build run history + structured findings + * - spawns the compiled `neurosploit` CLI binary for exploitation runs and + * streams its stdout (parsed into structured events) over SSE + * - spawns the interactive `neurosploit` REPL (no subcommand) as a child + * process and pipes stdin/stdout so the browser gets a REAL REPL + * connected to the CLI harness β€” not a reimplementation. + */ + +const http = require('node:http'); +const fs = require('node:fs'); +const fsp = fs.promises; +const path = require('node:path'); +const { spawn } = require('node:child_process'); +const crypto = require('node:crypto'); +const { EventEmitter } = require('node:events'); + +// --------------------------------------------------------------------------- +// Paths +// --------------------------------------------------------------------------- + +const WEB_DIR = __dirname; +const ROOT = path.resolve(WEB_DIR, '..'); // repo root β€” holds agents_md/, runs/ +const AGENTS_DIR = path.join(ROOT, 'agents_md'); +const RUNS_DIR = path.join(ROOT, 'runs'); +const PUBLIC_DIR = path.join(WEB_DIR, 'public'); + +function findBinary() { + const candidates = [ + path.join(ROOT, 'neurosploit-rs', 'target', 'release', 'neurosploit'), + path.join(ROOT, 'neurosploit-rs', 'target', 'debug', 'neurosploit'), + ]; + for (const c of candidates) { + if (fs.existsSync(c)) return c; + } + return null; +} +const BIN = findBinary(); + +const PORT = Number(process.env.NEUROSPLOIT_WEB_PORT || process.env.PORT || 4173); + +// --------------------------------------------------------------------------- +// 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. +// --------------------------------------------------------------------------- + +const KIND_DIRS = { + vulns: 'vuln', + ai: 'ai', + infra: 'infra', + code: 'code', + chains: 'chain', + recon: 'recon', + meta: 'meta', +}; + +// Ordered classifier: first matching rule wins. Mirrors the vocabulary used +// throughout agents_md/ so every agent lands in a sensible bucket for the +// lead board (screenshot-style category groups). +const CATEGORY_RULES = [ + [/^(llm_|mcp_|n8n_|redteam_|skill_|prompt_injection|ml_model_inversion|vector_db_injection)/, 'LLM Application'], + [/^(xss_|dom_xss|blind_xss|mutation_xss|dom_clobbering|postmessage_vulnerability)/, 'Cross-Site Scripting'], + [/^(jwt_|oauth_|oidc_|saml_|session_fixation|mfa_bypass|two_factor|twofa_|captcha_bypass|brute_force|default_credentials|weak_password|weak_jwt_secret|login_sqli_bypass|timing_side_channel_auth|timing_attack|refresh_token_abuse|auth_bypass|password_reset_poisoning)/, 'Auth & Session'], + [/^(idor|bola|bfla|access_control_bypass|privilege_escalation|forced_browsing|authenticated_surface_exploit|exposed_admin_panel|spa_hidden_admin|mass_assignment|register_privilege_mass_assign|api_excessive_data|excessive_data_exposure|account_takeover_chain)/, 'Broken Access Control'], + [/^(business_logic|spa_business_logic|coupon_logic_abuse|price_manipulation|workflow_step_skip|race_condition|idempotency_key_abuse|account_registration_and_forms)/, 'Business Logic'], + [/^(sqli_|nosql_injection|ldap_injection|xpath_injection|xslt_injection|ssti|command_injection|log_injection|crlf_injection|header_injection|email_injection|smtp_injection|soap_injection|orm_injection|expression_language_injection|graphql_injection|css_injection|html_injection|csv_injection|formula_injection_excel|dangling_markup_injection|client_side_template_injection|server_side_prototype_pollution|prototype_pollution|xxe|path_traversal|^lfi$|^rfi$|zip_slip|log4shell_jndi|pickle_deserialization|insecure_deserialization|yaml_deserialization|type_juggling)/, 'Injection'], + [/^(ssrf|gcp_metadata_ssrf|azure_imds_exposure|aws_imds_v2_bypass|host_header_injection|http_smuggling|http_desync|http2_request_smuggling|h2c_smuggling|reverse_proxy_path_confusion|websocket_)/, 'SSRF & Network'], + [/^(graphql_|api_rate_limiting|rest_api_versioning|api_key_exposure|exposed_api_docs|spa_api_discovery|param_miner|parameter_pollution|grpc_reflection_exposure|api_bola)/, 'API & GraphQL'], + [/^(aws_|azure_|gcp_|s3_bucket|gcs_bucket_misconfig|k8s_|docker_socket_exposure|container_escape|cloud_|terraform_state_exposure|helm_secret_exposure|ecr_public_exposure|serverless_|ci_cd_secret_leak|ad_)/, 'Cloud & Infra'], + [/^(clickjacking|tabnabbing|cors_misconfig|insecure_cookie_flags|security_headers|subdomain_takeover|open_redirect|second_order_redirect|oauth_open_redirect_chain|csrf)/, 'Client-Side'], + [/^(weak_encryption|weak_hashing|weak_random|padding_oracle|ecb_pattern_leak|ssl_issues|cleartext_transmission)/, 'Cryptography'], + [/^(rate_limit|graphql_dos|regex_dos|range_header_dos|web_cache_poisoning_dos|llm_model_dos)/, 'Rate Limiting & DoS'], + [/^(cache_poisoning|cdn_cache_key_poisoning|web_cache_deception|insecure_cdn|byte_range_cache|edge_side_includes)/, 'Cache & CDN'], + [/^(cve_|eol_|version_disclosure|wordpress_audit|joomla_audit|drupal_audit|cms_|outdated_|dependency_confusion|typosquatting_package|vulnerable_dependency|git_exposed_repo|git_svn_exposure_app|source_code_disclosure|backup_file_exposure|env_file_exposure|debug_mode|aspnet_|appserver_exposure|misconfig_|iis_|information_disclosure|sensitive_data_exposure|directory_listing)/, 'Recon & Fingerprint'], + [/^linux_/, 'Linux Host'], + [/^windows_/, 'Windows Host'], +]; + +function classify(name, kind) { + if (kind === 'chain') return 'Attack Chains'; + if (kind === 'recon') return 'Recon'; + if (kind === 'code') return 'Code Review'; + if (kind === 'meta') return 'Meta & Reporting'; + if (kind === 'ai') return 'LLM Application'; + for (const [re, cat] of CATEGORY_RULES) { + if (re.test(name)) return cat; + } + return 'Other'; +} + +function extractTitle(text, fallback) { + const m = text.match(/^#\s+(.+?)\s*$/m); + return m ? m[1].trim() : fallback; +} +function extractCwe(text) { + const m = text.match(/CWE-\d+/); + return m ? m[0] : ''; +} + +let agentCache = null; +let agentCacheAt = 0; + +async function loadAgents() { + const now = Date.now(); + if (agentCache && now - agentCacheAt < 5000) return agentCache; + + const agents = []; + for (const [dir, kind] of Object.entries(KIND_DIRS)) { + const full = path.join(AGENTS_DIR, dir); + let entries = []; + try { + entries = await fsp.readdir(full); + } catch { + continue; + } + for (const file of entries) { + if (!file.endsWith('.md')) continue; + const name = file.slice(0, -3); + const text = await fsp.readFile(path.join(full, file), 'utf8').catch(() => ''); + agents.push({ + id: name, + name, + title: extractTitle(text, name), + cwe: extractCwe(text), + kind, + category: classify(name, kind), + }); + } + } + agents.sort((a, b) => a.name.localeCompare(b.name)); + + const byCategory = new Map(); + for (const a of agents) { + if (!byCategory.has(a.category)) byCategory.set(a.category, []); + byCategory.get(a.category).push(a); + } + // Selectable leads only (exclude meta/orchestration from the pentest board β€” + // they're internal doctrine agents, not testable "leads"). + const LEAD_ORDER = [ + 'Business Logic', 'Broken Access Control', 'Injection', 'Cross-Site Scripting', + 'LLM Application', 'Auth & Session', 'SSRF & Network', 'API & GraphQL', + 'Cloud & Infra', 'Client-Side', 'Cryptography', 'Rate Limiting & DoS', + 'Cache & CDN', 'Recon & Fingerprint', 'Linux Host', 'Windows Host', + 'Attack Chains', 'Code Review', 'Recon', 'Other', + ]; + const categories = LEAD_ORDER + .filter((c) => byCategory.has(c) && c !== 'Meta & Reporting') + .map((c) => ({ category: c, agents: byCategory.get(c) })); + + agentCache = { total: agents.length, agents, categories }; + agentCacheAt = now; + return agentCache; +} + +// --------------------------------------------------------------------------- +// Runs β€” read runs//{meta,status,findings}.json +// --------------------------------------------------------------------------- + +async function readJsonSafe(p, fallback) { + try { + return JSON.parse(await fsp.readFile(p, 'utf8')); + } catch { + return fallback; + } +} + +async function listRuns() { + let ids = []; + try { + ids = (await fsp.readdir(RUNS_DIR)).filter((d) => d.startsWith('ns-')); + } catch { + return []; + } + const runs = await Promise.all(ids.map(async (id) => { + const dir = path.join(RUNS_DIR, id); + const [meta, status, findings] = await Promise.all([ + readJsonSafe(path.join(dir, 'meta.json'), {}), + readJsonSafe(path.join(dir, 'status.json'), {}), + readJsonSafe(path.join(dir, 'findings.json'), []), + ]); + const tsMatch = id.match(/^ns-(\d+)-/); + const ts = tsMatch ? Number(tsMatch[1]) : 0; + const sevCount = {}; + for (const f of findings) sevCount[f.severity] = (sevCount[f.severity] || 0) + 1; + return { + id, + ts, + target: status.target || meta.target || id.replace(/^ns-\d+-/, ''), + state: status.state || 'unknown', + findings: findings.length, + severities: sevCount, + hasReport: fs.existsSync(path.join(dir, 'report.html')) || fs.existsSync(path.join(dir, 'report.pdf')), + }; + })); + runs.sort((a, b) => b.ts - a.ts); + return runs; +} + +async function runDetail(id) { + const dir = safeRunDir(id); + if (!dir) return null; + const [meta, status, findings] = await Promise.all([ + readJsonSafe(path.join(dir, 'meta.json'), {}), + readJsonSafe(path.join(dir, 'status.json'), {}), + readJsonSafe(path.join(dir, 'findings.json'), []), + ]); + const assets = ['report.html', 'report.pdf', 'report.md', 'recon.md', 'exploitation.md'] + .filter((f) => fs.existsSync(path.join(dir, f))); + return { id, meta, status, findings, assets }; +} + +function safeRunDir(id) { + if (!/^[a-zA-Z0-9_.-]+$/.test(id)) return null; + const dir = path.join(RUNS_DIR, id); + if (!dir.startsWith(RUNS_DIR)) return null; + return dir; +} + +// --------------------------------------------------------------------------- +// Exploitation jobs β€” spawn `neurosploit --only ... -v` +// and parse its stdout into structured live state (mirrors app/src/repl.rs +// RunLive::ingest so the web UI gets the same phases/findings the TUI does). +// --------------------------------------------------------------------------- + +const jobs = new Map(); // id -> Job + +class Job extends EventEmitter { + constructor(id, cmd, args, target) { + super(); + this.id = id; + this.cmd = cmd; + this.args = args; + this.target = target || ''; + this.runId = null; // ns-- workdir basename, once known + this.phase = 'starting'; + this.findings = []; + this.feed = []; + this.agents = 0; + this.agentsDone = 0; + this.done = false; + this.exitCode = null; + this.reportUrl = null; + this.startedAt = Date.now(); + this.child = null; + } + push(evt) { + this.feed.push(evt); + if (this.feed.length > 2000) this.feed.shift(); + this.emit('event', evt); + } + snapshot() { + return { + id: this.id, + target: this.target, + runId: this.runId, + phase: this.phase, + findings: this.findings, + agents: this.agents, + agentsDone: this.agentsDone, + done: this.done, + exitCode: this.exitCode, + reportUrl: this.reportUrl, + startedAt: this.startedAt, + }; + } +} + +const ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g; +function stripAnsi(s) { return s.replace(ANSI_RE, ''); } + +function ingestLine(job, rawLine) { + const line = stripAnsi(rawLine); + const low = line.toLowerCase(); + job.push({ type: 'log', line }); + + if (low.includes('token/quota exhausted') || low.includes('run is paused')) job.phase = 'paused (quota)'; + else if (low.includes('authentication failed') || low.includes('circuit breaker')) job.phase = 'paused (auth)'; + else if (low.startsWith('recon') || low.startsWith('ai-recon') || low.includes('recon round') || low.startsWith('probe:')) job.phase = 'recon'; + else if (low.includes('selected') && low.includes('agent')) { + job.phase = 'planning'; + const n = line.split(/\s+/).map(Number).find((x) => Number.isFinite(x)); + if (n) job.agents = n; + } else if (low.startsWith('exploit') || low.startsWith('test ') || low.includes('launching agent')) job.phase = 'exploiting'; + else if (low.startsWith('vote') || low.includes('validating')) job.phase = 'validating'; + else if (low.startsWith('chain')) job.phase = 'chaining'; + else if (low.includes('phase complete') || low.includes('validated finding(s)')) job.phase = 'complete'; + + if (/candidate\(s\)/.test(low) && /^(exploit |test |analyze |review )/.test(low)) job.agentsDone += 1; + + const fj = line.match(/^finding_json:\s*(.+)$/); + if (fj) { + try { + const finding = JSON.parse(fj[1]); + job.findings.push(finding); + job.push({ type: 'finding', finding }); + } catch { /* ignore malformed line */ } + } + + const rep = line.match(/report:\s*(file:\/\/\S+)/); + if (rep) job.reportUrl = rep[1]; + + const rid = line.match(/run id\s*:\s*(\S+)/); + if (rid) job.runId = rid[1]; +} + +function buildArgs(body) { + const mode = body.mode || 'run'; + const args = [mode]; + if (mode === 'run' || mode === 'host' || mode === 'aitest') { + args.push(body.target); + } else if (mode === 'whitebox' || mode === 'skills') { + args.push(body.repo || body.target); + } else if (mode === 'greybox') { + args.push(body.repo); + args.push('--url', body.target); + } + for (const m of body.models || []) args.push('--model', m); + if (body.votes) args.push('--vote-n', String(body.votes)); + if (body.chainDepth !== undefined) args.push('--chain-depth', String(body.chainDepth)); + if (body.recon) args.push('--recon', String(body.recon)); + if (body.maxAgents) args.push('--max-agents', String(body.maxAgents)); + if (body.offline) args.push('--offline'); + if (body.subscription) args.push('--subscription'); + if (body.mcp) args.push('--mcp'); + if (body.creds) args.push('--creds', body.creds); + if (body.focus) args.push('--focus', body.focus); + if (body.objective) args.push('--objective', body.objective); + if (body.outOfScope) args.push('--out-of-scope', body.outOfScope); + for (const a of body.agents || []) args.push('--only', a); + args.push('--verbose'); + return args; +} + +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 job = new Job(id, BIN, args, body.repo || body.target || ''); + jobs.set(id, job); + + const child = spawn(BIN, args, { cwd: ROOT, env: process.env }); + job.child = child; + let buf = ''; + const onData = (chunk) => { + buf += chunk.toString('utf8'); + let idx; + while ((idx = buf.indexOf('\n')) !== -1) { + const line = buf.slice(0, idx); + buf = buf.slice(idx + 1); + if (line.length) ingestLine(job, line); + } + }; + child.stdout.on('data', onData); + child.stderr.on('data', onData); + child.on('close', (code) => { + if (buf.trim()) ingestLine(job, buf); + job.done = true; + job.exitCode = code; + job.phase = job.phase === 'paused (quota)' || job.phase === 'paused (auth)' ? job.phase : 'complete'; + job.push({ type: 'done', exitCode: code }); + }); + child.on('error', (err) => { + job.done = true; + job.push({ type: 'log', line: `[web] failed to start neurosploit: ${err.message}` }); + job.push({ type: 'done', exitCode: -1 }); + }); + return job; +} + +// --------------------------------------------------------------------------- +// REPL sessions β€” spawn `neurosploit` with no subcommand (Reader::Plain kicks +// in over a piped stdin) and forward stdin/stdout verbatim: a real REPL. +// --------------------------------------------------------------------------- + +const replSessions = new Map(); + +class ReplSession extends EventEmitter { + constructor(id, child) { + super(); + this.id = id; + this.child = child; + this.done = false; + this.buffer = []; + } + push(chunk) { + this.buffer.push(chunk); + if (this.buffer.length > 5000) this.buffer.shift(); + this.emit('data', chunk); + } +} + +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 session = new ReplSession(id, child); + replSessions.set(id, session); + const onData = (chunk) => session.push(stripAnsi(chunk.toString('utf8'))); + child.stdout.on('data', onData); + child.stderr.on('data', onData); + child.on('close', (code) => { + session.done = true; + session.push(`\n[repl session ended, exit code ${code}]\n`); + session.emit('close'); + }); + child.on('error', (err) => { + session.done = true; + session.push(`\n[failed to start neurosploit: ${err.message}]\n`); + session.emit('close'); + }); + return session; +} + +// --------------------------------------------------------------------------- +// Tiny HTTP plumbing (no framework) +// --------------------------------------------------------------------------- + +function sendJson(res, code, obj) { + const body = JSON.stringify(obj); + res.writeHead(code, { + 'Content-Type': 'application/json; charset=utf-8', + 'Content-Length': Buffer.byteLength(body), + 'Cache-Control': 'no-store', + }); + res.end(body); +} + +function readBody(req) { + return new Promise((resolve, reject) => { + let data = ''; + req.on('data', (c) => { data += c; if (data.length > 5_000_000) req.destroy(); }); + req.on('end', () => { + if (!data) return resolve({}); + try { resolve(JSON.parse(data)); } catch (e) { reject(e); } + }); + req.on('error', reject); + }); +} + +function sseInit(res) { + res.writeHead(200, { + 'Content-Type': 'text/event-stream; charset=utf-8', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + 'X-Accel-Buffering': 'no', + }); + res.write(':ok\n\n'); +} +function sseSend(res, event, data) { + res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); +} + +const MIME = { + '.html': 'text/html; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.svg': 'image/svg+xml', + '.png': 'image/png', + '.pdf': 'application/pdf', + '.md': 'text/plain; charset=utf-8', +}; + +async function serveStatic(req, res, urlPath) { + let rel = urlPath === '/' ? '/index.html' : urlPath; + const full = path.join(PUBLIC_DIR, rel); + if (!full.startsWith(PUBLIC_DIR)) { res.writeHead(403); res.end(); return; } + try { + const data = await fsp.readFile(full); + const ext = path.extname(full); + res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' }); + res.end(data); + } catch { + res.writeHead(404); + res.end('not found'); + } +} + +async function serveRunAsset(req, res, id, rest) { + const dir = safeRunDir(id); + if (!dir) { res.writeHead(400); res.end(); return; } + const full = path.join(dir, rest); + if (!full.startsWith(dir)) { res.writeHead(403); res.end(); return; } + try { + const data = await fsp.readFile(full); + const ext = path.extname(full); + res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' }); + res.end(data); + } catch { + res.writeHead(404); + res.end('not found'); + } +} + +const server = http.createServer(async (req, res) => { + const u = new URL(req.url, 'http://localhost'); + const p = u.pathname; + + try { + // ---- static ---- + if (req.method === 'GET' && !p.startsWith('/api/')) { + return serveStatic(req, res, p); + } + + // ---- agents / lead board ---- + if (req.method === 'GET' && p === '/api/agents') { + return sendJson(res, 200, await loadAgents()); + } + + // ---- runs ---- + if (req.method === 'GET' && p === '/api/runs') { + return sendJson(res, 200, await listRuns()); + } + let m = p.match(/^\/api\/runs\/([^/]+)$/); + if (req.method === 'GET' && m) { + const detail = await runDetail(decodeURIComponent(m[1])); + if (!detail) return sendJson(res, 404, { error: 'run not found' }); + return sendJson(res, 200, detail); + } + m = p.match(/^\/api\/runs\/([^/]+)\/asset\/(.+)$/); + if (req.method === 'GET' && m) { + return serveRunAsset(req, res, decodeURIComponent(m[1]), decodeURIComponent(m[2])); + } + + // ---- exploitation jobs ---- + if (req.method === 'GET' && p === '/api/exploit') { + return sendJson(res, 200, [...jobs.values()].map((j) => j.snapshot())); + } + if (req.method === 'POST' && p === '/api/exploit') { + const body = await readBody(req); + const job = startJob(body); + return sendJson(res, 200, { id: job.id }); + } + m = p.match(/^\/api\/exploit\/([^/]+)$/); + if (req.method === 'GET' && m) { + const job = jobs.get(m[1]); + if (!job) return sendJson(res, 404, { error: 'job not found' }); + return sendJson(res, 200, job.snapshot()); + } + m = p.match(/^\/api\/exploit\/([^/]+)\/stop$/); + if (req.method === 'POST' && m) { + const job = jobs.get(m[1]); + if (!job) return sendJson(res, 404, { error: 'job not found' }); + job.child?.kill('SIGINT'); + return sendJson(res, 200, { ok: true }); + } + m = p.match(/^\/api\/exploit\/([^/]+)\/events$/); + if (req.method === 'GET' && m) { + const job = jobs.get(m[1]); + if (!job) { res.writeHead(404); return res.end(); } + sseInit(res); + // replay what already happened + for (const evt of job.feed) sseSend(res, evt.type, evt); + sseSend(res, 'snapshot', job.snapshot()); + if (job.done) { sseSend(res, 'done', job.snapshot()); res.end(); return; } + const onEvt = (evt) => sseSend(res, evt.type, evt); + job.on('event', onEvt); + const ping = setInterval(() => res.write(':ping\n\n'), 20000); + req.on('close', () => { job.off('event', onEvt); clearInterval(ping); }); + return; + } + + // ---- REPL (real CLI harness session) ---- + if (req.method === 'POST' && p === '/api/repl') { + const session = startRepl(); + return sendJson(res, 200, { id: session.id }); + } + m = p.match(/^\/api\/repl\/([^/]+)\/input$/); + if (req.method === 'POST' && m) { + const session = replSessions.get(m[1]); + if (!session) return sendJson(res, 404, { error: 'session not found' }); + const body = await readBody(req); + session.child.stdin.write(String(body.line ?? '') + '\n'); + return sendJson(res, 200, { ok: true }); + } + m = p.match(/^\/api\/repl\/([^/]+)\/stop$/); + if (req.method === 'POST' && m) { + const session = replSessions.get(m[1]); + if (!session) return sendJson(res, 404, { error: 'session not found' }); + session.child.kill('SIGTERM'); + return sendJson(res, 200, { ok: true }); + } + m = p.match(/^\/api\/repl\/([^/]+)\/events$/); + if (req.method === 'GET' && m) { + const session = replSessions.get(m[1]); + if (!session) { res.writeHead(404); return res.end(); } + sseInit(res); + for (const chunk of session.buffer) sseSend(res, 'data', { chunk }); + if (session.done) { sseSend(res, 'close', {}); res.end(); return; } + const onData = (chunk) => sseSend(res, 'data', { chunk }); + const onClose = () => { sseSend(res, 'close', {}); res.end(); }; + session.on('data', onData); + session.on('close', onClose); + const ping = setInterval(() => res.write(':ping\n\n'), 20000); + req.on('close', () => { session.off('data', onData); session.off('close', onClose); clearInterval(ping); }); + return; + } + + if (req.method === 'GET' && p === '/api/meta') { + return sendJson(res, 200, { version: '4.0.0', binary: BIN, root: ROOT }); + } + + sendJson(res, 404, { error: 'not found' }); + } catch (err) { + sendJson(res, 500, { error: err.message }); + } +}); + +server.listen(PORT, () => { + console.log(`NeuroSploit v4.0.0 web console β†’ http://localhost:${PORT}`); + console.log(` binary : ${BIN || '(not found β€” build neurosploit-rs first)'}`); + console.log(` agents : ${AGENTS_DIR}`); + console.log(` runs : ${RUNS_DIR}`); +});