'use strict'; 'require view'; 'require form'; 'require uci'; 'require rpc'; 'require ui'; var callStatus = rpc.declare({ object: 'luci.mac-guardian', method: 'status', expect: { '': {} } }); var callGetClients = rpc.declare({ object: 'luci.mac-guardian', method: 'get_clients', expect: { '': {} } }); var callGetEvents = rpc.declare({ object: 'luci.mac-guardian', method: 'get_events', params: ['count'], expect: { '': {} } }); var callScan = rpc.declare({ object: 'luci.mac-guardian', method: 'scan' }); var callStart = rpc.declare({ object: 'luci.mac-guardian', method: 'start' }); var callStop = rpc.declare({ object: 'luci.mac-guardian', method: 'stop' }); var callRestart = rpc.declare({ object: 'luci.mac-guardian', method: 'restart' }); var callTrust = rpc.declare({ object: 'luci.mac-guardian', method: 'trust', params: ['mac'] }); var callBlock = rpc.declare({ object: 'luci.mac-guardian', method: 'block', params: ['mac'] }); var callDhcpStatus = rpc.declare({ object: 'luci.mac-guardian', method: 'dhcp_status', expect: { '': {} } }); var callDhcpCleanup = rpc.declare({ object: 'luci.mac-guardian', method: 'dhcp_cleanup', expect: { '': {} } }); function formatDate(ts) { if (!ts || ts === 0) return '-'; var d = new Date(ts * 1000); var pad = function(n) { return n < 10 ? '0' + n : n; }; return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()); } function statusBadge(status) { var colors = { 'trusted': '#080', 'suspect': '#c60', 'blocked': '#c00', 'unknown': '#888' }; var color = colors[status] || '#888'; return '' + status + ''; } return view.extend({ load: function() { return Promise.all([ uci.load('mac-guardian'), callStatus(), callGetClients(), callGetEvents(10), callDhcpStatus() ]); }, render: function(data) { var status = data[1]; var clientData = data[2]; var eventData = data[3]; var dhcpStatus = data[4] || {}; var clients = (clientData && clientData.clients) ? clientData.clients : []; var events = (eventData && eventData.events) ? eventData.events : []; var m, s, o; m = new form.Map('mac-guardian', _('MAC Guardian'), _('WiFi MAC address security monitor. Detects randomized MACs, spoofing, and MAC floods.')); // ========================================== // Status Section // ========================================== s = m.section(form.NamedSection, 'main', 'mac-guardian', _('Status')); s.anonymous = true; o = s.option(form.DummyValue, '_status'); o.rawhtml = true; o.cfgvalue = function() { var svcColor = status.service_status === 'running' ? '#080' : '#c00'; var svcLabel = status.service_status === 'running' ? 'Running' : 'Stopped'; var html = '
'; // Service card html += '
'; html += '

Service

'; html += '

Status: ' + svcLabel + '

'; html += '

Policy: ' + (status.policy || 'alert') + '

'; html += '

Interval: ' + (status.scan_interval || 30) + 's

'; html += '
'; // Clients card var cl = status.clients || {}; html += '
'; html += '

Clients

'; html += '

Total: ' + (cl.total || 0) + '

'; html += '

Trusted: ' + (cl.trusted || 0) + '

'; html += '

Suspect: ' + (cl.suspect || 0) + '

'; html += '

Blocked: ' + (cl.blocked || 0) + '

'; html += '
'; // Interfaces card var ifaces = status.interfaces || []; html += '
'; html += '

WiFi Interfaces

'; if (ifaces.length === 0) { html += '

None detected

'; } else { for (var i = 0; i < ifaces.length; i++) { html += '

' + ifaces[i].name + ' (' + ifaces[i].essid + ') - ' + ifaces[i].stations + ' STA

'; } } html += '
'; // DHCP Protection card var dhcpColor = dhcpStatus.enabled ? '#080' : '#888'; var dhcpLabel = dhcpStatus.enabled ? 'Enabled' : 'Disabled'; html += '
'; html += '

DHCP Protection

'; html += '

Status: ' + dhcpLabel + '

'; html += '

Leases: ' + (dhcpStatus.leases || 0) + '

'; html += '

Conflicts: ' + (dhcpStatus.conflicts || 0) + '

'; html += '

Stale: ' + (dhcpStatus.stale || 0) + '

'; html += '
'; html += '
'; return html; }; // Control buttons o = s.option(form.Button, '_start', _('Start')); o.inputtitle = _('Start'); o.inputstyle = 'apply'; o.onclick = function() { return callStart().then(function() { window.location.reload(); }); }; o = s.option(form.Button, '_stop', _('Stop')); o.inputtitle = _('Stop'); o.inputstyle = 'remove'; o.onclick = function() { return callStop().then(function() { window.location.reload(); }); }; o = s.option(form.Button, '_scan', _('Scan Now')); o.inputtitle = _('Scan'); o.inputstyle = 'reload'; o.onclick = function() { ui.showModal(_('Scanning'), [ E('p', { 'class': 'spinning' }, _('Scanning WiFi interfaces...')) ]); return callScan().then(function() { ui.hideModal(); window.location.reload(); }); }; o = s.option(form.Button, '_dhcp_cleanup', _('DHCP Cleanup')); o.inputtitle = _('Clean Up'); o.inputstyle = 'reload'; o.onclick = function() { ui.showModal(_('Cleaning'), [ E('p', { 'class': 'spinning' }, _('Running DHCP lease maintenance...')) ]); return callDhcpCleanup().then(function() { ui.hideModal(); window.location.reload(); }); }; // ========================================== // Clients Table // ========================================== s = m.section(form.NamedSection, 'main', 'mac-guardian', _('Known Clients')); s.anonymous = true; o = s.option(form.DummyValue, '_clients'); o.rawhtml = true; o.cfgvalue = function() { if (clients.length === 0) { return '

No clients detected yet. Run a scan or wait for devices to connect.

'; } var html = '
'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; for (var i = 0; i < clients.length; i++) { var c = clients[i]; var macDisplay = c.mac; if (c.randomized) { macDisplay += ' R'; } html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; } html += '
MACVendorHostnameInterfaceFirst SeenLast SeenStatusActions
' + macDisplay + '' + (c.vendor || '-') + '' + (c.hostname || '-') + '' + (c.iface || '-') + '' + formatDate(c.first_seen) + '' + formatDate(c.last_seen) + '' + statusBadge(c.status) + ''; if (c.status !== 'trusted') { html += ''; } if (c.status !== 'blocked') { html += ''; } html += '
'; html += '
'; return html; }; // ========================================== // Recent Alerts // ========================================== s = m.section(form.NamedSection, 'main', 'mac-guardian', _('Recent Alerts')); s.anonymous = true; o = s.option(form.DummyValue, '_events'); o.rawhtml = true; o.cfgvalue = function() { if (events.length === 0) { return '

No alerts recorded.

'; } var html = '
'; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; for (var i = events.length - 1; i >= 0; i--) { try { var ev = JSON.parse(events[i]); html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; html += ''; } catch(e) { continue; } } html += '
TimeEventMACInterfaceDetails
' + (ev.ts || '-') + '' + (ev.event || '-') + '' + (ev.mac || '-') + '' + (ev.iface || '-') + '' + (ev.details || '-') + '
'; html += '
'; return html; }; // ========================================== // Configuration // ========================================== s = m.section(form.NamedSection, 'main', 'mac-guardian', _('Configuration')); s.anonymous = true; o = s.option(form.Flag, 'enabled', _('Enabled'), _('Enable MAC Guardian service')); o.rmempty = false; o = s.option(form.Value, 'scan_interval', _('Scan Interval'), _('Seconds between WiFi scans')); o.datatype = 'uinteger'; o.default = '30'; o.placeholder = '30'; s = m.section(form.NamedSection, 'detection', 'detection', _('Detection')); s.anonymous = true; o = s.option(form.Flag, 'random_mac', _('Detect Randomized MACs'), _('Alert on locally-administered (randomized) MAC addresses')); o.default = '1'; o = s.option(form.Flag, 'spoof_detection', _('Detect Spoofing'), _('Alert when a MAC address appears on a different interface')); o.default = '1'; o = s.option(form.Flag, 'mac_flip', _('Detect MAC Floods'), _('Alert when many new MACs appear in a short window')); o.default = '1'; s = m.section(form.NamedSection, 'enforcement', 'enforcement', _('Enforcement')); s.anonymous = true; o = s.option(form.ListValue, 'policy', _('Policy'), _('Action to take on detected threats')); o.value('alert', _('Alert only')); o.value('quarantine', _('Quarantine (drop traffic)')); o.value('deny', _('Deny (drop + deauthenticate)')); o.default = 'alert'; // ========================================== // Bind action buttons // ========================================== var rendered = m.render(); return rendered.then(function(node) { node.addEventListener('click', function(ev) { var btn = ev.target.closest('[data-action]'); if (!btn) return; var mac = btn.getAttribute('data-mac'); var action = btn.getAttribute('data-action'); if (action === 'trust') { callTrust(mac).then(function() { ui.addNotification(null, E('p', _('MAC %s trusted').format(mac)), 'success'); window.location.reload(); }); } else if (action === 'block') { if (confirm(_('Block and deauthenticate %s?').format(mac))) { callBlock(mac).then(function() { ui.addNotification(null, E('p', _('MAC %s blocked').format(mac)), 'success'); window.location.reload(); }); } } }); return node; }); } });