From 33fd6ca508dd73fc44030445d758522de4b86506 Mon Sep 17 00:00:00 2001 From: CyberSecurityUP Date: Sun, 18 Oct 2026 18:43:02 -0300 Subject: [PATCH] feat(web): add Re-run button to detail view with pre-filled wizard Adds a "Re-run" button to the run detail view that opens the wizard pre-populated with the previous run's settings (target, model, votes, chain depth, recon, max agents, mode flags). Server now includes launch params in the detail API response by looking up the persisted job file. Previous findings and reports are preserved (new run ID). --- web/public/app.js | 36 ++++++++++++++++++++++++++++++++++++ web/public/index.html | 1 + web/server.js | 14 +++++++++++++- 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/web/public/app.js b/web/public/app.js index a816780..f847a88 100644 --- a/web/public/app.js +++ b/web/public/app.js @@ -1148,6 +1148,41 @@ $('#btnDeleteRun').addEventListener('click', async () => { } }); +$('#btnRerun')?.addEventListener('click', () => { + clearInterval(state.detailPoll); + const target = $('#detailTarget').textContent || ''; + const sub = $('#detailTargetSub').textContent || ''; + const launch = state.detailLaunch; + switchView('wizard'); + goToStep(0); + if (launch) { + if (launch.target) $('#fieldTarget').value = launch.target; + if (launch.name) $('#fieldName').value = launch.name + ' (re-run)'; + if (launch.repo) $('#fieldRepo').value = launch.repo; + if (launch.mode) { const sel = $('#fieldMode'); if (sel) sel.value = launch.mode; } + if (launch.votes) $('#fieldVotes').value = launch.votes; + if (launch.chainDepth !== undefined) $('#fieldChain').value = launch.chainDepth; + if (launch.recon) $('#fieldRecon').value = launch.recon; + if (launch.maxAgents) $('#fieldMaxAgents').value = launch.maxAgents; + if (launch.quick && $('#fieldQuick')) $('#fieldQuick').checked = true; + if (launch.research && $('#fieldResearch')) $('#fieldResearch').checked = true; + if (launch.subscription) state.authMode = 'subscription'; + if (launch.mcp && $('#fieldMcp')) $('#fieldMcp').checked = launch.mcp; + if (launch.models?.length) { + const parts = launch.models[0].split(':'); + if (parts.length >= 2) { + $('#fieldProvider').value = parts[0]; + $('#fieldProvider').dispatchEvent(new Event('change')); + setTimeout(() => { $('#fieldModelSelect').value = parts.slice(1).join(':'); }, 100); + } + } + } else { + $('#fieldTarget').value = sub || target; + $('#fieldName').value = (target || '') + ' (re-run)'; + } + toast('Settings loaded from previous run. Adjust and launch.', 'ok', 4000); +}); + $('#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'); }); @@ -1758,6 +1793,7 @@ async function loadDetail(id) { $('#detailState').textContent = detail.status?.state || 'unknown'; state.detailFindings = detail.findings; state.detailPocs = detail.pocs || []; + state.detailLaunch = detail.launch || null; // Regression vs the previous run of this target — new / recurring badges and a // "fixed since" banner. Best-effort: it never blocks the findings table. state.detailRegression = null; diff --git a/web/public/index.html b/web/public/index.html index d591094..a11c1c9 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -741,6 +741,7 @@ + diff --git a/web/server.js b/web/server.js index 1d0cc14..fe774d0 100644 --- a/web/server.js +++ b/web/server.js @@ -791,7 +791,19 @@ async function runDetail(id) { const liveRunIds = new Set([...jobs.values()].filter((j) => !j.done && j.runId).map((j) => j.runId)); if (!liveRunIds.has(id)) status.state = 'interrupted'; } - return { id, name: engagementNames.get(id) || '', meta, status, findings, assets, pocs, extras }; + let launch = null; + const liveJob = [...jobs.values()].find((j) => j.runId === id); + if (liveJob?.launch) launch = liveJob.launch; + else { + try { + const jfiles = await fsp.readdir(JOBS_DIR); + for (const jf of jfiles) { + const rec = JSON.parse(await fsp.readFile(path.join(JOBS_DIR, jf), 'utf8')); + if (rec.runId === id && rec.launch) { launch = rec.launch; break; } + } + } catch { /* no job files */ } + } + return { id, name: engagementNames.get(id) || '', meta, status, findings, assets, pocs, extras, launch }; } /// Relative file list under a directory (one level of nesting is enough for