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

+253 -15
View File
@@ -1,5 +1,5 @@
'use strict';
/* NeuroSploit v4.2.1 — web console frontend. Vanilla JS, no build step. */
/* NeuroSploit v4.3.2 — web console frontend. Vanilla JS, no build step. */
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
@@ -43,6 +43,7 @@ const state = {
live: { sort: 'severity', dir: 1, query: '', sev: null },
detail: { sort: 'severity', dir: 1, query: '', sev: null },
},
activeView: 'wizard',
};
// ---------------------------------------------------------------------------
@@ -96,6 +97,33 @@ function fieldError(id, msg) {
}
function clearFieldErrors() { $$('.field-error').forEach((el) => { el.textContent = ''; el.hidden = true; }); }
// ---------------------------------------------------------------------------
// view navigation
// ---------------------------------------------------------------------------
const ALL_VIEWS = ['dashView','wizardView','liveView','detailView','pentestsView','prreviewsView','issuesView','reposView','domainsView','networksView','integrationsView','settingsView'];
function switchView(viewName) {
state.activeView = viewName;
ALL_VIEWS.forEach(id => { const el = $(`#${id}`); if (el) el.hidden = true; });
const target = $(`#${viewName}View`);
if (target) target.hidden = false;
$$('.sb-nav-item').forEach(btn => {
btn.classList.toggle('active', btn.dataset.view === viewName);
});
const runHist = $('#sbRunHistory');
if (runHist) runHist.style.display = ['dash','pentests','wizard'].includes(viewName) ? '' : 'none';
if (viewName === 'pentests') loadPentests();
if (viewName === 'issues') loadIssues();
if (viewName === 'repos') loadRepos();
if (viewName === 'domains') loadDomains();
if (viewName === 'networks') loadNetworks();
if (viewName === 'integrations') loadIntegrations();
if (viewName === 'settings') loadSettings();
if (viewName === 'prreviews') loadPrReviews();
if (viewName === 'dash') renderDashboard();
}
function timeAgo(ts) {
if (!ts) return '';
const secs = Math.max(0, Math.floor(Date.now() / 1000 - ts));
@@ -1029,7 +1057,7 @@ function leaveLiveJob() {
state.currentJob = null;
termSyncTargets();
}
$('#btnBackToBoard').addEventListener('click', () => { leaveLiveJob(); show($('#liveView'), false); show($('#dashView'), false); show($('#wizardView'), true); });
$('#btnBackToBoard').addEventListener('click', () => { leaveLiveJob(); switchView('wizard'); });
// Regenerating from the evidence already on disk, rather than re-running the
// engagement: a run whose PDF was never produced (no `typst` at the time, or a
// since-improved template) would otherwise be unreportable.
@@ -1074,8 +1102,9 @@ $('#btnDeleteRun').addEventListener('click', async () => {
}
});
$('#btnDetailBack').addEventListener('click', () => { clearInterval(state.detailPoll); show($('#detailView'), false); show($('#dashView'), false); show($('#wizardView'), true); });
$('#btnNewEngagement').addEventListener('click', () => { leaveLiveJob(); clearInterval(state.detailPoll); show($('#detailView'), false); show($('#liveView'), false); show($('#dashView'), false); show($('#wizardView'), true); });
$('#btnDetailBack').addEventListener('click', () => { clearInterval(state.detailPoll); switchView('wizard'); });
$('#btnNewEngagement')?.addEventListener('click', () => { leaveLiveJob(); clearInterval(state.detailPoll); switchView('wizard'); });
if ($('#btnNewPentest2')) $('#btnNewPentest2').addEventListener('click', () => { leaveLiveJob(); clearInterval(state.detailPoll); switchView('wizard'); });
// ---------------------------------------------------------------------------
// Finding detail modal — full evidence/impact/remediation + any PoC script
@@ -2611,16 +2640,19 @@ function openFairParams(params) {
function showDashboard() {
leaveLiveJob();
clearInterval(state.detailPoll);
show($('#wizardView'), false);
show($('#liveView'), false);
show($('#detailView'), false);
show($('#dashView'), true);
renderDashboard();
switchView('dash');
}
$('#btnDashboard').addEventListener('click', showDashboard);
$('#btnDashRefresh').addEventListener('click', renderDashboard);
$('#dashRange').addEventListener('change', renderDashboard);
$('#runFilter').addEventListener('input', (e) => { state.runFilter = e.target.value; renderSidebar(); });
// Sidebar nav items
$$('.sb-nav-item').forEach(btn => {
btn.addEventListener('click', () => {
if (btn.dataset.view === 'wizard') { leaveLiveJob(); clearInterval(state.detailPoll); }
switchView(btn.dataset.view);
if (window.matchMedia(NARROW).matches) setSidebar(false);
});
});
$('#btnDashRefresh')?.addEventListener('click', renderDashboard);
$('#dashRange')?.addEventListener('change', renderDashboard);
$('#runFilter')?.addEventListener('input', (e) => { state.runFilter = e.target.value; renderSidebar(); });
// ---------------------------------------------------------------------------
// boot
@@ -2641,8 +2673,7 @@ $('#sidebarScrim').addEventListener('click', () => setSidebar(false));
$('#sbGroups').addEventListener('click', (e) => {
if (e.target.closest('.sb-run') && window.matchMedia(NARROW).matches) setSidebar(false);
});
$('#btnDashboard').addEventListener('click', () => { if (window.matchMedia(NARROW).matches) setSidebar(false); });
$('#btnNewEngagement').addEventListener('click', () => { if (window.matchMedia(NARROW).matches) setSidebar(false); });
$('#btnNewEngagement')?.addEventListener('click', () => { if (window.matchMedia(NARROW).matches) setSidebar(false); });
// Growing the window past the breakpoint leaves a scrim over a sidebar that is
// no longer a drawer.
window.matchMedia(NARROW).addEventListener('change', (m) => { if (!m.matches) setSidebar(false); });
@@ -3365,3 +3396,210 @@ const adoptGraph = (r) => {
$('#btnGraphRecompute')?.addEventListener('click', (e) => cliAction(e.currentTarget, 'Recompute graph', '/graph', {}, adoptGraph));
$('#btnGraphPush')?.addEventListener('click', (e) => cliAction(e.currentTarget, 'Push to Neo4j', '/graph', { push: true }, adoptGraph));
$('#btnRetest')?.addEventListener('click', (e) => cliAction(e.currentTarget, 'Re-test PoCs', '/retest', {}));
// ---------------------------------------------------------------------------
// new pages — pentests / PR reviews / issues / repos / domains / networks /
// integrations / settings
// ---------------------------------------------------------------------------
async function loadPentests() {
const body = $('#pentestsBody');
if (!body) return;
try {
const runs = await api('/api/runs');
if (!runs.length) { body.innerHTML = '<div class="empty-state">No pentests yet. Click <strong>+ New Pentest</strong> to start.</div>'; return; }
const rows = runs.map(r => {
const mode = (r.id || '').startsWith('ns-wb-') ? 'whitebox' : (r.id || '').startsWith('ns-gb-') ? 'greybox' : (r.id || '').startsWith('ns-host-') ? 'host' : 'blackbox';
const when = r.when ? timeAgo(Math.floor(new Date(r.when).getTime() / 1000)) : '';
return `<tr class="pentest-row" data-id="${esc(r.id)}">
<td class="mono">${esc(r.id)}</td>
<td>${esc(r.target || r.name || '')}</td>
<td>${esc(mode)}</td>
<td>${r.findings ?? 0}</td>
<td>${esc(r.state || 'done')}</td>
<td>${when}</td>
</tr>`;
}).join('');
body.innerHTML = `<table class="data-table"><thead><tr><th>ID</th><th>Target</th><th>Mode</th><th>Findings</th><th>Status</th><th>When</th></tr></thead><tbody>${rows}</tbody></table>`;
$$('.pentest-row', body).forEach(row => {
row.style.cursor = 'pointer';
row.addEventListener('click', () => { const run = runs.find(r => r.id === row.dataset.id); if (run) openRun(run); });
});
} catch (e) { body.innerHTML = `<div class="empty-state">Failed to load: ${esc(e.message)}</div>`; }
}
async function loadPrReviews() {
const body = $('#prreviewsBody');
if (!body) return;
try {
const runs = await api('/api/runs');
const prs = runs.filter(r => r.id && r.id.includes('-pr-'));
if (!prs.length) { body.innerHTML = '<div class="empty-state">No PR reviews yet. Run <code>neurosploit pr owner/repo 123</code> or use the MCP server.</div>'; return; }
const rows = prs.map(r => {
const when = r.when ? timeAgo(Math.floor(new Date(r.when).getTime() / 1000)) : '';
return `<tr class="pentest-row" data-id="${esc(r.id)}"><td class="mono">${esc(r.id)}</td><td>${esc(r.target || r.name || '')}</td><td>${r.findings ?? 0}</td><td>${esc(r.state || 'done')}</td><td>${when}</td></tr>`;
}).join('');
body.innerHTML = `<table class="data-table"><thead><tr><th>ID</th><th>Repository</th><th>Findings</th><th>Status</th><th>When</th></tr></thead><tbody>${rows}</tbody></table>`;
} catch (e) { body.innerHTML = `<div class="empty-state">Failed to load: ${esc(e.message)}</div>`; }
}
async function loadIssues() {
const body = $('#issuesBody');
if (!body) return;
try {
const data = await api('/api/issues');
if (!data.length) { body.innerHTML = '<div class="empty-state">No findings across any engagement yet.</div>'; return; }
const rows = data.map(f => {
const sc = sevClass(f.severity);
return `<tr><td><span class="pill ${sc}">${esc(f.severity)}</span></td><td>${esc(f.title)}</td><td class="mono">${esc(f.target || '')}</td><td class="mono">${esc(f.run || '')}</td><td>${esc(f.status || 'open')}</td></tr>`;
}).join('');
body.innerHTML = `<table class="data-table"><thead><tr><th>Severity</th><th>Title</th><th>Target</th><th>Run</th><th>Status</th></tr></thead><tbody>${rows}</tbody></table>`;
} catch (e) { body.innerHTML = `<div class="empty-state">Failed to load: ${esc(e.message)}</div>`; }
}
async function loadRepos() {
const body = $('#reposBody');
if (!body) return;
try {
const repos = await api('/api/config/repos');
if (!repos.length) { body.innerHTML = '<div class="empty-state">No repositories connected yet. Click <strong>+ Add Repository</strong> to connect one.</div>'; return; }
body.innerHTML = '<div class="config-grid">' + repos.map(r => `<div class="config-card">
<div class="config-card-title">${esc(r.name || r.url)}</div>
<div class="config-card-desc">${esc(r.description || r.url)}</div>
<div class="config-card-meta">${esc(r.provider || 'git')} · ${esc(r.branch || 'main')}</div>
<div class="config-card-actions"><button class="btn btn-danger" onclick="deleteRepo('${esc(r.id)}')">Remove</button></div>
</div>`).join('') + '</div>';
} catch { body.innerHTML = '<div class="empty-state">No repositories connected yet. Click <strong>+ Add Repository</strong> to connect one.</div>'; }
}
async function loadDomains() {
const body = $('#domainsBody');
if (!body) return;
try {
const domains = await api('/api/config/domains');
if (!domains.length) { body.innerHTML = '<div class="empty-state">No domains configured yet. Click <strong>+ Add Domain</strong> to add one.</div>'; return; }
body.innerHTML = '<div class="config-grid">' + domains.map(d => `<div class="config-card">
<div class="config-card-title">${esc(d.domain)}</div>
<div class="config-card-desc">${esc(d.description || 'Target domain')}</div>
<div class="config-card-meta">${d.subdomains ? d.subdomains.length + ' subdomains' : 'No subdomains'} · ${esc(d.scope || 'in-scope')}</div>
<div class="config-card-actions"><button class="btn btn-danger" onclick="deleteDomain('${esc(d.id)}')">Remove</button></div>
</div>`).join('') + '</div>';
} catch { body.innerHTML = '<div class="empty-state">No domains configured yet. Click <strong>+ Add Domain</strong> to add one.</div>'; }
}
async function loadNetworks() {
const body = $('#networksBody');
if (!body) return;
try {
const nets = await api('/api/config/networks');
if (!nets.length) { body.innerHTML = '<div class="empty-state">No networks configured yet. Click <strong>+ Add Network</strong> to add one.</div>'; return; }
body.innerHTML = '<div class="config-grid">' + nets.map(n => `<div class="config-card">
<div class="config-card-title">${esc(n.name || n.cidr)}</div>
<div class="config-card-desc">${esc(n.description || n.cidr)}</div>
<div class="config-card-meta">${esc(n.cidr)} · ${esc(n.transport || 'direct')}</div>
<div class="config-card-actions"><button class="btn btn-danger" onclick="deleteNetwork('${esc(n.id)}')">Remove</button></div>
</div>`).join('') + '</div>';
} catch { body.innerHTML = '<div class="empty-state">No networks configured yet. Click <strong>+ Add Network</strong> to add one.</div>'; }
}
async function loadIntegrations() {
const body = $('#integrationsBody');
if (!body) return;
try {
const integs = await api('/api/config/integrations');
const defs = [
{ id: 'github', name: 'GitHub', desc: 'Pull request reviews, commit status checks, and repository scanning' },
{ id: 'gitlab', name: 'GitLab', desc: 'Merge request reviews and repository scanning' },
{ id: 'jira', name: 'Jira', desc: 'Create Jira tickets for confirmed findings' },
{ id: 'slack', name: 'Slack', desc: 'Notifications for completed engagements and critical findings' },
{ id: 'mcp', name: 'MCP Server', desc: 'Model Context Protocol — drive NeuroSploit from Claude Code, Codex, Cursor, Windsurf' },
];
body.innerHTML = '<div class="config-grid">' + defs.map(c => {
const cfg = integs.find(i => i.id === c.id) || {};
const on = cfg.enabled;
return `<div class="config-card">
<div class="config-card-title">${esc(c.name)}</div>
<div class="config-card-desc">${esc(c.desc)}</div>
<div class="config-card-meta"><span class="integration-dot ${on ? 'connected' : 'disconnected'}"></span> ${on ? 'Connected' : 'Not configured'}</div>
<div class="config-card-actions"><button class="btn" onclick="configureIntegration('${c.id}')">${on ? 'Configure' : 'Connect'}</button></div>
</div>`;
}).join('') + '</div>';
} catch { body.innerHTML = '<div class="empty-state">Failed to load integrations.</div>'; }
}
async function loadSettings() {
const body = $('#settingsBody');
if (!body) return;
try {
const cfg = await api('/api/config/settings');
body.innerHTML = `<div class="config-grid">
<div class="config-card">
<div class="config-card-title">General</div>
<div class="form-group"><label>Default model</label><input type="text" id="settingModel" value="${esc(cfg.defaultModel || '')}" placeholder="anthropic:claude-opus-4-8" /></div>
<div class="form-group"><label>Default environment</label><select id="settingEnv">
<option value="lab" ${cfg.environment === 'lab' ? 'selected' : ''}>Lab</option>
<option value="development" ${cfg.environment === 'development' ? 'selected' : ''}>Development</option>
<option value="staging" ${cfg.environment === 'staging' ? 'selected' : ''}>Staging</option>
<option value="production" ${!cfg.environment || cfg.environment === 'production' ? 'selected' : ''}>Production</option>
</select></div>
<div class="form-group"><label>TypeSafe</label><select id="settingTypesafe">
<option value="auto" ${!cfg.typesafe || cfg.typesafe === 'auto' ? 'selected' : ''}>Auto</option>
<option value="on" ${cfg.typesafe === 'on' ? 'selected' : ''}>On</option>
<option value="off" ${cfg.typesafe === 'off' ? 'selected' : ''}>Off</option>
</select></div>
<div class="config-card-actions"><button class="btn btn-accent" onclick="saveSettings()">Save</button></div>
</div>
<div class="config-card">
<div class="config-card-title">Network</div>
<div class="form-group"><label>Default transport</label><input type="text" id="settingTransport" value="${esc(cfg.transport || '')}" placeholder="direct / socks5://host:port" /></div>
<div class="form-group"><label>OOB domain</label><input type="text" id="settingOob" value="${esc(cfg.oobDomain || '')}" placeholder="oob.yourdomain.com" /></div>
<div class="form-group"><label>Intercept proxy</label><input type="text" id="settingIntercept" value="${esc(cfg.intercept || '')}" placeholder="burp / caido / http://host:port" /></div>
<div class="config-card-actions"><button class="btn btn-accent" onclick="saveSettings()">Save</button></div>
</div>
<div class="config-card">
<div class="config-card-title">MCP Server</div>
<div class="config-card-desc">Run NeuroSploit as an MCP server for Claude Code, Codex, Cursor, Windsurf and other MCP clients.</div>
<div class="config-card-meta" style="margin-top:var(--sp-2)"><code>neurosploit mcp</code><br/>Config: <code>{"command":"neurosploit","args":["mcp"]}</code></div>
</div>
</div>`;
} catch { body.innerHTML = '<div class="empty-state">Failed to load settings.</div>'; }
}
function deleteRepo(id) { api(`/api/config/repos/${id}`, { method: 'DELETE' }).then(() => loadRepos()).catch(e => toast(e.message, 'error')); }
function deleteDomain(id) { api(`/api/config/domains/${id}`, { method: 'DELETE' }).then(() => loadDomains()).catch(e => toast(e.message, 'error')); }
function deleteNetwork(id) { api(`/api/config/networks/${id}`, { method: 'DELETE' }).then(() => loadNetworks()).catch(e => toast(e.message, 'error')); }
function configureIntegration(id) { toast('Integration configuration — coming soon', 'info'); }
async function saveSettings() {
const data = {
defaultModel: $('#settingModel')?.value || '',
environment: $('#settingEnv')?.value || 'production',
typesafe: $('#settingTypesafe')?.value || 'auto',
transport: $('#settingTransport')?.value || '',
oobDomain: $('#settingOob')?.value || '',
intercept: $('#settingIntercept')?.value || '',
};
try { await api('/api/config/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); toast('Settings saved', 'ok'); }
catch (e) { toast(`Failed: ${e.message}`, 'error'); }
}
// Add-item modals
function addRepoPrompt() {
const url = prompt('Repository URL or owner/repo:');
if (!url) return;
const name = prompt('Name (optional):') || '';
api('/api/config/repos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url, name, branch: 'main' }) })
.then(() => loadRepos()).catch(e => toast(e.message, 'error'));
}
function addDomainPrompt() {
const domain = prompt('Domain (e.g. app.example.com):');
if (!domain) return;
api('/api/config/domains', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ domain }) })
.then(() => loadDomains()).catch(e => toast(e.message, 'error'));
}
function addNetworkPrompt() {
const cidr = prompt('Network CIDR or host (e.g. 10.0.0.0/24):');
if (!cidr) return;
const name = prompt('Name (optional):') || '';
api('/api/config/networks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ cidr, name }) })
.then(() => loadNetworks()).catch(e => toast(e.message, 'error'));
}
+145 -15
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>NeuroSploit v4.3.1 — Console</title>
<title>NeuroSploit v4.3.2 — Console</title>
<link rel="icon" type="image/svg+xml" href="logo.svg">
<link rel="stylesheet" href="/vendor/xterm.css" />
<link rel="stylesheet" href="/style.css" />
@@ -35,18 +35,33 @@
<button class="icon-btn" id="btnThemeToggle" title="Toggle light / dark theme">☾</button>
</div>
<button class="sb-new" id="btnNewEngagement">+ New engagement</button>
<button class="sb-link" id="btnDashboard">▤ Dashboard</button>
<button class="sb-new" id="btnNewEngagement">+ New Pentest</button>
<div class="sb-search">
<span class="search-icon" aria-hidden="true">⌕</span>
<input id="runFilter" type="text" autocomplete="off" placeholder="Filter runs" aria-label="Filter runs" />
<nav class="sb-nav" id="sbNav">
<button class="sb-nav-item active" data-view="dash"><span class="sb-nav-icon">▤</span> Dashboard</button>
<button class="sb-nav-item" data-view="pentests"><span class="sb-nav-icon">⊞</span> Pentests</button>
<button class="sb-nav-item" data-view="prreviews"><span class="sb-nav-icon">⎇</span> PR Reviews</button>
<button class="sb-nav-item" data-view="issues"><span class="sb-nav-icon">⚑</span> Issues</button>
<div class="sb-nav-separator"></div>
<button class="sb-nav-item" data-view="repos"><span class="sb-nav-icon">◎</span> Repositories</button>
<button class="sb-nav-item" data-view="domains"><span class="sb-nav-icon">◇</span> Domains</button>
<button class="sb-nav-item" data-view="networks"><span class="sb-nav-icon">⬡</span> Networks</button>
<div class="sb-nav-separator"></div>
<button class="sb-nav-item" data-view="integrations"><span class="sb-nav-icon">⚙</span> Integrations</button>
<button class="sb-nav-item" data-view="settings"><span class="sb-nav-icon">☰</span> Settings</button>
</nav>
<div class="sb-run-history" id="sbRunHistory">
<div class="sb-search">
<span class="search-icon" aria-hidden="true">⌕</span>
<input id="runFilter" type="text" autocomplete="off" placeholder="Filter runs" aria-label="Filter runs" />
</div>
<div class="sb-groups" id="sbGroups"><!-- populated by app.js --></div>
</div>
<div class="sb-groups" id="sbGroups"><!-- populated by app.js --></div>
<div class="sb-bottom">
<span class="sb-version" id="sbVersion">v4.3.1</span>
<span class="sb-version" id="sbVersion">v4.3.2</span>
<div class="sb-bottom-actions">
<button class="icon-btn" id="btnOpenAuth" title="Auth &amp; API keys">🔑</button>
<button class="icon-btn" id="btnOpenRepl" title="Open terminal (Ctrl+`)">❭_</button>
@@ -78,11 +93,126 @@
</div>
</section>
<!-- ============ PENTESTS ============ -->
<section class="page-view" id="pentestsView" hidden>
<header class="topbar">
<div>
<div class="topbar-title">Pentests</div>
<div class="topbar-sub">All engagements — black-box, white-box, greybox, host, AI, mobile, container</div>
</div>
<div class="topbar-spacer"></div>
<input type="text" id="pentestSearch" class="topbar-search" placeholder="Search pentests…" />
<button class="btn btn-accent" id="btnNewPentest2">+ New Pentest</button>
</header>
<div class="page-body" id="pentestsBody"><div class="empty-state">Loading…</div></div>
</section>
<!-- ============ PR REVIEWS ============ -->
<section class="page-view" id="prreviewsView" hidden>
<header class="topbar">
<div>
<div class="topbar-title">PR Reviews</div>
<div class="topbar-sub">Security reviews of pull requests</div>
</div>
<div class="topbar-spacer"></div>
<button class="btn btn-accent" id="btnNewPrReview">+ New Review</button>
</header>
<div class="page-body" id="prreviewsBody"><div class="empty-state">No PR reviews yet. Run <code>neurosploit pr owner/repo 123</code> or use the MCP server.</div></div>
</section>
<!-- ============ ISSUES ============ -->
<section class="page-view" id="issuesView" hidden>
<header class="topbar">
<div>
<div class="topbar-title">Issues</div>
<div class="topbar-sub">All findings across every engagement</div>
</div>
<div class="topbar-spacer"></div>
<select id="issuesSevFilter" class="dash-range">
<option value="">All severities</option>
<option value="critical">Critical</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
<select id="issuesStatusFilter" class="dash-range">
<option value="">All statuses</option>
<option value="open">Open</option>
<option value="fixed">Fixed</option>
</select>
<input type="text" id="issuesSearch" class="topbar-search" placeholder="Search issues…" />
</header>
<div class="page-body" id="issuesBody"><div class="empty-state">Loading…</div></div>
</section>
<!-- ============ REPOSITORIES ============ -->
<section class="page-view" id="reposView" hidden>
<header class="topbar">
<div>
<div class="topbar-title">Repositories</div>
<div class="topbar-sub">Connected source repositories for white-box, greybox, and PR reviews</div>
</div>
<div class="topbar-spacer"></div>
<button class="btn btn-accent" id="btnAddRepo">+ Add Repository</button>
</header>
<div class="page-body" id="reposBody"><div class="empty-state">No repositories connected yet.</div></div>
</section>
<!-- ============ DOMAINS ============ -->
<section class="page-view" id="domainsView" hidden>
<header class="topbar">
<div>
<div class="topbar-title">Domains</div>
<div class="topbar-sub">Target domains and subdomains for black-box engagements</div>
</div>
<div class="topbar-spacer"></div>
<button class="btn btn-accent" id="btnAddDomain">+ Add Domain</button>
</header>
<div class="page-body" id="domainsBody"><div class="empty-state">No domains configured yet.</div></div>
</section>
<!-- ============ NETWORKS ============ -->
<section class="page-view" id="networksView" hidden>
<header class="topbar">
<div>
<div class="topbar-title">Networks</div>
<div class="topbar-sub">Network ranges and hosts for infrastructure scanning</div>
</div>
<div class="topbar-spacer"></div>
<button class="btn btn-accent" id="btnAddNetwork">+ Add Network</button>
</header>
<div class="page-body" id="networksBody"><div class="empty-state">No networks configured yet.</div></div>
</section>
<!-- ============ INTEGRATIONS ============ -->
<section class="page-view" id="integrationsView" hidden>
<header class="topbar">
<div>
<div class="topbar-title">Integrations</div>
<div class="topbar-sub">Connect external services — GitHub, GitLab, Jira, Slack, CI/CD</div>
</div>
<div class="topbar-spacer"></div>
</header>
<div class="page-body" id="integrationsBody"><div class="empty-state">Loading…</div></div>
</section>
<!-- ============ SETTINGS ============ -->
<section class="page-view" id="settingsView" hidden>
<header class="topbar">
<div>
<div class="topbar-title">Settings</div>
<div class="topbar-sub">Console configuration, API keys, and preferences</div>
</div>
<div class="topbar-spacer"></div>
</header>
<div class="page-body" id="settingsBody"><div class="empty-state">Loading…</div></div>
</section>
<!-- ============ WIZARD (new engagement) ============ -->
<section class="wizard" id="wizardView">
<header class="topbar">
<div>
<div class="topbar-title">New engagement</div>
<div class="topbar-title">New Pentest</div>
<div class="topbar-sub">Asset → Scope &amp; Auth → Leads → Model &amp; Run → Review</div>
</div>
<div class="topbar-spacer"></div>
@@ -350,7 +480,7 @@
<a class="btn" id="btnDownloadLog" download>Download log</a>
<button class="btn" id="btnReportNow" title="Write the report from what has been found so far">Report so far</button>
<button class="btn btn-danger" id="btnStopRun">Stop</button>
<button class="btn" id="btnBackToBoard">← New engagement</button>
<button class="btn" id="btnBackToBoard">← Back</button>
</div>
</header>
<div class="progress-wrap">
@@ -416,7 +546,7 @@
<button class="btn" id="btnRetest" title="Re-run every finding's recorded proof (neurosploit poc) and see what still reproduces">⟳ Re-test</button>
<a class="btn" id="detailOpenAudit" target="_blank" hidden title="Every action this run took, hash-chained">Audit trail</a>
<button class="btn btn-danger" id="btnDeleteRun" title="Permanently delete this session and its report">🗑 Delete</button>
<button class="btn" id="btnDetailBack">← New engagement</button>
<button class="btn" id="btnDetailBack">← Back</button>
</div>
</header>
<nav class="run-tabs">
@@ -639,9 +769,9 @@
</div>
<div class="field-group">
<label class="field-label" for="collectionFile">API collection (Postman / OpenAPI / Swagger / HAR)</label>
<input id="collectionFile" type="file" accept=".json,application/json,.har" />
<input id="collectionPath" type="text" placeholder="…or a path on disk: api.postman_collection.json" />
<div class="field-help" id="collectionHelp">Imports the endpoints as known surface — every route is tested directly, alongside recon. JSON only (convert YAML specs first).</div>
<input id="collectionFile" type="file" accept=".json,application/json,.har" multiple />
<input id="collectionPath" type="text" placeholder="…or a path on disk: file.json or a directory of .json files" />
<div class="field-help" id="collectionHelp">Imports endpoints as known surface — file or directory of .json files. Every route is tested directly, alongside recon.</div>
</div>
</div>
</div>
+73
View File
@@ -207,6 +207,79 @@ a { color: var(--accent); text-decoration: none; }
}
.sb-link:hover { background: var(--surface-3); color: var(--text); }
.sb-nav { display: flex; flex-direction: column; gap: 1px; padding: 0 var(--sp-2) var(--sp-3); }
.sb-nav-item {
display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-2) var(--sp-3);
border: none; border-radius: var(--radius-sm);
background: transparent; color: var(--text-dim); font-size: var(--fs-sm); text-align: left;
cursor: pointer; transition: background var(--t-fast), color var(--t-fast);
}
.sb-nav-item:hover { background: var(--surface-3); color: var(--text); }
.sb-nav-item.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.sb-nav-icon { width: 18px; text-align: center; font-size: var(--fs-base); flex: none; }
.sb-nav-separator { height: 1px; background: var(--border); margin: var(--sp-2) var(--sp-3); }
.sb-run-history { display: flex; flex-direction: column; flex: 1; overflow: hidden; }
.page-view { display: none; flex-direction: column; flex: 1; overflow: hidden; }
.page-view:not([hidden]) { display: flex; }
.page-body { flex: 1; overflow-y: auto; padding: var(--sp-5); }
.topbar-search {
padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius-sm);
background: var(--surface); color: var(--text); font-size: var(--fs-sm); width: 220px;
}
.data-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.data-table th { text-align: left; padding: var(--sp-2) var(--sp-3); font-weight: 600; color: var(--text-dim); border-bottom: 2px solid var(--border); font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .04em; }
.data-table td { padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--border); vertical-align: top; }
.data-table tr:hover td { background: var(--surface-2); }
.data-table .mono { font-family: var(--mono); font-size: var(--fs-xs); }
.config-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: var(--sp-4); }
.config-card {
border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--sp-4);
background: var(--surface); transition: border-color var(--t-fast);
}
.config-card:hover { border-color: var(--border-strong); }
.config-card-title { font-size: var(--fs-lg); font-weight: 600; margin-bottom: var(--sp-2); display: flex; align-items: center; gap: var(--sp-2); }
.config-card-desc { font-size: var(--fs-sm); color: var(--text-dim); margin-bottom: var(--sp-3); }
.config-card-meta { font-size: var(--fs-xs); color: var(--text-faint); font-family: var(--mono); }
.config-card-actions { display: flex; gap: var(--sp-2); margin-top: var(--sp-3); padding-top: var(--sp-3); border-top: 1px solid var(--border); }
.btn-accent {
background: var(--accent); color: var(--accent-contrast); border: 1px solid var(--accent);
padding: var(--sp-2) var(--sp-4); border-radius: var(--radius-sm); font-size: var(--fs-sm); font-weight: 600;
}
.btn-accent:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.integration-card { display: flex; align-items: center; gap: var(--sp-4); }
.integration-icon { width: 40px; height: 40px; border-radius: var(--radius-sm); background: var(--surface-3); display: flex; align-items: center; justify-content: center; font-size: var(--fs-xl); flex: none; }
.integration-info { flex: 1; }
.integration-status { font-size: var(--fs-xs); display: flex; align-items: center; gap: var(--sp-1); }
.integration-dot { width: 8px; height: 8px; border-radius: 50%; }
.integration-dot.connected { background: var(--sev-low-fg); }
.integration-dot.disconnected { background: var(--text-faint); }
.stats-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--sp-4); margin-bottom: var(--sp-5); }
.stat-card {
border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--sp-4);
background: var(--surface);
}
.stat-label { font-size: var(--fs-xs); color: var(--text-dim); text-transform: uppercase; letter-spacing: .04em; font-weight: 600; }
.stat-value { font-size: var(--fs-2xl); font-weight: 700; margin-top: var(--sp-1); }
.stat-sub { font-size: var(--fs-xs); color: var(--text-faint); margin-top: var(--sp-1); }
.form-modal { max-width: 520px; }
.form-group { margin-bottom: var(--sp-4); }
.form-group label { display: block; font-size: var(--fs-sm); font-weight: 600; margin-bottom: var(--sp-1); }
.form-group input, .form-group select, .form-group textarea {
width: 100%; padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius-sm);
background: var(--surface); color: var(--text); font-size: var(--fs-sm);
}
.form-group .field-help { font-size: var(--fs-xs); color: var(--text-faint); margin-top: var(--sp-1); }
.form-actions { display: flex; gap: var(--sp-2); justify-content: flex-end; margin-top: var(--sp-5); }
.sb-search { position: relative; margin: 0 var(--sp-4) var(--sp-3); }
.sb-search input { padding-left: 26px; font-size: var(--fs-sm); }
.sb-search .search-icon { left: 8px; }
+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 });