release: v4.3.2 — bulk collection import, full MCP server, web console overhaul

Collection import:
- parse_path() auto-detects file vs directory; parse_dir() walks a folder of
  .json collections (Postman/OpenAPI/Swagger/HAR), merges and deduplicates
  endpoints across all files. walkdir (already a dep) now used in collection.rs.
- CLI --collection, REPL /collection, pipeline injection, and web console all
  accept a directory path transparently.

MCP server (neurosploit mcp):
- Expanded from 8 to 24 tools: added whitebox, greybox, host, aitest, mobile,
  skills, pr, graph, audit, poc, traffic, assurance, agents, models, and
  import_collection. neurosploit_run gained --collection and --objective params.
- Clients: Claude Code, Codex CLI, Cursor, Windsurf, VS Code — all documented
  in tool descriptions.

Web console:
- Full sidebar navigation: Dashboard, Pentests, PR Reviews, Issues,
  Repositories, Domains, Networks, Integrations, Settings. Chat removed.
- switchView() navigation system with lazy-loading per page.
- Pentests page: tabular list of all engagements with click-to-detail.
- PR Reviews page: filtered view of PR review runs.
- Issues page: aggregate findings across every run, filterable by severity.
- Repositories page: connect/manage source repos (persisted in .neurosploit/).
- Domains page: manage target domains for black-box scans.
- Networks page: manage network ranges for infra scanning.
- Integrations page: GitHub, GitLab, Jira, Slack, MCP status cards.
- Settings page: default model, environment, TypeSafe, network, proxy config.
- Server: CRUD API endpoints for repos/domains/networks/integrations/settings
  with JSON file persistence in .neurosploit/ directory.
- New CSS: nav items, config cards, data tables, stat cards, integration cards,
  form modals, accent buttons.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqW6BbQgqGkEDn3RYuCVoM
This commit is contained in:
CyberSecurityUPandClaude Opus 4.6 committed 2026-10-16 16:05:38 -03:00
1 parent e8b7f61890
commit 71339a9ddb
11 files changed
+1072 -93

No files matched your search

+110 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
'use strict';
/**
* NeuroSploit v4.3.1 — web console backend.
* NeuroSploit v4.3.2 — web console backend.
*
* Zero-dependency Node HTTP server that:
* - serves the static SPA in ./public
@@ -57,6 +57,22 @@ function saveEngagementName(runId, name) {
// interrupted run/whitebox/greybox job, RESUME it (relaunch the REPL, which
// auto-recovers the harness's own on-disk checkpoint and carries findings
// forward). Secrets (API keys, creds-file contents) are never written here.
const CONFIG_DIR = path.join(ROOT, '.neurosploit');
function configFile(name) { return path.join(CONFIG_DIR, `${name}.json`); }
async function ensureConfigDir() { await fsp.mkdir(CONFIG_DIR, { recursive: true }); }
async function readConfig(name) {
try { return JSON.parse(await fsp.readFile(configFile(name), 'utf8')); }
catch { return []; }
}
async function readConfigObj(name) {
try { return JSON.parse(await fsp.readFile(configFile(name), 'utf8')); }
catch { return {}; }
}
async function writeConfig(name, data) {
await ensureConfigDir();
await fsp.writeFile(configFile(name), JSON.stringify(data, null, 2));
}
const JOBS_DIR = path.join(ROOT, '.neurosploit', 'web-jobs');
function jobFile(id) { return path.join(JOBS_DIR, `${id}.json`); }
function persistJob(job) {
@@ -1942,6 +1958,99 @@ const server = http.createServer(async (req, res) => {
return sendJson(res, 200, { ok: true });
}
// ---- Issues (aggregate findings across all runs) ----
if (req.method === 'GET' && p === '/api/issues') {
const dirs = await fsp.readdir(RUNS_DIR).catch(() => []);
const all = [];
for (const d of dirs) {
const fp = path.join(RUNS_DIR, d, 'findings.json');
try {
const raw = await fsp.readFile(fp, 'utf8');
const findings = JSON.parse(raw);
for (const f of findings) all.push({ ...f, run: d, target: f.target || d });
} catch { /* skip */ }
}
const sRank = (s) => { s = (s || '').toLowerCase(); return s.includes('crit') ? 0 : s.includes('high') ? 1 : s.includes('med') ? 2 : s.includes('low') ? 3 : 4; };
all.sort((a, b) => sRank(a.severity) - sRank(b.severity));
return sendJson(res, 200, all);
}
// ---- Config CRUD: repos ----
if (req.method === 'GET' && p === '/api/config/repos') return sendJson(res, 200, await readConfig('repos'));
if (req.method === 'POST' && p === '/api/config/repos') {
const body = await readBody(req);
const repos = await readConfig('repos');
const item = { id: crypto.randomUUID(), ...body, createdAt: new Date().toISOString() };
repos.push(item);
await writeConfig('repos', repos);
return sendJson(res, 200, item);
}
m = p.match(/^\/api\/config\/repos\/([^/]+)$/);
if (req.method === 'DELETE' && m) {
let repos = await readConfig('repos');
repos = repos.filter(r => r.id !== decodeURIComponent(m[1]));
await writeConfig('repos', repos);
return sendJson(res, 200, { ok: true });
}
// ---- Config CRUD: domains ----
if (req.method === 'GET' && p === '/api/config/domains') return sendJson(res, 200, await readConfig('domains'));
if (req.method === 'POST' && p === '/api/config/domains') {
const body = await readBody(req);
const domains = await readConfig('domains');
const item = { id: crypto.randomUUID(), ...body, createdAt: new Date().toISOString() };
domains.push(item);
await writeConfig('domains', domains);
return sendJson(res, 200, item);
}
m = p.match(/^\/api\/config\/domains\/([^/]+)$/);
if (req.method === 'DELETE' && m) {
let domains = await readConfig('domains');
domains = domains.filter(d => d.id !== decodeURIComponent(m[1]));
await writeConfig('domains', domains);
return sendJson(res, 200, { ok: true });
}
// ---- Config CRUD: networks ----
if (req.method === 'GET' && p === '/api/config/networks') return sendJson(res, 200, await readConfig('networks'));
if (req.method === 'POST' && p === '/api/config/networks') {
const body = await readBody(req);
const networks = await readConfig('networks');
const item = { id: crypto.randomUUID(), ...body, createdAt: new Date().toISOString() };
networks.push(item);
await writeConfig('networks', networks);
return sendJson(res, 200, item);
}
m = p.match(/^\/api\/config\/networks\/([^/]+)$/);
if (req.method === 'DELETE' && m) {
let networks = await readConfig('networks');
networks = networks.filter(n => n.id !== decodeURIComponent(m[1]));
await writeConfig('networks', networks);
return sendJson(res, 200, { ok: true });
}
// ---- Config CRUD: integrations ----
if (req.method === 'GET' && p === '/api/config/integrations') return sendJson(res, 200, await readConfig('integrations'));
m = p.match(/^\/api\/config\/integrations\/([^/]+)$/);
if (req.method === 'PUT' && m) {
const body = await readBody(req);
let integs = await readConfig('integrations');
const id = decodeURIComponent(m[1]);
const idx = integs.findIndex(i => i.id === id);
if (idx >= 0) integs[idx] = { ...integs[idx], ...body };
else integs.push({ id, ...body });
await writeConfig('integrations', integs);
return sendJson(res, 200, integs.find(i => i.id === id));
}
// ---- Config: settings ----
if (req.method === 'GET' && p === '/api/config/settings') return sendJson(res, 200, await readConfigObj('settings'));
if (req.method === 'PUT' && p === '/api/config/settings') {
const body = await readBody(req);
await writeConfig('settings', body);
return sendJson(res, 200, body);
}
sendJson(res, 404, { error: 'not found' });
} catch (err) {
sendJson(res, 500, { error: err.message });