'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 += '| MAC | ';
html += 'Vendor | ';
html += 'Hostname | ';
html += 'Interface | ';
html += 'First Seen | ';
html += 'Last Seen | ';
html += 'Status | ';
html += 'Actions | ';
html += '
';
for (var i = 0; i < clients.length; i++) {
var c = clients[i];
var macDisplay = c.mac;
if (c.randomized) {
macDisplay += ' R';
}
html += '';
html += '| ' + macDisplay + ' | ';
html += '' + (c.vendor || '-') + ' | ';
html += '' + (c.hostname || '-') + ' | ';
html += '' + (c.iface || '-') + ' | ';
html += '' + formatDate(c.first_seen) + ' | ';
html += '' + formatDate(c.last_seen) + ' | ';
html += '' + statusBadge(c.status) + ' | ';
html += '';
if (c.status !== 'trusted') {
html += '';
}
if (c.status !== 'blocked') {
html += '';
}
html += ' | ';
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 += '| Time | ';
html += 'Event | ';
html += 'MAC | ';
html += 'Interface | ';
html += 'Details | ';
html += '
';
for (var i = events.length - 1; i >= 0; i--) {
try {
var ev = JSON.parse(events[i]);
html += '';
html += '| ' + (ev.ts || '-') + ' | ';
html += '' + (ev.event || '-') + ' | ';
html += '' + (ev.mac || '-') + ' | ';
html += '' + (ev.iface || '-') + ' | ';
html += '' + (ev.details || '-') + ' | ';
html += '
';
} catch(e) {
continue;
}
}
html += '
';
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;
});
}
});