feat(web): require an engagement name before launch

Wizard's Asset step now opens with a required 'Engagement name' field
(validated before advancing or launching). The name isn't a harness/CLI
concept, so it's persisted server-side as runId -> name in
.neurosploit/web-engagement-names.json (keyed off the CLI's own run id,
captured from its 'run id : ns-...' log line) so the sidebar, live run
header, and run detail can label a run by name instead of the raw
target/run-id, surviving a server restart.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129WdYHccPsH27k5GGuwijd
This commit is contained in:
CyberSecurityUP
2026-08-23 14:20:34 -03:00
co-authored by Claude Sonnet 5
parent bb659412fc
commit 3dcfeb7377
4 changed files with 59 additions and 13 deletions
+17 -7
View File
@@ -98,6 +98,7 @@ function goToStep(n) {
function validateStep(n) {
if (n === 0) {
if (!$('#fieldName').value.trim()) { alert('Name the engagement first — it identifies this run in the sidebar and history.'); $('#fieldName').focus(); return false; }
const target = $('#fieldTarget').value.trim();
if (!target) { alert(`${MODE_LABELS[state.mode].target} is required.`); return false; }
if (state.mode === 'greybox' && !$('#fieldRepo').value.trim()) { alert('Source repo is required for grey-box.'); return false; }
@@ -113,9 +114,11 @@ $$('.step-tab').forEach((tab) => tab.addEventListener('click', () => {
}));
function updateWizardSummary() {
const name = $('#fieldName').value.trim() || '(unnamed)';
const target = $('#fieldTarget').value.trim() || '(not set)';
$('#wizardSummary').innerHTML = `Step ${state.step + 1} of ${STEP_COUNT} · <b>${esc(state.mode)}</b> · <b>${esc(target)}</b>`;
$('#wizardSummary').innerHTML = `Step ${state.step + 1} of ${STEP_COUNT} · <b>${esc(name)}</b> · ${esc(state.mode)} · ${esc(target)}`;
}
$('#fieldName').addEventListener('input', updateWizardSummary);
// mode tiles
function selectMode(mode) {
@@ -292,6 +295,7 @@ function renderReview() {
const provider = $('#fieldProvider').value;
const model = $('#fieldModelSelect').value;
const items = [
{ k: 'Engagement name', v: $('#fieldName').value.trim() || '(not set)' },
{ k: 'Mode', v: state.mode },
{ k: MODE_LABELS[state.mode].target, v: target || '(not set)', mono: true },
...(MODE_LABELS[state.mode].showRepo ? [{ k: 'Source repo', v: repo || '(not set)', mono: true }] : []),
@@ -314,7 +318,9 @@ function renderReview() {
$('#btnLaunch').addEventListener('click', startExploitation);
async function startExploitation() {
if (!validateStep(0)) { goToStep(0); return; }
const mode = state.mode;
const name = $('#fieldName').value.trim();
const target = $('#fieldTarget').value.trim();
const repo = $('#fieldRepo').value.trim();
const provider = $('#fieldProvider').value;
@@ -323,6 +329,7 @@ async function startExploitation() {
const body = {
mode,
name,
target: mode === 'whitebox' ? undefined : target,
repo: mode === 'whitebox' ? target : (repo || undefined),
models: provider && model ? [`${provider}:${model}`] : [],
@@ -344,7 +351,7 @@ async function startExploitation() {
$('#btnLaunch').textContent = 'Starting…';
try {
const { id } = await api('/api/exploit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
attachLiveJob(id, body.target || body.repo);
attachLiveJob(id, body.target || body.repo, name);
} catch (e) {
alert('Failed to start: ' + e.message);
} finally {
@@ -367,14 +374,15 @@ function bindRunTabs(scopeEl) {
bindRunTabs($('#liveView'));
bindRunTabs($('#detailView'));
function attachLiveJob(id, target) {
function attachLiveJob(id, target, name) {
if (state.currentJob?.es) state.currentJob.es.close();
state.currentJob = { id, es: null, findings: [], target, phase: 'starting', agents: 0, agentsDone: 0, reportUrl: null, runId: null };
state.currentJob = { id, es: null, findings: [], target, name, phase: 'starting', agents: 0, agentsDone: 0, reportUrl: null, runId: null };
show($('#wizardView'), false);
show($('#detailView'), false);
show($('#liveView'), true);
$('#liveTarget').textContent = target || '—';
$('#liveTarget').textContent = name || target || '—';
$('#liveTargetSub').textContent = name ? target : '';
$('#livePhase').textContent = 'starting';
$('#phaseDot').style.background = '';
$('#liveFindingsTable tbody').innerHTML = '';
@@ -525,7 +533,7 @@ function renderSidebar() {
for (const r of g.items) {
const btn = document.createElement('button');
btn.className = 'sb-run' + (state.currentDetailId === r.id ? ' active' : '');
btn.innerHTML = `<span class="name">${esc(r.target)}</span><span class="sub">${esc(r.id)} · ${r.findings} finding(s)</span>`;
btn.innerHTML = `<span class="name">${esc(r.name || r.target)}</span><span class="sub">${r.name ? esc(r.target) + ' · ' : ''}${r.findings} finding(s)</span>`;
btn.addEventListener('click', () => openRun(r));
items.appendChild(btn);
const isThisJob = r.state === 'running' && state.currentJob && r.id === state.currentJob.runId;
@@ -556,7 +564,9 @@ function openRun(run) {
async function loadDetail(id) {
clearInterval(state.detailPoll);
const detail = await api(`/api/runs/${encodeURIComponent(id)}`);
$('#detailTarget').textContent = detail.status?.target || detail.meta?.target || id;
const target = detail.status?.target || detail.meta?.target || id;
$('#detailTarget').textContent = detail.name || target;
$('#detailTargetSub').textContent = detail.name ? target : '';
$('#detailState').textContent = detail.status?.state || 'unknown';
$('#detailFindingsCount').textContent = detail.findings.length;
const tbody = $('#detailFindingsTable tbody');
+7
View File
@@ -57,6 +57,11 @@
<!-- Step 1 — Asset -->
<div class="wizard-panel" data-panel="0">
<div class="field-group">
<label class="field-label">Engagement name</label>
<input id="fieldName" type="text" placeholder="e.g. Keystone Digital Banking" />
<div class="field-help">Identifies this engagement in the sidebar and run history — required.</div>
</div>
<div>
<div class="section-title">What are you testing?</div>
<div class="section-desc">Pick the engagement type — this decides which CLI subcommand runs underneath.</div>
@@ -197,6 +202,7 @@
<header class="run-head">
<div>
<div class="run-target" id="liveTarget"></div>
<div class="run-meta" id="liveTargetSub" style="font-family: var(--mono);"></div>
<div class="run-meta"><span class="phase-dot" id="phaseDot"></span><span id="livePhase">starting</span></div>
</div>
<div class="run-actions">
@@ -226,6 +232,7 @@
<header class="run-head">
<div>
<div class="run-target" id="detailTarget"></div>
<div class="run-meta" id="detailTargetSub" style="font-family: var(--mono);"></div>
<div class="run-meta"><span class="phase-dot static" id="detailDot"></span><span id="detailState"></span></div>
</div>
<div class="run-actions">