- InterceptoR: Refactor to use shared KissTheme.wrap() module - Remove duplicate inline CSS (~200 lines) - Use shared theme for sidebar navigation - IoT Guard: Update to KISS dark theme styling - Use KissTheme.wrap() with sidebar - Update stat cards to use KISS classes - Update device chips and anomaly table styling - mitmproxy: Add KISS theme wrapper - Add KissTheme.wrap() for sidebar navigation - Update info card styling to match theme - System Hub: Update to KISS theme - Add KissTheme.wrap() for sidebar navigation - Update quick actions to use kiss-btn class - Inject KISS-compatible extra styles for cards Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
267 lines
7.9 KiB
JavaScript
267 lines
7.9 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require rpc';
|
|
'require poll';
|
|
'require secubox/kiss-theme';
|
|
|
|
var callStatus = rpc.declare({
|
|
object: 'luci.iot-guard',
|
|
method: 'status',
|
|
expect: {}
|
|
});
|
|
|
|
var callGetDevices = rpc.declare({
|
|
object: 'luci.iot-guard',
|
|
method: 'get_devices',
|
|
expect: {}
|
|
});
|
|
|
|
var callGetAnomalies = rpc.declare({
|
|
object: 'luci.iot-guard',
|
|
method: 'get_anomalies',
|
|
params: ['limit'],
|
|
expect: {}
|
|
});
|
|
|
|
var callScan = rpc.declare({
|
|
object: 'luci.iot-guard',
|
|
method: 'scan',
|
|
expect: {}
|
|
});
|
|
|
|
// Device class icons
|
|
var CLASS_ICONS = {
|
|
camera: '📷',
|
|
thermostat: '🌡️',
|
|
lighting: '💡',
|
|
plug: '🔌',
|
|
assistant: '🎤',
|
|
media: '📺',
|
|
lock: '🔒',
|
|
sensor: '📡',
|
|
diy: '🛠️',
|
|
mixed: '🔗',
|
|
unknown: '❓'
|
|
};
|
|
|
|
return view.extend({
|
|
handleScan: function() {
|
|
return callScan().then(function() {
|
|
window.location.reload();
|
|
});
|
|
},
|
|
|
|
load: function() {
|
|
return Promise.all([
|
|
callStatus(),
|
|
callGetDevices(),
|
|
callGetAnomalies(5)
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
var status = data[0] || {};
|
|
var devices = (data[1] && data[1].devices) || [];
|
|
var anomalies = (data[2] && data[2].anomalies) || [];
|
|
var self = this;
|
|
|
|
// Poll for updates
|
|
poll.add(L.bind(function() {
|
|
return callStatus().then(L.bind(function(status) {
|
|
this.updateStats(status);
|
|
}, this));
|
|
}, this), 10);
|
|
|
|
var content = [
|
|
// Header
|
|
E('div', { 'style': 'margin-bottom: 24px;' }, [
|
|
E('h2', { 'style': 'font-size: 24px; font-weight: 700; margin: 0 0 8px 0;' }, '📡 IoT Guard'),
|
|
E('p', { 'style': 'color: var(--kiss-muted); margin: 0;' }, 'Device Isolation & Security Monitoring')
|
|
]),
|
|
|
|
// Stats Grid
|
|
E('div', { 'class': 'kiss-grid kiss-grid-auto', 'style': 'margin-bottom: 24px;' }, [
|
|
this.renderStatCard('Devices', status.total_devices || 0, 'var(--kiss-blue)'),
|
|
this.renderStatCard('Isolated', status.isolated || 0, 'var(--kiss-yellow)'),
|
|
this.renderStatCard('Blocked', status.blocked || 0, 'var(--kiss-red)'),
|
|
this.renderStatCard('High Risk', status.high_risk || 0, 'var(--kiss-red)'),
|
|
this.renderStatCard('Anomalies', status.anomalies || 0, 'var(--kiss-orange)'),
|
|
this.renderScoreCard('Security Score', status.security_score || 0)
|
|
]),
|
|
|
|
// Action buttons
|
|
E('div', { 'class': 'kiss-card', 'style': 'display: flex; gap: 12px; flex-wrap: wrap;' }, [
|
|
E('button', {
|
|
'class': 'kiss-btn kiss-btn-green',
|
|
'click': L.bind(this.handleScan, this)
|
|
}, '🔍 Scan Network'),
|
|
E('a', {
|
|
'href': L.url('admin/secubox/services/iot-guard/devices'),
|
|
'class': 'kiss-btn',
|
|
'style': 'text-decoration: none;'
|
|
}, '📋 View All Devices'),
|
|
E('a', {
|
|
'href': L.url('admin/secubox/services/iot-guard/settings'),
|
|
'class': 'kiss-btn',
|
|
'style': 'text-decoration: none;'
|
|
}, '⚙️ Settings')
|
|
]),
|
|
|
|
// Device Grid by Risk
|
|
E('div', { 'class': 'kiss-card' }, [
|
|
E('div', { 'class': 'kiss-card-title' }, '🎯 Devices by Risk Level'),
|
|
this.renderDeviceGrid(devices)
|
|
]),
|
|
|
|
// Recent Anomalies
|
|
E('div', { 'class': 'kiss-card' }, [
|
|
E('div', { 'class': 'kiss-card-title' }, '⚠️ Recent Anomalies'),
|
|
this.renderAnomaliesTable(anomalies)
|
|
])
|
|
];
|
|
|
|
return KissTheme.wrap(content, 'admin/secubox/services/iot-guard');
|
|
},
|
|
|
|
renderStatCard: function(label, value, color) {
|
|
return E('div', { 'class': 'kiss-stat', 'style': 'border-left: 3px solid ' + color + ';' }, [
|
|
E('div', {
|
|
'class': 'kiss-stat-value',
|
|
'style': 'color: ' + color + ';',
|
|
'data-stat': label.toLowerCase().replace(' ', '-')
|
|
}, String(value)),
|
|
E('div', { 'class': 'kiss-stat-label' }, label)
|
|
]);
|
|
},
|
|
|
|
renderScoreCard: function(label, score) {
|
|
var color = score >= 70 ? 'var(--kiss-green)' : (score >= 40 ? 'var(--kiss-yellow)' : 'var(--kiss-red)');
|
|
return E('div', { 'class': 'kiss-stat', 'style': 'border-left: 3px solid ' + color + ';' }, [
|
|
E('div', {
|
|
'class': 'kiss-stat-value',
|
|
'style': 'color: ' + color + ';',
|
|
'data-stat': 'security-score'
|
|
}, score + '%'),
|
|
E('div', { 'class': 'kiss-stat-label' }, label)
|
|
]);
|
|
},
|
|
|
|
renderDeviceGrid: function(devices) {
|
|
var self = this;
|
|
var RISK_COLORS = {
|
|
high: 'var(--kiss-red)',
|
|
medium: 'var(--kiss-yellow)',
|
|
low: 'var(--kiss-green)',
|
|
unknown: 'var(--kiss-muted)'
|
|
};
|
|
|
|
var groups = { high: [], medium: [], low: [], unknown: [] };
|
|
|
|
devices.forEach(function(d) {
|
|
var risk = d.risk_level || 'unknown';
|
|
if (groups[risk]) {
|
|
groups[risk].push(d);
|
|
} else {
|
|
groups.unknown.push(d);
|
|
}
|
|
});
|
|
|
|
var rows = [];
|
|
|
|
['high', 'medium', 'low'].forEach(function(risk) {
|
|
if (groups[risk].length === 0) return;
|
|
|
|
rows.push(E('div', { 'style': 'margin-bottom: 16px;' }, [
|
|
E('div', {
|
|
'style': 'color: ' + RISK_COLORS[risk] + '; font-weight: 600; margin-bottom: 10px; font-size: 12px; text-transform: uppercase; letter-spacing: 1px;'
|
|
}, risk + ' Risk (' + groups[risk].length + ')'),
|
|
E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 10px;' },
|
|
groups[risk].slice(0, 12).map(function(d) {
|
|
return self.renderDeviceChip(d, RISK_COLORS);
|
|
})
|
|
)
|
|
]));
|
|
});
|
|
|
|
if (rows.length === 0) {
|
|
return E('div', { 'style': 'color: var(--kiss-muted); padding: 30px; text-align: center;' }, [
|
|
E('div', { 'style': 'font-size: 48px; margin-bottom: 12px;' }, '📡'),
|
|
E('p', {}, 'No IoT devices detected.'),
|
|
E('p', { 'style': 'font-size: 12px;' }, 'Click "Scan Network" to discover devices.')
|
|
]);
|
|
}
|
|
|
|
return E('div', {}, rows);
|
|
},
|
|
|
|
renderDeviceChip: function(device, RISK_COLORS) {
|
|
var icon = CLASS_ICONS[device.device_class] || CLASS_ICONS.unknown;
|
|
var color = RISK_COLORS[device.risk_level] || RISK_COLORS.unknown;
|
|
|
|
var statusBadge = '';
|
|
if (device.isolated) statusBadge = ' 🔸';
|
|
else if (device.blocked) statusBadge = ' 🚫';
|
|
else if (device.trusted) statusBadge = ' ✓';
|
|
|
|
var label = device.hostname || device.ip || device.mac.substring(9);
|
|
|
|
return E('a', {
|
|
'href': L.url('admin/secubox/services/iot-guard/devices') + '?mac=' + encodeURIComponent(device.mac),
|
|
'class': 'kiss-btn',
|
|
'style': 'text-decoration: none; border-color: ' + color + '66;',
|
|
'title': device.vendor + ' - ' + device.mac
|
|
}, [
|
|
E('span', {}, icon),
|
|
E('span', {}, label.length > 15 ? label.substring(0, 12) + '...' : label),
|
|
statusBadge ? E('span', { 'style': 'font-size: 10px;' }, statusBadge) : ''
|
|
]);
|
|
},
|
|
|
|
renderAnomaliesTable: function(anomalies) {
|
|
if (!anomalies || anomalies.length === 0) {
|
|
return E('div', { 'style': 'text-align: center; padding: 30px; color: var(--kiss-green);' }, [
|
|
E('div', { 'style': 'font-size: 48px; margin-bottom: 12px;' }, '✅'),
|
|
E('p', {}, 'No recent anomalies detected')
|
|
]);
|
|
}
|
|
|
|
var rows = anomalies.map(function(a) {
|
|
var sevColor = a.severity === 'high' ? 'var(--kiss-red)' : (a.severity === 'medium' ? 'var(--kiss-yellow)' : 'var(--kiss-muted)');
|
|
return E('tr', {}, [
|
|
E('td', { 'class': 'td' }, a.timestamp ? a.timestamp.substring(11, 16) : '-'),
|
|
E('td', { 'class': 'td' }, a.hostname || a.mac),
|
|
E('td', { 'class': 'td' }, a.type),
|
|
E('td', { 'class': 'td', 'style': 'color: ' + sevColor + '; font-weight: 600;' }, a.severity),
|
|
E('td', { 'class': 'td', 'style': 'color: var(--kiss-muted);' }, a.description)
|
|
]);
|
|
});
|
|
|
|
return E('table', { 'class': 'kiss-table' }, [
|
|
E('thead', {}, E('tr', {}, [
|
|
E('th', { 'class': 'th' }, 'Time'),
|
|
E('th', { 'class': 'th' }, 'Device'),
|
|
E('th', { 'class': 'th' }, 'Type'),
|
|
E('th', { 'class': 'th' }, 'Severity'),
|
|
E('th', { 'class': 'th' }, 'Description')
|
|
])),
|
|
E('tbody', {}, rows)
|
|
]);
|
|
},
|
|
|
|
updateStats: function(status) {
|
|
var updates = {
|
|
'devices': status.total_devices,
|
|
'isolated': status.isolated,
|
|
'blocked': status.blocked,
|
|
'high-risk': status.high_risk,
|
|
'anomalies': status.anomalies,
|
|
'security-score': status.security_score + '%'
|
|
};
|
|
|
|
Object.keys(updates).forEach(function(key) {
|
|
var el = document.querySelector('[data-stat="' + key + '"]');
|
|
if (el) el.textContent = String(updates[key]);
|
|
});
|
|
}
|
|
});
|