feat: attacks workflow — playbook steps + auto-targeting deauth panel

Evil WPA/Open pages now show a state-aware playbook (current step
highlighted, contextual hint) and the Deauth Targeting panel
auto-fills the live attack SSID and refreshes clients automatically.
Enterprise page gains a playbook hint card.
This commit is contained in:
2026-08-18 21:43:05 -05:00
parent 41b3c849ee
commit e1eba04775
@@ -1394,11 +1394,41 @@ function attackLauncher(kind, opts) {
}
let deauthCard = null;
const ssidRef = { current: '' };
if (opts.deauth) {
deauthCard = deauthPanel();
deauthCard = deauthPanel(ssidRef);
box.appendChild(deauthCard);
}
if (opts.playbook) {
const pb = h('div', { class: 'pineap-title-card' });
pb.appendChild(h('div', { class: 'pineap-card-title' }, 'Playbook'));
const pbBody = h('div', { style: 'font-size:13px;line-height:1.8' });
pb.appendChild(pbBody);
box.appendChild(pb);
const steps = opts.playbook.steps;
const stepEls = {};
const stepList = h('ol', { style: 'margin:0;padding-left:20px' });
steps.forEach((s) => {
const li = h('li', { style: 'margin-top:4px' }, h('span', { text: s }));
stepEls[s] = li;
stepList.appendChild(li);
});
pbBody.appendChild(stepList);
const pbHint = h('div', { class: 'muted', style: 'font-size:12px;margin-top:6px' });
pbBody.appendChild(pbHint);
const pbTick = (status, which) => {
const activeStep = opts.playbook.currentStep(status, which);
steps.forEach((s) => {
stepEls[s].style.color = s === activeStep ? 'var(--primary)' : '';
stepEls[s].style.fontWeight = s === activeStep ? '600' : '';
});
pbHint.textContent = opts.playbook.hint(status, which) || '';
};
pbTick(null, null);
box.__pbTick = pbTick;
}
function load() {
PagerAPI.get('/api/attacks/status').then((r) => {
const s = r.data || {};
@@ -1416,6 +1446,11 @@ function attackLauncher(kind, opts) {
text: 'Configured but interface not live yet — radio reload may still be in progress.' }));
}
capIface = (which && (which.band === '5' || which.band === '6')) ? 'wlan1mon' : 'wlan0mon';
if (ssidRef) {
ssidRef.current = which && which.ssid ? which.ssid : '';
if (ssidRef.tick) ssidRef.tick();
}
if (box.__pbTick) box.__pbTick(s, which);
if (opts.handshakes) {
hsBox.innerHTML = '';
hsBox.appendChild(h('div', { class: 'pineap-card-title', text: 'Handshakes Captured (' + (s.handshakes || 0) + ')' }));
@@ -1525,7 +1560,7 @@ views.harness = (root) => {
return { destroy: () => {} };
};
function deauthPanel() {
function deauthPanel(ssidRef) {
const wrap = h('div', { class: 'pineap-title-card' });
wrap.appendChild(h('div', { class: 'pineap-card-title' }, 'Deauth Targeting'));
const body = h('div', { style: 'font-size:13px' });
@@ -1533,46 +1568,58 @@ function deauthPanel() {
const ssidBox = h('input', { placeholder: 'SSID to find clients for' });
const apSel = h('select', {});
const clTable = h('div', {});
body.appendChild(h('div', { class: 'row' }, ssidBox,
h('div', {}, btn('Find', () => {
const q = ssidBox.value.trim();
if (!q) return;
PagerAPI.get('/api/attacks/clients?ssid=' + encodeURIComponent(q)).then((r) => {
const d = r.data || {};
apSel.innerHTML = '';
(d.aps || []).forEach((a) => apSel.appendChild(h('option', {
value: (a.bssid || '') + '|' + (a.channel || 1),
text: (a.ssid || q) + ' — ' + (a.bssid || '?') + ' ch' + (a.channel || 1)
let lastLookup = '';
function lookup(q) {
if (!q || q === lastLookup) return;
lastLookup = q;
PagerAPI.get('/api/attacks/clients?ssid=' + encodeURIComponent(q)).then((r) => {
const d = r.data || {};
apSel.innerHTML = '';
(d.aps || []).forEach((a) => apSel.appendChild(h('option', {
value: (a.bssid || '') + '|' + (a.channel || 1),
text: (a.ssid || q) + ' — ' + (a.bssid || '?') + ' ch' + (a.channel || 1)
})));
if (!(d.aps || []).length) apSel.appendChild(h('option', { value: '|1', text: 'No APs found — check SSID' }));
clTable.innerHTML = '';
const cl = (d.clients || []).slice(0, 30);
if (!cl.length) {
clTable.appendChild(h('div', { class: 'empty', text: 'No devices in recon yet.' }));
return;
}
clTable.appendChild(table(
[{ key: 'mac', label: 'MAC' }, { key: 'freq', label: 'Freq (MHz)' },
{ key: 'signal', label: 'Signal' }, { key: 'packets', label: 'Packets' },
{ key: '_deauth', label: '' }],
cl.map((c) => {
const mac = c.mac || c.client_mac || '';
return { mac: mac || '—', freq: c.freq || '', signal: c.signal || '',
packets: c.packets || '', _deauth: btn('Deauth', () => {
const parts = apSel.value.split('|');
if (!mac || !parts[0]) { App.toast('Pick an AP and device first', 'error'); return; }
PagerAPI.post('/api/attacks/deauth', {
bssid: parts[0], client: mac, channel: parseInt(parts[1], 10)
}).then(() => App.toast('Deauth frames sent'))
.catch((e) => App.toast(e.message || 'Deauth failed', 'error'));
}, 'danger') };
})));
if (!(d.aps || []).length) apSel.appendChild(h('option', { value: '|1', text: 'No APs found — check SSID' }));
clTable.innerHTML = '';
const cl = (d.clients || []).slice(0, 30);
if (!cl.length) {
clTable.appendChild(h('div', { class: 'empty', text: 'No devices in recon yet.' }));
return;
}
clTable.appendChild(table(
[{ key: 'mac', label: 'MAC' }, { key: 'freq', label: 'Freq (MHz)' },
{ key: 'signal', label: 'Signal' }, { key: 'packets', label: 'Packets' },
{ key: '_deauth', label: '' }],
cl.map((c) => {
const mac = c.mac || c.client_mac || '';
return { mac: mac || '—', freq: c.freq || '', signal: c.signal || '',
packets: c.packets || '', _deauth: btn('Deauth', () => {
const parts = apSel.value.split('|');
if (!mac || !parts[0]) { App.toast('Pick an AP and device first', 'error'); return; }
PagerAPI.post('/api/attacks/deauth', {
bssid: parts[0], client: mac, channel: parseInt(parts[1], 10)
}).then(() => App.toast('Deauth frames sent'))
.catch((e) => App.toast(e.message || 'Deauth failed', 'error'));
}, 'danger') };
})));
}).catch(() => App.toast('Lookup failed', 'error'));
}))));
}).catch(() => App.toast('Lookup failed', 'error'));
}
body.appendChild(h('div', { class: 'row' }, ssidBox,
h('div', {}, btn('Find', () => lookup(ssidBox.value.trim())))));
body.appendChild(apSel);
body.appendChild(clTable);
body.appendChild(h('div', { class: 'muted', style: 'font-size:12px;margin-top:4px',
text: 'Only deauth targets you are authorized to test.' }));
if (ssidRef) {
ssidRef.tick = () => {
const liveSsid = ssidRef.current && ssidRef.current.trim();
if (liveSsid && ssidBox.value.trim() !== liveSsid) {
ssidBox.value = liveSsid;
lastLookup = '';
lookup(liveSsid);
}
};
}
return wrap;
}
@@ -1583,14 +1630,42 @@ views.attacks_wpa = attackLauncher('wpa', {
encodings: EVIL_ENC,
handshakes: true,
export: true,
deauth: true
deauth: true,
playbook: {
steps: ['Deploy the evil twin',
'Wait for a client to associate',
'Deauth the target client to force the 4-way',
'Export .hc22000 and crack with hashcat'],
currentStep: (s, w) => {
if (!w || !w.enabled) return 'Deploy the evil twin';
if (!s || !(s.handshakes > 0)) return 'Wait for a client to associate';
return 'Export .hc22000 and crack with hashcat';
},
hint: (s, w) => {
if (!w || !w.enabled) return '1. Set the target SSID and passphrase, pick a channel, Deploy. 2. When the target client is near, use Deauth Targeting below. 3. Captured handshakes appear above — Export and run the hashcat command.';
if (s && s.handshakes > 0) return 'Handshake captured! Export .hc22000 and run hashcat -m 22000.';
return 'AP is live on ' + (w.ssid || 'the target') + '. Watch the handshakes list — use Deauth Targeting to nudge the client.';
}
}
});
views.attacks_open = attackLauncher('open', {
title: 'Evil Open',
subtitle: 'Open network evil twin',
bssid: true,
country: true
country: true,
playbook: {
steps: ['Deploy the open AP',
'Wait for clients to associate',
'Watch connected clients under PineAP → Clients'],
currentStep: (s, w) => {
if (!w || !w.enabled) return 'Deploy the open AP';
return 'Wait for clients to associate';
},
hint: (s, w) => !w || !w.enabled
? 'Set the SSID (optionally spoof a BSSID), pick a channel, Deploy.'
: 'Open AP is live on ' + (w.ssid || 'the target') + ' — clients that join appear in the Clients list.'
}
});
views.attacks_enterprise = (root) => {
@@ -1628,6 +1703,13 @@ views.attacks_enterprise = (root) => {
const status = attackStatusCard();
box.appendChild(status.card);
const pb = h('div', { class: 'pineap-title-card' });
pb.appendChild(h('div', { class: 'pineap-card-title' }, 'Playbook'));
const pbHint = h('div', { style: 'font-size:13px;line-height:1.8', text:
'1. Deploy the enterprise twin (5 GHz). 2. When a client attempts 802.1X, its identity and MSCHAPv2 challenge/response land in the tables below. 3. Crack offline: hashcat -m 5500 on the challenge data.' });
pb.appendChild(pbHint);
box.appendChild(pb);
const pApe = h('div', { class: 'pineap-title-card' });
pApe.appendChild(h('div', { class: 'pineap-card-title' }, 'PineAPE'));
const pApeBody = h('div', {});