feat: Attacks UI (Overview + Evil WPA/Open/Enterprise launchers)

Side-menu Attacks section with deploy/stop launchers, live status card
(device truth), monitor capture toggle, hashcat .hc22000 export + download,
deauth targeting panel, enterprise cred tables and PineAPE toggles.
This commit is contained in:
2026-08-18 19:30:26 -05:00
parent 01c84caaaa
commit 2ff0c4d320
5 changed files with 426 additions and 0 deletions
@@ -3133,6 +3133,18 @@ def h_attacks_export_hc22000(ctx):
'hashcat': 'hashcat -m 22000 %s -a 0 wordlist.txt' % outname}
def h_attacks_download_hc22000(ctx):
name = _unquote_plus(ctx.args[0])
if not re.match(r'^handshakes_\d+\.hc22000$', name):
return 400, {'error': 'invalid name'}
full = os.path.join('/root/loot/hc22000', name)
if not os.path.isfile(full):
return 404, {'error': 'not found'}
with open(full, 'rb') as f:
body = f.read()
return 200, Download(body, 'application/octet-stream', name)
def h_attacks_deauth(ctx):
body = ctx.body or {}
bssid = (body.get('bssid') or '').strip().upper()
@@ -4277,6 +4289,7 @@ ROUTER.add('POST', r'/api/attacks/stop', h_attacks_stop)
ROUTER.add('GET', r'/api/attacks/status', h_attacks_status)
ROUTER.add('POST', r'/api/attacks/capture', h_attacks_capture)
ROUTER.add('GET', r'/api/attacks/export/hc22000', h_attacks_export_hc22000)
ROUTER.add('GET', r'/api/attacks/export/hc22000/([^/]+)', h_attacks_download_hc22000)
ROUTER.add('POST', r'/api/attacks/deauth', h_attacks_deauth)
ROUTER.add('GET', r'/api/attacks/clients', h_attacks_clients)
ROUTER.add('GET', r'/api/health', h_health)
@@ -185,6 +185,7 @@ body {
.badge { display: inline-block; padding: 2px 10px; border-radius: 10px; font-size: 11px; }
.badge.on { background: #e8f5e9; color: #2e7d32; }
.badge.off { background: #fff3e0; color: #e65100; }
.badge.warn { background: #fff8e1; color: #f57f17; }
.badge.unknown { background: #eeeeee; color: #616161; }
.btn {
background: var(--primary); color: #fff; border: 0; border-radius: 2px;
@@ -468,6 +469,7 @@ html.dark .modal { background: #303030; }
.pineap-infobox { border-radius: 2px; padding: 10px 12px; margin-top: 10px; font-size: 13px; display: flex; flex-direction: column; gap: 8px; }
.pineap-infobox.error { background: #fdecea; color: #b71c1c; border: 1px solid #f5c6cb; }
.pineap-infobox.info { background: #e3f2fd; color: #0d47a1; border: 1px solid #90caf9; }
.pineap-infobox.warn { background: #fff8e1; color: #f57f17; border: 1px solid #ffe082; }
.pineap-infobox-actions { display: flex; gap: 8px; flex-wrap: wrap; }
html.dark .pineap-infobox.error { background: #4a2020; color: #ffb4a9; border-color: #6b2d2d; }
html.dark .pineap-infobox.info { background: #10263a; color: #9cc7f0; border-color: #1d3a54; }
@@ -29,6 +29,7 @@ const App = (() => {
const railItems = [
{ key: 'dashboard', label: 'Dashboard', hash: '#/dashboard', icon: 'dashboard' },
{ key: 'attacks', label: 'Attacks', hash: '#/attacks', icon: 'attack' },
{ key: 'pineap', label: 'PineAP', hash: '#/pineap', icon: 'wifi' },
{ key: 'recon', label: 'Recon', hash: '#/recon', icon: 'recon' },
{ key: 'logging', label: 'Logging', hash: '#/logging', icon: 'logging' },
@@ -393,6 +394,10 @@ const App = (() => {
const routes = {
'#/dashboard': 'dashboard',
'#/attacks': 'attacks',
'#/attacks/wpa': 'attacks_wpa',
'#/attacks/open': 'attacks_open',
'#/attacks/enterprise': 'attacks_enterprise',
'#/pineap': 'pineap',
'#/pineap/open': 'pineap_open',
'#/pineap/evilwpa': 'pineap_evilwpa',
@@ -1,6 +1,7 @@
'use strict';
window.PineappleIcons = {
attack: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12,2L15,9L22,12L15,15L12,22L9,15L2,12L9,9L12,2M12,6.5L10.5,10.5L6.5,12L10.5,13.5L12,17.5L13.5,13.5L17.5,12L13.5,10.5L12,6.5Z"/></svg>',
dashboard: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12,16A3,3 0 0,1 9,13C9,11.88 9.61,10.9 10.5,10.39L20.21,4.77L14.68,14.35C14.18,15.33 13.17,16 12,16M12,3C13.81,3 15.5,3.5 16.97,4.32L14.87,5.53C14,5.19 13,5 12,5A8,8 0 0,0 4,13C4,15.21 4.89,17.21 6.34,18.65H6.35C6.74,19.04 6.74,19.67 6.35,20.06C5.96,20.45 5.32,20.45 4.93,20.07V20.07C3.12,18.26 2,15.76 2,13A10,10 0 0,1 12,3M22,13C22,15.76 20.88,18.26 19.07,20.07V20.07C18.68,20.45 18.05,20.45 17.66,20.06C17.27,19.67 17.27,19.04 17.66,18.65V18.65C19.11,17.2 20,15.21 20,13C20,12 19.81,11 19.46,10.1L20.67,8C21.5,9.5 22,11.18 22,13Z"/></svg>',
pineap: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12,21L15.6,16.2C16.2,15.4 16.8,14.5 17.2,13.6C18.1,11.5 18,9 18,9C18,6.5 16.5,4.3 15,3.5C13.5,2.7 10.5,2.7 9,3.5C7.5,4.3 6,6.5 6,9C6,9 5.9,11.5 6.8,13.6C7.2,14.5 7.8,15.4 8.4,16.2L12,21M12,5.5C13.4,5.5 14.5,6.6 14.5,8C14.5,9.4 13.4,10.5 12,10.5C10.6,10.5 9.5,9.4 9.5,8C9.5,6.6 10.6,5.5 12,5.5M7.1,13.1C7.1,13.1 8.2,14 12,14C15.8,14 16.9,13.1 16.9,13.1L15.9,12.1C15.9,12.1 14.8,12.8 12,12.8C9.2,12.8 8.1,12.1 8.1,12.1L7.1,13.1M12,17C10,17 9,17.6 9,17.6L10.3,19.3C10.3,19.3 11.1,19 12,19C12.9,19 13.7,19.3 13.7,19.3L15,17.6C15,17.6 14,17 12,17Z"/></svg>',
recon: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M11,6H13V13H11V6M9,20A1,1 0 0,1 8,21H5A1,1 0 0,1 4,20V15L6,6H10V13A1,1 0 0,1 9,14V20M10,5H7V3H10V5M15,20V14A1,1 0 0,1 14,13V6H18L20,15V20A1,1 0 0,1 19,21H16A1,1 0 0,1 15,20M14,5V3H17V5H14Z"/></svg>',
@@ -1143,6 +1143,411 @@ function reconFiltered(rows, q, colsArr) {
return rows.filter((r) => colsArr.some((c) => String(r[c.key] == null ? '' : r[c.key]).toLowerCase().indexOf(ql) !== -1));
}
// ---------------------------------------------------------------------------
// Attacks: one-click Evil WPA / Open / Enterprise launchers.
// ---------------------------------------------------------------------------
const ATTACK_TABS = [
{ label: 'Overview', hash: '#/attacks' },
{ label: 'Evil WPA', hash: '#/attacks/wpa' },
{ label: 'Open AP', hash: '#/attacks/open' },
{ label: 'Evil Enterprise', hash: '#/attacks/enterprise' }
];
function attacksShell(root, activeHash) {
root.appendChild(h('h1', { class: 'page-title', text: 'Attacks' }));
tabBar(root, ATTACK_TABS, activeHash);
return h('div', {}, h('div', { class: 'muted', style: 'font-size:12px;margin:8px 0' },
'Targets: only networks you are authorized to test. The device is the source of truth — every change is verified against it.'));
}
function attackBadge(ap) {
if (!ap) return badge(false);
if (!ap.enabled) return h('span', { class: 'badge off', text: 'OFF' });
return h('span', { class: 'badge' + (ap.live ? ' on' : ' warn'), text: ap.live ? 'LIVE' : 'CONFIGURED' });
}
function attackStatusCard() {
const card = h('div', { class: 'pineap-title-card' });
card.appendChild(h('div', { class: 'pineap-card-title' }, 'Status'));
const rows = h('div', { style: 'font-size:13px;line-height:1.9' });
card.appendChild(rows);
const append = (label, node) => rows.appendChild(
h('div', { class: 'row' }, h('div', { style: 'min-width:130px', text: label }), node));
return { card, rows, append };
}
function verifiedToast(result) {
if (result && result.verified) App.toast('Applied and verified on device');
else if (result && result.ok) App.toast('Applied (verification pending — radio reloading)', 'error');
else App.toast('Deploy failed', 'error');
}
views.attacks = (root) => {
const box = attacksShell(root, '#/attacks');
const wrap = h('div', { class: 'pineap-title-card-container' });
box.appendChild(wrap);
const kinds = [
['wpa', 'Evil WPA (PSK)', 'Clone a WPA2/WPA3-PSK network and capture the four-way handshake.', '#/attacks/wpa'],
['open', 'Evil Open', 'Advertise an open network and watch who connects.', '#/attacks/open'],
['enterprise', 'Evil Enterprise', 'Serve WPA2/3-Enterprise with PineAPE and harvest 802.1X credentials.', '#/attacks/enterprise']
];
const statusEls = {};
kinds.forEach(([kind, label, desc, hash]) => {
const card = h('div', { class: 'pineap-title-card' });
const st = h('span', { class: 'badge', text: '—' });
statusEls[kind] = st;
card.appendChild(h('div', { class: 'pineap-card-title' },
h('a', { href: hash, style: 'color:var(--primary);cursor:pointer',
onclick: (e) => { e.preventDefault(); App.go(hash); } }, label)));
card.appendChild(h('div', { class: 'muted', style: 'font-size:12px;margin:6px 0', text: desc }));
card.appendChild(h('div', { class: 'row' }, st,
h('a', { class: 'btn ghost', href: hash, style: 'text-decoration:none',
onclick: (e) => { e.preventDefault(); App.go(hash); } }, 'Configure')));
wrap.appendChild(card);
});
function load() {
PagerAPI.get('/api/attacks/status').then((r) => {
const s = r.data || {};
const live = (x) => !!(x && x.enabled);
const summary = {
wpa: live(s.wpa && s.wpa.radio0) || live(s.wpa && s.wpa.radio1) ? 'LIVE' : 'OFF',
open: live(s.open && s.open.radio0) || live(s.open && s.open.radio1) ? 'LIVE' : 'OFF',
enterprise: live(s.enterprise && s.enterprise.ap) ? 'LIVE' : 'OFF'
};
Object.keys(summary).forEach((k) => {
statusEls[k].textContent = summary[k];
statusEls[k].className = 'badge ' + (summary[k] === 'LIVE' ? 'on' : 'off');
});
}).catch(() => {});
}
load();
const iv = setInterval(load, 5000);
return { destroy: () => clearInterval(iv) };
};
function attackLauncher(kind, opts) {
return (root) => {
const box = attacksShell(root, '#/attacks/' + kind);
const form = h('div', { class: 'pineap-title-card' });
form.appendChild(h('div', { class: 'pineap-card-title' },
opts.title + (opts.subtitle ? ' — ' + opts.subtitle : '')));
box.appendChild(form);
const f = h('div', {});
form.appendChild(f);
const ssidIn = h('input', { id: 'atk-ssid' });
const hiddenCb = h('input', { type: 'checkbox', id: 'atk-hidden' });
const chanSel = h('select', { id: 'atk-channel' });
chanSelect(chanSel, null);
const bandHint = h('div', { class: 'muted', style: 'font-size:12px;margin-top:4px' });
let pskIn = null, encSel = null;
chanSel.addEventListener('change', () => {
const b = bandOfChannel(chanSel.value);
bandHint.textContent = b === '6' ? '6 GHz requires WPA3 (SAE/OWE).' : '';
if (encSel) {
Array.prototype.forEach.call(encSel.options, (o) => {
o.disabled = b === '6' && o.value !== 'sae' && o.value !== 'owe';
});
if (b === '6' && encSel.value === 'psk2') encSel.value = 'sae';
}
});
f.appendChild(h('label', {}, 'SSID (target network)', ssidIn));
if (opts.passphrase) {
pskIn = h('input', { id: 'atk-psk', type: 'password', autocomplete: 'new-password' });
encSel = h('select', { id: 'atk-enc' });
(opts.encodings || EVIL_ENC).forEach(([v, l]) => encSel.appendChild(h('option', { value: v, text: l })));
f.appendChild(h('label', {}, 'Passphrase', pskIn));
f.appendChild(h('label', {}, 'Encryption', encSel));
}
let bssidIn = null, coSel = null;
if (opts.bssid) {
bssidIn = h('input', { id: 'atk-bssid', placeholder: 'AA:BB:CC:DD:EE:FF' });
f.appendChild(h('label', {}, 'BSSID (spoof, optional)', bssidIn));
}
if (opts.country) {
coSel = h('select', { id: 'atk-country' });
OPEN_COUNTRIES.forEach(([v, l]) => coSel.appendChild(h('option', { value: v, text: l })));
f.appendChild(h('label', {}, 'Country', coSel));
}
f.appendChild(h('label', { class: 'switch' }, hiddenCb, h('span', { class: 'track' }), 'Hidden'));
f.appendChild(h('label', {}, 'Channel', chanSel));
f.appendChild(bandHint);
f.appendChild(h('div', { class: 'row', style: 'margin-top:10px' },
h('div', {}, btn('Deploy Attack', () => {
const body = {
kind, ssid: ssidIn.value.trim(),
hidden: hiddenCb.checked,
channel: chanSel.value ? parseInt(chanSel.value, 10) : null
};
if (pskIn) { body.passphrase = pskIn.value; body.enctype = encSel.value; }
if (bssidIn) body.bssid = bssidIn.value.trim();
if (coSel) body.country = coSel.value;
PagerAPI.post('/api/attacks/deploy', body)
.then((r) => { verifiedToast(r.data || {}); load(); })
.catch((e) => { App.toast(e.message || 'Deploy failed', 'error'); });
})),
h('div', {}, btn('Stop Attack', () => {
PagerAPI.post('/api/attacks/stop', { kind })
.then(() => { App.toast('Attack stopped'); load(); })
.catch(() => App.toast('Stop failed', 'error'));
}, 'danger')),
h('div', { class: 'muted', style: 'align-self:center;font-size:12px' },
'Deploy reconfigures the radio — you may be disconnected briefly.')));
const status = attackStatusCard();
box.appendChild(status.card);
const hsBox = h('div', {});
const captureBox = h('div', { class: 'pineap-title-card' });
captureBox.appendChild(h('div', { class: 'pineap-card-title' }, 'Monitor Capture'));
const capBody = h('div', { style: 'font-size:13px' });
captureBox.appendChild(capBody);
box.appendChild(captureBox);
let capIface = 'wlan0mon';
function capRow(st) {
capBody.innerHTML = '';
const run = !!(st && st.running);
capBody.appendChild(h('div', { class: 'row' },
h('span', { text: run ? ('Capturing on ' + (st.iface || capIface)) : 'Not capturing' }),
h('div', {}, btn(run ? 'Stop Capture' : 'Start Capture', () => {
PagerAPI.post('/api/attacks/capture', {
iface: capIface, action: run ? 'stop' : 'start'
}).then((r) => capRow(r.data || {})).catch(() => App.toast('Capture failed', 'error'));
}))));
capBody.appendChild(h('div', { class: 'muted', style: 'font-size:12px;margin-top:4px',
text: run && st.path ? st.path : '' }));
}
if (opts.export) {
const exp = h('div', { class: 'pineap-title-card' });
exp.appendChild(h('div', { class: 'pineap-card-title' }, 'Hashcat Export'));
const expBody = h('div', { style: 'font-size:13px;line-height:1.8' });
exp.appendChild(expBody);
expBody.appendChild(h('div', {}, btn('Export .hc22000', () => {
PagerAPI.get('/api/attacks/export/hc22000').then((r) => {
const d = r.data || {};
if (d.file) {
expBody.appendChild(h('div', { class: 'row', style: 'margin-top:8px' },
h('a', { class: 'btn ghost', href: '/api/attacks/export/hc22000/' + d.name,
style: 'text-decoration:none', download: d.name }, 'Download ' + d.name),
h('code', { style: 'font-size:12px', text: d.hashcat || '' })));
App.toast('Export ready');
} else App.toast('No captures to export', 'error');
}).catch(() => App.toast('Export failed', 'error'));
})));
box.appendChild(exp);
}
let deauthCard = null;
if (opts.deauth) {
deauthCard = deauthPanel();
box.appendChild(deauthCard);
}
function load() {
PagerAPI.get('/api/attacks/status').then((r) => {
const s = r.data || {};
const ap = s[kind] || {};
const which = (ap.radio0 && ap.radio0.enabled) ? ap.radio0
: (ap.radio1 && ap.radio1.enabled) ? ap.radio1 : null;
status.rows.innerHTML = '';
status.append('Attack', attackBadge(which));
status.append('SSID', h('span', { text: which && which.ssid ? which.ssid : '—' }));
status.append('Interface', h('span', { text: which ? which.iface : '—' }));
status.append('Band', h('span', { text: which && which.band ? which.band + ' GHz' : '—' }));
status.append('Channel', h('span', { text: which && which.channel != null ? which.channel : '—' }));
if (which && !which.live) {
status.append('Note', h('span', { class: 'pineap-infobox warn',
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 (opts.handshakes) {
hsBox.innerHTML = '';
hsBox.appendChild(h('div', { class: 'pineap-card-title', text: 'Handshakes Captured (' + (s.handshakes || 0) + ')' }));
PagerAPI.get('/api/pineap/handshakes').then((hr) => {
const hd = (hr.data || {}).parsed || [];
const rows = hd.slice(-10).reverse().map((x) => ({
ssid: x.ssid || '—', bssid: x.bssid || '—',
when: fmtTime(x.ts || 0), type: x.type || ''
}));
hsBox.appendChild(rows.length ? table(
[{ key: 'when', label: 'When' }, { key: 'ssid', label: 'SSID' },
{ key: 'bssid', label: 'BSSID' }, { key: 'type', label: 'Type' }], rows)
: h('div', { class: 'empty', text: 'No handshakes yet.' }));
}).catch(() => {});
box.appendChild(hsBox);
}
}).catch(() => {});
PagerAPI.post('/api/attacks/capture', { action: 'status' }).then((r) => capRow(r.data || {})).catch(() => {});
}
load();
const iv = setInterval(load, 5000);
return { destroy: () => clearInterval(iv) };
};
}
function deauthPanel() {
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' });
wrap.appendChild(body);
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)
})));
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'));
}))));
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.' }));
return wrap;
}
views.attacks_wpa = attackLauncher('wpa', {
title: 'Evil WPA',
subtitle: 'WPA2-PSK / WPA3-SAE / WPA3-OWE evil twin with handshake capture',
passphrase: true,
encodings: EVIL_ENC,
handshakes: true,
export: true,
deauth: true
});
views.attacks_open = attackLauncher('open', {
title: 'Evil Open',
subtitle: 'Open network evil twin',
bssid: true,
country: true
});
views.attacks_enterprise = (root) => {
const box = attacksShell(root, '#/attacks/enterprise');
const form = h('div', { class: 'pineap-title-card' });
form.appendChild(h('div', { class: 'pineap-card-title' },
'Evil Enterprise — WPA2/3-Enterprise with PineAPE credential harvest'));
box.appendChild(form);
const f = h('div', {});
form.appendChild(f);
const ssidIn = h('input', { id: 'ent-ssid' });
const encSel = h('select', { id: 'ent-enc' });
[['wpa2', 'WPA2 Enterprise'], ['wpa3', 'WPA3 Enterprise']]
.forEach(([v, l]) => encSel.appendChild(h('option', { value: v, text: l })));
const pskIn = h('input', { id: 'ent-pass', type: 'password', autocomplete: 'new-password' });
const hiddenCb = h('input', { type: 'checkbox', id: 'ent-hidden' });
f.appendChild(h('label', {}, 'SSID', ssidIn));
f.appendChild(h('label', {}, 'Encryption', encSel));
f.appendChild(h('label', {}, 'Passphrase (EAP server secret)', pskIn));
f.appendChild(h('label', { class: 'switch' }, hiddenCb, h('span', { class: 'track' }), 'Hidden'));
f.appendChild(h('div', { class: 'row', style: 'margin-top:10px' },
h('div', {}, btn('Deploy Attack', () => {
PagerAPI.post('/api/attacks/deploy', {
kind: 'enterprise', ssid: ssidIn.value.trim(),
enctype: encSel.value, passphrase: pskIn.value, hidden: hiddenCb.checked
}).then((r) => { verifiedToast(r.data || {}); load(); })
.catch((e) => App.toast(e.message || 'Deploy failed', 'error'));
})),
h('div', {}, btn('Stop Attack', () => {
PagerAPI.post('/api/attacks/stop', { kind: 'enterprise' })
.then(() => { App.toast('Attack stopped'); load(); })
.catch(() => App.toast('Stop failed', 'error'));
}, 'danger'))));
const status = attackStatusCard();
box.appendChild(status.card);
const pApe = h('div', { class: 'pineap-title-card' });
pApe.appendChild(h('div', { class: 'pineap-card-title' }, 'PineAPE'));
const pApeBody = h('div', {});
pApe.appendChild(pApeBody);
box.appendChild(pApe);
const authCb = h('input', { type: 'checkbox', id: 'ent-auth' });
authCb.addEventListener('change', () => PagerAPI.post('/api/pineap/hostapd', { pineape_auth_pass: authCb.checked })
.then(load).catch(() => { authCb.checked = !authCb.checked; App.toast('Failed', 'error'); }));
const tables = [['Basic Data', '/api/pineap/enterprise/basic', 'basic'],
['Challenge Data', '/api/pineap/enterprise/challenge', 'challenge']]
.map(([name, endpoint, tableKey]) => {
const tb = h('div', { class: 'pineap-title-card' });
const body = h('div', {});
tb.appendChild(h('div', { class: 'pineap-card-title-flex' },
h('span', { text: name }),
h('span', { class: 'toolbar-spacer' }),
btn('Clear', () => PagerAPI.post('/api/pineap/enterprise/clear', { table: tableKey }).then(load), 'danger')));
tb.appendChild(body);
box.appendChild(tb);
return { body, endpoint };
});
function load() {
PagerAPI.get('/api/attacks/status').then((r) => {
const s = r.data || {};
const ent = s.enterprise || {};
const ap = ent.ap || null;
status.rows.innerHTML = '';
status.append('Attack', attackBadge(ap));
status.append('SSID', h('span', { text: ap && ap.ssid ? ap.ssid : '—' }));
status.append('Interface', h('span', { text: ap ? ap.iface : '—' }));
status.append('Band', h('span', { text: '2.4 GHz' }));
status.append('Channel', h('span', { text: ap && ap.channel != null ? ap.channel : '—' }));
status.append('Credentials', h('span', { text: String(ent.creds || 0) }));
pApeBody.innerHTML = '';
pApeBody.appendChild(h('label', { class: 'switch' }, authCb, h('span', { class: 'track' }), 'Auth Pass Capture'));
if (ent.pineape) {
pApeBody.appendChild(h('div', { class: 'muted', style: 'font-size:12px',
text: 'PineAPE ' + (ent.pineape.enabled ? 'enabled' : 'disabled') }));
}
}).catch(() => {});
tables.forEach((t) => {
PagerAPI.get(t.endpoint).then((r) => {
const rows = (r.data.rows || []).slice();
t.body.innerHTML = '';
const cols = rows.length ? Object.keys(rows[0]).map((k) => ({ label: k, key: k }))
: [{ label: '—', key: '_none' }];
t.body.appendChild(table(cols, rows));
if (!rows.length) t.body.appendChild(h('div', { class: 'empty', text: 'No data captured.' }));
}).catch(() => {});
});
}
load();
const iv = setInterval(load, 5000);
return { destroy: () => clearInterval(iv) };
};
function reconEncBucket(enc) {
const s = (enc || '').trim();
if (s === 'Open') return 'Open';