mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-10-03 22:46:57 +02:00
feat(web): live 'what's being tested' agent panel
Parse per-agent activity from the stream (launching agent / exploit·analyze·test <name> via <model> -> N candidate(s) / failed) into a status map, surfaced in the live view as a grid of chips coloured by state (pending/running/found/done/failed) with a per-agent finding count and a running/done tally. Answers 'what is it testing right now' at a glance; resets per engagement. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
e1350c0c4a
commit
8051464a84
4 files changed
+83
No files matched your search
@@ -600,8 +600,11 @@ function attachLiveJob(id, target, name, pinnedAgents) {
|
||||
state.currentJob = {
|
||||
id, es: null, findings: [], target, name, phase: 'starting', agents: 0, agentsDone: 0,
|
||||
reportUrl: null, runId: null, pinnedAgents: pinnedAgents || [], pocs: [], pocPoll: null,
|
||||
agentStatus: {},
|
||||
};
|
||||
localStorage.setItem(ACTIVE_JOB_KEY, id);
|
||||
if ($('#agentGrid')) $('#agentGrid').innerHTML = '';
|
||||
if ($('#agentPanel')) $('#agentPanel').hidden = true;
|
||||
|
||||
show($('#wizardView'), false);
|
||||
show($('#detailView'), false);
|
||||
@@ -631,6 +634,7 @@ function attachLiveJob(id, target, name, pinnedAgents) {
|
||||
state.currentJob.es = es;
|
||||
es.addEventListener('log', (e) => appendLog(JSON.parse(e.data).line));
|
||||
es.addEventListener('finding', (e) => addFinding(JSON.parse(e.data).finding));
|
||||
es.addEventListener('agent', (e) => { const a = JSON.parse(e.data); if (state.currentJob) { (state.currentJob.agentStatus ||= {})[a.name] = { status: a.status, found: a.found || 0 }; renderAgentPanel(state.currentJob.agentStatus); } });
|
||||
es.addEventListener('snapshot', (e) => applySnapshot(JSON.parse(e.data)));
|
||||
es.addEventListener('done', (e) => {
|
||||
applySnapshot(JSON.parse(e.data));
|
||||
@@ -813,7 +817,30 @@ function addFinding(f) {
|
||||
renderAttackPath($('#liveAttackPath'), state.currentJob.findings, state.currentJob.target);
|
||||
}
|
||||
|
||||
// Live "what's being tested" grid: one chip per agent, coloured by status.
|
||||
function renderAgentPanel(statusMap) {
|
||||
const grid = $('#agentGrid');
|
||||
const panel = $('#agentPanel');
|
||||
if (!grid || !panel) return;
|
||||
const entries = Object.entries(statusMap || {});
|
||||
if (!entries.length) { panel.hidden = true; return; }
|
||||
panel.hidden = false;
|
||||
// Order: running first, then found, then pending, then done, then failed.
|
||||
const rank = (s, found) => s === 'running' ? 0 : (s === 'done' && found > 0) ? 1 : s === 'pending' ? 2 : s === 'done' ? 3 : 4;
|
||||
entries.sort((a, b) => rank(a[1].status, a[1].found) - rank(b[1].status, b[1].found) || a[0].localeCompare(b[0]));
|
||||
const cls = (s, found) => s === 'running' ? 'running' : s === 'failed' ? 'failed' : (s === 'done' && found > 0) ? 'found' : s === 'done' ? 'done' : 'pending';
|
||||
grid.innerHTML = entries.map(([name, st]) =>
|
||||
`<span class="agent-chip ap-${cls(st.status, st.found)}" title="${esc(name)} — ${esc(st.status)}${st.found ? ` · ${st.found} finding(s)` : ''}">${st.status === 'running' ? '<i class="spin"></i>' : ''}${esc(name)}${st.found ? `<b>${st.found}</b>` : ''}</span>`).join('');
|
||||
const done = entries.filter(([, s]) => s.status === 'done' || s.status === 'failed').length;
|
||||
const run = entries.filter(([, s]) => s.status === 'running').length;
|
||||
$('#agentPanel .agent-panel-head > span').textContent = `What's being tested — ${run} running · ${done}/${entries.length} done`;
|
||||
}
|
||||
|
||||
function applySnapshot(snap) {
|
||||
if (snap.agentStatus) {
|
||||
if (state.currentJob) state.currentJob.agentStatus = snap.agentStatus;
|
||||
renderAgentPanel(snap.agentStatus);
|
||||
}
|
||||
$('#livePhase').textContent = snap.phase;
|
||||
state.currentJob.runId = snap.runId;
|
||||
state.currentJob.interactive = !!snap.interactive;
|
||||
|
||||
@@ -334,6 +334,13 @@
|
||||
<div class="progress-bar" id="progressBar"><div class="progress-fill" id="progressFill"></div></div>
|
||||
<div class="progress-label" id="progressLabel">0 / 0 agents</div>
|
||||
</div>
|
||||
<div class="agent-panel" id="agentPanel" hidden>
|
||||
<div class="agent-panel-head">
|
||||
<span>What's being tested</span>
|
||||
<span class="agent-legend"><i class="ap running"></i>testing <i class="ap done"></i>done <i class="ap found"></i>found <i class="ap failed"></i>error</span>
|
||||
</div>
|
||||
<div class="agent-grid" id="agentGrid"></div>
|
||||
</div>
|
||||
<nav class="run-tabs">
|
||||
<button class="run-tab active" data-tab="findings">Findings <span id="liveFindingsCount">0</span></button>
|
||||
<button class="run-tab" data-tab="attackpath">Generative Attack Path Chaining</button>
|
||||
|
||||
@@ -806,3 +806,20 @@ body.resizing-ns { user-select: none; cursor: ns-resize; }
|
||||
.resume-bar .resume-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
|
||||
.resume-bar .resume-x { background: transparent; border: none; color: var(--text-dim); cursor: pointer; font-size: var(--fs-sm); padding: 2px 6px; border-radius: var(--radius-xs); }
|
||||
.resume-bar .resume-x:hover { background: var(--surface-3); color: var(--text); }
|
||||
|
||||
/* Live "what's being tested" agent panel. */
|
||||
.agent-panel { margin: 0 var(--sp-5) var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); overflow: hidden; }
|
||||
.agent-panel-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); padding: var(--sp-2) var(--sp-3); font-size: var(--fs-xs); font-weight: 600; color: var(--text-dim); border-bottom: 1px solid var(--border); background: var(--surface-2); }
|
||||
.agent-legend { display: flex; align-items: center; gap: var(--sp-2); font-weight: 400; color: var(--text-faint); }
|
||||
.agent-legend .ap { width: 8px; height: 8px; border-radius: 2px; display: inline-block; margin-right: 3px; vertical-align: middle; }
|
||||
.ap.running { background: var(--accent); } .ap.done { background: var(--text-faint); } .ap.found { background: var(--sev-high-fg); } .ap.failed { background: var(--sev-critical-fg); }
|
||||
.agent-grid { display: flex; flex-wrap: wrap; gap: 5px; padding: var(--sp-3); max-height: 180px; overflow-y: auto; }
|
||||
.agent-chip { display: inline-flex; align-items: center; gap: 4px; font-family: var(--mono); font-size: var(--fs-2xs); padding: 2px 7px; border-radius: var(--radius-pill); border: 1px solid var(--border); background: var(--surface-2); color: var(--text-dim); white-space: nowrap; }
|
||||
.agent-chip b { background: var(--sev-high-bg); color: var(--sev-high-fg); border-radius: var(--radius-pill); padding: 0 5px; font-size: 9px; }
|
||||
.agent-chip.ap-running { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
|
||||
.agent-chip.ap-found { border-color: var(--sev-high-fg); color: var(--sev-high-fg); }
|
||||
.agent-chip.ap-done { opacity: .6; }
|
||||
.agent-chip.ap-failed { border-color: var(--sev-critical-fg); color: var(--sev-critical-fg); text-decoration: line-through; }
|
||||
.agent-chip .spin { width: 7px; height: 7px; border: 1.5px solid currentColor; border-right-color: transparent; border-radius: 50%; display: inline-block; animation: aspin .7s linear infinite; }
|
||||
@keyframes aspin { to { transform: rotate(360deg); } }
|
||||
@media (prefers-reduced-motion: reduce) { .agent-chip .spin { animation: none; } }
|
||||
@@ -653,6 +653,7 @@ class Job extends EventEmitter {
|
||||
startedAt: this.startedAt,
|
||||
interrupted: !!this.interrupted,
|
||||
resumable: !!this.resumable,
|
||||
agentStatus: this.agentStatus || {},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -701,6 +702,37 @@ function ingestLine(job, rawLine) {
|
||||
|
||||
const rid = line.match(/run id\s*:\s*(\S+)/);
|
||||
if (rid) { job.runId = rid[1]; saveEngagementName(job.runId, job.name); }
|
||||
|
||||
// --- live per-agent status, for the "what's being tested" panel ---
|
||||
if (!job.agentStatus) job.agentStatus = {};
|
||||
// Roster: "selected N ... agent(s): a, b, c"
|
||||
const roster = line.match(/selected\s+\d+[^:]*agent\(s\):\s*(.+)$/i);
|
||||
if (roster) {
|
||||
for (const nm of roster[1].split(/[,\s]+/).map((s) => s.trim()).filter(Boolean)) {
|
||||
if (!job.agentStatus[nm]) job.agentStatus[nm] = { status: 'pending', found: 0 };
|
||||
}
|
||||
}
|
||||
// Start: "▶ launching agent: <name> (<title>)"
|
||||
const launch = line.match(/launching agent:\s*([^\s(]+)/);
|
||||
if (launch) {
|
||||
const nm = launch[1];
|
||||
job.agentStatus[nm] = { ...(job.agentStatus[nm] || {}), status: 'running', found: job.agentStatus[nm]?.found || 0, title: (line.match(/\(([^)]+)\)\s*$/) || [])[1] || '' };
|
||||
job.push({ type: 'agent', name: nm, status: 'running' });
|
||||
}
|
||||
// Done: "exploit|analyze|test <name> via <model> → N candidate(s)"
|
||||
const done = line.match(/^\s*(?:exploit|analyze|test)\s+(\S+)\s+via\s+.+?(?:→|->)\s*(\d+)\s+candidate/i);
|
||||
if (done) {
|
||||
const nm = done[1]; const n = Number(done[2]) || 0;
|
||||
job.agentStatus[nm] = { ...(job.agentStatus[nm] || {}), status: 'done', found: n };
|
||||
job.push({ type: 'agent', name: nm, status: 'done', found: n });
|
||||
}
|
||||
// Failed: "<verb> <name> failed: ..."
|
||||
const failed = line.match(/^\s*(?:exploit|analyze|test|chain)\s+(\S+)\s+failed/i);
|
||||
if (failed) {
|
||||
const nm = failed[1];
|
||||
job.agentStatus[nm] = { ...(job.agentStatus[nm] || {}), status: 'failed' };
|
||||
job.push({ type: 'agent', name: nm, status: 'failed' });
|
||||
}
|
||||
}
|
||||
|
||||
function buildArgs(body) {
|
||||
|
||||
Reference in new issue
Block a user