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,10 +1568,10 @@ 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;
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 = '';
@@ -1568,11 +1603,23 @@ function deauthPanel() {
}, 'danger') };
})));
}).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', {});