From ecced9d7137e04109d48a4e9d49a9caf6917070a Mon Sep 17 00:00:00 2001 From: CyberSecurityUP Date: Fri, 16 Oct 2026 17:25:24 -0300 Subject: [PATCH] feat(web): per-finding attack path, LLM config modal, severity badges, cloud mode - Graph tab: sidebar with per-finding filter + Overview; description panel shows finding details when selected; walk-based subgraph isolation - Settings: single Config button opens modal with all LLM providers; removed redundant MCP Server card - Issues: colored severity badges (critical/high/medium/low/info) - Sidebar: removed key and terminal icons from footer - Server: cloud environment overrides, vault & guardrails config endpoints Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_01UqW6BbQgqGkEDn3RYuCVoM --- web/public/app.js | 343 +++++++++++++++++++++++++++++++++++++++--- web/public/index.html | 220 ++++++++++++++++++++++++++- web/public/style.css | 36 ++++- web/server.js | 51 ++++++- 4 files changed, 620 insertions(+), 30 deletions(-) diff --git a/web/public/app.js b/web/public/app.js index 1512a24..a00b702 100644 --- a/web/public/app.js +++ b/web/public/app.js @@ -10,6 +10,7 @@ const MODE_LABELS = { greybox: { target: 'Target URL', help: 'The running application to exploit, alongside the source repo below.', showRepo: true, placeholder: 'https://target.example.com' }, host: { target: 'Target host / IP', help: 'Runs Linux / Windows / Active Directory agents.', showRepo: false, placeholder: '10.0.0.10' }, aitest: { target: 'AI endpoint URL', help: 'A live AI agent, LLM chat, or MCP endpoint (OWASP LLM Top 10).', showRepo: false, placeholder: 'https://target.example.com/chat' }, + cloud: { target: 'Cloud account / project', help: 'Test a connected cloud environment — select from configured AWS, GCP, or Azure integrations.', showRepo: false, placeholder: 'aws / gcp / azure', showCloudPicker: true }, }; const STEP_COUNT = 5; @@ -101,7 +102,7 @@ function clearFieldErrors() { $$('.field-error').forEach((el) => { el.textConten // view navigation // --------------------------------------------------------------------------- -const ALL_VIEWS = ['dashView','wizardView','liveView','detailView','pentestsView','prreviewsView','issuesView','reposView','domainsView','networksView','integrationsView','settingsView']; +const ALL_VIEWS = ['dashView','wizardView','liveView','detailView','pentestsView','prreviewsView','issuesView','reposView','domainsView','networksView','vaultView','guardrailsView','integrationsView','settingsView']; function switchView(viewName) { state.activeView = viewName; @@ -112,7 +113,7 @@ function switchView(viewName) { btn.classList.toggle('active', btn.dataset.view === viewName); }); const runHist = $('#sbRunHistory'); - if (runHist) runHist.style.display = ['dash','wizard','live','detail'].includes(viewName) ? '' : 'none'; + if (runHist) runHist.style.display = ['live','detail'].includes(viewName) ? '' : 'none'; if (viewName === 'pentests') loadPentests(); if (viewName === 'issues') loadIssues(); if (viewName === 'repos') loadRepos(); @@ -121,6 +122,8 @@ function switchView(viewName) { if (viewName === 'integrations') loadIntegrations(); if (viewName === 'settings') loadSettings(); if (viewName === 'prreviews') loadPrReviews(); + if (viewName === 'vault') loadVault(); + if (viewName === 'guardrails') loadGuardrails(); if (viewName === 'dash') renderDashboard(); } @@ -225,11 +228,39 @@ function selectMode(mode) { $('#targetHelp').textContent = cfg.help; $('#fieldTarget').placeholder = cfg.placeholder; show($('#fieldRepoGroup'), cfg.showRepo); + show($('#fieldCloudGroup'), !!cfg.showCloudPicker); + if (cfg.showCloudPicker) populateCloudPicker(); updateWizardSummary(); } $$('.mode-tile').forEach((tile) => tile.addEventListener('click', () => selectMode(tile.dataset.mode))); $('#fieldTarget').addEventListener('input', updateWizardSummary); +async function populateCloudPicker() { + const sel = $('#fieldCloudProvider'); + if (!sel) return; + sel.innerHTML = ''; + try { + const integs = await api('/api/config/integrations'); + const clouds = [ + { id: 'aws', label: 'AWS' }, + { id: 'gcp', label: 'GCP' }, + { id: 'azure', label: 'Azure' }, + ]; + for (const c of clouds) { + const cfg = integs.find(i => i.id === c.id); + const on = cfg?.enabled; + sel.innerHTML += ``; + } + } catch { /* keep empty */ } +} +$('#fieldCloudProvider')?.addEventListener('change', (e) => { + const v = e.target.value; + if (v) { + $('#fieldTarget').value = v; + updateWizardSummary(); + } +}); + // --------------------------------------------------------------------------- // agents / lead board (step 3) // --------------------------------------------------------------------------- @@ -659,6 +690,7 @@ async function startExploitation() { creds: state.credsPath || undefined, collectionPath: state.collectionPath || undefined, collectionData: state.collectionData || undefined, + cloudProvider: mode === 'cloud' ? ($('#fieldCloudProvider')?.value || target) : undefined, capability: state.authz.capability || undefined, inScope: state.authz.inScope.split(/[,;\s]+/).filter(Boolean), environment: state.authz.environment, @@ -1642,7 +1674,8 @@ function openRun(run) { if (run.state === 'running') { resolveJobId(run).then((jid) => { if (!jid) { // no live job (server restarted): fall back to static detail - show($('#wizardView'), false); show($('#liveView'), false); show($('#dashView'), false); show($('#detailView'), true); + ALL_VIEWS.forEach(id => { const el = $(`#${id}`); if (el) el.hidden = true; }); + show($('#detailView'), true); loadDetail(run.id); renderSidebar(); return; } localStorage.setItem(ACTIVE_JOB_KEY, jid); @@ -1656,7 +1689,8 @@ function openRun(run) { resumeFromSidebar(run); return; } - show($('#wizardView'), false); show($('#liveView'), false); show($('#dashView'), false); show($('#detailView'), true); + ALL_VIEWS.forEach(id => { const el = $(`#${id}`); if (el) el.hidden = true; }); + show($('#detailView'), true); loadDetail(run.id); renderSidebar(); } @@ -1761,7 +1795,8 @@ function openAuthModal() { $('#collectionPath').value = state.collectionPath; refreshKeyStatus(); } -['#btnOpenAuth', '#btnOpenAuth2', '#btnOpenAuth3'].forEach((sel) => $(sel)?.addEventListener('click', openAuthModal)); +$('#btnOpenAuth')?.addEventListener('click', () => switchView('vault')); +['#btnOpenAuth2', '#btnOpenAuth3'].forEach((sel) => $(sel)?.addEventListener('click', openAuthModal)); $('#btnCloseAuth').addEventListener('click', () => show($('#authModal'), false)); $('#authModal').addEventListener('click', (e) => { if (e.target.id === 'authModal') show($('#authModal'), false); }); @@ -2226,7 +2261,7 @@ function termOpen(mode) { function termClose() { show($('#termDock'), false); } function termToggle() { $('#termDock').hidden ? termOpen() : termClose(); } -$('#btnOpenRepl').addEventListener('click', () => termOpen()); +$('#btnOpenRepl')?.addEventListener('click', () => termOpen()); $('#btnOpenTerm2').addEventListener('click', () => termOpen()); $('#btnOpenTerm3').addEventListener('click', () => termOpen()); $('#btnSendPromptTerm').addEventListener('click', () => termOpen('job')); @@ -2644,12 +2679,14 @@ function showDashboard() { } // Sidebar nav items $$('.sb-nav-item').forEach(btn => { + if (!btn.dataset.view) return; btn.addEventListener('click', () => { if (btn.dataset.view === 'wizard') { leaveLiveJob(); clearInterval(state.detailPoll); } switchView(btn.dataset.view); if (window.matchMedia(NARROW).matches) setSidebar(false); }); }); +$('#sbTerminal')?.addEventListener('click', () => termOpen()); $('#btnDashRefresh')?.addEventListener('click', renderDashboard); $('#dashRange')?.addEventListener('change', renderDashboard); $('#runFilter')?.addEventListener('input', (e) => { state.runFilter = e.target.value; renderSidebar(); }); @@ -2680,6 +2717,7 @@ window.matchMedia(NARROW).addEventListener('change', (m) => { if (!m.matches) se async function boot() { applyTheme(); + switchView('dash'); selectMode('run'); goToStep(0); renderCustomLeads(); @@ -2810,12 +2848,21 @@ function extrasPanelsHtml(uid) {
`; + $('#btnOpenProviderKeys')?.addEventListener('click', () => openProviderKeysModal()); } catch { body.innerHTML = '
Failed to load settings.
'; } } @@ -3628,6 +3764,115 @@ function configureIntegration(id) { $('#integrationModal').dataset.integId = id; $('#integrationModal').hidden = false; } +// --------------------------------------------------------------------------- +// Vault page +// --------------------------------------------------------------------------- + +function loadVault() { + const header = $('#vaultAuthHeader'); + if (header) header.value = state.auth?.header || ''; + const credsPath = $('#vaultCredsPath'); + if (credsPath) credsPath.value = state.credsPath || ''; + const collPath = $('#vaultCollectionPath'); + if (collPath) collPath.value = state.collectionPath || ''; + renderVaultRoles(); +} + +function renderVaultRoles() { + const list = $('#vaultRoleList'); + if (!list) return; + if (!state.auth) state.auth = { header: '', roles: [] }; + list.innerHTML = state.auth.roles.map((r, i) => ` +
+ + + +
+ `).join(''); + $$('input[data-ri]', list).forEach(inp => inp.addEventListener('change', () => { + state.auth.roles[Number(inp.dataset.ri)][inp.dataset.rf] = inp.value; + })); + $$('[data-rdel]', list).forEach(btn => btn.addEventListener('click', () => { + state.auth.roles.splice(Number(btn.dataset.rdel), 1); + renderVaultRoles(); + })); +} + +$('#btnVaultAddRole')?.addEventListener('click', () => { + if (!state.auth) state.auth = { header: '', roles: [] }; + state.auth.roles.push({ name: '', header: '' }); + renderVaultRoles(); +}); + +$('#btnVaultSave')?.addEventListener('click', async () => { + state.auth.header = $('#vaultAuthHeader')?.value || ''; + state.credsPath = $('#vaultCredsPath')?.value || ''; + state.collectionPath = $('#vaultCollectionPath')?.value || ''; + try { + await api('/api/config/vault', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ auth: state.auth, credsPath: state.credsPath, collectionPath: state.collectionPath }), + }); + toast('Vault saved', 'ok'); + } catch (e) { toast(`Failed: ${e.message}`, 'error'); } +}); + +// --------------------------------------------------------------------------- +// Guardrails page +// --------------------------------------------------------------------------- + +function loadGuardrails() { + api('/api/config/guardrails').then(cfg => { + if (!cfg) cfg = {}; + $('#grCapToken').value = cfg.capToken || ''; + $('#grInScope').value = cfg.inScope || ''; + $('#grScopeHard').value = cfg.scopeHard || ''; + $('#grScopeExclude').value = cfg.scopeExclude || ''; + $('#grScopeObserve').value = cfg.scopeObserve || ''; + $('#grDestructive').checked = !!cfg.destructive; + $('#grAccounts').checked = cfg.accounts !== false; + $('#grMaxAccounts').value = cfg.maxAccounts ?? 3; + $('#grRate').value = cfg.rate ?? 240; + $('#grForbidden').value = cfg.forbidden || ''; + $('#grNotes').value = cfg.notes || ''; + $('#grEnv').value = cfg.environment || 'production'; + $('#grPolicy').value = cfg.policy || 'web'; + $('#grTransport').value = cfg.transport || ''; + $('#grOobDomain').value = cfg.oobDomain || ''; + $('#grOobHttp').value = cfg.oobHttp || ''; + $('#grOobDns').value = cfg.oobDns || ''; + $('#grSms').value = cfg.sms || ''; + }).catch(() => {}); +} + +$('#btnGuardrailsSave')?.addEventListener('click', async () => { + const data = { + capToken: $('#grCapToken')?.value || '', + inScope: $('#grInScope')?.value || '', + scopeHard: $('#grScopeHard')?.value || '', + scopeExclude: $('#grScopeExclude')?.value || '', + scopeObserve: $('#grScopeObserve')?.value || '', + destructive: $('#grDestructive')?.checked || false, + accounts: $('#grAccounts')?.checked !== false, + maxAccounts: Number($('#grMaxAccounts')?.value) || 3, + rate: Number($('#grRate')?.value) || 240, + forbidden: $('#grForbidden')?.value || '', + notes: $('#grNotes')?.value || '', + environment: $('#grEnv')?.value || 'production', + policy: $('#grPolicy')?.value || 'web', + transport: $('#grTransport')?.value || '', + oobDomain: $('#grOobDomain')?.value || '', + oobHttp: $('#grOobHttp')?.value || '', + oobDns: $('#grOobDns')?.value || '', + sms: $('#grSms')?.value || '', + }; + try { + await api('/api/config/guardrails', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); + toast('Guardrails saved', 'ok'); + } catch (e) { toast(`Failed: ${e.message}`, 'error'); } +}); + async function saveSettings() { const data = { defaultModel: $('#settingModel')?.value || '', @@ -3641,6 +3886,66 @@ async function saveSettings() { catch (e) { toast(`Failed: ${e.message}`, 'error'); } } +async function openProviderKeysModal() { + if (!state.providers?.length) { + try { state.providers = await api('/api/providers'); } catch { state.providers = []; } + } + try { state.keys = await api('/api/keys'); } catch { state.keys = []; } + let existing = $('#providerKeysModal'); + if (!existing) { + document.body.insertAdjacentHTML('beforeend', ``); + existing = $('#providerKeysModal'); + } + const body = $('#providerKeysModalBody'); + body.innerHTML = state.providers.map((p) => { + const set = state.keys.find((k) => k.provider === p.key)?.set; + return ` +
+ + ${esc(p.label)} + ${p.kind === 'cli' ? 'cli+api' : 'api only'} + + + +
+ `; + }).join(''); + const doSave = async (btn) => { + const provider = btn.dataset.ssave; + const input = body.querySelector(`input[data-sprovider="${provider}"]`); + const vtag = body.querySelector(`[data-sverify="${provider}"]`); + const p = state.providers.find(x => x.key === provider); + const key = input.value.trim(); + if (!key) return; + btn.disabled = true; btn.textContent = 'Saving…'; + if (vtag) { vtag.className = 'key-verify checking'; vtag.textContent = '· verifying…'; } + try { + const r = await api('/api/keys', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ provider, key }) }); + input.value = ''; + const v = r.verify || { ok: null, message: 'saved' }; + if (vtag) { + vtag.className = `key-verify ${v.ok === true ? 'ok' : v.ok === false ? 'bad' : 'unknown'}`; + vtag.textContent = v.ok === true ? '✓ valid' : v.ok === false ? '✗ ' + v.message : '• ' + v.message; + } + toast(v.ok === true ? `${p?.label || provider}: key valid` : v.ok === false ? `${p?.label || provider}: ${v.message}` : `${p?.label || provider}: ${v.message}`, v.ok === false ? 'error' : 'ok'); + state.keys = await api('/api/keys').catch(() => state.keys); + const dot = btn.closest('.provider-row')?.querySelector('.dot'); + if (dot) dot.classList.add('set'); + input.placeholder = '•••••••• (set)'; + } catch (e) { + if (vtag) { vtag.className = 'key-verify bad'; vtag.textContent = '✗ ' + e.message; } + } finally { btn.disabled = false; btn.textContent = 'Save'; } + }; + $$('[data-ssave]', body).forEach((btn) => btn.addEventListener('click', () => doSave(btn))); + $$('input[data-sprovider]', body).forEach((inp) => inp.addEventListener('keydown', (e) => { + if (e.key === 'Enter') body.querySelector(`[data-ssave="${inp.dataset.sprovider}"]`)?.click(); + })); + existing.hidden = false; +} + // Add-item modals function openModal(id) { $(id).hidden = false; } function closeModal(id) { $(id).hidden = true; } @@ -3655,6 +3960,7 @@ $('#btnAddRepo')?.addEventListener('click', () => { $('#btnAddDomain')?.addEventListener('click', () => { $('#domainName').value = ''; $('#domainDesc').value = ''; + if ($('#domainType')) $('#domainType').value = 'web'; if ($('#domainScope')) $('#domainScope').value = 'in-scope'; openModal('#domainModal'); }); @@ -3688,6 +3994,7 @@ $('#btnDomainSubmit')?.addEventListener('click', async () => { try { await api('/api/config/domains', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ domain, + type: $('#domainType')?.value || 'web', description: $('#domainDesc')?.value?.trim() || '', scope: $('#domainScope')?.value || 'in-scope', })}); diff --git a/web/public/index.html b/web/public/index.html index e11af3c..21c3d2c 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -47,6 +47,10 @@
+ + +
+ @@ -62,10 +66,6 @@
v4.3.2 -
- - -
@@ -184,12 +184,201 @@
No networks configured yet.
+ + + + + +