Skip to content

Commit 31bd57e

Browse files
committed
feat: add smart/raw presets, quick chips, saved filters, and details drawer
Extend findings UI with per-context smart column presets plus raw mode, quick filter chips, named filter-set persistence per scan type, and a right-side details drawer with copy/export actions. Made-with: Cursor
1 parent 1ef2696 commit 31bd57e

1 file changed

Lines changed: 224 additions & 8 deletions

File tree

  • internal/modules/gobot/ui

internal/modules/gobot/ui/app.js

Lines changed: 224 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1955,16 +1955,23 @@ const MODULE_RENDERERS = {
19551955

19561956
function getUnifiedTableColumns(activeKind) {
19571957
const active = String(activeKind || '');
1958-
const moduleTab = active.startsWith('mod:') ? active.slice(4) : (active === 'misconfig' ? 'misconfig' : '');
1958+
const moduleTab = active.startsWith('mod:') ? active.slice(4) : (
1959+
active === 'misconfig' ? 'misconfig'
1960+
: active === 'nuclei' ? 'nuclei'
1961+
: active === 'ffuf' ? 'ffuf-fuzzing'
1962+
: (active === 'apkx' || active.startsWith('apkcat:')) ? 'apkx' : ''
1963+
);
19591964
switch (moduleTab) {
1965+
case 'apkx':
1966+
return ['PATH', 'CATEGORY', 'MATCHER VALUE', 'MODULE'];
19601967
case 'nuclei':
19611968
return ['TARGET', 'SEV', 'TEMPLATE', 'MATCH'];
19621969
case 'gf-patterns':
19631970
return ['TARGET', 'PATTERN', 'VALUE', 'SOURCE'];
19641971
case 'misconfig':
19651972
return ['TARGET', 'SEV', 'SERVICE', 'FINDING'];
19661973
case 'ffuf-fuzzing':
1967-
return ['TARGET', 'STATUS', 'PATH/WORD', 'DETAIL'];
1974+
return ['URL', 'STATUS', 'WORD', 'LENGTH'];
19681975
default:
19691976
return ['TARGET', 'SEV', 'VULNERABILITY TYPE', 'MODULE'];
19701977
}
@@ -4536,6 +4543,18 @@ async function loadReconUnifiedTable(scanId, allFiles, containerId, scanRecord)
45364543

45374544

45384545
let searchJsOnly = false;
4546+
let presetMode = 'smart';
4547+
let quickChip = 'none';
4548+
let currentRenderedRows = [];
4549+
const presetStorageKey = `autoar.recon.filtersets.${stNorm || 'generic'}`;
4550+
let savedFilterSets = {};
4551+
4552+
const loadSavedSets = () => {
4553+
try { savedFilterSets = JSON.parse(localStorage.getItem(presetStorageKey) || '{}') || {}; } catch { savedFilterSets = {}; }
4554+
};
4555+
const persistSavedSets = () => {
4556+
try { localStorage.setItem(presetStorageKey, JSON.stringify(savedFilterSets)); } catch (_) { }
4557+
};
45394558

45404559
const rowMatch = (r) => {
45414560
const k = r.kind || 'other';
@@ -4558,6 +4577,14 @@ async function loadReconUnifiedTable(scanId, allFiles, containerId, scanRecord)
45584577
const sev = String(r.severity || 'info').toLowerCase();
45594578
if (sev !== filterSeverity) return false;
45604579
}
4580+
const sev = String(r.severity || 'info').toLowerCase();
4581+
const targetStr = String(r.target || '').toLowerCase();
4582+
const findingStr = String(r.finding || '').toLowerCase();
4583+
if (quickChip === 'highplus' && !(sev === 'high' || sev === 'critical')) return false;
4584+
if (quickChip === 'hasurl' && !(/https?:\/\//i.test(targetStr) || /https?:\/\//i.test(findingStr))) return false;
4585+
if (quickChip === 'exported' && !(findingStr.includes('exported') || String(r.apk_category || '').toLowerCase().includes('exported'))) return false;
4586+
if (quickChip === 'secrets' && !/(secret|token|apikey|api key|password|authorization)/i.test(findingStr)) return false;
4587+
if (quickChip === 'onlyjs' && !(/\.m?jsx?(\?|$)/i.test(targetStr) || findingStr.includes('javascript') || String(r.apk_category || '').toLowerCase().includes('js'))) return false;
45614588
return true;
45624589
};
45634590

@@ -4568,7 +4595,7 @@ async function loadReconUnifiedTable(scanId, allFiles, containerId, scanRecord)
45684595
<div style="padding:10px 12px;border-bottom:1px solid var(--border);font-size:11px;color:var(--text-muted);letter-spacing:.6px;text-transform:uppercase">Findings Views</div>
45694596
<div id="recon-left-rail" style="display:flex;flex-direction:column;gap:6px;padding:8px;overflow-y:auto;overflow-x:hidden;max-height:780px;scrollbar-width:thin"></div>
45704597
</aside>
4571-
<section style="min-width:0">
4598+
<section style="min-width:0;position:relative">
45724599
<div id="recon-apk-meta" style="display:none;padding:10px 12px;border-bottom:1px solid var(--border);background:rgba(34,211,238,.06)"></div>
45734600
<div id="recon-filter-bar" style="display:grid;grid-template-columns:minmax(200px,1.5fr) 140px 140px minmax(180px,1fr) auto;gap:8px;padding:10px;border-bottom:1px solid var(--border);background:rgba(2,6,23,.5)">
45744601
<input id="recon-filter-host" type="search" placeholder="🔍 Filter by target URL..." style="padding:8px 10px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);font-size:12px"/>
@@ -4591,6 +4618,21 @@ async function loadReconUnifiedTable(scanId, allFiles, containerId, scanRecord)
45914618
<span><span id="recon-unified-shown">0</span> rows</span>
45924619
</div>
45934620
</div>
4621+
<div id="recon-quick-tools" style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 10px;border-bottom:1px solid var(--border);background:rgba(2,6,23,.38)">
4622+
<div id="recon-quick-chips" style="display:flex;align-items:center;gap:6px;flex-wrap:wrap"></div>
4623+
<div style="margin-left:auto;display:flex;align-items:center;gap:6px">
4624+
<select id="recon-view-mode" style="padding:6px 8px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);font-size:11px">
4625+
<option value="smart">Smart columns</option>
4626+
<option value="raw">Raw columns</option>
4627+
</select>
4628+
<select id="recon-saved-filters" style="min-width:160px;padding:6px 8px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);font-size:11px">
4629+
<option value="">Saved filters…</option>
4630+
</select>
4631+
<input id="recon-filter-name" type="text" placeholder="Filter name" style="width:130px;padding:6px 8px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);font-size:11px"/>
4632+
<button id="recon-save-filter" type="button" style="padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:rgba(34,211,238,.1);color:var(--accent-cyan);font-size:11px;cursor:pointer">Save</button>
4633+
<button id="recon-delete-filter" type="button" style="padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:rgba(248,113,113,.08);color:#fca5a5;font-size:11px;cursor:pointer">Delete</button>
4634+
</div>
4635+
</div>
45944636
<!-- Standard findings table -->
45954637
<div id="recon-standard-view">
45964638
<div class="result-table-wrap" style="max-height:640px;overflow:auto">
@@ -4624,6 +4666,13 @@ async function loadReconUnifiedTable(scanId, allFiles, containerId, scanRecord)
46244666
<div id="recon-assets-content" style="padding:16px;min-height:200px;max-height:680px;overflow:auto">
46254667
<div style="text-align:center;padding:40px;color:var(--text-muted)">Loading assets…</div>
46264668
</div>
4669+
</div>
4670+
<div id="recon-details-drawer" style="display:none;position:absolute;top:0;right:0;width:420px;height:100%;background:rgba(2,6,23,.98);border-left:1px solid var(--border);z-index:20;box-shadow:-12px 0 40px rgba(0,0,0,.45)">
4671+
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px;border-bottom:1px solid var(--border)">
4672+
<div style="font-size:12px;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.5px">Finding Details</div>
4673+
<button id="recon-drawer-close" type="button" style="background:transparent;border:none;color:var(--text-muted);font-size:18px;cursor:pointer">✕</button>
4674+
</div>
4675+
<div id="recon-drawer-body" style="padding:12px;overflow:auto;height:calc(100% - 52px)"></div>
46274676
</div>
46284677
</section>
46294678
</div>
@@ -4632,10 +4681,19 @@ async function loadReconUnifiedTable(scanId, allFiles, containerId, scanRecord)
46324681
const tabsEl = root.querySelector('#recon-left-rail');
46334682
const apkMetaBar = root.querySelector('#recon-apk-meta');
46344683
const filterBar = root.querySelector('#recon-filter-bar');
4684+
const chipBar = root.querySelector('#recon-quick-chips');
4685+
const viewModeSel = root.querySelector('#recon-view-mode');
4686+
const savedFiltersSel = root.querySelector('#recon-saved-filters');
4687+
const saveFilterBtn = root.querySelector('#recon-save-filter');
4688+
const deleteFilterBtn = root.querySelector('#recon-delete-filter');
4689+
const filterNameInput = root.querySelector('#recon-filter-name');
46354690
const standardView = root.querySelector('#recon-standard-view');
46364691
const assetsView = root.querySelector('#recon-assets-view');
46374692
const assetsContent = root.querySelector('#recon-assets-content');
46384693
const standardTable = root.querySelector('#recon-standard-view table.dashboard-table');
4694+
const drawer = root.querySelector('#recon-details-drawer');
4695+
const drawerBody = root.querySelector('#recon-drawer-body');
4696+
const drawerClose = root.querySelector('#recon-drawer-close');
46394697

46404698
if (apkMetaBar) {
46414699
if (isAPKScan && apkPackageInfo) {
@@ -4728,6 +4786,58 @@ async function loadReconUnifiedTable(scanId, allFiles, containerId, scanRecord)
47284786
});
47294787
}
47304788

4789+
const renderSavedFilters = () => {
4790+
if (!savedFiltersSel) return;
4791+
const names = Object.keys(savedFilterSets).sort();
4792+
savedFiltersSel.innerHTML = '<option value="">Saved filters…</option>' + names.map((n) => `<option value="${esc(n)}">${esc(n)}</option>`).join('');
4793+
};
4794+
const readCurrentFilterSet = () => ({
4795+
activeKind,
4796+
searchHost,
4797+
searchTitle,
4798+
filterSeverity,
4799+
searchModule,
4800+
searchJsOnly,
4801+
quickChip,
4802+
presetMode,
4803+
});
4804+
const applyFilterSet = (fs) => {
4805+
if (!fs) return;
4806+
activeKind = fs.activeKind || activeKind;
4807+
searchHost = String(fs.searchHost || '');
4808+
searchTitle = String(fs.searchTitle || '');
4809+
filterSeverity = String(fs.filterSeverity || 'any');
4810+
searchModule = String(fs.searchModule || 'all');
4811+
searchJsOnly = !!fs.searchJsOnly;
4812+
quickChip = String(fs.quickChip || 'none');
4813+
presetMode = String(fs.presetMode || 'smart');
4814+
const hostInputEl = root.querySelector('#recon-filter-host');
4815+
const titleInputEl = root.querySelector('#recon-filter-title');
4816+
const severitySelEl = root.querySelector('#recon-filter-severity');
4817+
const jsChkEl = root.querySelector('#recon-filter-js-only');
4818+
if (hostInputEl) hostInputEl.value = searchHost;
4819+
if (titleInputEl) titleInputEl.value = searchTitle;
4820+
if (severitySelEl) severitySelEl.value = filterSeverity;
4821+
if (modSelect) modSelect.value = searchModule;
4822+
if (jsChkEl) jsChkEl.checked = searchJsOnly;
4823+
if (viewModeSel) viewModeSel.value = presetMode;
4824+
};
4825+
4826+
const chipDefs = [
4827+
{ id: 'highplus', label: 'High+' },
4828+
{ id: 'hasurl', label: 'Has URL' },
4829+
{ id: 'exported', label: 'Exported Components' },
4830+
{ id: 'secrets', label: 'Secrets' },
4831+
{ id: 'onlyjs', label: 'Only JS' },
4832+
];
4833+
const renderChips = () => {
4834+
if (!chipBar) return;
4835+
chipBar.innerHTML = chipDefs.map((c) => {
4836+
const active = quickChip === c.id;
4837+
return `<button type="button" data-chip="${escAttr(c.id)}" style="padding:5px 10px;border:1px solid ${active ? 'rgba(34,211,238,.5)' : 'var(--border)'};border-radius:999px;background:${active ? 'rgba(34,211,238,.13)' : 'rgba(255,255,255,.02)'};color:${active ? 'var(--accent-cyan)' : 'var(--text-secondary)'};font-size:11px;cursor:pointer">${esc(c.label)}</button>`;
4838+
}).join('');
4839+
};
4840+
47314841
const renderTabs = () => {
47324842
if (!tabsEl) return;
47334843
tabsEl.innerHTML = UNIQUE_TABS.map(([kind, label]) => {
@@ -4764,7 +4874,9 @@ async function loadReconUnifiedTable(scanId, allFiles, containerId, scanRecord)
47644874
const cap = root.querySelector('#recon-unified-cap');
47654875
if (shown) shown.textContent = String(filtered.length);
47664876
if (headRow) {
4767-
const cols = getUnifiedTableColumns(activeKind);
4877+
const cols = presetMode === 'raw'
4878+
? ['TARGET', 'SEV', 'VULNERABILITY TYPE', 'MODULE']
4879+
: getUnifiedTableColumns(activeKind);
47684880
headRow.innerHTML = `
47694881
<th style="width:36px;text-align:center;padding-left:10px">
47704882
<input type="checkbox" id="findings-select-all" title="Select all" style="width:14px;height:14px;accent-color:var(--accent-cyan);cursor:pointer">
@@ -4776,6 +4888,7 @@ async function loadReconUnifiedTable(scanId, allFiles, containerId, scanRecord)
47764888
`;
47774889
}
47784890
if (tbody) {
4891+
currentRenderedRows = slice;
47794892
tbody.innerHTML = slice.length ? slice.map((r, idx) => {
47804893
const sev = String(r.severity || '').toLowerCase().replace(/[\-]/g, '').trim();
47814894
const sevMeta = {
@@ -4788,8 +4901,14 @@ async function loadReconUnifiedTable(scanId, allFiles, containerId, scanRecord)
47884901
}[sev] || { color: '#718096', bg: '#71809615', label: '—' };
47894902

47904903
const modInfo = getModuleDisplayInfo(r.module);
4904+
if (presetMode === 'raw') return renderDefaultRow(r, idx, modInfo, sevMeta);
47914905
return renderRowForUnifiedTab(r, idx, activeKind, modInfo, sevMeta);
47924906
}).join('') : '<tr><td colspan="5" style="text-align:center;padding:28px;color:var(--text-muted);font-size:13px">No findings match the current filter.</td></tr>';
4907+
if (slice.length) {
4908+
Array.from(tbody.querySelectorAll('.findings-row')).forEach((tr, i) => {
4909+
tr.setAttribute('data-row-index', String(i));
4910+
});
4911+
}
47934912
}
47944913
const pagContainer = root.querySelector('#recon-pagination');
47954914
if (pagContainer) {
@@ -4881,8 +5000,53 @@ async function loadReconUnifiedTable(scanId, allFiles, containerId, scanRecord)
48815000
};
48825001

48835002
// Initial load
5003+
loadSavedSets();
5004+
renderSavedFilters();
5005+
renderChips();
48845006
switchReconView(activeKind);
48855007

5008+
const renderDrawerRow = (r) => {
5009+
if (!drawerBody) return;
5010+
const safeJson = esc(JSON.stringify(r, null, 2));
5011+
drawerBody.innerHTML = `
5012+
<div style="display:grid;gap:10px">
5013+
<div><div style="font-size:11px;color:var(--text-muted)">Target</div><div style="font-family:var(--font-mono,monospace);font-size:12px;color:var(--text-primary);word-break:break-all">${esc(String(r.target || r.host || '—'))}</div></div>
5014+
<div><div style="font-size:11px;color:var(--text-muted)">Severity</div><div style="font-size:12px;color:var(--text-primary)">${esc(String(r.severity || '—'))}</div></div>
5015+
<div><div style="font-size:11px;color:var(--text-muted)">Module</div><div style="font-size:12px;color:var(--text-primary)">${esc(String(r.module || '—'))}</div></div>
5016+
<div><div style="font-size:11px;color:var(--text-muted)">Finding</div><div style="font-family:var(--font-mono,monospace);font-size:12px;color:var(--text-primary);word-break:break-word">${esc(String(r.finding || '—'))}</div></div>
5017+
<div><div style="font-size:11px;color:var(--text-muted)">Source File</div><div style="font-family:var(--font-mono,monospace);font-size:12px;color:var(--text-primary);word-break:break-all">${esc(String(r.file || '—'))}</div></div>
5018+
<div style="display:flex;gap:8px;flex-wrap:wrap;padding-top:4px">
5019+
<button id="drawer-copy-finding" type="button" style="padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:rgba(34,211,238,.1);color:var(--accent-cyan);font-size:11px;cursor:pointer">Copy Finding</button>
5020+
<button id="drawer-copy-json" type="button" style="padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:rgba(255,255,255,.03);color:var(--text-primary);font-size:11px;cursor:pointer">Copy JSON</button>
5021+
<button id="drawer-export-json" type="button" style="padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:rgba(52,211,153,.1);color:#34d399;font-size:11px;cursor:pointer">Export JSON</button>
5022+
</div>
5023+
<pre style="margin:0;padding:10px;border:1px solid var(--border);border-radius:8px;background:rgba(255,255,255,.02);font-size:11px;line-height:1.45;color:var(--text-muted);overflow:auto;max-height:280px">${safeJson}</pre>
5024+
</div>`;
5025+
const copyFinding = drawerBody.querySelector('#drawer-copy-finding');
5026+
const copyJson = drawerBody.querySelector('#drawer-copy-json');
5027+
const exportJson = drawerBody.querySelector('#drawer-export-json');
5028+
if (copyFinding) copyFinding.onclick = async () => { await copyToClipboard(String(r.finding || '')); showToast('success', 'Copied', 'Finding copied'); };
5029+
if (copyJson) copyJson.onclick = async () => { await copyToClipboard(JSON.stringify(r, null, 2)); showToast('success', 'Copied', 'JSON copied'); };
5030+
if (exportJson) exportJson.onclick = () => {
5031+
const blob = new Blob([JSON.stringify(r, null, 2)], { type: 'application/json' });
5032+
const url = URL.createObjectURL(blob);
5033+
const a = document.createElement('a');
5034+
a.href = url;
5035+
a.download = `finding-${Date.now()}.json`;
5036+
document.body.appendChild(a);
5037+
a.click();
5038+
a.remove();
5039+
URL.revokeObjectURL(url);
5040+
};
5041+
};
5042+
const openDrawerForRow = (r) => {
5043+
if (!drawer) return;
5044+
renderDrawerRow(r);
5045+
drawer.style.display = 'block';
5046+
};
5047+
const closeDrawer = () => { if (drawer) drawer.style.display = 'none'; };
5048+
if (drawerClose) drawerClose.addEventListener('click', closeDrawer);
5049+
48865050
// ── Draggable column widths ───────────────────────────────────────────────
48875051
const ensureColPixels = () => {
48885052
const cg = root.querySelector('#recon-colgroup');
@@ -4934,6 +5098,57 @@ async function loadReconUnifiedTable(scanId, allFiles, containerId, scanRecord)
49345098
const kind = tabBtn.getAttribute('data-recon-kind') || 'all';
49355099
switchReconView(kind);
49365100
});
5101+
if (chipBar) {
5102+
chipBar.addEventListener('click', (e) => {
5103+
const btn = e.target.closest('[data-chip]');
5104+
if (!btn) return;
5105+
const id = String(btn.getAttribute('data-chip') || 'none');
5106+
quickChip = quickChip === id ? 'none' : id;
5107+
renderChips();
5108+
_currentPage = 1;
5109+
renderBody();
5110+
});
5111+
}
5112+
if (viewModeSel) {
5113+
viewModeSel.addEventListener('change', () => {
5114+
presetMode = String(viewModeSel.value || 'smart');
5115+
_currentPage = 1;
5116+
renderBody();
5117+
});
5118+
}
5119+
if (saveFilterBtn) {
5120+
saveFilterBtn.addEventListener('click', () => {
5121+
const name = String(filterNameInput?.value || '').trim();
5122+
if (!name) {
5123+
showToast('error', 'Missing name', 'Enter a filter name first');
5124+
return;
5125+
}
5126+
savedFilterSets[name] = readCurrentFilterSet();
5127+
persistSavedSets();
5128+
renderSavedFilters();
5129+
if (savedFiltersSel) savedFiltersSel.value = name;
5130+
showToast('success', 'Saved', `Filter "${name}" saved`);
5131+
});
5132+
}
5133+
if (deleteFilterBtn) {
5134+
deleteFilterBtn.addEventListener('click', () => {
5135+
const name = String(savedFiltersSel?.value || '').trim();
5136+
if (!name || !savedFilterSets[name]) return;
5137+
delete savedFilterSets[name];
5138+
persistSavedSets();
5139+
renderSavedFilters();
5140+
showToast('success', 'Deleted', `Filter "${name}" deleted`);
5141+
});
5142+
}
5143+
if (savedFiltersSel) {
5144+
savedFiltersSel.addEventListener('change', () => {
5145+
const name = String(savedFiltersSel.value || '').trim();
5146+
if (!name || !savedFilterSets[name]) return;
5147+
applyFilterSet(savedFilterSets[name]);
5148+
renderChips();
5149+
switchReconView(activeKind);
5150+
});
5151+
}
49375152
root.addEventListener('mousedown', (e) => {
49385153
const handle = e.target.closest('.col-resizer');
49395154
if (!handle || !root.contains(handle)) return;
@@ -5036,13 +5251,14 @@ async function loadReconUnifiedTable(scanId, allFiles, containerId, scanRecord)
50365251
}
50375252
});
50385253

5039-
// Clicking a row (not a link/checkbox) toggles its checkbox
5254+
// Clicking a row opens details drawer (selection stays checkbox-driven)
50405255
root.addEventListener('click', e => {
50415256
const row = e.target.closest('.findings-row');
50425257
if (!row) return;
5043-
if (e.target.tagName === 'A' || e.target.tagName === 'INPUT') return;
5044-
const chk = row.querySelector('.finding-chk');
5045-
if (chk) { chk.checked = !chk.checked; _updateToolbar(); }
5258+
if (e.target.tagName === 'A' || e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return;
5259+
const idx = Number(row.getAttribute('data-row-index') || '-1');
5260+
const data = idx >= 0 ? currentRenderedRows[idx] : null;
5261+
if (data) openDrawerForRow(data);
50465262
});
50475263

50485264
// Toolbar actions

0 commit comments

Comments
 (0)