mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-10-19 14:07:16 +02:00
fix(web): functional modals for repos/domains/networks/integrations
Replace browser prompt() dialogs with proper floating modals for Add Repository, Add Domain, and Add Network buttons. Replace placeholder configureIntegration toast with real per-service config modal (GitHub, GitLab, Jira, Slack, MCP). Add overlay click and Escape key to close all modals. Fix Issues page to show real target URL from run meta.json. Hide run history sidebar on asset pages. 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
71339a9ddb
commit
84a2cca4e8
3 files changed
+274
-23
No files matched your search
+156
-22
@@ -112,7 +112,7 @@ function switchView(viewName) {
|
||||
btn.classList.toggle('active', btn.dataset.view === viewName);
|
||||
});
|
||||
const runHist = $('#sbRunHistory');
|
||||
if (runHist) runHist.style.display = ['dash','pentests','wizard'].includes(viewName) ? '' : 'none';
|
||||
if (runHist) runHist.style.display = ['dash','wizard','live','detail'].includes(viewName) ? '' : 'none';
|
||||
if (viewName === 'pentests') loadPentests();
|
||||
if (viewName === 'issues') loadIssues();
|
||||
if (viewName === 'repos') loadRepos();
|
||||
@@ -3513,6 +3513,9 @@ async function loadIntegrations() {
|
||||
{ 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' },
|
||||
{ id: 'aws', name: 'AWS', desc: 'Cloud security testing — IAM, S3, Lambda, EC2, ECS, RDS misconfigurations' },
|
||||
{ id: 'gcp', name: 'GCP', desc: 'Cloud security testing — IAM, GCS, Cloud Functions, GKE, Cloud SQL misconfigurations' },
|
||||
{ id: 'azure', name: 'Azure', desc: 'Cloud security testing — Entra ID, Blob Storage, Functions, AKS, SQL misconfigurations' },
|
||||
];
|
||||
body.innerHTML = '<div class="config-grid">' + defs.map(c => {
|
||||
const cfg = integs.find(i => i.id === c.id) || {};
|
||||
@@ -3568,7 +3571,63 @@ async function loadSettings() {
|
||||
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'); }
|
||||
function configureIntegration(id) {
|
||||
const fields = {
|
||||
github: [
|
||||
{ id: 'integToken', label: 'Personal Access Token', type: 'password', placeholder: 'ghp_...' },
|
||||
{ id: 'integOrg', label: 'Organization / Owner', placeholder: 'my-org' },
|
||||
{ id: 'integWebhook', label: 'Webhook Secret (optional)', type: 'password', placeholder: 'whsec_...' },
|
||||
],
|
||||
gitlab: [
|
||||
{ id: 'integToken', label: 'Access Token', type: 'password', placeholder: 'glpat-...' },
|
||||
{ id: 'integUrl', label: 'GitLab URL', placeholder: 'https://gitlab.com' },
|
||||
{ id: 'integProject', label: 'Project ID or path', placeholder: 'group/project' },
|
||||
],
|
||||
jira: [
|
||||
{ id: 'integUrl', label: 'Jira URL', placeholder: 'https://myteam.atlassian.net' },
|
||||
{ id: 'integEmail', label: 'Email', placeholder: 'user@example.com' },
|
||||
{ id: 'integToken', label: 'API Token', type: 'password', placeholder: '' },
|
||||
{ id: 'integProject', label: 'Project Key', placeholder: 'SEC' },
|
||||
],
|
||||
slack: [
|
||||
{ id: 'integWebhook', label: 'Webhook URL', placeholder: 'https://hooks.slack.com/services/...' },
|
||||
{ id: 'integChannel', label: 'Channel (optional)', placeholder: '#security-alerts' },
|
||||
],
|
||||
mcp: [
|
||||
{ id: 'integPort', label: 'MCP Server Port', placeholder: '9100' },
|
||||
{ id: 'integTransport', label: 'Transport', placeholder: 'stdio' },
|
||||
],
|
||||
aws: [
|
||||
{ id: 'integAccessKey', label: 'Access Key ID', placeholder: 'AKIA...' },
|
||||
{ id: 'integSecretKey', label: 'Secret Access Key', type: 'password', placeholder: '' },
|
||||
{ id: 'integRegion', label: 'Default Region', placeholder: 'us-east-1' },
|
||||
{ id: 'integProfile', label: 'AWS Profile (optional)', placeholder: 'default' },
|
||||
{ id: 'integRoleArn', label: 'Assume Role ARN (optional)', placeholder: 'arn:aws:iam::123456:role/SecurityAudit' },
|
||||
],
|
||||
gcp: [
|
||||
{ id: 'integServiceAccount', label: 'Service Account Key (JSON path)', placeholder: '/path/to/sa-key.json' },
|
||||
{ id: 'integProject', label: 'Project ID', placeholder: 'my-project-123' },
|
||||
{ id: 'integRegion', label: 'Default Region', placeholder: 'us-central1' },
|
||||
],
|
||||
azure: [
|
||||
{ id: 'integTenantId', label: 'Tenant ID', placeholder: '' },
|
||||
{ id: 'integClientId', label: 'Client (App) ID', placeholder: '' },
|
||||
{ id: 'integClientSecret', label: 'Client Secret', type: 'password', placeholder: '' },
|
||||
{ id: 'integSubscriptionId', label: 'Subscription ID', placeholder: '' },
|
||||
],
|
||||
};
|
||||
const defs = fields[id] || [];
|
||||
const title = { github: 'GitHub', gitlab: 'GitLab', jira: 'Jira', slack: 'Slack', mcp: 'MCP Server', aws: 'AWS', gcp: 'GCP', azure: 'Azure' }[id] || id;
|
||||
$('#integModalTitle').textContent = `Configure ${title}`;
|
||||
$('#integModalBody').innerHTML = defs.map(f =>
|
||||
`<div class="field-group">
|
||||
<label class="field-label" for="${f.id}">${esc(f.label)}</label>
|
||||
<input id="${f.id}" type="${f.type || 'text'}" placeholder="${esc(f.placeholder || '')}" spellcheck="false" />
|
||||
</div>`
|
||||
).join('');
|
||||
$('#integrationModal').dataset.integId = id;
|
||||
$('#integrationModal').hidden = false;
|
||||
}
|
||||
async function saveSettings() {
|
||||
const data = {
|
||||
defaultModel: $('#settingModel')?.value || '',
|
||||
@@ -3583,23 +3642,98 @@ async function saveSettings() {
|
||||
}
|
||||
|
||||
// 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'));
|
||||
}
|
||||
function openModal(id) { $(id).hidden = false; }
|
||||
function closeModal(id) { $(id).hidden = true; }
|
||||
|
||||
$('#btnAddRepo')?.addEventListener('click', () => {
|
||||
$('#repoUrl').value = '';
|
||||
$('#repoName').value = '';
|
||||
$('#repoBranch').value = 'main';
|
||||
$('#repoDesc').value = '';
|
||||
openModal('#repoModal');
|
||||
});
|
||||
$('#btnAddDomain')?.addEventListener('click', () => {
|
||||
$('#domainName').value = '';
|
||||
$('#domainDesc').value = '';
|
||||
if ($('#domainScope')) $('#domainScope').value = 'in-scope';
|
||||
openModal('#domainModal');
|
||||
});
|
||||
$('#btnAddNetwork')?.addEventListener('click', () => {
|
||||
$('#networkCidr').value = '';
|
||||
$('#networkName').value = '';
|
||||
$('#networkDesc').value = '';
|
||||
$('#networkTransport').value = '';
|
||||
openModal('#networkModal');
|
||||
});
|
||||
|
||||
$('#btnRepoSubmit')?.addEventListener('click', async () => {
|
||||
const url = $('#repoUrl')?.value?.trim();
|
||||
if (!url) { toast('Repository URL is required', 'error'); return; }
|
||||
try {
|
||||
await api('/api/config/repos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
|
||||
url,
|
||||
name: $('#repoName')?.value?.trim() || '',
|
||||
branch: $('#repoBranch')?.value?.trim() || 'main',
|
||||
description: $('#repoDesc')?.value?.trim() || '',
|
||||
})});
|
||||
closeModal('#repoModal');
|
||||
toast('Repository added', 'ok');
|
||||
loadRepos();
|
||||
} catch (e) { toast(e.message, 'error'); }
|
||||
});
|
||||
|
||||
$('#btnDomainSubmit')?.addEventListener('click', async () => {
|
||||
const domain = $('#domainName')?.value?.trim();
|
||||
if (!domain) { toast('Domain is required', 'error'); return; }
|
||||
try {
|
||||
await api('/api/config/domains', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
|
||||
domain,
|
||||
description: $('#domainDesc')?.value?.trim() || '',
|
||||
scope: $('#domainScope')?.value || 'in-scope',
|
||||
})});
|
||||
closeModal('#domainModal');
|
||||
toast('Domain added', 'ok');
|
||||
loadDomains();
|
||||
} catch (e) { toast(e.message, 'error'); }
|
||||
});
|
||||
|
||||
$('#btnNetworkSubmit')?.addEventListener('click', async () => {
|
||||
const cidr = $('#networkCidr')?.value?.trim();
|
||||
if (!cidr) { toast('CIDR or host is required', 'error'); return; }
|
||||
try {
|
||||
await api('/api/config/networks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
|
||||
cidr,
|
||||
name: $('#networkName')?.value?.trim() || '',
|
||||
description: $('#networkDesc')?.value?.trim() || '',
|
||||
transport: $('#networkTransport')?.value?.trim() || '',
|
||||
})});
|
||||
closeModal('#networkModal');
|
||||
toast('Network added', 'ok');
|
||||
loadNetworks();
|
||||
} catch (e) { toast(e.message, 'error'); }
|
||||
});
|
||||
|
||||
$('#btnIntegSubmit')?.addEventListener('click', async () => {
|
||||
const id = $('#integrationModal').dataset.integId;
|
||||
if (!id) return;
|
||||
const inputs = $('#integModalBody').querySelectorAll('input');
|
||||
const config = {};
|
||||
inputs.forEach(inp => { config[inp.id.replace('integ', '').toLowerCase()] = inp.value.trim(); });
|
||||
try {
|
||||
await api(`/api/config/integrations/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: true, config }) });
|
||||
closeModal('#integrationModal');
|
||||
toast(`${id} integration saved`, 'ok');
|
||||
loadIntegrations();
|
||||
} catch (e) { toast(e.message, 'error'); }
|
||||
});
|
||||
|
||||
['#repoModal', '#domainModal', '#networkModal', '#integrationModal'].forEach(sel => {
|
||||
$(sel)?.addEventListener('click', (e) => { if (e.target === $(sel)) closeModal(sel); });
|
||||
});
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
['#repoModal', '#domainModal', '#networkModal', '#integrationModal'].forEach(sel => {
|
||||
if (!$(sel)?.hidden) closeModal(sel);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -848,6 +848,121 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ ADD REPOSITORY MODAL ============ -->
|
||||
<div class="modal-overlay" id="repoModal" hidden>
|
||||
<div class="modal modal-sm">
|
||||
<div class="modal-head">
|
||||
<div class="title">Add Repository</div>
|
||||
<button class="icon-btn" onclick="$('#repoModal').hidden=true">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="repoUrl">Repository URL or path</label>
|
||||
<input id="repoUrl" type="text" placeholder="https://github.com/owner/repo or /path/to/repo" spellcheck="false" />
|
||||
<div class="field-help">GitHub URL, owner/repo shorthand, GitLab URL, or a local path.</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="repoName">Display name (optional)</label>
|
||||
<input id="repoName" type="text" placeholder="My API" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="repoBranch">Default branch</label>
|
||||
<input id="repoBranch" type="text" placeholder="main" value="main" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="repoDesc">Description (optional)</label>
|
||||
<input id="repoDesc" type="text" placeholder="Production API service" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<button class="btn" onclick="$('#repoModal').hidden=true">Cancel</button>
|
||||
<button class="btn btn-primary" id="btnRepoSubmit">Add Repository</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ ADD DOMAIN MODAL ============ -->
|
||||
<div class="modal-overlay" id="domainModal" hidden>
|
||||
<div class="modal modal-sm">
|
||||
<div class="modal-head">
|
||||
<div class="title">Add Domain</div>
|
||||
<button class="icon-btn" onclick="$('#domainModal').hidden=true">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="domainName">Domain</label>
|
||||
<input id="domainName" type="text" placeholder="app.example.com" spellcheck="false" />
|
||||
<div class="field-help">The target domain for black-box scanning. Subdomains will be discovered during recon.</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="domainDesc">Description (optional)</label>
|
||||
<input id="domainDesc" type="text" placeholder="Production web application" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="domainScope">Scope</label>
|
||||
<select id="domainScope">
|
||||
<option value="in-scope" selected>In scope</option>
|
||||
<option value="observe-only">Observe only</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<button class="btn" onclick="$('#domainModal').hidden=true">Cancel</button>
|
||||
<button class="btn btn-primary" id="btnDomainSubmit">Add Domain</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ ADD NETWORK MODAL ============ -->
|
||||
<div class="modal-overlay" id="networkModal" hidden>
|
||||
<div class="modal modal-sm">
|
||||
<div class="modal-head">
|
||||
<div class="title">Add Network</div>
|
||||
<button class="icon-btn" onclick="$('#networkModal').hidden=true">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="networkCidr">CIDR or host</label>
|
||||
<input id="networkCidr" type="text" placeholder="10.0.0.0/24 or 192.168.1.10" spellcheck="false" />
|
||||
<div class="field-help">Network range in CIDR notation, or a single IP/hostname.</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="networkName">Name (optional)</label>
|
||||
<input id="networkName" type="text" placeholder="Corporate LAN" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="networkDesc">Description (optional)</label>
|
||||
<input id="networkDesc" type="text" placeholder="Internal network segment" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="networkTransport">Transport</label>
|
||||
<input id="networkTransport" type="text" placeholder="direct / socks5://host:port / ssh://user@bastion" spellcheck="false" />
|
||||
<div class="field-help">How to reach this network. Leave empty for direct access.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<button class="btn" onclick="$('#networkModal').hidden=true">Cancel</button>
|
||||
<button class="btn btn-primary" id="btnNetworkSubmit">Add Network</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ INTEGRATION CONFIG MODAL ============ -->
|
||||
<div class="modal-overlay" id="integrationModal" hidden>
|
||||
<div class="modal modal-sm">
|
||||
<div class="modal-head">
|
||||
<div class="title" id="integModalTitle">Configure Integration</div>
|
||||
<button class="icon-btn" onclick="$('#integrationModal').hidden=true">✕</button>
|
||||
</div>
|
||||
<div class="modal-body" id="integModalBody">
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<button class="btn" onclick="$('#integrationModal').hidden=true">Cancel</button>
|
||||
<button class="btn btn-primary" id="btnIntegSubmit">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toasts" id="toasts"></div>
|
||||
|
||||
<script src="/vendor/xterm.js"></script>
|
||||
|
||||
+3
-1
@@ -1967,7 +1967,9 @@ const server = http.createServer(async (req, res) => {
|
||||
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 });
|
||||
let runTarget = d;
|
||||
try { const meta = JSON.parse(await fsp.readFile(path.join(RUNS_DIR, d, 'meta.json'), 'utf8')); runTarget = meta.target || d; } catch {}
|
||||
for (const f of findings) all.push({ ...f, run: d, target: f.target || runTarget });
|
||||
} 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; };
|
||||
|
||||
Reference in new issue
Block a user