mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-10-21 23:11:50 +02:00
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:
1 parent
e8b7f61890
commit
71339a9ddb
11 files changed
+1072
-93
No files matched your search
+253
-15
@@ -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
@@ -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 & 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 & Auth → Leads → Model & 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>
|
||||
|
||||
@@ -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
@@ -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 });
|
||||
|
||||
Reference in new issue
Block a user