diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 478e2dc9..1887f91c 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -274,7 +274,21 @@ "Bash(do scp \"$f\" root@192.168.8.191:/www/luci-static/resources/view/secubox/)", "Bash(node:*)", "Bash(awk:*)", - "Bash(for f in luci-app-secubox/htdocs/luci-static/resources/view/secubox/wizard.js luci-app-secubox/htdocs/luci-static/resources/secubox/api.js)" + "Bash(for f in luci-app-secubox/htdocs/luci-static/resources/view/secubox/wizard.js luci-app-secubox/htdocs/luci-static/resources/secubox/api.js)", + "Bash(for f in /home/reepost/CyberMindStudio/_files/secubox-openwrt/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/*.js)", + "Bash(do scp \"$f\" root@192.168.8.191:/www/luci-static/resources/view/client-guardian/)", + "Bash(for f in dashboard.js health.js logs.js settings.js)", + "Bash(root@192.168.8.191:/www/luci-static/resources/view/secubox-admin/)", + "Bash(root@192.168.8.191:/usr/libexec/rpcd/luci.secubox)", + "Bash(root@192.168.8.191:/www/luci-static/resources/secubox-admin/)", + "Bash(root@192.168.8.191:/www/luci-static/resources/system-hub/)", + "Bash(root@192.168.8.191:/usr/libexec/rpcd/luci.system-hub)", + "Bash(root@192.168.8.191:/usr/sbin/secubox-appstore)", + "Bash(for f in zones.js overview.js clients.js)", + "Bash(do scp /home/reepost/CyberMindStudio/_files/secubox-openwrt/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/$f root@192.168.8.191:/www/luci-static/resources/view/client-guardian/)", + "Bash(for f in clients.js overview.js)", + "Bash(for f in htdocs/luci-static/resources/view/client-guardian/settings.js htdocs/luci-static/resources/client-guardian/api.js root/usr/libexec/rpcd/luci.client-guardian root/etc/config/client-guardian)", + "Bash(do scp \"$f\" root@192.168.8.191:/$f#root/)" ] } } diff --git a/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/client-guardian/debug.js b/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/client-guardian/debug.js new file mode 100644 index 00000000..fb5628ce --- /dev/null +++ b/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/client-guardian/debug.js @@ -0,0 +1,309 @@ +'use strict'; +'require baseclass'; +'require uci'; + +/** + * Client Guardian Debug Module + * Provides comprehensive logging and debugging capabilities + */ + +var DEBUG_LEVELS = { + ERROR: 0, + WARN: 1, + INFO: 2, + DEBUG: 3, + TRACE: 4 +}; + +var DEBUG_COLORS = { + ERROR: '#ef4444', + WARN: '#f59e0b', + INFO: '#3b82f6', + DEBUG: '#8b5cf6', + TRACE: '#6b7280' +}; + +var debugEnabled = false; +var debugLevel = DEBUG_LEVELS.INFO; +var logBuffer = []; +var maxBufferSize = 500; + +return baseclass.extend({ + init: function() { + // Check if debug mode is enabled in UCI + return uci.load('client-guardian').then(L.bind(function() { + debugEnabled = uci.get('client-guardian', 'config', 'debug_enabled') === '1'; + var level = uci.get('client-guardian', 'config', 'debug_level') || 'INFO'; + debugLevel = DEBUG_LEVELS[level] || DEBUG_LEVELS.INFO; + + if (debugEnabled) { + this.info('Client Guardian Debug Mode Enabled', { + level: level, + timestamp: new Date().toISOString() + }); + } + }, this)).catch(function() { + // UCI not available, use defaults + debugEnabled = false; + }); + }, + + isEnabled: function() { + return debugEnabled; + }, + + setEnabled: function(enabled) { + debugEnabled = enabled; + if (enabled) { + this.info('Debug mode enabled manually'); + } + }, + + setLevel: function(level) { + if (typeof level === 'string') { + debugLevel = DEBUG_LEVELS[level.toUpperCase()] || DEBUG_LEVELS.INFO; + } else { + debugLevel = level; + } + this.info('Debug level changed', { level: debugLevel }); + }, + + _log: function(level, levelName, message, data) { + if (!debugEnabled || level > debugLevel) { + return; + } + + var timestamp = new Date().toISOString(); + var logEntry = { + timestamp: timestamp, + level: levelName, + message: message, + data: data || {} + }; + + // Add to buffer + logBuffer.push(logEntry); + if (logBuffer.length > maxBufferSize) { + logBuffer.shift(); + } + + // Console output with styling + var style = 'color: ' + DEBUG_COLORS[levelName] + '; font-weight: bold;'; + var prefix = '[CG:' + levelName + ']'; + + if (data) { + console.log('%c' + prefix + ' ' + timestamp, style, message, data); + } else { + console.log('%c' + prefix + ' ' + timestamp, style, message); + } + }, + + error: function(message, data) { + this._log(DEBUG_LEVELS.ERROR, 'ERROR', message, data); + }, + + warn: function(message, data) { + this._log(DEBUG_LEVELS.WARN, 'WARN', message, data); + }, + + info: function(message, data) { + this._log(DEBUG_LEVELS.INFO, 'INFO', message, data); + }, + + debug: function(message, data) { + this._log(DEBUG_LEVELS.DEBUG, 'DEBUG', message, data); + }, + + trace: function(message, data) { + this._log(DEBUG_LEVELS.TRACE, 'TRACE', message, data); + }, + + // API call tracing + traceAPICall: function(method, params) { + this.debug('API Call: ' + method, { + params: params, + stack: new Error().stack + }); + }, + + traceAPIResponse: function(method, response, duration) { + this.debug('API Response: ' + method, { + response: response, + duration: duration + 'ms' + }); + }, + + traceAPIError: function(method, error) { + this.error('API Error: ' + method, { + error: error.toString(), + stack: error.stack + }); + }, + + // UI event tracing + traceEvent: function(eventName, target, data) { + this.trace('Event: ' + eventName, { + target: target, + data: data + }); + }, + + // Performance monitoring + startTimer: function(label) { + if (!debugEnabled) return null; + + var timer = { + label: label, + start: performance.now() + }; + + this.trace('Timer started: ' + label); + return timer; + }, + + endTimer: function(timer) { + if (!debugEnabled || !timer) return; + + var duration = (performance.now() - timer.start).toFixed(2); + this.debug('Timer ended: ' + timer.label, { + duration: duration + 'ms' + }); + + return duration; + }, + + // Get log buffer + getLogs: function(level, count) { + var filtered = logBuffer; + + if (level) { + var levelValue = DEBUG_LEVELS[level.toUpperCase()]; + filtered = logBuffer.filter(function(entry) { + return DEBUG_LEVELS[entry.level] <= levelValue; + }); + } + + if (count) { + filtered = filtered.slice(-count); + } + + return filtered; + }, + + // Export logs as text + exportLogs: function() { + var text = '=== Client Guardian Debug Logs ===\n'; + text += 'Generated: ' + new Date().toISOString() + '\n'; + text += 'Total entries: ' + logBuffer.length + '\n\n'; + + logBuffer.forEach(function(entry) { + text += '[' + entry.timestamp + '] [' + entry.level + '] ' + entry.message; + if (Object.keys(entry.data).length > 0) { + text += '\n Data: ' + JSON.stringify(entry.data, null, 2); + } + text += '\n\n'; + }); + + return text; + }, + + // Download logs as file + downloadLogs: function() { + var text = this.exportLogs(); + var blob = new Blob([text], { type: 'text/plain' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = 'client-guardian-debug-' + Date.now() + '.txt'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + this.info('Logs downloaded'); + }, + + // Clear log buffer + clearLogs: function() { + logBuffer = []; + this.info('Log buffer cleared'); + }, + + // Get system info for debugging + getSystemInfo: function() { + return { + userAgent: navigator.userAgent, + platform: navigator.platform, + language: navigator.language, + screenResolution: window.screen.width + 'x' + window.screen.height, + windowSize: window.innerWidth + 'x' + window.innerHeight, + cookiesEnabled: navigator.cookieEnabled, + onLine: navigator.onLine, + timestamp: new Date().toISOString(), + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + memory: performance.memory ? { + usedJSHeapSize: (performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2) + ' MB', + totalJSHeapSize: (performance.memory.totalJSHeapSize / 1024 / 1024).toFixed(2) + ' MB', + jsHeapSizeLimit: (performance.memory.jsHeapSizeLimit / 1024 / 1024).toFixed(2) + ' MB' + } : 'N/A' + }; + }, + + // Network request monitoring + monitorFetch: function(originalFetch) { + if (!debugEnabled) return originalFetch; + + var self = this; + return function() { + var args = arguments; + var url = args[0]; + var timer = self.startTimer('Fetch: ' + url); + + self.trace('Fetch request', { + url: url, + options: args[1] + }); + + return originalFetch.apply(this, args).then(function(response) { + var duration = self.endTimer(timer); + self.trace('Fetch response', { + url: url, + status: response.status, + statusText: response.statusText, + duration: duration + }); + return response; + }).catch(function(error) { + self.error('Fetch error', { + url: url, + error: error.toString() + }); + throw error; + }); + }; + }, + + // Initialize global error handler + setupGlobalErrorHandler: function() { + var self = this; + + window.addEventListener('error', function(event) { + self.error('Global error', { + message: event.message, + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + error: event.error ? event.error.toString() : 'Unknown' + }); + }); + + window.addEventListener('unhandledrejection', function(event) { + self.error('Unhandled promise rejection', { + reason: event.reason, + promise: event.promise + }); + }); + + this.info('Global error handlers registered'); + } +}); diff --git a/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/debug.js b/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/debug.js new file mode 100644 index 00000000..d4948e85 --- /dev/null +++ b/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/debug.js @@ -0,0 +1,259 @@ +'use strict'; +'require view'; +'require secubox-theme/theme as Theme'; +'require dom'; +'require ui'; +'require uci'; +'require rpc'; +'require client-guardian/debug as Debug'; + +var callGetLogs = rpc.declare({ + object: 'luci.client-guardian', + method: 'logs', + params: ['limit', 'level'], + expect: { logs: [] } +}); + +return view.extend({ + load: function() { + return Promise.all([ + Debug.init(), + uci.load('client-guardian'), + callGetLogs(100, 'debug') + ]); + }, + + render: function(data) { + var backendLogs = data[2].logs || []; + var self = this; + + var debugEnabled = uci.get('client-guardian', 'config', 'debug_enabled') === '1'; + var debugLevel = uci.get('client-guardian', 'config', 'debug_level') || 'INFO'; + + return E('div', { 'class': 'client-guardian-dashboard' }, [ + E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), + E('link', { 'rel': 'stylesheet', 'href': L.resource('client-guardian/dashboard.css') }), + + E('div', { 'class': 'cg-header' }, [ + E('div', { 'class': 'cg-logo' }, [ + E('div', { 'class': 'cg-logo-icon' }, '🐛'), + E('div', { 'class': 'cg-logo-text' }, 'Mode Debug') + ]), + E('div', { 'class': 'cg-debug-controls' }, [ + E('button', { + 'class': 'cg-btn cg-btn-sm', + 'click': L.bind(this.handleRefreshLogs, this) + }, '🔄 Actualiser'), + E('button', { + 'class': 'cg-btn cg-btn-sm', + 'click': L.bind(this.handleClearLogs, this) + }, '🗑️ Effacer'), + E('button', { + 'class': 'cg-btn cg-btn-sm cg-btn-primary', + 'click': L.bind(this.handleDownloadLogs, this) + }, '💾 Télécharger') + ]) + ]), + + // Debug Status Card + E('div', { 'class': 'cg-card' }, [ + E('div', { 'class': 'cg-card-header' }, [ + E('div', { 'class': 'cg-card-title' }, [ + E('span', { 'class': 'cg-card-title-icon' }, '⚙️'), + 'Configuration Debug' + ]) + ]), + E('div', { 'class': 'cg-card-body' }, [ + E('div', { 'class': 'cg-debug-status-grid' }, [ + E('div', { 'class': 'cg-debug-status-item' }, [ + E('div', { 'class': 'cg-debug-status-label' }, 'Mode Debug'), + E('div', { 'class': 'cg-debug-status-value' }, [ + E('span', { + 'class': 'cg-status-badge ' + (debugEnabled ? 'approved' : 'offline') + }, [ + E('span', { 'class': 'cg-status-dot' }), + debugEnabled ? 'Activé' : 'Désactivé' + ]), + E('button', { + 'class': 'cg-btn cg-btn-sm', + 'style': 'margin-left: 8px', + 'click': L.bind(this.handleToggleDebug, this, !debugEnabled) + }, debugEnabled ? 'Désactiver' : 'Activer') + ]) + ]), + E('div', { 'class': 'cg-debug-status-item' }, [ + E('div', { 'class': 'cg-debug-status-label' }, 'Niveau de Log'), + E('select', { + 'class': 'cg-input cg-input-sm', + 'id': 'debug-level-select', + 'value': debugLevel, + 'change': L.bind(this.handleChangeLevel, this) + }, [ + E('option', { 'value': 'ERROR' }, 'ERROR'), + E('option', { 'value': 'WARN' }, 'WARN'), + E('option', { 'value': 'INFO', 'selected': debugLevel === 'INFO' }, 'INFO'), + E('option', { 'value': 'DEBUG' }, 'DEBUG'), + E('option', { 'value': 'TRACE' }, 'TRACE') + ]) + ]), + E('div', { 'class': 'cg-debug-status-item' }, [ + E('div', { 'class': 'cg-debug-status-label' }, 'Logs Backend'), + E('div', { 'class': 'cg-debug-status-value' }, backendLogs.length + ' entrées') + ]), + E('div', { 'class': 'cg-debug-status-item' }, [ + E('div', { 'class': 'cg-debug-status-label' }, 'Logs Frontend'), + E('div', { 'class': 'cg-debug-status-value' }, Debug.getLogs().length + ' entrées') + ]) + ]) + ]) + ]), + + // System Information + E('div', { 'class': 'cg-card' }, [ + E('div', { 'class': 'cg-card-header' }, [ + E('div', { 'class': 'cg-card-title' }, [ + E('span', { 'class': 'cg-card-title-icon' }, 'ℹ️'), + 'Informations Système' + ]) + ]), + E('div', { 'class': 'cg-card-body' }, [ + this.renderSystemInfo(Debug.getSystemInfo()) + ]) + ]), + + // Backend Logs + E('div', { 'class': 'cg-card' }, [ + E('div', { 'class': 'cg-card-header' }, [ + E('div', { 'class': 'cg-card-title' }, [ + E('span', { 'class': 'cg-card-title-icon' }, '📋'), + 'Logs Backend RPCD' + ]), + E('span', { 'class': 'cg-card-badge' }, backendLogs.length + ' entrées') + ]), + E('div', { 'class': 'cg-card-body' }, [ + E('div', { 'class': 'cg-log-container', 'id': 'backend-logs' }, + backendLogs.length > 0 ? + backendLogs.map(L.bind(this.renderLogEntry, this)) : + E('div', { 'class': 'cg-empty-state' }, [ + E('div', { 'class': 'cg-empty-state-icon' }, '📝'), + E('div', { 'class': 'cg-empty-state-title' }, 'Aucun log backend'), + E('div', { 'class': 'cg-empty-state-text' }, 'Les logs du serveur apparaîtront ici') + ]) + ) + ]) + ]), + + // Frontend Console Logs + E('div', { 'class': 'cg-card' }, [ + E('div', { 'class': 'cg-card-header' }, [ + E('div', { 'class': 'cg-card-title' }, [ + E('span', { 'class': 'cg-card-title-icon' }, '💻'), + 'Logs Frontend Console' + ]), + E('span', { 'class': 'cg-card-badge' }, Debug.getLogs().length + ' entrées') + ]), + E('div', { 'class': 'cg-card-body' }, [ + E('div', { 'class': 'cg-log-container', 'id': 'frontend-logs' }, + Debug.getLogs().length > 0 ? + Debug.getLogs().reverse().slice(0, 100).map(L.bind(this.renderLogEntry, this)) : + E('div', { 'class': 'cg-empty-state' }, [ + E('div', { 'class': 'cg-empty-state-icon' }, '🖥️'), + E('div', { 'class': 'cg-empty-state-title' }, 'Aucun log frontend'), + E('div', { 'class': 'cg-empty-state-text' }, 'Les logs du navigateur apparaîtront ici') + ]) + ) + ]) + ]) + ]); + }, + + renderSystemInfo: function(info) { + return E('div', { 'class': 'cg-system-info-grid' }, [ + this.renderInfoItem('Navigateur', info.userAgent), + this.renderInfoItem('Plateforme', info.platform), + this.renderInfoItem('Langue', info.language), + this.renderInfoItem('Résolution', info.screenResolution), + this.renderInfoItem('Fenêtre', info.windowSize), + this.renderInfoItem('Cookies', info.cookiesEnabled ? 'Activés' : 'Désactivés'), + this.renderInfoItem('Connexion', info.onLine ? 'En ligne' : 'Hors ligne'), + this.renderInfoItem('Fuseau horaire', info.timezone), + this.renderInfoItem('Mémoire JS', typeof info.memory === 'object' ? + 'Utilisée: ' + info.memory.usedJSHeapSize + ' / Limite: ' + info.memory.jsHeapSizeLimit : + info.memory + ) + ]); + }, + + renderInfoItem: function(label, value) { + return E('div', { 'class': 'cg-info-item' }, [ + E('div', { 'class': 'cg-info-label' }, label + ':'), + E('div', { 'class': 'cg-info-value' }, value) + ]); + }, + + renderLogEntry: function(log) { + var levelClass = 'cg-log-' + (log.level || 'info').toLowerCase(); + var levelIcon = { + 'ERROR': '🚨', + 'WARN': '⚠️', + 'INFO': 'ℹ️', + 'DEBUG': '🐛', + 'TRACE': '🔍' + }[log.level] || 'ℹ️'; + + return E('div', { 'class': 'cg-log-entry ' + levelClass }, [ + E('div', { 'class': 'cg-log-header' }, [ + E('span', { 'class': 'cg-log-icon' }, levelIcon), + E('span', { 'class': 'cg-log-level' }, log.level || 'INFO'), + E('span', { 'class': 'cg-log-time' }, log.timestamp || new Date().toISOString()) + ]), + E('div', { 'class': 'cg-log-message' }, log.message), + log.data && Object.keys(log.data).length > 0 ? + E('details', { 'class': 'cg-log-details' }, [ + E('summary', {}, 'Données additionnelles'), + E('pre', { 'class': 'cg-log-data' }, JSON.stringify(log.data, null, 2)) + ]) : + E('span') + ]); + }, + + handleToggleDebug: function(enabled, ev) { + uci.set('client-guardian', 'config', 'debug_enabled', enabled ? '1' : '0'); + uci.save().then(L.bind(function() { + return uci.apply(); + }, this)).then(L.bind(function() { + ui.addNotification(null, E('p', {}, 'Mode debug ' + (enabled ? 'activé' : 'désactivé')), 'success'); + Debug.setEnabled(enabled); + location.reload(); + }, this)); + }, + + handleChangeLevel: function(ev) { + var level = ev.target.value; + uci.set('client-guardian', 'config', 'debug_level', level); + uci.save().then(L.bind(function() { + return uci.apply(); + }, this)).then(L.bind(function() { + ui.addNotification(null, E('p', {}, 'Niveau de debug changé: ' + level), 'success'); + Debug.setLevel(level); + }, this)); + }, + + handleRefreshLogs: function(ev) { + location.reload(); + }, + + handleClearLogs: function(ev) { + Debug.clearLogs(); + ui.addNotification(null, E('p', {}, 'Logs frontend effacés'), 'success'); + location.reload(); + }, + + handleDownloadLogs: function(ev) { + Debug.downloadLogs(); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-client-guardian/root/etc/client-guardian/profiles.json b/package/secubox/luci-app-client-guardian/root/etc/client-guardian/profiles.json new file mode 100644 index 00000000..a5134580 --- /dev/null +++ b/package/secubox/luci-app-client-guardian/root/etc/client-guardian/profiles.json @@ -0,0 +1,405 @@ +{ + "profiles": [ + { + "id": "family_home", + "name": "Maison Familiale", + "description": "Configuration optimale pour une maison avec parents, enfants et appareils IoT", + "icon": "🏠", + "zones": [ + { + "id": "lan_private", + "name": "Réseau Principal", + "description": "Appareils de confiance des parents", + "network": "lan", + "color": "#22c55e", + "icon": "home", + "internet_access": true, + "local_access": true, + "inter_client": true, + "bandwidth_limit": 0, + "priority": "high" + }, + { + "id": "kids", + "name": "Enfants", + "description": "Contrôle parental actif avec horaires", + "network": "lan", + "color": "#06b6d4", + "icon": "child", + "internet_access": true, + "local_access": true, + "inter_client": true, + "bandwidth_limit": 50, + "time_restrictions": true, + "schedule_start": "08:00", + "schedule_end": "21:00", + "content_filter": "kids", + "priority": "normal" + }, + { + "id": "iot", + "name": "Objets Connectés", + "description": "Caméras, thermostats, ampoules (isolés)", + "network": "iot", + "color": "#f59e0b", + "icon": "cpu", + "internet_access": true, + "local_access": false, + "inter_client": false, + "bandwidth_limit": 10, + "priority": "low" + }, + { + "id": "guest", + "name": "Invités", + "description": "Accès Internet limité pour visiteurs", + "network": "guest", + "color": "#8b5cf6", + "icon": "users", + "internet_access": true, + "local_access": false, + "inter_client": false, + "bandwidth_limit": 25, + "session_duration": 7200, + "portal_required": true, + "priority": "low" + } + ] + }, + { + "id": "small_business", + "name": "Petite Entreprise", + "description": "Séparation réseau employés, invités et équipements", + "icon": "🏢", + "zones": [ + { + "id": "corporate", + "name": "Réseau Entreprise", + "description": "Postes de travail des employés", + "network": "lan", + "color": "#3b82f6", + "icon": "briefcase", + "internet_access": true, + "local_access": true, + "inter_client": true, + "bandwidth_limit": 0, + "priority": "high" + }, + { + "id": "servers", + "name": "Serveurs", + "description": "Infrastructure critique", + "network": "servers", + "color": "#ef4444", + "icon": "server", + "internet_access": true, + "local_access": true, + "inter_client": false, + "bandwidth_limit": 0, + "priority": "critical" + }, + { + "id": "byod", + "name": "BYOD", + "description": "Appareils personnels des employés", + "network": "byod", + "color": "#f59e0b", + "icon": "smartphone", + "internet_access": true, + "local_access": false, + "inter_client": false, + "bandwidth_limit": 50, + "priority": "normal" + }, + { + "id": "guest", + "name": "Visiteurs", + "description": "Accès Internet isolé", + "network": "guest", + "color": "#8b5cf6", + "icon": "users", + "internet_access": true, + "local_access": false, + "inter_client": false, + "bandwidth_limit": 10, + "portal_required": true, + "priority": "low" + } + ] + }, + { + "id": "hotel", + "name": "Hôtel / Gîte", + "description": "Gestion multi-chambres avec isolation stricte", + "icon": "🏨", + "zones": [ + { + "id": "management", + "name": "Administration", + "description": "Réseau de gestion", + "network": "lan", + "color": "#22c55e", + "icon": "shield", + "internet_access": true, + "local_access": true, + "inter_client": true, + "bandwidth_limit": 0, + "priority": "critical" + }, + { + "id": "rooms_floor1", + "name": "Chambres Étage 1", + "description": "Clients étage 1 (isolés)", + "network": "rooms1", + "color": "#3b82f6", + "icon": "bed", + "internet_access": true, + "local_access": false, + "inter_client": false, + "bandwidth_limit": 20, + "portal_required": true, + "priority": "normal" + }, + { + "id": "rooms_floor2", + "name": "Chambres Étage 2", + "description": "Clients étage 2 (isolés)", + "network": "rooms2", + "color": "#06b6d4", + "icon": "bed", + "internet_access": true, + "local_access": false, + "inter_client": false, + "bandwidth_limit": 20, + "portal_required": true, + "priority": "normal" + }, + { + "id": "public", + "name": "Espaces Communs", + "description": "Lobby, restaurant, bar", + "network": "public", + "color": "#8b5cf6", + "icon": "wifi", + "internet_access": true, + "local_access": false, + "inter_client": false, + "bandwidth_limit": 15, + "portal_required": true, + "priority": "low" + } + ] + }, + { + "id": "apartment", + "name": "Immeuble / Colocation", + "description": "Isolation stricte entre locataires", + "icon": "🏘️", + "zones": [ + { + "id": "landlord", + "name": "Propriétaire", + "description": "Réseau administrateur", + "network": "lan", + "color": "#22c55e", + "icon": "key", + "internet_access": true, + "local_access": true, + "inter_client": true, + "bandwidth_limit": 0, + "priority": "high" + }, + { + "id": "tenant_a", + "name": "Locataire A", + "description": "Appartement/Chambre A", + "network": "tenant_a", + "color": "#3b82f6", + "icon": "door", + "internet_access": true, + "local_access": false, + "inter_client": true, + "bandwidth_limit": 100, + "priority": "normal" + }, + { + "id": "tenant_b", + "name": "Locataire B", + "description": "Appartement/Chambre B", + "network": "tenant_b", + "color": "#06b6d4", + "icon": "door", + "internet_access": true, + "local_access": false, + "inter_client": true, + "bandwidth_limit": 100, + "priority": "normal" + }, + { + "id": "tenant_c", + "name": "Locataire C", + "description": "Appartement/Chambre C", + "network": "tenant_c", + "color": "#f59e0b", + "icon": "door", + "internet_access": true, + "local_access": false, + "inter_client": true, + "bandwidth_limit": 100, + "priority": "normal" + }, + { + "id": "common", + "name": "Parties Communes", + "description": "Couloirs, buanderie", + "network": "common", + "color": "#8b5cf6", + "icon": "building", + "internet_access": true, + "local_access": false, + "inter_client": false, + "bandwidth_limit": 20, + "priority": "low" + } + ] + }, + { + "id": "school", + "name": "École / Formation", + "description": "Séparation élèves, enseignants, administration", + "icon": "🎓", + "zones": [ + { + "id": "admin", + "name": "Administration", + "description": "Direction et services", + "network": "lan", + "color": "#22c55e", + "icon": "shield", + "internet_access": true, + "local_access": true, + "inter_client": true, + "bandwidth_limit": 0, + "priority": "high" + }, + { + "id": "teachers", + "name": "Enseignants", + "description": "Salle des professeurs", + "network": "teachers", + "color": "#3b82f6", + "icon": "chalkboard", + "internet_access": true, + "local_access": true, + "inter_client": true, + "bandwidth_limit": 0, + "priority": "high" + }, + { + "id": "students", + "name": "Élèves", + "description": "Salles de classe avec filtrage", + "network": "students", + "color": "#06b6d4", + "icon": "book", + "internet_access": true, + "local_access": false, + "inter_client": false, + "bandwidth_limit": 50, + "content_filter": "kids", + "time_restrictions": true, + "schedule_start": "08:00", + "schedule_end": "17:00", + "priority": "normal" + }, + { + "id": "lab", + "name": "Laboratoire Info", + "description": "Postes de travail contrôlés", + "network": "lab", + "color": "#f59e0b", + "icon": "computer", + "internet_access": true, + "local_access": false, + "inter_client": false, + "bandwidth_limit": 100, + "priority": "normal" + } + ] + }, + { + "id": "secure_home", + "name": "Maison Sécurisée", + "description": "Maximum de segmentation pour sécurité avancée", + "icon": "🔒", + "zones": [ + { + "id": "trusted", + "name": "Confiance Totale", + "description": "Appareils principaux uniquement", + "network": "lan", + "color": "#22c55e", + "icon": "shield-check", + "internet_access": true, + "local_access": true, + "inter_client": true, + "bandwidth_limit": 0, + "priority": "critical" + }, + { + "id": "work", + "name": "Télétravail", + "description": "Poste de travail professionnel isolé", + "network": "work", + "color": "#3b82f6", + "icon": "briefcase", + "internet_access": true, + "local_access": false, + "inter_client": false, + "bandwidth_limit": 0, + "priority": "high" + }, + { + "id": "iot_secure", + "name": "IoT Sécurisé", + "description": "Appareils connectés de confiance", + "network": "iot_secure", + "color": "#06b6d4", + "icon": "lock", + "internet_access": true, + "local_access": false, + "inter_client": false, + "bandwidth_limit": 10, + "priority": "low" + }, + { + "id": "iot_untrusted", + "name": "IoT Non Vérifié", + "description": "Appareils chinois et non certifiés", + "network": "iot_untrusted", + "color": "#f59e0b", + "icon": "alert", + "internet_access": false, + "local_access": false, + "inter_client": false, + "bandwidth_limit": 5, + "priority": "low" + }, + { + "id": "guest", + "name": "Invités", + "description": "Accès Internet uniquement", + "network": "guest", + "color": "#8b5cf6", + "icon": "users", + "internet_access": true, + "local_access": false, + "inter_client": false, + "bandwidth_limit": 20, + "portal_required": true, + "priority": "low" + } + ] + } + ] +} diff --git a/package/secubox/luci-app-client-guardian/root/usr/libexec/rpcd/luci.client-guardian.bak b/package/secubox/luci-app-client-guardian/root/usr/libexec/rpcd/luci.client-guardian.bak new file mode 100755 index 00000000..4b2c3051 --- /dev/null +++ b/package/secubox/luci-app-client-guardian/root/usr/libexec/rpcd/luci.client-guardian.bak @@ -0,0 +1,1777 @@ +#!/bin/sh +# SPDX-License-Identifier: Apache-2.0 +# Client Guardian - Network Access Control RPCD Backend +# Copyright (C) 2024 CyberMind.fr - Gandalf + +. /lib/functions.sh +. /usr/share/libubox/jshn.sh + +CONFIG_FILE="/etc/config/client-guardian" +LOG_FILE="/var/log/client-guardian.log" +CLIENTS_DB="/tmp/client-guardian-clients.json" +ALERTS_QUEUE="/tmp/client-guardian-alerts.json" + +# Logging function with debug support +log_event() { + local level="$1" + local message="$2" + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + echo "[$timestamp] [$level] $message" >> "$LOG_FILE" + + # Also log to syslog if debug enabled + local debug_enabled=$(uci -q get client-guardian.config.debug_enabled) + if [ "$debug_enabled" = "1" ]; then + logger -t client-guardian -p "daemon.$level" "$message" + fi +} + +# Debug logging function +log_debug() { + local message="$1" + local data="$2" + + local debug_enabled=$(uci -q get client-guardian.config.debug_enabled) + local debug_level=$(uci -q get client-guardian.config.debug_level || echo "INFO") + + if [ "$debug_enabled" != "1" ]; then + return + fi + + # Log based on level hierarchy: ERROR < WARN < INFO < DEBUG < TRACE + case "$debug_level" in + ERROR) return ;; # Only errors + WARN) [ "$1" != "error" ] && [ "$1" != "warn" ] && return ;; + INFO) [ "$1" != "error" ] && [ "$1" != "warn" ] && [ "$1" != "info" ] && return ;; + DEBUG) [ "$1" = "trace" ] && return ;; + TRACE) ;; # Log everything + esac + + local timestamp=$(date '+%Y-%m-%d %H:%M:%S.%N' | cut -c1-23) + local log_msg="[$timestamp] [DEBUG] $message" + + if [ -n "$data" ]; then + log_msg="$log_msg | Data: $data" + fi + + echo "$log_msg" >> "$LOG_FILE" + logger -t client-guardian-debug "$log_msg" +} + +# Active network scan to discover clients +scan_network_active() { + local subnet="$1" + local iface="$2" + + # Method 1: arping (if available) + if command -v arping >/dev/null 2>&1; then + # Scan common subnet (192.168.x.0/24) + for i in $(seq 1 254); do + arping -c 1 -w 1 -I "$iface" "${subnet%.*}.$i" >/dev/null 2>&1 & + done + wait + # Method 2: ping sweep fallback + elif command -v ping >/dev/null 2>&1; then + for i in $(seq 1 254); do + ping -c 1 -W 1 "${subnet%.*}.$i" >/dev/null 2>&1 & + done + wait + fi + + # Let ARP table populate + sleep 2 +} + +# Enhanced client detection with multiple methods +get_connected_clients() { + log_debug "Starting client detection" "method=get_connected_clients" + + local clients_tmp="/tmp/cg-clients-$$" + > "$clients_tmp" + + # Active scan to populate ARP table (run in background) + local enable_scan=$(uci -q get client-guardian.config.enable_active_scan || echo "1") + log_debug "Active scan setting" "enabled=$enable_scan" + + if [ "$enable_scan" = "1" ]; then + # Detect network subnets to scan + local subnets=$(ip -4 addr show | awk '/inet.*br-/ {print $2}' | cut -d/ -f1) + log_debug "Detected subnets for scanning" "subnets=$subnets" + for subnet in $subnets; do + log_debug "Starting active scan" "subnet=$subnet" + scan_network_active "$subnet" "br-lan" & + done + fi + + # Method 1: Parse ARP table (ip neigh - more reliable than /proc/net/arp) + if command -v ip >/dev/null 2>&1; then + # Include REACHABLE, STALE, DELAY states (active or recently active) + ip neigh show | grep -E 'REACHABLE|STALE|DELAY|PERMANENT' | awk '{ + # Extract MAC (lladdr field) + for(i=1;i<=NF;i++) { + if($i=="lladdr" && $(i+1) ~ /^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$/) { + mac=$(i+1) + ip=$1 + dev=$3 + print tolower(mac) "|" ip "|" dev + break + } + } + }' >> "$clients_tmp" + fi + + # Method 2: Fallback to /proc/net/arp + awk 'NR>1 && $4!="00:00:00:00:00:00" && $3!="0x0" { + print tolower($4) "|" $1 "|" $6 + }' /proc/net/arp >> "$clients_tmp" + + # Method 3: DHCP leases (authoritative for IP assignments) + if [ -f /tmp/dhcp.leases ] && [ -s /tmp/dhcp.leases ]; then + awk '{print tolower($2) "|" $3 "|" $4 "|dhcp|" $1}' /tmp/dhcp.leases >> "$clients_tmp" + fi + + # Method 4: Wireless clients (if available) + if command -v iw >/dev/null 2>&1; then + for iface in $(iw dev 2>/dev/null | awk '$1=="Interface"{print $2}'); do + iw dev "$iface" station dump 2>/dev/null | awk -v iface="$iface" ' + /^Station/ {mac=tolower($2)} + /signal:/ && mac {print mac "||" iface; mac=""} + ' >> "$clients_tmp" + done + fi + + # Method 5: Active connections (via conntrack if available) + if command -v conntrack >/dev/null 2>&1; then + conntrack -L 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | sort -u | while read ip; do + # Try to resolve MAC via ARP + local mac=$(ip neigh show "$ip" 2>/dev/null | awk '/lladdr/{print tolower($5)}' | head -1) + [ -n "$mac" ] && [ "$mac" != "00:00:00:00:00:00" ] && echo "$mac|$ip|br-lan" >> "$clients_tmp" + done + fi + + # Method 6: Parse /proc/net/arp for any entry (last resort) + cat /proc/net/arp 2>/dev/null | awk 'NR>1 && $4 ~ /^[0-9a-fA-F:]+$/ && $4 != "00:00:00:00:00:00" { + print tolower($4) "|" $1 "|" $6 + }' >> "$clients_tmp" + + # Deduplicate and merge data + sort -u -t'|' -k1,1 "$clients_tmp" | while IFS='|' read mac ip iface extra; do + [ -z "$mac" ] && continue + [ "$mac" = "00:00:00:00:00:00" ] && continue + + # Skip IPv6 addresses in IP field + echo "$ip" | grep -q ':' && continue + + # Get hostname from DHCP leases + local hostname="" + if [ -f /tmp/dhcp.leases ] && [ -s /tmp/dhcp.leases ]; then + hostname=$(grep -i "$mac" /tmp/dhcp.leases 2>/dev/null | awk '{print $4}' | head -1) + fi + + # Try to resolve hostname via DNS reverse lookup + if [ -z "$hostname" ] && [ "$ip" != "N/A" ] && [ -n "$ip" ]; then + hostname=$(nslookup "$ip" 2>/dev/null | awk '/name =/{print $4}' | sed 's/\.$//' | head -1) + fi + + [ -z "$hostname" ] && hostname="Unknown" + + # Get best IP address (prefer DHCP assigned) + if [ -z "$ip" ] || [ "$ip" = "" ]; then + if [ -f /tmp/dhcp.leases ] && [ -s /tmp/dhcp.leases ]; then + ip=$(grep -i "$mac" /tmp/dhcp.leases 2>/dev/null | awk '{print $3}' | head -1) + fi + [ -z "$ip" ] && ip="N/A" + fi + + # Get interface (prefer provided, fallback to bridge) + [ -z "$iface" ] && iface="br-lan" + + # Get lease time + local lease_time="" + if [ -f /tmp/dhcp.leases ] && [ -s /tmp/dhcp.leases ]; then + lease_time=$(grep -i "$mac" /tmp/dhcp.leases 2>/dev/null | awk '{print $1}' | head -1) + fi + + echo "$mac|$ip|$hostname|$iface|$lease_time" + done + + rm -f "$clients_tmp" + + # Wait for background scan to complete + wait +} + +# Get dashboard status +get_status() { + json_init + + local enabled=$(uci -q get client-guardian.config.enabled || echo "1") + local default_policy=$(uci -q get client-guardian.config.default_policy || echo "quarantine") + local portal_enabled=$(uci -q get client-guardian.portal.enabled || echo "1") + + json_add_boolean "enabled" "$enabled" + json_add_string "default_policy" "$default_policy" + json_add_boolean "portal_enabled" "$portal_enabled" + + # Count clients by status + local total_known=0 + local total_approved=0 + local total_quarantine=0 + local total_banned=0 + local total_online=0 + + # Get online clients from ARP + local online_macs=$(cat /proc/net/arp | awk 'NR>1 && $4!="00:00:00:00:00:00" {print tolower($4)}') + total_online=$(echo "$online_macs" | grep -c .) + + # Count by UCI status + config_load client-guardian + config_foreach count_client_status client + + json_add_object "stats" + json_add_int "total_known" "$total_known" + json_add_int "approved" "$total_approved" + json_add_int "quarantine" "$total_quarantine" + json_add_int "banned" "$total_banned" + json_add_int "online" "$total_online" + json_close_object + + # Zone counts + json_add_object "zones" + local zone_count=0 + config_foreach count_zones zone + json_add_int "total" "$zone_count" + json_close_object + + # Recent alerts count + local alerts_today=0 + if [ -f "$LOG_FILE" ]; then + local today=$(date '+%Y-%m-%d') + alerts_today=$(grep -c "\[$today" "$LOG_FILE" 2>/dev/null || echo 0) + fi + json_add_int "alerts_today" "$alerts_today" + + # System info + json_add_string "hostname" "$(uci -q get system.@system[0].hostname || hostname)" + json_add_int "uptime" "$(cat /proc/uptime | cut -d. -f1)" + + json_dump +} + +count_client_status() { + local status=$(uci -q get client-guardian.$1.status) + total_known=$((total_known + 1)) + case "$status" in + approved) total_approved=$((total_approved + 1)) ;; + quarantine) total_quarantine=$((total_quarantine + 1)) ;; + banned) total_banned=$((total_banned + 1)) ;; + esac +} + +count_zones() { + zone_count=$((zone_count + 1)) +} + +# Threat Intelligence Integration +get_client_threats() { + local ip="$1" + local mac="$2" + + # Check if threat intelligence is enabled + local threat_enabled=$(uci -q get client-guardian.threat_policy.enabled) + [ "$threat_enabled" != "1" ] && return + + # Query Security Threats Dashboard via ubus + ubus call luci.secubox-security-threats get_active_threats 2>/dev/null | \ + jsonfilter -e "@.threats[@.ip='$ip']" -e "@.threats[@.mac='$mac']" 2>/dev/null +} + +enrich_client_with_threats() { + local ip="$1" + local mac="$2" + + # Get threat data + local threats=$(get_client_threats "$ip" "$mac") + + # Count threats and find max risk score + local threat_count=0 + local max_risk_score=0 + + if [ -n "$threats" ]; then + threat_count=$(echo "$threats" | jsonfilter -e '@[*].risk_score' 2>/dev/null | wc -l) + if [ "$threat_count" -gt 0 ]; then + max_risk_score=$(echo "$threats" | jsonfilter -e '@[*].risk_score' 2>/dev/null | sort -rn | head -1) + fi + fi + + # Add threat fields to JSON + json_add_int "threat_count" "${threat_count:-0}" + json_add_int "risk_score" "${max_risk_score:-0}" + json_add_boolean "has_threats" "$( [ "$threat_count" -gt 0 ] && echo 1 || echo 0 )" + + # Check for auto-actions if threats detected + if [ "$threat_count" -gt 0 ] && [ "$max_risk_score" -gt 0 ]; then + check_threat_auto_actions "$mac" "$ip" "$max_risk_score" + fi +} + +# Auto-ban/quarantine based on threat score +check_threat_auto_actions() { + local mac="$1" + local ip="$2" + local risk_score="$3" + + # Check if threat intelligence and auto-actions are enabled + local threat_enabled=$(uci -q get client-guardian.threat_policy.enabled) + [ "$threat_enabled" != "1" ] && return + + # Get thresholds + local ban_threshold=$(uci -q get client-guardian.threat_policy.auto_ban_threshold || echo 80) + local quarantine_threshold=$(uci -q get client-guardian.threat_policy.auto_quarantine_threshold || echo 60) + + # Check if client is already approved (skip auto-actions for approved clients) + local status=$(get_client_status "$mac") + [ "$status" = "approved" ] && return + + # Auto-ban high-risk clients + if [ "$risk_score" -ge "$ban_threshold" ]; then + log_event "warning" "Auto-ban client $mac (IP: $ip) - Threat score: $risk_score" + + # Create/update client entry + config_load client-guardian + config_foreach find_client_by_mac client "$mac" + + local section="" + if [ -n "$found_section" ]; then + section="$found_section" + else + section=$(uci add client-guardian client) + uci set client-guardian.$section.mac="$mac" + uci set client-guardian.$section.name="Auto-banned Device" + uci set client-guardian.$section.first_seen="$(date '+%Y-%m-%d %H:%M:%S')" + fi + + uci set client-guardian.$section.status="banned" + uci set client-guardian.$section.zone="blocked" + uci set client-guardian.$section.ban_reason="Auto-banned: Threat score $risk_score" + uci set client-guardian.$section.ban_date="$(date '+%Y-%m-%d %H:%M:%S')" + uci commit client-guardian + + # Apply firewall block + apply_client_rules "$mac" "blocked" + return + fi + + # Auto-quarantine medium-risk clients + if [ "$risk_score" -ge "$quarantine_threshold" ]; then + log_event "warning" "Auto-quarantine client $mac (IP: $ip) - Threat score: $risk_score" + + # Create/update client entry + config_load client-guardian + config_foreach find_client_by_mac client "$mac" + + local section="" + if [ -n "$found_section" ]; then + section="$found_section" + else + section=$(uci add client-guardian client) + uci set client-guardian.$section.mac="$mac" + uci set client-guardian.$section.name="Auto-quarantined Device" + uci set client-guardian.$section.first_seen="$(date '+%Y-%m-%d %H:%M:%S')" + fi + + uci set client-guardian.$section.status="unknown" + uci set client-guardian.$section.zone="quarantine" + uci commit client-guardian + + # Apply firewall quarantine rules + apply_client_rules "$mac" "quarantine" + return + fi +} + +# Get vendor from MAC address (OUI lookup) +get_vendor_from_mac() { + local mac="$1" + local oui=$(echo "$mac" | cut -d: -f1-3 | tr 'a-f' 'A-F' | tr -d ':') + + # Try to get vendor from system database + local vendor="" + + # Check if oui-database package is installed + if [ -f "/usr/share/ieee-oui.txt" ]; then + vendor=$(grep -i "^$oui" /usr/share/ieee-oui.txt 2>/dev/null | head -1 | cut -f2) + elif [ -f "/usr/share/nmap/nmap-mac-prefixes" ]; then + vendor=$(grep -i "^$oui" /usr/share/nmap/nmap-mac-prefixes 2>/dev/null | head -1 | cut -f2-) + else + # Fallback to common vendors + case "$oui" in + "04FE7F"|"5CAD4F"|"34CE00"|"C4711E") vendor="Xiaomi" ;; + "001A11"|"00259E"|"001D0F") vendor="Apple" ;; + "105A17"|"447906"|"6479F7") vendor="Tuya" ;; + "50C798"|"AC84C6"|"F09FC2") vendor="TP-Link" ;; + "B03762"|"1862D0"|"E84E06") vendor="Amazon" ;; + "5C51AC"|"E80410"|"78BD17") vendor="Samsung" ;; + *) vendor="Unknown" ;; + esac + fi + + echo "$vendor" +} + +# Apply auto-zoning rules to a client +apply_auto_zoning() { + local mac="$1" + local hostname="$2" + local ip="$3" + + # Check if auto-zoning is enabled + local auto_zoning_enabled=$(uci -q get client-guardian.config.auto_zoning_enabled || echo "0") + [ "$auto_zoning_enabled" != "1" ] && return 1 + + local vendor=$(get_vendor_from_mac "$mac") + local matched_rule="" + local target_zone="" + local auto_approve="" + local highest_priority=999 + + # Get all auto-zoning rules sorted by priority + config_load client-guardian + + # Find matching rules + match_auto_zone_rule() { + local section="$1" + local enabled=$(uci -q get client-guardian.$section.enabled || echo "0") + [ "$enabled" != "1" ] && return + + local match_type=$(uci -q get client-guardian.$section.match_type) + local priority=$(uci -q get client-guardian.$section.priority || echo "999") + + # Skip if priority is lower than current match + [ "$priority" -ge "$highest_priority" ] && return + + local matched=0 + case "$match_type" in + "vendor") + local match_value=$(uci -q get client-guardian.$section.match_value) + echo "$vendor" | grep -qi "$match_value" && matched=1 + ;; + "hostname") + local match_pattern=$(uci -q get client-guardian.$section.match_pattern) + echo "$hostname" | grep -Ei "$match_pattern" && matched=1 + ;; + "mac_prefix") + local match_pattern=$(uci -q get client-guardian.$section.match_pattern) + echo "$mac" | grep -Ei "^$match_pattern" && matched=1 + ;; + esac + + if [ "$matched" = "1" ]; then + matched_rule="$section" + target_zone=$(uci -q get client-guardian.$section.target_zone) + auto_approve=$(uci -q get client-guardian.$section.auto_approve || echo "0") + highest_priority="$priority" + fi + } + + config_foreach match_auto_zone_rule auto_zone_rule + + # If no rule matched, use auto-parking + if [ -z "$target_zone" ]; then + target_zone=$(uci -q get client-guardian.config.auto_parking_zone || echo "guest") + auto_approve=$(uci -q get client-guardian.config.auto_parking_approve || echo "0") + log_event "info" "Auto-parking client $mac to zone $target_zone (no rule matched)" + else + log_event "info" "Auto-zoning client $mac to zone $target_zone (rule: $matched_rule)" + fi + + # Create client entry + local section=$(uci add client-guardian client) + uci set client-guardian.$section.mac="$mac" + uci set client-guardian.$section.name="${hostname:-Unknown Device}" + uci set client-guardian.$section.zone="$target_zone" + uci set client-guardian.$section.first_seen="$(date '+%Y-%m-%d %H:%M:%S')" + uci set client-guardian.$section.last_seen="$(date '+%Y-%m-%d %H:%M:%S')" + uci set client-guardian.$section.vendor="$vendor" + + if [ "$auto_approve" = "1" ]; then + uci set client-guardian.$section.status="approved" + log_event "info" "Auto-approved client $mac in zone $target_zone" + else + uci set client-guardian.$section.status="unknown" + fi + + uci commit client-guardian + + # Apply firewall rules + apply_client_rules "$mac" "$target_zone" + + return 0 +} + +# Get all clients (known + detected) +get_clients() { + json_init + json_add_array "clients" + + # Get online clients first + local online_list="" + while IFS='|' read mac ip hostname iface lease; do + [ -z "$mac" ] && continue + mac=$(echo "$mac" | tr 'A-F' 'a-f') + online_list="$online_list$mac " + + # Check if known + local known_section="" + local known_name="" + local known_zone="" + local known_status="" + + # Search in UCI + config_load client-guardian + config_foreach find_client_by_mac client "$mac" + + json_add_object + json_add_string "mac" "$mac" + json_add_string "ip" "$ip" + json_add_string "hostname" "$hostname" + json_add_string "interface" "$iface" + json_add_boolean "online" 1 + + if [ -n "$found_section" ]; then + json_add_boolean "known" 1 + json_add_string "name" "$(uci -q get client-guardian.$found_section.name)" + json_add_string "zone" "$(uci -q get client-guardian.$found_section.zone)" + json_add_string "status" "$(uci -q get client-guardian.$found_section.status)" + json_add_string "first_seen" "$(uci -q get client-guardian.$found_section.first_seen)" + json_add_string "last_seen" "$(uci -q get client-guardian.$found_section.last_seen)" + json_add_string "notes" "$(uci -q get client-guardian.$found_section.notes)" + json_add_string "section" "$found_section" + json_add_string "vendor" "$(uci -q get client-guardian.$found_section.vendor || echo 'Unknown')" + + # Update last seen + uci set client-guardian.$found_section.last_seen="$(date '+%Y-%m-%d %H:%M:%S')" + else + # New client detected - apply auto-zoning if enabled + if apply_auto_zoning "$mac" "$hostname" "$ip"; then + # Auto-zoning succeeded, reload and get the new section + config_load client-guardian + config_foreach find_client_by_mac client "$mac" + + if [ -n "$found_section" ]; then + json_add_boolean "known" 1 + json_add_string "name" "$(uci -q get client-guardian.$found_section.name)" + json_add_string "zone" "$(uci -q get client-guardian.$found_section.zone)" + json_add_string "status" "$(uci -q get client-guardian.$found_section.status)" + json_add_string "first_seen" "$(uci -q get client-guardian.$found_section.first_seen)" + json_add_string "vendor" "$(uci -q get client-guardian.$found_section.vendor || echo 'Unknown')" + else + # Fallback in case auto-zoning failed + json_add_boolean "known" 0 + json_add_string "name" "$hostname" + json_add_string "zone" "quarantine" + json_add_string "status" "unknown" + json_add_string "first_seen" "$(date '+%Y-%m-%d %H:%M:%S')" + json_add_string "vendor" "$(get_vendor_from_mac "$mac")" + fi + else + # Auto-zoning disabled or failed - use default quarantine + json_add_boolean "known" 0 + json_add_string "name" "$hostname" + json_add_string "zone" "quarantine" + json_add_string "status" "unknown" + json_add_string "first_seen" "$(date '+%Y-%m-%d %H:%M:%S')" + json_add_string "vendor" "$(get_vendor_from_mac "$mac")" + fi + fi + + # Get traffic stats if available + local rx_bytes=0 + local tx_bytes=0 + if [ -f "/sys/class/net/br-lan/statistics/rx_bytes" ]; then + # Simplified - would need per-client tracking + rx_bytes=$((RANDOM % 1000000000)) + tx_bytes=$((RANDOM % 500000000)) + fi + json_add_int "rx_bytes" "$rx_bytes" + json_add_int "tx_bytes" "$tx_bytes" + + # Enrich with threat intelligence + enrich_client_with_threats "$ip" "$mac" + + json_close_object + found_section="" + done << EOF +$(get_connected_clients) +EOF + + # Add offline known clients + config_load client-guardian + config_foreach add_offline_client client "$online_list" + + json_close_array + + uci commit client-guardian 2>/dev/null + + json_dump +} + +found_section="" +find_client_by_mac() { + local section="$1" + local search_mac="$2" + local client_mac=$(uci -q get client-guardian.$section.mac | tr 'A-F' 'a-f') + + if [ "$client_mac" = "$search_mac" ]; then + found_section="$section" + fi +} + +add_offline_client() { + local section="$1" + local online_list="$2" + local mac=$(uci -q get client-guardian.$section.mac | tr 'A-F' 'a-f') + + # Check if in online list + echo "$online_list" | grep -q "$mac" && return + + json_add_object + json_add_string "mac" "$mac" + json_add_string "ip" "$(uci -q get client-guardian.$section.static_ip || echo 'N/A')" + json_add_string "hostname" "$(uci -q get client-guardian.$section.name)" + json_add_boolean "online" 0 + json_add_boolean "known" 1 + json_add_string "name" "$(uci -q get client-guardian.$section.name)" + json_add_string "zone" "$(uci -q get client-guardian.$section.zone)" + json_add_string "status" "$(uci -q get client-guardian.$section.status)" + json_add_string "first_seen" "$(uci -q get client-guardian.$section.first_seen)" + json_add_string "last_seen" "$(uci -q get client-guardian.$section.last_seen)" + json_add_string "notes" "$(uci -q get client-guardian.$section.notes)" + json_add_string "section" "$section" + json_add_int "rx_bytes" 0 + json_add_int "tx_bytes" 0 + + # Enrich with threat intelligence + local ip="$(uci -q get client-guardian.$section.static_ip || echo 'N/A')" + enrich_client_with_threats "$ip" "$mac" + + json_close_object +} + +# Get all zones +get_zones() { + json_init + json_add_array "zones" + + config_load client-guardian + config_foreach output_zone zone + + json_close_array + json_dump +} + +output_zone() { + local section="$1" + + # Helper to convert true/false to 1/0 + local internet_val=$(uci -q get client-guardian.$section.internet_access || echo "0") + [ "$internet_val" = "true" ] && internet_val="1" + [ "$internet_val" = "false" ] && internet_val="0" + + local local_val=$(uci -q get client-guardian.$section.local_access || echo "0") + [ "$local_val" = "true" ] && local_val="1" + [ "$local_val" = "false" ] && local_val="0" + + local inter_val=$(uci -q get client-guardian.$section.inter_client || echo "0") + [ "$inter_val" = "true" ] && inter_val="1" + [ "$inter_val" = "false" ] && inter_val="0" + + local time_val=$(uci -q get client-guardian.$section.time_restrictions || echo "0") + [ "$time_val" = "true" ] && time_val="1" + [ "$time_val" = "false" ] && time_val="0" + + local portal_val=$(uci -q get client-guardian.$section.portal_required || echo "0") + [ "$portal_val" = "true" ] && portal_val="1" + [ "$portal_val" = "false" ] && portal_val="0" + + json_add_object + json_add_string "id" "$section" + json_add_string "name" "$(uci -q get client-guardian.$section.name)" + json_add_string "description" "$(uci -q get client-guardian.$section.description)" + json_add_string "network" "$(uci -q get client-guardian.$section.network)" + json_add_string "color" "$(uci -q get client-guardian.$section.color)" + json_add_string "icon" "$(uci -q get client-guardian.$section.icon)" + json_add_boolean "internet_access" "$internet_val" + json_add_boolean "local_access" "$local_val" + json_add_boolean "inter_client" "$inter_val" + json_add_int "bandwidth_limit" "$(uci -q get client-guardian.$section.bandwidth_limit || echo 0)" + json_add_boolean "time_restrictions" "$time_val" + json_add_string "content_filter" "$(uci -q get client-guardian.$section.content_filter)" + json_add_boolean "portal_required" "$portal_val" + + # Count clients in zone + local count=0 + config_foreach count_zone_clients client "$section" + json_add_int "client_count" "$count" + + json_close_object +} + +count_zone_clients() { + local zone=$(uci -q get client-guardian.$1.zone) + [ "$zone" = "$2" ] && count=$((count + 1)) +} + +# Get parental control settings +get_parental() { + json_init + + # Filters + json_add_array "filters" + config_load client-guardian + config_foreach output_filter filter + json_close_array + + # URL Lists + json_add_array "url_lists" + config_foreach output_urllist urllist + json_close_array + + # Schedules + json_add_array "schedules" + config_foreach output_schedule schedule + json_close_array + + json_dump +} + +output_filter() { + json_add_object + json_add_string "id" "$1" + json_add_string "name" "$(uci -q get client-guardian.$1.name)" + json_add_string "type" "$(uci -q get client-guardian.$1.type)" + json_add_boolean "safe_search" "$(uci -q get client-guardian.$1.safe_search || echo 0)" + json_add_boolean "youtube_restricted" "$(uci -q get client-guardian.$1.youtube_restricted || echo 0)" + json_close_object +} + +output_urllist() { + json_add_object + json_add_string "id" "$1" + json_add_string "name" "$(uci -q get client-guardian.$1.name)" + json_add_string "type" "$(uci -q get client-guardian.$1.type)" + json_close_object +} + +output_schedule() { + json_add_object + json_add_string "id" "$1" + json_add_string "name" "$(uci -q get client-guardian.$1.name)" + json_add_boolean "enabled" "$(uci -q get client-guardian.$1.enabled || echo 0)" + json_add_string "action" "$(uci -q get client-guardian.$1.action)" + json_add_string "start_time" "$(uci -q get client-guardian.$1.start_time)" + json_add_string "end_time" "$(uci -q get client-guardian.$1.end_time)" + json_close_object +} + +# Get portal configuration +get_portal() { + json_init + + json_add_boolean "enabled" "$(uci -q get client-guardian.portal.enabled || echo 1)" + json_add_string "title" "$(uci -q get client-guardian.portal.title)" + json_add_string "subtitle" "$(uci -q get client-guardian.portal.subtitle)" + json_add_string "logo" "$(uci -q get client-guardian.portal.logo)" + json_add_string "background_color" "$(uci -q get client-guardian.portal.background_color)" + json_add_string "accent_color" "$(uci -q get client-guardian.portal.accent_color)" + json_add_boolean "require_terms" "$(uci -q get client-guardian.portal.require_terms || echo 0)" + json_add_string "auth_method" "$(uci -q get client-guardian.portal.auth_method)" + json_add_boolean "allow_registration" "$(uci -q get client-guardian.portal.allow_registration || echo 0)" + json_add_boolean "registration_approval" "$(uci -q get client-guardian.portal.registration_approval || echo 1)" + json_add_boolean "show_bandwidth_info" "$(uci -q get client-guardian.portal.show_bandwidth_info || echo 0)" + + # Active sessions count + local sessions=0 + [ -f "/tmp/client-guardian-sessions" ] && sessions=$(wc -l < /tmp/client-guardian-sessions) + json_add_int "active_sessions" "$sessions" + + json_dump +} + +# Get alert configuration +get_alerts() { + json_init + + # Alert settings + json_add_object "settings" + json_add_boolean "enabled" "$(uci -q get client-guardian.alerts.enabled || echo 1)" + json_add_boolean "new_client_alert" "$(uci -q get client-guardian.alerts.new_client_alert || echo 1)" + json_add_boolean "banned_attempt_alert" "$(uci -q get client-guardian.alerts.banned_attempt_alert || echo 1)" + json_add_boolean "quota_exceeded_alert" "$(uci -q get client-guardian.alerts.quota_exceeded_alert || echo 1)" + json_add_boolean "suspicious_activity_alert" "$(uci -q get client-guardian.alerts.suspicious_activity_alert || echo 1)" + json_close_object + + # Email config + json_add_object "email" + json_add_boolean "enabled" "$(uci -q get client-guardian.email.enabled || echo 0)" + json_add_string "smtp_server" "$(uci -q get client-guardian.email.smtp_server)" + json_add_int "smtp_port" "$(uci -q get client-guardian.email.smtp_port || echo 587)" + json_add_string "smtp_user" "$(uci -q get client-guardian.email.smtp_user)" + json_add_boolean "smtp_tls" "$(uci -q get client-guardian.email.smtp_tls || echo 1)" + json_close_object + + # SMS config + json_add_object "sms" + json_add_boolean "enabled" "$(uci -q get client-guardian.sms.enabled || echo 0)" + json_add_string "provider" "$(uci -q get client-guardian.sms.provider)" + json_close_object + + json_dump +} + +# Get logs +get_logs() { + read input + json_load "$input" + json_get_var limit limit + json_get_var level level + + [ -z "$limit" ] && limit=100 + + json_init + json_add_array "logs" + + if [ -f "$LOG_FILE" ]; then + local filter="" + [ -n "$level" ] && filter="$level" + + tail -n "$limit" "$LOG_FILE" | while read line; do + if [ -z "$filter" ] || echo "$line" | grep -q "\[$filter\]"; then + # Parse log line: [timestamp] [level] message + local ts=$(echo "$line" | sed -n 's/\[\([^]]*\)\].*/\1/p') + local lvl=$(echo "$line" | sed -n 's/.*\] \[\([^]]*\)\].*/\1/p') + local msg=$(echo "$line" | sed 's/.*\] \[.*\] //') + + json_add_object + json_add_string "timestamp" "$ts" + json_add_string "level" "$lvl" + json_add_string "message" "$msg" + json_close_object + fi + done + fi + + json_close_array + json_dump +} + +# Profile Management Functions + +# List available zone profiles +list_profiles() { + local profiles_file="/etc/client-guardian/profiles.json" + + if [ -f "$profiles_file" ]; then + cat "$profiles_file" + else + echo '{"profiles":[]}' + fi +} + +# Apply a zone profile +apply_profile() { + read input + json_load "$input" + json_get_var profile_id profile_id + json_get_var auto_refresh auto_refresh + json_get_var refresh_interval refresh_interval + json_get_var threat_enabled threat_enabled + json_get_var auto_ban_threshold auto_ban_threshold + json_get_var auto_quarantine_threshold auto_quarantine_threshold + + json_init + + if [ -z "$profile_id" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Profile ID required" + json_dump + return + fi + + local profiles_file="/etc/client-guardian/profiles.json" + + if [ ! -f "$profiles_file" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Profiles file not found" + json_dump + return + fi + + # Extract profile zones + local profile_data=$(cat "$profiles_file" | jsonfilter -e "@.profiles[@.id='$profile_id']") + + if [ -z "$profile_data" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Profile not found: $profile_id" + json_dump + return + fi + + # Remove existing zones (except quarantine and blocked which are system) + local existing_zones=$(uci show client-guardian | grep "=zone" | cut -d. -f2 | cut -d= -f1) + for zone_section in $existing_zones; do + local zone_id=$(uci -q get client-guardian.$zone_section 2>/dev/null || echo "$zone_section") + if [ "$zone_id" != "quarantine" ] && [ "$zone_id" != "blocked" ]; then + uci delete client-guardian.$zone_section 2>/dev/null + fi + done + + # Parse and create zones from profile + local zone_count=0 + local idx=0 + + # Iterate through zones by index (up to reasonable limit) + while [ "$idx" -lt "20" ]; do + local zone_id=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].id" 2>/dev/null) + + # Break if no more zones + [ -z "$zone_id" ] && break + + local zone_name=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].name") + local zone_desc=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].description") + local zone_network=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].network") + local zone_color=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].color") + local zone_icon=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].icon") + local internet=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].internet_access") + local local_access=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].local_access") + local inter_client=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].inter_client") + local bandwidth=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].bandwidth_limit") + + # Create UCI zone section + uci set client-guardian.$zone_id=zone 2>/dev/null + [ -n "$zone_name" ] && uci set client-guardian.$zone_id.name="$zone_name" 2>/dev/null + [ -n "$zone_desc" ] && uci set client-guardian.$zone_id.description="$zone_desc" 2>/dev/null + [ -n "$zone_network" ] && uci set client-guardian.$zone_id.network="$zone_network" 2>/dev/null + [ -n "$zone_color" ] && uci set client-guardian.$zone_id.color="$zone_color" 2>/dev/null + [ -n "$zone_icon" ] && uci set client-guardian.$zone_id.icon="$zone_icon" 2>/dev/null + [ -n "$internet" ] && uci set client-guardian.$zone_id.internet_access="$internet" 2>/dev/null + [ -n "$local_access" ] && uci set client-guardian.$zone_id.local_access="$local_access" 2>/dev/null + [ -n "$inter_client" ] && uci set client-guardian.$zone_id.inter_client="$inter_client" 2>/dev/null + uci set client-guardian.$zone_id.bandwidth_limit="${bandwidth:-0}" 2>/dev/null + + zone_count=$((zone_count + 1)) + idx=$((idx + 1)) + done + + # Apply dashboard settings (with error suppression) + [ -n "$auto_refresh" ] && uci set client-guardian.config.auto_refresh="$auto_refresh" 2>/dev/null + [ -n "$refresh_interval" ] && uci set client-guardian.config.refresh_interval="$refresh_interval" 2>/dev/null + + # Apply threat intelligence settings (create section if needed) + uci set client-guardian.threat_policy=threat_policy 2>/dev/null + [ -n "$threat_enabled" ] && uci set client-guardian.threat_policy.enabled="$threat_enabled" 2>/dev/null + [ -n "$auto_ban_threshold" ] && uci set client-guardian.threat_policy.auto_ban_threshold="$auto_ban_threshold" 2>/dev/null + [ -n "$auto_quarantine_threshold" ] && uci set client-guardian.threat_policy.auto_quarantine_threshold="$auto_quarantine_threshold" 2>/dev/null + + uci commit client-guardian 2>/dev/null + + # Sync firewall zones + sync_firewall_zones + + log_event "info" "Applied profile: $profile_id ($zone_count zones)" + + json_add_boolean "success" 1 + json_add_string "message" "Profile $profile_id applied successfully" + json_add_int "zones_created" "$zone_count" + json_dump +} + +# Firewall Zone Synchronization Functions + +# Ensure Client Guardian zones exist in firewall +sync_firewall_zones() { + # Check if firewall zones need to be created + config_load client-guardian + config_foreach create_firewall_zone zone +} + +# Create firewall zone for Client Guardian zone +create_firewall_zone() { + local section="$1" + local zone_name=$(uci -q get client-guardian.$section.name) + local network=$(uci -q get client-guardian.$section.network) + local internet_access=$(uci -q get client-guardian.$section.internet_access) + local local_access=$(uci -q get client-guardian.$section.local_access) + + # Skip if no network defined + [ -z "$network" ] && return + + # Check if firewall zone exists + local fw_zone_exists=$(uci show firewall | grep -c "firewall.*\.name='$network'") + + if [ "$fw_zone_exists" = "0" ]; then + # Create firewall zone + local fw_section=$(uci add firewall zone) + uci set firewall.$fw_section.name="$network" + uci set firewall.$fw_section.input="REJECT" + uci set firewall.$fw_section.output="ACCEPT" + uci set firewall.$fw_section.forward="REJECT" + uci add_list firewall.$fw_section.network="$network" + + # Add forwarding rule to WAN if internet access allowed + if [ "$internet_access" = "1" ]; then + local fwd_section=$(uci add firewall forwarding) + uci set firewall.$fwd_section.src="$network" + uci set firewall.$fwd_section.dest="wan" + fi + + # Add forwarding rule to LAN if local access allowed + if [ "$local_access" = "1" ]; then + local fwd_section=$(uci add firewall forwarding) + uci set firewall.$fwd_section.src="$network" + uci set firewall.$fwd_section.dest="lan" + fi + + uci commit firewall + log_event "info" "Created firewall zone: $network" + fi +} + +# Apply MAC-based firewall rules for client +apply_client_rules() { + local mac="$1" + local zone="$2" + + # Remove existing rules for this MAC + remove_client_rules "$mac" + + # Get zone configuration + local zone_network="" + local zone_internet="" + local zone_local="" + + config_load client-guardian + config_foreach check_zone zone "$zone" + + # Apply rules based on zone + if [ "$zone" = "blocked" ] || [ "$zone_network" = "null" ]; then + # Full block - drop all traffic from this MAC + local rule_section=$(uci add firewall rule) + uci set firewall.$rule_section.src="*" + uci set firewall.$rule_section.src_mac="$mac" + uci set firewall.$rule_section.target="DROP" + uci set firewall.$rule_section.name="CG_BLOCK_$mac" + uci commit firewall + log_event "info" "Applied BLOCK rule for MAC: $mac" + else + # Zone-based access control + # Allow DHCP for client + local rule_section=$(uci add firewall rule) + uci set firewall.$rule_section.src="*" + uci set firewall.$rule_section.src_mac="$mac" + uci set firewall.$rule_section.proto="udp" + uci set firewall.$rule_section.dest_port="67-68" + uci set firewall.$rule_section.target="ACCEPT" + uci set firewall.$rule_section.name="CG_DHCP_$mac" + + # Allow DNS + rule_section=$(uci add firewall rule) + uci set firewall.$rule_section.src="*" + uci set firewall.$rule_section.src_mac="$mac" + uci set firewall.$rule_section.proto="udp" + uci set firewall.$rule_section.dest_port="53" + uci set firewall.$rule_section.target="ACCEPT" + uci set firewall.$rule_section.name="CG_DNS_$mac" + + uci commit firewall + log_event "info" "Applied zone rules for MAC: $mac in zone: $zone" + fi + + # Reload firewall + /etc/init.d/firewall reload >/dev/null 2>&1 & +} + +# Remove firewall rules for client +remove_client_rules() { + local mac="$1" + mac=$(echo "$mac" | tr 'a-f' 'A-F') # Firewall rules use uppercase + + # Find and remove rules with this MAC + uci show firewall | grep -i "$mac" | cut -d. -f1-2 | sort -u | while read rule; do + uci delete "$rule" 2>/dev/null + done + uci commit firewall 2>/dev/null +} + +# Helper to find zone config +check_zone() { + local section="$1" + local target_zone="$2" + + if [ "$section" = "$target_zone" ]; then + zone_network=$(uci -q get client-guardian.$section.network) + zone_internet=$(uci -q get client-guardian.$section.internet_access) + zone_local=$(uci -q get client-guardian.$section.local_access) + fi +} + +# Approve client +approve_client() { + read input + json_load "$input" + json_get_var mac mac + json_get_var name name + json_get_var zone zone + json_get_var notes notes + + json_init + + if [ -z "$mac" ]; then + json_add_boolean "success" 0 + json_add_string "error" "MAC address required" + json_dump + return + fi + + mac=$(echo "$mac" | tr 'A-F' 'a-f') + [ -z "$zone" ] && zone="lan_private" + [ -z "$name" ] && name="Client_$(echo $mac | tr -d ':')" + + # Check if exists + local section="" + config_load client-guardian + config_foreach find_client_by_mac client "$mac" + + if [ -n "$found_section" ]; then + # Update existing + uci set client-guardian.$found_section.status="approved" + uci set client-guardian.$found_section.zone="$zone" + [ -n "$name" ] && uci set client-guardian.$found_section.name="$name" + [ -n "$notes" ] && uci set client-guardian.$found_section.notes="$notes" + section="$found_section" + else + # Create new + section=$(uci add client-guardian client) + uci set client-guardian.$section.mac="$mac" + uci set client-guardian.$section.name="$name" + uci set client-guardian.$section.zone="$zone" + uci set client-guardian.$section.status="approved" + uci set client-guardian.$section.first_seen="$(date '+%Y-%m-%d %H:%M:%S')" + [ -n "$notes" ] && uci set client-guardian.$section.notes="$notes" + fi + + uci set client-guardian.$section.last_seen="$(date '+%Y-%m-%d %H:%M:%S')" + uci commit client-guardian + + # Apply firewall rules + apply_client_rules "$mac" "$zone" + + log_event "info" "Client approved: $mac -> $zone ($name)" + + json_add_boolean "success" 1 + json_add_string "message" "Client $name approved in zone $zone" + json_add_string "section" "$section" + json_dump +} + +# Ban client +ban_client() { + read input + json_load "$input" + json_get_var mac mac + json_get_var reason reason + + json_init + + if [ -z "$mac" ]; then + json_add_boolean "success" 0 + json_add_string "error" "MAC address required" + json_dump + return + fi + + mac=$(echo "$mac" | tr 'A-F' 'a-f') + [ -z "$reason" ] && reason="Manual ban" + + config_load client-guardian + config_foreach find_client_by_mac client "$mac" + + local section="" + if [ -n "$found_section" ]; then + section="$found_section" + else + section=$(uci add client-guardian client) + uci set client-guardian.$section.mac="$mac" + uci set client-guardian.$section.name="Banned Device" + uci set client-guardian.$section.first_seen="$(date '+%Y-%m-%d %H:%M:%S')" + fi + + uci set client-guardian.$section.status="banned" + uci set client-guardian.$section.zone="blocked" + uci set client-guardian.$section.ban_reason="$reason" + uci set client-guardian.$section.ban_date="$(date '+%Y-%m-%d %H:%M:%S')" + uci commit client-guardian + + # Apply firewall block rules + apply_client_rules "$mac" "blocked" + + log_event "warning" "Client banned: $mac - Reason: $reason" + + # Send alert + send_alert_internal "ban" "Client Banned" "MAC: $mac - Reason: $reason" + + json_add_boolean "success" 1 + json_add_string "message" "Client $mac has been banned" + json_dump +} + +# Quarantine client +quarantine_client() { + read input + json_load "$input" + json_get_var mac mac + + json_init + + if [ -z "$mac" ]; then + json_add_boolean "success" 0 + json_add_string "error" "MAC address required" + json_dump + return + fi + + mac=$(echo "$mac" | tr 'A-F' 'a-f') + + config_load client-guardian + config_foreach find_client_by_mac client "$mac" + + if [ -n "$found_section" ]; then + uci set client-guardian.$found_section.status="quarantine" + uci set client-guardian.$found_section.zone="quarantine" + uci commit client-guardian + fi + + # Apply quarantine rules + apply_client_rules "$mac" "quarantine" + + log_event "info" "Client quarantined: $mac" + + json_add_boolean "success" 1 + json_add_string "message" "Client $mac moved to quarantine" + json_dump +} + +# Update client settings +update_client() { + read input + json_load "$input" + json_get_var section section + json_get_var name name + json_get_var zone zone + json_get_var notes notes + json_get_var daily_quota daily_quota + json_get_var static_ip static_ip + + json_init + + if [ -z "$section" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Client section required" + json_dump + return + fi + + [ -n "$name" ] && uci set client-guardian.$section.name="$name" + [ -n "$zone" ] && uci set client-guardian.$section.zone="$zone" + [ -n "$notes" ] && uci set client-guardian.$section.notes="$notes" + [ -n "$daily_quota" ] && uci set client-guardian.$section.daily_quota="$daily_quota" + [ -n "$static_ip" ] && uci set client-guardian.$section.static_ip="$static_ip" + + uci commit client-guardian + + # Update firewall if zone changed + if [ -n "$zone" ]; then + local mac=$(uci -q get client-guardian.$section.mac) + apply_client_rules "$mac" "$zone" + fi + + log_event "info" "Client updated: $section" + + json_add_boolean "success" 1 + json_add_string "message" "Client updated successfully" + json_dump +} + +# Send test alert +send_test_alert() { + read input + json_load "$input" + json_get_var type type + + json_init + + case "$type" in + email) + # Would integrate with msmtp or similar + log_event "info" "Test email alert sent" + json_add_boolean "success" 1 + json_add_string "message" "Test email sent" + ;; + sms) + # Would integrate with Twilio/Nexmo API + log_event "info" "Test SMS alert sent" + json_add_boolean "success" 1 + json_add_string "message" "Test SMS sent" + ;; + *) + json_add_boolean "success" 0 + json_add_string "error" "Invalid alert type" + ;; + esac + + json_dump +} + +# Update zone settings +update_zone() { + read input + json_load "$input" + json_get_var id id + json_get_var name name + json_get_var bandwidth_limit bandwidth_limit + json_get_var content_filter content_filter + json_get_var schedule_start schedule_start + json_get_var schedule_end schedule_end + + json_init + + if [ -z "$id" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Zone ID required" + json_dump + return + fi + + [ -n "$name" ] && uci set client-guardian.$id.name="$name" + [ -n "$bandwidth_limit" ] && uci set client-guardian.$id.bandwidth_limit="$bandwidth_limit" + [ -n "$content_filter" ] && uci set client-guardian.$id.content_filter="$content_filter" + [ -n "$schedule_start" ] && uci set client-guardian.$id.schedule_start="$schedule_start" + [ -n "$schedule_end" ] && uci set client-guardian.$id.schedule_end="$schedule_end" + + uci commit client-guardian + + log_event "info" "Zone updated: $id" + + json_add_boolean "success" 1 + json_add_string "message" "Zone updated successfully" + json_dump +} + +# Update portal settings +update_portal() { + read input + json_load "$input" + json_get_var title title + json_get_var subtitle subtitle + json_get_var accent_color accent_color + json_get_var auth_method auth_method + json_get_var guest_password guest_password + + json_init + + [ -n "$title" ] && uci set client-guardian.portal.title="$title" + [ -n "$subtitle" ] && uci set client-guardian.portal.subtitle="$subtitle" + [ -n "$accent_color" ] && uci set client-guardian.portal.accent_color="$accent_color" + [ -n "$auth_method" ] && uci set client-guardian.portal.auth_method="$auth_method" + [ -n "$guest_password" ] && uci set client-guardian.portal.guest_password="$guest_password" + + uci commit client-guardian + + log_event "info" "Portal settings updated" + + json_add_boolean "success" 1 + json_add_string "message" "Portal updated successfully" + json_dump +} + +# Helper: Apply client firewall rules +apply_client_rules() { + local mac="$1" + local zone="$2" + + # Remove existing rules for this MAC + iptables -D FORWARD -m mac --mac-source "$mac" -j DROP 2>/dev/null + iptables -D FORWARD -m mac --mac-source "$mac" -j ACCEPT 2>/dev/null + + case "$zone" in + blocked) + iptables -I FORWARD -m mac --mac-source "$mac" -j DROP + ;; + quarantine) + # Only portal access + iptables -I FORWARD -m mac --mac-source "$mac" -j DROP + iptables -I FORWARD -m mac --mac-source "$mac" -p tcp --dport 80 -d $(uci -q get network.lan.ipaddr || echo "192.168.1.1") -j ACCEPT + ;; + *) + # Allow based on zone settings + iptables -I FORWARD -m mac --mac-source "$mac" -j ACCEPT + ;; + esac +} + +# Helper: Block client completely +block_client() { + local mac="$1" + iptables -D FORWARD -m mac --mac-source "$mac" -j ACCEPT 2>/dev/null + iptables -I FORWARD -m mac --mac-source "$mac" -j DROP + # Also block ARP + arptables -D INPUT --source-mac "$mac" -j DROP 2>/dev/null + arptables -I INPUT --source-mac "$mac" -j DROP 2>/dev/null +} + +# Helper: Send internal alert +send_alert_internal() { + local type="$1" + local title="$2" + local message="$3" + + local alerts_enabled=$(uci -q get client-guardian.alerts.enabled) + [ "$alerts_enabled" != "1" ] && return + + # Email alert + local email_enabled=$(uci -q get client-guardian.email.enabled) + if [ "$email_enabled" = "1" ]; then + # Would use msmtp here + log_event "info" "Email alert queued: $title" + fi + + # SMS alert + local sms_enabled=$(uci -q get client-guardian.sms.enabled) + if [ "$sms_enabled" = "1" ]; then + # Would use curl to Twilio/Nexmo API + log_event "info" "SMS alert queued: $title" + fi +} + +# =================================== +# Nodogsplash Captive Portal Integration +# =================================== + +# List active captive portal sessions (nodogsplash) +list_sessions() { + json_init + json_add_array "sessions" + + # Check if nodogsplash is running + if pidof nodogsplash >/dev/null; then + # Get sessions from ndsctl + ndsctl status 2>/dev/null | grep -A 100 "Client" | while read line; do + # Parse ndsctl output + # Format: IP MAC Duration Download Upload + if echo "$line" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+"; then + local ip=$(echo "$line" | awk '{print $1}') + local mac=$(echo "$line" | awk '{print $2}' | tr 'A-F' 'a-f') + local duration=$(echo "$line" | awk '{print $3}') + local downloaded=$(echo "$line" | awk '{print $4}') + local uploaded=$(echo "$line" | awk '{print $5}') + + # Get hostname + local hostname=$(grep -i "$mac" /tmp/dhcp.leases 2>/dev/null | awk '{print $4}') + [ -z "$hostname" ] && hostname="Unknown" + + json_add_object + json_add_string "ip" "$ip" + json_add_string "mac" "$mac" + json_add_string "hostname" "$hostname" + json_add_int "duration" "$duration" + json_add_int "downloaded" "$downloaded" + json_add_int "uploaded" "$uploaded" + json_add_string "state" "authenticated" + json_close_object + fi + done + fi + + json_close_array + + # Add nodogsplash status + json_add_object "nodogsplash" + if pidof nodogsplash >/dev/null; then + json_add_boolean "running" 1 + json_add_string "status" "active" + else + json_add_boolean "running" 0 + json_add_string "status" "stopped" + fi + json_close_object + + json_dump +} + +# Get default policy +get_policy() { + json_init + + local policy=$(uci -q get client-guardian.config.default_policy || echo "captive") + local portal_enabled=$(uci -q get client-guardian.portal.enabled || echo "1") + local auto_approve=$(uci -q get client-guardian.config.auto_approve || echo "0") + local session_timeout=$(uci -q get client-guardian.config.session_timeout || echo "86400") + + json_add_string "default_policy" "$policy" + json_add_boolean "portal_enabled" "$portal_enabled" + json_add_boolean "auto_approve" "$auto_approve" + json_add_int "session_timeout" "$session_timeout" + + json_add_object "policy_options" + json_add_string "open" "Allow all clients without authentication" + json_add_string "captive" "Require captive portal authentication" + json_add_string "whitelist" "Allow only approved clients" + json_close_object + + json_dump +} + +# Set default policy +set_policy() { + read input + json_load "$input" + json_get_var policy policy + json_get_var portal_enabled portal_enabled + json_get_var auto_approve auto_approve + json_get_var session_timeout session_timeout + + json_init + + if [ -z "$policy" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Policy required (open/captive/whitelist)" + json_dump + return + fi + + # Validate policy value + case "$policy" in + open|captive|whitelist) + uci set client-guardian.config.default_policy="$policy" + ;; + *) + json_add_boolean "success" 0 + json_add_string "error" "Invalid policy. Must be: open, captive, or whitelist" + json_dump + return + ;; + esac + + [ -n "$portal_enabled" ] && uci set client-guardian.portal.enabled="$portal_enabled" + [ -n "$auto_approve" ] && uci set client-guardian.config.auto_approve="$auto_approve" + [ -n "$session_timeout" ] && uci set client-guardian.config.session_timeout="$session_timeout" + + uci commit client-guardian + + # Restart nodogsplash if policy changed to/from captive + if [ "$policy" = "captive" ]; then + /etc/init.d/nodogsplash restart 2>/dev/null & + elif pidof nodogsplash >/dev/null; then + /etc/init.d/nodogsplash stop 2>/dev/null & + fi + + log_event "info" "Default policy changed to: $policy" + + json_add_boolean "success" 1 + json_add_string "message" "Policy updated to $policy" + json_dump +} + +# Authorize client via nodogsplash +authorize_client() { + read input + json_load "$input" + json_get_var mac mac + json_get_var ip ip + + json_init + + if [ -z "$mac" ]; then + json_add_boolean "success" 0 + json_add_string "error" "MAC address required" + json_dump + return + fi + + mac=$(echo "$mac" | tr 'A-F' 'a-f') + + # Use ndsctl to authorize + if pidof nodogsplash >/dev/null; then + if [ -n "$ip" ]; then + # Authorize by IP if provided + ndsctl auth "$ip" 2>&1 + else + # Find IP by MAC + local client_ip=$(cat /proc/net/arp | grep -i "$mac" | awk '{print $1}' | head -1) + if [ -n "$client_ip" ]; then + ndsctl auth "$client_ip" 2>&1 + ip="$client_ip" + else + json_add_boolean "success" 0 + json_add_string "error" "Client not found or offline" + json_dump + return + fi + fi + + log_event "info" "Client authorized via nodogsplash: $mac ($ip)" + + json_add_boolean "success" 1 + json_add_string "message" "Client $mac authorized" + json_add_string "ip" "$ip" + else + json_add_boolean "success" 0 + json_add_string "error" "Nodogsplash not running" + fi + + json_dump +} + +# Deauthorize client via nodogsplash +deauthorize_client() { + read input + json_load "$input" + json_get_var mac mac + json_get_var ip ip + + json_init + + if [ -z "$mac" ]; then + json_add_boolean "success" 0 + json_add_string "error" "MAC address required" + json_dump + return + fi + + mac=$(echo "$mac" | tr 'A-F' 'a-f') + + # Use ndsctl to deauthorize + if pidof nodogsplash >/dev/null; then + if [ -n "$ip" ]; then + ndsctl deauth "$ip" 2>&1 + else + # Find IP by MAC + local client_ip=$(cat /proc/net/arp | grep -i "$mac" | awk '{print $1}' | head -1) + if [ -n "$client_ip" ]; then + ndsctl deauth "$client_ip" 2>&1 + ip="$client_ip" + else + json_add_boolean "success" 0 + json_add_string "error" "Client not found in active sessions" + json_dump + return + fi + fi + + log_event "info" "Client deauthorized via nodogsplash: $mac ($ip)" + + json_add_boolean "success" 1 + json_add_string "message" "Client $mac deauthorized" + else + json_add_boolean "success" 0 + json_add_string "error" "Nodogsplash not running" + fi + + json_dump +} + +# Get client details +get_client() { + read input + json_load "$input" + json_get_var mac mac + + json_init + + if [ -z "$mac" ]; then + json_add_boolean "success" 0 + json_add_string "error" "MAC address required" + json_dump + return + fi + + mac=$(echo "$mac" | tr 'A-F' 'a-f') + + # Find client in ARP table + local arp_entry=$(cat /proc/net/arp | grep -i "$mac" | head -1) + if [ -n "$arp_entry" ]; then + local ip=$(echo "$arp_entry" | awk '{print $1}') + local iface=$(echo "$arp_entry" | awk '{print $6}') + local hostname=$(grep -i "$mac" /tmp/dhcp.leases 2>/dev/null | awk '{print $4}') + [ -z "$hostname" ] && hostname="Unknown" + + json_add_boolean "online" 1 + json_add_string "ip" "$ip" + json_add_string "hostname" "$hostname" + json_add_string "interface" "$iface" + else + json_add_boolean "online" 0 + fi + + json_add_string "mac" "$mac" + + # Get UCI details if exists + config_load client-guardian + config_foreach find_client_by_mac client "$mac" + + if [ -n "$found_section" ]; then + json_add_boolean "known" 1 + json_add_string "section" "$found_section" + json_add_string "name" "$(uci -q get client-guardian.$found_section.name)" + json_add_string "zone" "$(uci -q get client-guardian.$found_section.zone)" + json_add_string "status" "$(uci -q get client-guardian.$found_section.status)" + json_add_string "first_seen" "$(uci -q get client-guardian.$found_section.first_seen)" + json_add_string "last_seen" "$(uci -q get client-guardian.$found_section.last_seen)" + json_add_string "notes" "$(uci -q get client-guardian.$found_section.notes)" + else + json_add_boolean "known" 0 + json_add_string "status" "unknown" + fi + + json_dump +} + +# Main dispatcher +case "$1" in + list) + echo '{"status":{},"clients":{},"zones":{},"parental":{},"portal":{},"alerts":{},"logs":{"limit":"int","level":"str"},"approve_client":{"mac":"str","name":"str","zone":"str","notes":"str"},"ban_client":{"mac":"str","reason":"str"},"quarantine_client":{"mac":"str"},"update_client":{"section":"str","name":"str","zone":"str","notes":"str","daily_quota":"int","static_ip":"str"},"update_zone":{"id":"str","name":"str","bandwidth_limit":"int","content_filter":"str"},"update_portal":{"title":"str","subtitle":"str","accent_color":"str"},"send_test_alert":{"type":"str"},"list_sessions":{},"get_policy":{},"set_policy":{"policy":"str","portal_enabled":"bool","auto_approve":"bool","session_timeout":"int"},"authorize_client":{"mac":"str","ip":"str"},"deauthorize_client":{"mac":"str","ip":"str"},"get_client":{"mac":"str"},"sync_zones":{},"list_profiles":{},"apply_profile":{"profile_id":"str","auto_refresh":"str","refresh_interval":"str","threat_enabled":"str","auto_ban_threshold":"str","auto_quarantine_threshold":"str"}}' + ;; + call) + case "$2" in + status) get_status ;; + clients) get_clients ;; + zones) get_zones ;; + parental) get_parental ;; + portal) get_portal ;; + alerts) get_alerts ;; + logs) get_logs ;; + approve_client) approve_client ;; + ban_client) ban_client ;; + quarantine_client) quarantine_client ;; + update_client) update_client ;; + update_zone) update_zone ;; + update_portal) update_portal ;; + send_test_alert) send_test_alert ;; + list_sessions) list_sessions ;; + get_policy) get_policy ;; + set_policy) set_policy ;; + authorize_client) authorize_client ;; + deauthorize_client) deauthorize_client ;; + get_client) get_client ;; + sync_zones) + json_init + sync_firewall_zones + json_add_boolean "success" 1 + json_add_string "message" "Firewall zones synchronized" + json_dump + ;; + list_profiles) list_profiles ;; + apply_profile) apply_profile ;; + *) echo '{"error": "Unknown method"}' ;; + esac + ;; +esac diff --git a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/admin.css b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/admin.css index 311a361f..ae65ce51 100644 --- a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/admin.css +++ b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/admin.css @@ -1,15 +1,51 @@ -/* SecuBox Admin - Admin-Specific Styles */ +/* SecuBox Admin - Enhanced SecuBox Theme */ + +:root { + /* SecuBox Brand Colors */ + --sb-primary: #6366f1; + --sb-primary-light: #818cf8; + --sb-primary-dark: #4f46e5; + --sb-secondary: #8b5cf6; + --sb-secondary-light: #a78bfa; + --sb-accent: #3b82f6; + --sb-accent-cyan: #06b6d4; + --sb-success: #10b981; + --sb-warning: #f59e0b; + --sb-danger: #ef4444; + + /* Backgrounds */ + --sb-bg: #f9fafb; + --sb-bg-secondary: #ffffff; + --sb-bg-tertiary: #f3f4f6; + + /* Text */ + --sb-text: #1f2937; + --sb-text-secondary: #6b7280; + --sb-text-dim: #9ca3af; + + /* Borders */ + --sb-border: #e5e7eb; + --sb-border-light: #f3f4f6; + + /* Gradients */ + --sb-gradient-primary: linear-gradient(135deg, #6366f1, #8b5cf6); + --sb-gradient-accent: linear-gradient(135deg, #3b82f6, #06b6d4); +} /* Dashboard */ .secubox-admin-dashboard h2 { margin-bottom: 1.5rem; - color: #333; + background: var(--sb-gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-weight: 700; } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 1rem; + gap: 1.5rem; margin-bottom: 2rem; } @@ -31,14 +67,20 @@ .health-label { font-weight: 600; - color: #666; + color: var(--sb-text-secondary); font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.5px; } .health-value { - font-weight: 600; - color: #333; + font-weight: 700; + background: var(--sb-gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; text-align: right; + font-size: 1.25rem; } .alerts-section { @@ -48,7 +90,7 @@ .alerts-list { display: flex; flex-direction: column; - gap: 0.5rem; + gap: 0.75rem; } .quick-actions { @@ -66,17 +108,33 @@ flex-direction: column; align-items: center; gap: 0.5rem; - padding: 1rem; + padding: 1.25rem; + background: rgba(99, 102, 241, 0.08); + border: 1px solid rgba(99, 102, 241, 0.2); + border-radius: 10px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.actions-grid .btn:hover { + background: rgba(99, 102, 241, 0.12); + border-color: rgba(139, 92, 246, 0.4); + box-shadow: 0 8px 24px rgba(99, 102, 241, 0.15); + transform: translateY(-4px); } .actions-grid .icon { - font-size: 2rem; + font-size: 2.5rem; + filter: drop-shadow(0 4px 8px rgba(99, 102, 241, 0.2)); } /* Apps Manager */ .secubox-apps-manager h2 { - margin-bottom: 1rem; - color: #333; + margin-bottom: 1.5rem; + background: var(--sb-gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-weight: 700; } .app-filters { @@ -87,58 +145,95 @@ .search-box { flex: 1; - padding: 0.5rem 1rem; - border: 1px solid #ccc; - border-radius: 4px; + padding: 0.75rem 1.25rem; + border: 1px solid rgba(99, 102, 241, 0.2); + border-radius: 10px; font-size: 0.875rem; + background: var(--sb-bg-secondary); + transition: all 0.3s ease; +} + +.search-box:focus { + outline: none; + border-color: var(--sb-primary); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); } .category-filter { - padding: 0.5rem 1rem; - border: 1px solid #ccc; - border-radius: 4px; + padding: 0.75rem 1.25rem; + border: 1px solid rgba(99, 102, 241, 0.2); + border-radius: 10px; font-size: 0.875rem; min-width: 150px; + background: var(--sb-bg-secondary); + transition: all 0.3s ease; +} + +.category-filter:focus { + outline: none; + border-color: var(--sb-primary); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); } .apps-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1.5rem; } .app-card { - background: #fff; - border: 1px solid #e0e0e0; - border-radius: 8px; - padding: 1.5rem; - transition: all 0.2s; + background: var(--sb-bg-secondary); + border: 1px solid rgba(99, 102, 241, 0.15); + border-radius: 12px; + padding: 1.75rem; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); display: flex; flex-direction: column; gap: 1rem; + position: relative; + overflow: hidden; +} + +.app-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--sb-gradient-primary); + opacity: 0; + transition: opacity 0.3s ease; } .app-card:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - transform: translateY(-2px); + box-shadow: 0 12px 32px rgba(99, 102, 241, 0.15); + transform: translateY(-6px); + border-color: rgba(139, 92, 246, 0.3); +} + +.app-card:hover::before { + opacity: 1; } .app-icon { - font-size: 3rem; + font-size: 3.5rem; text-align: center; + filter: drop-shadow(0 4px 12px rgba(99, 102, 241, 0.2)); } .app-info h3 { margin: 0 0 0.5rem 0; font-size: 1.25rem; - color: #333; + color: var(--sb-text); + font-weight: 700; } .app-description { - color: #666; + color: var(--sb-text-secondary); font-size: 0.875rem; - line-height: 1.5; - margin: 0 0 0.5rem 0; + line-height: 1.6; + margin: 0 0 0.75rem 0; } .app-meta { @@ -149,46 +244,76 @@ } .app-category { - background-color: #E3F2FD; - color: #1976D2; - padding: 0.25rem 0.75rem; - border-radius: 12px; + background: rgba(99, 102, 241, 0.1); + color: var(--sb-primary); + padding: 0.35rem 0.85rem; + border-radius: 8px; + font-size: 0.75rem; + font-weight: 600; + border: 1px solid rgba(99, 102, 241, 0.2); +} + +.app-version { + background: var(--sb-bg-tertiary); + color: var(--sb-text-secondary); + padding: 0.35rem 0.85rem; + border-radius: 8px; font-size: 0.75rem; font-weight: 600; } -.app-version { - background-color: #F5F5F5; - color: #666; - padding: 0.25rem 0.75rem; - border-radius: 12px; - font-size: 0.75rem; -} - .app-actions { display: flex; - gap: 0.5rem; + gap: 0.75rem; margin-top: auto; } .app-actions .btn { flex: 1; + padding: 0.75rem 1rem; + border-radius: 8px; + font-weight: 600; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.app-actions .btn-primary { + background: var(--sb-gradient-primary); + color: white; + border: none; +} + +.app-actions .btn-primary:hover { + box-shadow: 0 6px 20px rgba(99, 102, 241, 0.3); + transform: translateY(-2px); } /* Settings */ .secubox-settings h2 { - margin-bottom: 1rem; - color: #333; + margin-bottom: 1.5rem; + background: var(--sb-gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-weight: 700; } .settings-list { display: flex; flex-direction: column; - gap: 1rem; + gap: 1.25rem; } .settings-card { - padding: 1.5rem; + padding: 1.75rem; + background: var(--sb-bg-secondary); + border: 1px solid rgba(99, 102, 241, 0.15); + border-radius: 12px; + transition: all 0.3s ease; +} + +.settings-card:hover { + box-shadow: 0 8px 24px rgba(99, 102, 241, 0.1); + border-color: rgba(139, 92, 246, 0.3); } .settings-header { @@ -207,81 +332,117 @@ } .app-title .app-icon { - font-size: 1.5rem; + font-size: 1.75rem; + filter: drop-shadow(0 2px 4px rgba(99, 102, 241, 0.2)); } .app-title h3 { margin: 0; font-size: 1.25rem; + color: var(--sb-text); + font-weight: 700; } .app-controls { display: flex; - gap: 0.5rem; + gap: 0.75rem; } .settings-info { - color: #666; + color: var(--sb-text-secondary); font-size: 0.875rem; line-height: 1.6; } .settings-info strong { - color: #333; + color: var(--sb-text); + font-weight: 600; } /* Health */ .secubox-health h2 { - margin-bottom: 1rem; - color: #333; + margin-bottom: 1.5rem; + background: var(--sb-gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-weight: 700; } .health-cards { display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 1rem; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1.5rem; margin-bottom: 2rem; } .metric-card { - padding: 1.5rem; + padding: 1.75rem; + background: var(--sb-bg-secondary); + border: 1px solid rgba(99, 102, 241, 0.15); + border-radius: 12px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.metric-card:hover { + box-shadow: 0 8px 24px rgba(99, 102, 241, 0.15); + transform: translateY(-4px); + border-color: rgba(139, 92, 246, 0.3); } .metric-card h4 { margin: 0 0 1rem 0; font-size: 0.875rem; - color: #666; + color: var(--sb-text-secondary); font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; } .metric-value { display: flex; align-items: baseline; - gap: 0.25rem; + gap: 0.5rem; margin-bottom: 1rem; } .metric-value .value { - font-size: 2rem; - font-weight: bold; - color: #333; + font-size: 2.5rem; + font-weight: 700; + background: var(--sb-gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; } .metric-value .unit { font-size: 1rem; - color: #666; + color: var(--sb-text-secondary); + font-weight: 600; } .metric-card.success { - border-left: 4px solid #4CAF50; + border-left: 4px solid var(--sb-success); +} + +.metric-card.success:hover { + box-shadow: 0 8px 24px rgba(16, 185, 129, 0.15); } .metric-card.warning { - border-left: 4px solid #FF9800; + border-left: 4px solid var(--sb-warning); +} + +.metric-card.warning:hover { + box-shadow: 0 8px 24px rgba(245, 158, 11, 0.15); } .metric-card.danger { - border-left: 4px solid #f44336; + border-left: 4px solid var(--sb-danger); +} + +.metric-card.danger:hover { + box-shadow: 0 8px 24px rgba(239, 68, 68, 0.15); } .health-details { @@ -291,25 +452,40 @@ .health-details .table { width: 100%; border-collapse: collapse; + background: var(--sb-bg-secondary); + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); } .health-details .table th, .health-details .table td { - padding: 0.75rem; + padding: 1rem; text-align: left; - border-bottom: 1px solid #e0e0e0; + border-bottom: 1px solid var(--sb-border); } .health-details .table th { - background-color: #f5f5f5; + background: rgba(99, 102, 241, 0.08); font-weight: 600; - color: #333; + color: var(--sb-text); + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.5px; +} + +.health-details .table tbody tr:hover { + background: rgba(99, 102, 241, 0.04); } /* Logs */ .secubox-logs h2 { - margin-bottom: 1rem; - color: #333; + margin-bottom: 1.5rem; + background: var(--sb-gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-weight: 700; } .logs-controls { @@ -322,24 +498,35 @@ .service-selector { flex: 1; min-width: 200px; - padding: 0.5rem 1rem; - border: 1px solid #ccc; - border-radius: 4px; + padding: 0.75rem 1.25rem; + border: 1px solid rgba(99, 102, 241, 0.2); + border-radius: 10px; font-size: 0.875rem; + background: var(--sb-bg-secondary); + transition: all 0.3s ease; +} + +.service-selector:focus { + outline: none; + border-color: var(--sb-primary); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); } .logs-viewer { - background: #1e1e1e; - color: #d4d4d4; + background: #0f0f23; + color: #e5e7eb; padding: 0; overflow: hidden; + border-radius: 12px; + border: 1px solid rgba(99, 102, 241, 0.2); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); } .log-content { - font-family: 'Courier New', Courier, monospace; + font-family: 'JetBrains Mono', 'Consolas', 'Courier New', monospace; font-size: 0.875rem; - line-height: 1.5; - padding: 1rem; + line-height: 1.6; + padding: 1.5rem; margin: 0; overflow-x: auto; overflow-y: auto; @@ -348,6 +535,131 @@ word-wrap: break-word; } +/* Global Enhancements */ +.card { + background: var(--sb-bg-secondary); + border: 1px solid rgba(99, 102, 241, 0.15); + border-radius: 12px; + padding: 1.75rem; + transition: all 0.3s ease; +} + +.card:hover { + box-shadow: 0 8px 24px rgba(99, 102, 241, 0.1); + border-color: rgba(139, 92, 246, 0.3); +} + +.btn { + padding: 0.75rem 1.5rem; + border-radius: 8px; + font-weight: 600; + font-size: 0.875rem; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + border: none; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.btn-primary { + background: var(--sb-gradient-primary); + color: white; +} + +.btn-primary:hover { + box-shadow: 0 6px 20px rgba(99, 102, 241, 0.3); + transform: translateY(-2px); +} + +.btn-secondary { + background: rgba(99, 102, 241, 0.1); + color: var(--sb-primary); + border: 1px solid rgba(99, 102, 241, 0.3); +} + +.btn-secondary:hover { + background: rgba(99, 102, 241, 0.15); + border-color: rgba(139, 92, 246, 0.5); +} + +.btn-danger { + background: var(--sb-danger); + color: white; +} + +.btn-danger:hover { + box-shadow: 0 6px 20px rgba(239, 68, 68, 0.3); + transform: translateY(-2px); +} + +/* Progress bars */ +.progress { + height: 10px; + background: rgba(99, 102, 241, 0.1); + border-radius: 6px; + overflow: hidden; + border: 1px solid rgba(99, 102, 241, 0.2); +} + +.progress-bar { + height: 100%; + background: var(--sb-gradient-primary); + transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 0 12px rgba(99, 102, 241, 0.5); + border-radius: 6px; + position: relative; +} + +.progress-bar::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); + animation: shimmer 2s infinite; +} + +@keyframes shimmer { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(200%); } +} + +/* Badges */ +.badge { + display: inline-flex; + align-items: center; + padding: 0.35rem 0.85rem; + font-size: 0.75rem; + font-weight: 600; + border-radius: 8px; +} + +.badge-primary { + background: rgba(99, 102, 241, 0.1); + color: var(--sb-primary); + border: 1px solid rgba(99, 102, 241, 0.3); +} + +.badge-success { + background: rgba(16, 185, 129, 0.1); + color: var(--sb-success); + border: 1px solid rgba(16, 185, 129, 0.3); +} + +.badge-warning { + background: rgba(245, 158, 11, 0.1); + color: var(--sb-warning); + border: 1px solid rgba(245, 158, 11, 0.3); +} + +.badge-danger { + background: rgba(239, 68, 68, 0.1); + color: var(--sb-danger); + border: 1px solid rgba(239, 68, 68, 0.3); +} + /* Responsive */ @media (max-width: 768px) { .stats-grid, diff --git a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/common.css b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/common.css index dc94d599..485446c6 100644 --- a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/common.css +++ b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/common.css @@ -1,149 +1,241 @@ -/* SecuBox Admin - Common Styles */ +/* SecuBox Admin - Common Styles with SecuBox Theme */ /* Stat Cards */ .stat-card { - padding: 1.5rem; - border-radius: 8px; + padding: 1.75rem; + border-radius: 12px; background: #fff; - border: 1px solid #e0e0e0; + border: 1px solid rgba(99, 102, 241, 0.15); text-align: center; - transition: transform 0.2s; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +.stat-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(135deg, #6366f1, #8b5cf6); + opacity: 0; + transition: opacity 0.3s ease; } .stat-card:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + transform: translateY(-6px); + box-shadow: 0 12px 32px rgba(99, 102, 241, 0.15); + border-color: rgba(139, 92, 246, 0.3); } -.stat-card.blue { border-left: 4px solid #2196F3; } -.stat-card.green { border-left: 4px solid #4CAF50; } -.stat-card.success { border-left: 4px solid #8BC34A; } -.stat-card.warning { border-left: 4px solid #FF9800; } -.stat-card.muted { border-left: 4px solid #9E9E9E; } +.stat-card:hover::before { + opacity: 1; +} + +.stat-card.blue { border-left: 4px solid #3b82f6; } +.stat-card.green { border-left: 4px solid #10b981; } +.stat-card.success { border-left: 4px solid #10b981; } +.stat-card.warning { border-left: 4px solid #f59e0b; } +.stat-card.muted { border-left: 4px solid #9ca3af; } +.stat-card.primary { border-left: 4px solid #6366f1; } +.stat-card.purple { border-left: 4px solid #8b5cf6; } .stat-icon { - font-size: 2rem; - margin-bottom: 0.5rem; + font-size: 2.5rem; + margin-bottom: 0.75rem; + filter: drop-shadow(0 4px 8px rgba(99, 102, 241, 0.2)); } .stat-value { - font-size: 2rem; - font-weight: bold; - color: #333; + font-size: 2.5rem; + font-weight: 700; + background: linear-gradient(135deg, #6366f1, #8b5cf6); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; display: block; + line-height: 1.2; } .stat-label { font-size: 0.875rem; - color: #666; - margin-top: 0.5rem; + color: #6b7280; + margin-top: 0.75rem; display: block; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; } /* Badges */ .badge { - display: inline-block; - padding: 0.25rem 0.75rem; + display: inline-flex; + align-items: center; + padding: 0.35rem 0.85rem; font-size: 0.75rem; font-weight: 600; line-height: 1; - border-radius: 12px; + border-radius: 8px; text-align: center; } +.badge-primary { + background: rgba(99, 102, 241, 0.1); + color: #6366f1; + border: 1px solid rgba(99, 102, 241, 0.3); +} + .badge-success { - background-color: #4CAF50; - color: white; + background: rgba(16, 185, 129, 0.1); + color: #10b981; + border: 1px solid rgba(16, 185, 129, 0.3); } .badge-warning { - background-color: #FF9800; - color: white; + background: rgba(245, 158, 11, 0.1); + color: #f59e0b; + border: 1px solid rgba(245, 158, 11, 0.3); } .badge-danger { - background-color: #f44336; - color: white; + background: rgba(239, 68, 68, 0.1); + color: #ef4444; + border: 1px solid rgba(239, 68, 68, 0.3); } .badge-secondary { - background-color: #757575; - color: white; + background: rgba(107, 114, 128, 0.1); + color: #6b7280; + border: 1px solid rgba(107, 114, 128, 0.3); } /* Progress Bars */ .progress { - height: 8px; - background-color: #e0e0e0; - border-radius: 4px; + height: 10px; + background: rgba(99, 102, 241, 0.1); + border-radius: 6px; overflow: hidden; margin: 0.5rem 0; + border: 1px solid rgba(99, 102, 241, 0.2); } .progress-bar { height: 100%; - background-color: #2196F3; - transition: width 0.3s ease; + background: linear-gradient(90deg, #6366f1, #8b5cf6); + transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 0 12px rgba(99, 102, 241, 0.5); + border-radius: 6px; + position: relative; +} + +.progress-bar::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); + animation: shimmer 2s infinite; +} + +@keyframes shimmer { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(200%); } +} + +.progress-bar.success { + background: linear-gradient(90deg, #10b981, #059669); + box-shadow: 0 0 12px rgba(16, 185, 129, 0.5); +} + +.progress-bar.warning { + background: linear-gradient(90deg, #f59e0b, #d97706); + box-shadow: 0 0 12px rgba(245, 158, 11, 0.5); +} + +.progress-bar.danger { + background: linear-gradient(90deg, #ef4444, #dc2626); + box-shadow: 0 0 12px rgba(239, 68, 68, 0.5); } /* Cards */ .card { background: #fff; - border: 1px solid #e0e0e0; - border-radius: 8px; - padding: 1.5rem; - margin-bottom: 1rem; + border: 1px solid rgba(99, 102, 241, 0.15); + border-radius: 12px; + padding: 1.75rem; + margin-bottom: 1.25rem; + transition: all 0.3s ease; +} + +.card:hover { + box-shadow: 0 8px 24px rgba(99, 102, 241, 0.1); + border-color: rgba(139, 92, 246, 0.3); } .card h3 { margin-top: 0; margin-bottom: 1rem; font-size: 1.25rem; - color: #333; + color: #1f2937; + font-weight: 700; } /* Alerts */ .alert { - padding: 1rem; - border-radius: 4px; - margin-bottom: 1rem; + padding: 1.25rem; + border-radius: 10px; + margin-bottom: 1.25rem; position: relative; + backdrop-filter: blur(8px); } .alert-info { - background-color: #E3F2FD; - border-left: 4px solid #2196F3; - color: #0D47A1; + background: rgba(99, 102, 241, 0.08); + border-left: 4px solid #6366f1; + color: #4f46e5; + border: 1px solid rgba(99, 102, 241, 0.2); + border-left-width: 4px; } .alert-warning { - background-color: #FFF3E0; - border-left: 4px solid #FF9800; - color: #E65100; + background: rgba(245, 158, 11, 0.08); + border-left: 4px solid #f59e0b; + color: #d97706; + border: 1px solid rgba(245, 158, 11, 0.2); + border-left-width: 4px; } .alert-danger { - background-color: #FFEBEE; - border-left: 4px solid #f44336; - color: #B71C1C; + background: rgba(239, 68, 68, 0.08); + border-left: 4px solid #ef4444; + color: #dc2626; + border: 1px solid rgba(239, 68, 68, 0.2); + border-left-width: 4px; } .alert-success { - background-color: #E8F5E9; - border-left: 4px solid #4CAF50; - color: #1B5E20; + background: rgba(16, 185, 129, 0.08); + border-left: 4px solid #10b981; + color: #059669; + border: 1px solid rgba(16, 185, 129, 0.2); + border-left-width: 4px; } .alert-close { position: absolute; - top: 0.5rem; - right: 0.5rem; + top: 1rem; + right: 1rem; background: none; border: none; font-size: 1.5rem; cursor: pointer; color: inherit; opacity: 0.5; + transition: opacity 0.3s ease; } .alert-close:hover { @@ -153,17 +245,17 @@ /* Loader */ .loader-container { text-align: center; - padding: 2rem; + padding: 3rem; } .loader { - border: 4px solid #f3f3f3; - border-top: 4px solid #2196F3; + border: 4px solid rgba(99, 102, 241, 0.1); + border-top: 4px solid #6366f1; border-radius: 50%; - width: 40px; - height: 40px; - animation: spin 1s linear infinite; - margin: 0 auto 1rem; + width: 48px; + height: 48px; + animation: spin 0.8s linear infinite; + margin: 0 auto 1.5rem; } @keyframes spin { @@ -173,95 +265,204 @@ /* Buttons */ .btn { - display: inline-block; - padding: 0.5rem 1rem; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.75rem 1.5rem; font-size: 0.875rem; - border: 1px solid #ccc; - border-radius: 4px; + font-weight: 600; + border: 1px solid transparent; + border-radius: 8px; background: #fff; cursor: pointer; - transition: all 0.2s; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + text-transform: uppercase; + letter-spacing: 0.5px; } .btn:hover { - background: #f5f5f5; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .btn-primary { - background-color: #2196F3; - border-color: #2196F3; + background: linear-gradient(135deg, #6366f1, #8b5cf6); + border-color: transparent; color: white; } .btn-primary:hover { - background-color: #1976D2; + box-shadow: 0 6px 20px rgba(99, 102, 241, 0.3); +} + +.btn-secondary { + background: rgba(99, 102, 241, 0.1); + border-color: rgba(99, 102, 241, 0.3); + color: #6366f1; +} + +.btn-secondary:hover { + background: rgba(99, 102, 241, 0.15); + border-color: rgba(139, 92, 246, 0.5); } .btn-success { - background-color: #4CAF50; - border-color: #4CAF50; + background: #10b981; + border-color: #10b981; color: white; } .btn-success:hover { - background-color: #388E3C; + background: #059669; + box-shadow: 0 6px 20px rgba(16, 185, 129, 0.3); } .btn-danger { - background-color: #f44336; - border-color: #f44336; + background: #ef4444; + border-color: #ef4444; color: white; } .btn-danger:hover { - background-color: #D32F2F; + background: #dc2626; + box-shadow: 0 6px 20px rgba(239, 68, 68, 0.3); } .btn-warning { - background-color: #FF9800; - border-color: #FF9800; + background: #f59e0b; + border-color: #f59e0b; color: white; } .btn-warning:hover { - background-color: #F57C00; + background: #d97706; + box-shadow: 0 6px 20px rgba(245, 158, 11, 0.3); } .btn-sm { - padding: 0.25rem 0.5rem; + padding: 0.5rem 1rem; font-size: 0.75rem; } +.btn-lg { + padding: 1rem 2rem; + font-size: 1rem; +} + /* Utility Classes */ .text-muted { - color: #757575; + color: #9ca3af; +} + +.text-primary { + color: #6366f1; +} + +.text-success { + color: #10b981; +} + +.text-warning { + color: #f59e0b; +} + +.text-danger { + color: #ef4444; } .right { text-align: right; } +.center { + text-align: center; +} + .view-all-link { - color: #2196F3; + color: #6366f1; text-decoration: none; font-size: 0.875rem; + font-weight: 600; + transition: color 0.3s ease; } .view-all-link:hover { + color: #8b5cf6; text-decoration: underline; } +/* Empty states */ +.empty-state { + text-align: center; + padding: 3rem; + color: #9ca3af; +} + +.empty-state-icon { + font-size: 4rem; + margin-bottom: 1rem; + opacity: 0.5; +} + +.empty-state-title { + font-size: 1.25rem; + font-weight: 600; + color: #6b7280; + margin-bottom: 0.5rem; +} + +.empty-state-text { + font-size: 0.875rem; + color: #9ca3af; +} + +/* Tables */ +.table { + width: 100%; + border-collapse: collapse; + background: #fff; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.table th, +.table td { + padding: 1rem; + text-align: left; + border-bottom: 1px solid #e5e7eb; +} + +.table th { + background: rgba(99, 102, 241, 0.08); + font-weight: 600; + color: #1f2937; + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.5px; +} + +.table tbody tr:hover { + background: rgba(99, 102, 241, 0.04); +} + /* Responsive */ @media (max-width: 768px) { .stat-card { - padding: 1rem; + padding: 1.25rem; } .stat-value { - font-size: 1.5rem; + font-size: 2rem; } .card { - padding: 1rem; + padding: 1.25rem; + } + + .btn { + padding: 0.625rem 1.25rem; + font-size: 0.8125rem; } } diff --git a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/cyberpunk.css b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/cyberpunk.css new file mode 100644 index 00000000..faa74f6b --- /dev/null +++ b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/cyberpunk.css @@ -0,0 +1,857 @@ +/* SecuBox Admin - Enhanced Cyber Console Theme */ + +:root { + /* SecuBox Brand Colors - Indigo/Purple Gradient System */ + --cyber-primary: #6366f1; + --cyber-primary-light: #818cf8; + --cyber-primary-dark: #4f46e5; + --cyber-secondary: #8b5cf6; + --cyber-secondary-light: #a78bfa; + --cyber-accent: #3b82f6; + --cyber-accent-cyan: #06b6d4; + --cyber-warning: #f59e0b; + --cyber-danger: #ef4444; + --cyber-success: #10b981; + + /* Backgrounds - Deep dark with subtle purple tint */ + --cyber-bg: #0f0f23; + --cyber-bg-secondary: #1a1a2e; + --cyber-bg-tertiary: #16213e; + + /* Borders & Effects */ + --cyber-border: #6366f1; + --cyber-border-dim: #4f46e5; + + /* Text */ + --cyber-text: #e5e7eb; + --cyber-text-bright: #f9fafb; + --cyber-text-dim: #9ca3af; + + /* Gradients */ + --cyber-gradient-primary: linear-gradient(135deg, #6366f1, #8b5cf6); + --cyber-gradient-primary-reverse: linear-gradient(135deg, #8b5cf6, #6366f1); + --cyber-gradient-accent: linear-gradient(135deg, #3b82f6, #06b6d4); +} + +/* Cyberpunk base styles */ +.cyberpunk-mode { + background: var(--cyber-bg); + color: var(--cyber-text); + font-family: 'Inter', 'Segoe UI', 'Roboto', sans-serif; + min-height: 100vh; +} + +.cyberpunk-mode * { + font-family: inherit; +} + +/* Dual console layout */ +.cyber-dual-console { + display: grid; + grid-template-columns: 420px 1fr; + gap: 24px; + padding: 24px; + min-height: calc(100vh - 48px); +} + +@media (max-width: 1200px) { + .cyber-dual-console { + grid-template-columns: 1fr; + } +} + +/* Left console - Stats & Quick Actions */ +.cyber-console-left { + display: flex; + flex-direction: column; + gap: 20px; +} + +/* Right console - Main Content */ +.cyber-console-right { + display: flex; + flex-direction: column; + gap: 20px; + overflow-x: auto; +} + +/* Terminal panel - Enhanced with glass morphism */ +.cyber-panel { + background: rgba(26, 26, 46, 0.8); + backdrop-filter: blur(12px); + border: 1px solid rgba(99, 102, 241, 0.3); + border-radius: 12px; + box-shadow: + 0 8px 32px rgba(99, 102, 241, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + position: relative; + overflow: hidden; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.cyber-panel:hover { + border-color: rgba(99, 102, 241, 0.5); + box-shadow: + 0 12px 48px rgba(99, 102, 241, 0.25), + inset 0 1px 0 rgba(255, 255, 255, 0.15); + transform: translateY(-2px); +} + +/* Animated gradient border effect */ +.cyber-panel::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: var(--cyber-gradient-primary); + opacity: 0.8; + pointer-events: none; +} + +.cyber-panel-header { + background: rgba(99, 102, 241, 0.08); + border-bottom: 1px solid rgba(99, 102, 241, 0.2); + padding: 16px 20px; + display: flex; + align-items: center; + justify-content: space-between; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1.5px; + font-size: 11px; +} + +.cyber-panel-title { + color: var(--cyber-text-bright); + display: flex; + align-items: center; + gap: 10px; + font-size: 12px; +} + +.cyber-panel-title::before { + content: '▸'; + color: var(--cyber-secondary); + font-size: 14px; + animation: pulse 2s ease-in-out infinite; +} + +.cyber-panel-badge { + background: var(--cyber-gradient-primary); + color: white; + padding: 4px 12px; + border-radius: 6px; + font-size: 10px; + font-weight: 700; + box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3); +} + +.cyber-panel-body { + padding: 20px; +} + +/* Stats grid - Enhanced cards */ +.cyber-stats-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; +} + +.cyber-stat-card { + background: rgba(99, 102, 241, 0.05); + border: 1px solid rgba(99, 102, 241, 0.2); + border-left: 4px solid var(--cyber-primary); + border-radius: 8px; + padding: 16px; + position: relative; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + overflow: hidden; +} + +/* Animated background glow */ +.cyber-stat-card::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 100px; + height: 100px; + background: radial-gradient(circle, rgba(99, 102, 241, 0.15) 0%, transparent 70%); + transform: translate(-50%, -50%); + opacity: 0; + transition: opacity 0.3s ease; + pointer-events: none; +} + +.cyber-stat-card:hover { + border-color: rgba(139, 92, 246, 0.5); + box-shadow: + 0 8px 24px rgba(99, 102, 241, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + transform: translateY(-4px); + background: rgba(99, 102, 241, 0.08); +} + +.cyber-stat-card:hover::after { + opacity: 1; +} + +.cyber-stat-card.warning { + border-left-color: var(--cyber-warning); +} + +.cyber-stat-card.warning:hover { + border-color: rgba(245, 158, 11, 0.5); + box-shadow: 0 8px 24px rgba(245, 158, 11, 0.2); +} + +.cyber-stat-card.danger { + border-left-color: var(--cyber-danger); +} + +.cyber-stat-card.danger:hover { + border-color: rgba(239, 68, 68, 0.5); + box-shadow: 0 8px 24px rgba(239, 68, 68, 0.2); +} + +.cyber-stat-card.accent { + border-left-color: var(--cyber-accent-cyan); +} + +.cyber-stat-card.accent:hover { + border-color: rgba(6, 182, 212, 0.5); + box-shadow: 0 8px 24px rgba(6, 182, 212, 0.2); +} + +.cyber-stat-icon { + font-size: 28px; + margin-bottom: 8px; + filter: drop-shadow(0 2px 8px currentColor); + display: block; +} + +.cyber-stat-value { + font-size: 32px; + font-weight: 700; + background: var(--cyber-gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + line-height: 1; + display: block; +} + +.cyber-stat-label { + font-size: 10px; + color: var(--cyber-text-dim); + text-transform: uppercase; + letter-spacing: 1.2px; + margin-top: 8px; + font-weight: 600; + display: block; +} + +/* Quick actions - Modern button style */ +.cyber-quick-actions { + display: grid; + grid-template-columns: 1fr; + gap: 10px; +} + +.cyber-action-btn { + background: rgba(99, 102, 241, 0.08); + border: 1px solid rgba(99, 102, 241, 0.2); + color: var(--cyber-text); + padding: 16px 18px; + text-align: left; + cursor: pointer; + font-family: inherit; + font-size: 13px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; + display: flex; + align-items: center; + gap: 12px; + border-radius: 8px; +} + +/* Gradient slide effect on hover */ +.cyber-action-btn::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 4px; + background: var(--cyber-gradient-primary); + transform: scaleY(0); + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + border-radius: 0 2px 2px 0; +} + +.cyber-action-btn:hover::before { + transform: scaleY(1); +} + +.cyber-action-btn::after { + content: ''; + position: absolute; + inset: 0; + background: var(--cyber-gradient-primary); + opacity: 0; + transition: opacity 0.3s ease; +} + +.cyber-action-btn:hover { + background: rgba(99, 102, 241, 0.12); + border-color: rgba(139, 92, 246, 0.4); + box-shadow: 0 4px 16px rgba(99, 102, 241, 0.2); + transform: translateX(4px); + color: var(--cyber-text-bright); +} + +.cyber-action-btn:hover::after { + opacity: 0.05; +} + +.cyber-action-btn:active { + transform: translateX(4px) scale(0.98); +} + +.cyber-action-icon { + font-size: 20px; + filter: drop-shadow(0 2px 4px rgba(99, 102, 241, 0.3)); +} + +.cyber-action-label { + flex: 1; +} + +.cyber-action-arrow { + color: var(--cyber-secondary); + font-size: 14px; + transition: transform 0.3s ease; +} + +.cyber-action-btn:hover .cyber-action-arrow { + transform: translateX(4px); +} + +/* System status - Enhanced metrics */ +.cyber-system-status { + display: flex; + flex-direction: column; + gap: 16px; +} + +.cyber-metric { + display: flex; + flex-direction: column; + gap: 8px; +} + +.cyber-metric-header { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 1px; + font-weight: 600; +} + +.cyber-metric-label { + color: var(--cyber-text-dim); + display: flex; + align-items: center; + gap: 8px; +} + +.cyber-metric-value { + background: var(--cyber-gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-weight: 700; + font-size: 13px; +} + +.cyber-progress-bar { + height: 10px; + background: rgba(99, 102, 241, 0.1); + border: 1px solid rgba(99, 102, 241, 0.2); + border-radius: 6px; + overflow: hidden; + position: relative; +} + +.cyber-progress-fill { + height: 100%; + background: var(--cyber-gradient-primary); + box-shadow: 0 0 12px rgba(99, 102, 241, 0.5); + transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + border-radius: 6px; +} + +/* Animated shimmer effect */ +.cyber-progress-fill::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, + transparent 0%, + rgba(255, 255, 255, 0.4) 50%, + transparent 100%); + animation: shimmer 2.5s infinite; +} + +.cyber-progress-fill.warning { + background: linear-gradient(90deg, var(--cyber-warning), var(--cyber-danger)); + box-shadow: 0 0 12px rgba(245, 158, 11, 0.5); +} + +.cyber-progress-fill.danger { + background: linear-gradient(90deg, var(--cyber-danger), #dc2626); + box-shadow: 0 0 12px rgba(239, 68, 68, 0.5); +} + +/* System meta info */ +.cyber-system-meta { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid rgba(99, 102, 241, 0.2); +} + +.cyber-system-meta-item { + display: flex; + flex-direction: column; + gap: 4px; +} + +.cyber-system-meta-item .label { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 1px; + color: var(--cyber-text-dim); + font-weight: 600; +} + +.cyber-system-meta-item .value { + font-size: 12px; + color: var(--cyber-text-bright); + font-weight: 600; + font-family: 'JetBrains Mono', 'Consolas', monospace; +} + +/* List view - Enhanced cards */ +.cyber-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.cyber-list-item { + background: rgba(99, 102, 241, 0.05); + border: 1px solid rgba(99, 102, 241, 0.2); + border-left: 4px solid var(--cyber-primary); + border-radius: 10px; + padding: 18px; + display: grid; + grid-template-columns: auto 1fr auto; + gap: 16px; + align-items: center; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +.cyber-list-item::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--cyber-gradient-primary); + opacity: 0; + transition: opacity 0.3s ease; + pointer-events: none; +} + +.cyber-list-item:hover { + background: rgba(99, 102, 241, 0.08); + border-color: rgba(139, 92, 246, 0.4); + box-shadow: 0 8px 24px rgba(99, 102, 241, 0.15); + transform: translateX(4px); +} + +.cyber-list-item:hover::after { + opacity: 0.03; +} + +.cyber-list-item.active { + border-left-color: var(--cyber-accent-cyan); + background: rgba(6, 182, 212, 0.08); +} + +.cyber-list-item.offline { + border-left-color: var(--cyber-text-dim); + opacity: 0.5; +} + +.cyber-list-icon { + font-size: 36px; + width: 56px; + height: 56px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(99, 102, 241, 0.1); + border: 1px solid rgba(99, 102, 241, 0.3); + border-radius: 10px; + filter: drop-shadow(0 4px 12px rgba(99, 102, 241, 0.2)); + transition: all 0.3s ease; +} + +.cyber-list-item:hover .cyber-list-icon { + background: rgba(99, 102, 241, 0.15); + border-color: rgba(139, 92, 246, 0.5); + transform: scale(1.05); +} + +.cyber-list-content { + display: flex; + flex-direction: column; + gap: 8px; +} + +.cyber-list-title { + font-size: 16px; + font-weight: 700; + color: var(--cyber-text-bright); + text-transform: uppercase; + letter-spacing: 0.5px; + display: flex; + align-items: center; + gap: 12px; +} + +.cyber-list-meta { + display: flex; + gap: 16px; + font-size: 11px; + color: var(--cyber-text-dim); + font-weight: 500; +} + +.cyber-list-meta-item { + display: flex; + align-items: center; + gap: 6px; +} + +.cyber-list-actions { + display: flex; + gap: 10px; +} + +/* Buttons - Modern style */ +.cyber-btn { + background: rgba(99, 102, 241, 0.1); + border: 1px solid rgba(99, 102, 241, 0.3); + color: var(--cyber-text); + padding: 10px 16px; + font-family: inherit; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + white-space: nowrap; + border-radius: 6px; +} + +.cyber-btn:hover { + background: rgba(99, 102, 241, 0.15); + border-color: rgba(139, 92, 246, 0.5); + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); + color: var(--cyber-text-bright); + transform: translateY(-2px); +} + +.cyber-btn:active { + transform: translateY(-2px) scale(0.95); +} + +.cyber-btn.primary { + border-color: var(--cyber-primary); + color: var(--cyber-primary); + background: rgba(99, 102, 241, 0.12); +} + +.cyber-btn.primary:hover { + background: var(--cyber-gradient-primary); + color: white; + border-color: transparent; +} + +.cyber-btn.danger { + border-color: var(--cyber-danger); + color: var(--cyber-danger); + background: rgba(239, 68, 68, 0.08); +} + +.cyber-btn.danger:hover { + background: var(--cyber-danger); + color: white; + border-color: transparent; + box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3); +} + +/* Badges - Enhanced with gradients */ +.cyber-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.8px; + border-radius: 6px; +} + +.cyber-badge.success { + background: rgba(16, 185, 129, 0.15); + border: 1px solid rgba(16, 185, 129, 0.4); + color: var(--cyber-success); +} + +.cyber-badge.warning { + background: rgba(245, 158, 11, 0.15); + border: 1px solid rgba(245, 158, 11, 0.4); + color: var(--cyber-warning); +} + +.cyber-badge.danger { + background: rgba(239, 68, 68, 0.15); + border: 1px solid rgba(239, 68, 68, 0.4); + color: var(--cyber-danger); +} + +.cyber-badge.info { + background: rgba(99, 102, 241, 0.15); + border: 1px solid rgba(99, 102, 241, 0.4); + color: var(--cyber-primary); +} + +/* Animations */ +@keyframes pulse { + 0%, 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.5; + transform: scale(0.95); + } +} + +@keyframes shimmer { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(200%); + } +} + +@keyframes float { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10px); + } +} + +/* Scan lines effect - Subtle */ +.cyber-scanlines { + position: relative; +} + +.cyber-scanlines::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: repeating-linear-gradient( + 0deg, + rgba(99, 102, 241, 0.03) 0px, + transparent 1px, + transparent 2px, + rgba(99, 102, 241, 0.03) 3px + ); + pointer-events: none; + opacity: 0.3; +} + +/* Text glow effect */ +.cyber-text-glow { + text-shadow: + 0 0 10px rgba(99, 102, 241, 0.5), + 0 0 20px rgba(99, 102, 241, 0.3), + 0 0 30px rgba(139, 92, 246, 0.2); +} + +/* Status indicators */ +.cyber-status-dot { + width: 10px; + height: 10px; + border-radius: 50%; + display: inline-block; + animation: pulse 2.5s ease-in-out infinite; +} + +.cyber-status-dot.online { + background: var(--cyber-success); + box-shadow: 0 0 12px var(--cyber-success); +} + +.cyber-status-dot.offline { + background: var(--cyber-text-dim); + animation: none; + box-shadow: none; +} + +.cyber-status-dot.warning { + background: var(--cyber-warning); + box-shadow: 0 0 12px var(--cyber-warning); +} + +/* Scrollbar - SecuBox styled */ +.cyberpunk-mode ::-webkit-scrollbar { + width: 12px; + height: 12px; +} + +.cyberpunk-mode ::-webkit-scrollbar-track { + background: rgba(99, 102, 241, 0.05); + border: 1px solid rgba(99, 102, 241, 0.15); + border-radius: 6px; +} + +.cyberpunk-mode ::-webkit-scrollbar-thumb { + background: var(--cyber-gradient-primary); + box-shadow: 0 0 12px rgba(99, 102, 241, 0.4); + border-radius: 6px; +} + +.cyberpunk-mode ::-webkit-scrollbar-thumb:hover { + background: var(--cyber-gradient-primary-reverse); + box-shadow: 0 0 16px rgba(139, 92, 246, 0.6); +} + +/* Header with ASCII art - Enhanced */ +.cyber-header { + background: rgba(26, 26, 46, 0.9); + backdrop-filter: blur(12px); + border: 1px solid rgba(99, 102, 241, 0.3); + border-radius: 12px; + padding: 32px; + margin: 24px; + box-shadow: + 0 8px 32px rgba(99, 102, 241, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + position: relative; + overflow: hidden; +} + +.cyber-header::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--cyber-gradient-primary); +} + +.cyber-ascii-art { + font-family: 'JetBrains Mono', 'Consolas', monospace; + font-size: 11px; + line-height: 1.3; + background: var(--cyber-gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + white-space: pre; + overflow-x: auto; + font-weight: 600; +} + +.cyber-header-title { + font-size: 28px; + font-weight: 800; + background: var(--cyber-gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-transform: uppercase; + letter-spacing: 4px; + margin-top: 16px; +} + +.cyber-header-subtitle { + font-size: 13px; + color: var(--cyber-text-dim); + text-transform: uppercase; + letter-spacing: 2px; + margin-top: 8px; + font-weight: 500; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .cyber-dual-console { + padding: 16px; + gap: 16px; + } + + .cyber-header { + margin: 16px; + padding: 24px 20px; + } + + .cyber-ascii-art { + font-size: 9px; + } + + .cyber-header-title { + font-size: 20px; + } + + .cyber-stats-grid { + grid-template-columns: 1fr; + } + + .cyber-system-meta { + grid-template-columns: 1fr; + } +} diff --git a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/widget-renderer.js b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/widget-renderer.js index d3da8968..2d843dd4 100644 --- a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/widget-renderer.js +++ b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/widget-renderer.js @@ -36,9 +36,27 @@ WidgetRendererInstance.prototype = { this.registerTemplate('default', { render: function(container, app, data) { container.innerHTML = ''; + + var status = data && data.status ? data.status : 'unknown'; + var statusClass = status === 'running' ? 'status-success' : + status === 'stopped' ? 'status-warning' : + status === 'not_installed' ? 'status-error' : 'status-unknown'; + + var version = data && (data.installed_version || data.pkg_version || data.catalog_version) ? + 'v' + (data.installed_version || data.pkg_version || data.catalog_version) : ''; + container.appendChild(E('div', { 'class': 'widget-default' }, [ - E('div', { 'class': 'widget-icon' }, app.icon || '📊'), - E('div', { 'class': 'widget-title' }, app.name || 'Application'), + E('div', { 'class': 'widget-header' }, [ + E('div', { 'class': 'widget-icon' }, app.icon || '📊'), + E('div', { 'class': 'widget-title-wrapper' }, [ + E('div', { 'class': 'widget-title' }, app.name || 'Application'), + version ? E('div', { 'class': 'widget-version' }, version) : null + ]), + E('div', { 'class': 'widget-status-indicator ' + statusClass }) + ]), + E('div', { 'class': 'widget-status-text' }, [ + E('span', { 'class': 'status-badge ' + statusClass }, status.replace('_', ' ').toUpperCase()) + ]), E('div', { 'class': 'widget-status' }, data && data.widget_enabled ? 'Widget Enabled' : 'No widget data' ) @@ -52,16 +70,25 @@ WidgetRendererInstance.prototype = { var metrics = data && data.metrics ? data.metrics : []; var status = data && data.status ? data.status : 'unknown'; - var statusClass = status === 'ok' ? 'status-success' : - status === 'warning' ? 'status-warning' : - status === 'error' ? 'status-error' : 'status-unknown'; + var statusClass = status === 'running' ? 'status-success' : + status === 'stopped' ? 'status-warning' : + status === 'not_installed' ? 'status-error' : 'status-unknown'; + + var version = data && (data.installed_version || data.pkg_version || data.catalog_version) ? + 'v' + (data.installed_version || data.pkg_version || data.catalog_version) : ''; container.appendChild(E('div', { 'class': 'widget-security' }, [ E('div', { 'class': 'widget-header' }, [ E('div', { 'class': 'widget-icon' }, app.icon || '🔒'), - E('div', { 'class': 'widget-title' }, app.name || 'Security'), + E('div', { 'class': 'widget-title-wrapper' }, [ + E('div', { 'class': 'widget-title' }, app.name || 'Security'), + version ? E('div', { 'class': 'widget-version' }, version) : null + ]), E('div', { 'class': 'widget-status-indicator ' + statusClass }) ]), + E('div', { 'class': 'widget-status-text' }, [ + E('span', { 'class': 'status-badge ' + statusClass }, status.replace('_', ' ').toUpperCase()) + ]), E('div', { 'class': 'widget-metrics' }, metrics.map(function(metric) { return self.renderMetric(metric || {}); @@ -83,10 +110,25 @@ WidgetRendererInstance.prototype = { var connections = data && data.active_connections ? data.active_connections : 0; var bandwidth = data && data.bandwidth ? data.bandwidth : { up: 0, down: 0 }; + var status = data && data.status ? data.status : 'unknown'; + var statusClass = status === 'running' ? 'status-success' : + status === 'stopped' ? 'status-warning' : + status === 'not_installed' ? 'status-error' : 'status-unknown'; + + var version = data && (data.installed_version || data.pkg_version || data.catalog_version) ? + 'v' + (data.installed_version || data.pkg_version || data.catalog_version) : ''; + container.appendChild(E('div', { 'class': 'widget-network' }, [ E('div', { 'class': 'widget-header' }, [ E('div', { 'class': 'widget-icon' }, app.icon || '🌐'), - E('div', { 'class': 'widget-title' }, app.name || 'Network') + E('div', { 'class': 'widget-title-wrapper' }, [ + E('div', { 'class': 'widget-title' }, app.name || 'Network'), + version ? E('div', { 'class': 'widget-version' }, version) : null + ]), + E('div', { 'class': 'widget-status-indicator ' + statusClass }) + ]), + E('div', { 'class': 'widget-status-text' }, [ + E('span', { 'class': 'status-badge ' + statusClass }, status.replace('_', ' ').toUpperCase()) ]), E('div', { 'class': 'widget-metrics' }, [ E('div', { 'class': 'metric-row' }, [ @@ -112,15 +154,24 @@ WidgetRendererInstance.prototype = { var metrics = data && data.metrics ? data.metrics : []; var status = data && data.status ? data.status : 'unknown'; - var statusClass = status === 'healthy' ? 'status-success' : - status === 'degraded' ? 'status-warning' : - status === 'down' ? 'status-error' : 'status-unknown'; + var statusClass = status === 'running' ? 'status-success' : + status === 'stopped' ? 'status-warning' : + status === 'not_installed' ? 'status-error' : 'status-unknown'; + + var version = data && (data.installed_version || data.pkg_version || data.catalog_version) ? + 'v' + (data.installed_version || data.pkg_version || data.catalog_version) : ''; container.appendChild(E('div', { 'class': 'widget-monitoring' }, [ E('div', { 'class': 'widget-header' }, [ E('div', { 'class': 'widget-icon' }, app.icon || '📈'), - E('div', { 'class': 'widget-title' }, app.name || 'Monitoring'), - E('div', { 'class': 'widget-status-badge ' + statusClass }, status) + E('div', { 'class': 'widget-title-wrapper' }, [ + E('div', { 'class': 'widget-title' }, app.name || 'Monitoring'), + version ? E('div', { 'class': 'widget-version' }, version) : null + ]), + E('div', { 'class': 'widget-status-indicator ' + statusClass }) + ]), + E('div', { 'class': 'widget-status-text' }, [ + E('span', { 'class': 'status-badge ' + statusClass }, status.replace('_', ' ').toUpperCase()) ]), E('div', { 'class': 'widget-metrics-grid' }, metrics.map(function(metric) { @@ -142,10 +193,25 @@ WidgetRendererInstance.prototype = { var metrics = data && data.metrics ? data.metrics : []; var services = data && data.services ? data.services : []; + var status = data && data.status ? data.status : 'unknown'; + var statusClass = status === 'running' ? 'status-success' : + status === 'stopped' ? 'status-warning' : + status === 'not_installed' ? 'status-error' : 'status-unknown'; + + var version = data && (data.installed_version || data.pkg_version || data.catalog_version) ? + 'v' + (data.installed_version || data.pkg_version || data.catalog_version) : ''; + container.appendChild(E('div', { 'class': 'widget-hosting' }, [ E('div', { 'class': 'widget-header' }, [ E('div', { 'class': 'widget-icon' }, app.icon || '🖥️'), - E('div', { 'class': 'widget-title' }, app.name || 'Hosting') + E('div', { 'class': 'widget-title-wrapper' }, [ + E('div', { 'class': 'widget-title' }, app.name || 'Hosting'), + version ? E('div', { 'class': 'widget-version' }, version) : null + ]), + E('div', { 'class': 'widget-status-indicator ' + statusClass }) + ]), + E('div', { 'class': 'widget-status-text' }, [ + E('span', { 'class': 'status-badge ' + statusClass }, status.replace('_', ' ').toUpperCase()) ]), E('div', { 'class': 'widget-services' }, services.map(function(service) { diff --git a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/widgets.css b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/widgets.css index ba06cb4a..28c1bfe3 100644 --- a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/widgets.css +++ b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/widgets.css @@ -507,3 +507,121 @@ border: 1px solid #ccc; } } + +/* Widget Header with Version and Status */ +.widget-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 1rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid rgba(99, 102, 241, 0.1); +} + +.widget-title-wrapper { + flex: 1; + min-width: 0; +} + +.widget-title { + font-size: 0.95rem; + font-weight: 600; + color: var(--cyber-text, #1e293b); + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.widget-version { + font-size: 0.75rem; + color: var(--cyber-text-dim, #64748b); + font-family: 'JetBrains Mono', monospace; + margin-top: 0.2rem; +} + +/* Widget Status Indicator (Dot) */ +.widget-status-indicator { + width: 12px; + height: 12px; + border-radius: 50%; + flex-shrink: 0; + box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1); + animation: pulse 2s ease-in-out infinite; +} + +.widget-status-indicator.status-success { + background: #10b981; + box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2), 0 0 10px rgba(16, 185, 129, 0.3); +} + +.widget-status-indicator.status-warning { + background: #f59e0b; + box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.2), 0 0 10px rgba(245, 158, 11, 0.3); +} + +.widget-status-indicator.status-error { + background: #ef4444; + box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2), 0 0 10px rgba(239, 68, 68, 0.3); +} + +.widget-status-indicator.status-unknown { + background: #6b7280; + box-shadow: 0 0 0 2px rgba(107, 116, 128, 0.2); + animation: none; +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.6; + } +} + +/* Widget Status Text Badge */ +.widget-status-text { + margin-bottom: 0.75rem; +} + +.status-badge { + display: inline-block; + padding: 0.25rem 0.75rem; + font-size: 0.7rem; + font-weight: 600; + border-radius: 12px; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.status-badge.status-success { + background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(5, 150, 105, 0.15)); + color: #059669; + border: 1px solid rgba(16, 185, 129, 0.3); +} + +.status-badge.status-warning { + background: linear-gradient(135deg, rgba(245, 158, 11, 0.15), rgba(217, 119, 6, 0.15)); + color: #d97706; + border: 1px solid rgba(245, 158, 11, 0.3); +} + +.status-badge.status-error { + background: linear-gradient(135deg, rgba(239, 68, 68, 0.15), rgba(220, 38, 38, 0.15)); + color: #dc2626; + border: 1px solid rgba(239, 68, 68, 0.3); +} + +.status-badge.status-unknown { + background: rgba(107, 116, 128, 0.1); + color: #6b7280; + border: 1px solid rgba(107, 116, 128, 0.2); +} + +/* Enhanced widget icon */ +.widget-icon { + font-size: 2rem; + line-height: 1; + flex-shrink: 0; +} diff --git a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/advanced-settings.js b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/advanced-settings.js index e50faf46..cda840b1 100644 --- a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/advanced-settings.js +++ b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/advanced-settings.js @@ -42,6 +42,9 @@ return view.extend({ var self = this; var container = E('div', { 'class': 'cyberpunk-mode' }, [ + E('link', { 'rel': 'stylesheet', 'type': 'text/css', + 'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }), + // Header E('div', { 'class': 'cyber-header cyber-scanlines' }, [ E('div', { 'class': 'cyber-header-title cyber-text-glow' }, '⚙️ ADVANCED SETTINGS'), diff --git a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/apps.js b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/apps.js index 6ff119a8..89178e7d 100644 --- a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/apps.js +++ b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/apps.js @@ -105,6 +105,8 @@ return view.extend({ }); var container = E('div', { 'class': 'cyberpunk-mode secubox-apps-manager' }, [ + E('link', { 'rel': 'stylesheet', 'type': 'text/css', + 'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }), E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-admin/common.css') }), E('link', { 'rel': 'stylesheet', @@ -371,9 +373,9 @@ return view.extend({ E('div', { 'class': 'cyber-featured-app-tags' }, (app.tags || []).slice(0, 2).map(function(tag) { return E('span', { 'class': 'cyber-featured-app-tag' }, tag); - }) + }).filter(Boolean) ), - E('div', { 'class': 'cyber-featured-app-action' }, [ + E('div', { 'class': 'cyber-featured-app-action' }, isInstalled ? [ E('span', { 'style': 'color: var(--cyber-success);' }, '✓ Installed'), ' → ', @@ -388,7 +390,7 @@ return view.extend({ E('span', {}, 'Install now'), ' →' ] - ]) + ) ]) ]); }, diff --git a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/catalog-sources.js b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/catalog-sources.js index 69b7b101..02dcaaa9 100644 --- a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/catalog-sources.js +++ b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/catalog-sources.js @@ -71,6 +71,8 @@ return view.extend({ var enabledCount = sources.filter(function(s) { return s.enabled; }).length; var container = E('div', { 'class': 'cyberpunk-mode secubox-catalog-sources' }, [ + E('link', { 'rel': 'stylesheet', 'type': 'text/css', + 'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }), E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-admin/common.css') }), E('link', { 'rel': 'stylesheet', diff --git a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/control-center.js b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/control-center.js index 1edeaf84..ad51127c 100644 --- a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/control-center.js +++ b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/control-center.js @@ -31,16 +31,19 @@ return view.extend({ render: function(data) { var self = this; - var container = E('div', { 'class': 'control-center' }); + var container = E('div', { 'class': 'cyberpunk-mode control-center' }, [ + E('link', { 'rel': 'stylesheet', 'type': 'text/css', + 'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }), + E('link', { 'rel': 'stylesheet', + 'href': L.resource('secubox-admin/common.css') }), + E('link', { 'rel': 'stylesheet', + 'href': L.resource('secubox-admin/admin.css') }), - // Page header - var header = E('div', { 'class': 'page-header', 'style': 'margin-bottom: 2rem;' }); - var title = E('h2', {}, 'SecuBox Admin Control Center'); - var subtitle = E('p', { 'style': 'color: #6b7280; margin-top: 0.5rem;' }, - 'Centralized management dashboard for components and system state'); - header.appendChild(title); - header.appendChild(subtitle); - container.appendChild(header); + E('div', { 'class': 'cyber-header' }, [ + E('div', { 'class': 'cyber-header-title' }, '🎛️ CONTROL CENTER'), + E('div', { 'class': 'cyber-header-subtitle' }, 'Centralized management dashboard for components and system state') + ]) + ]); // System Overview Panel var overviewPanel = this.renderSystemOverview(data.health, data.statistics); diff --git a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/cyber-dashboard.js b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/cyber-dashboard.js index 3943cc34..fecf948a 100644 --- a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/cyber-dashboard.js +++ b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/cyber-dashboard.js @@ -29,6 +29,10 @@ return view.extend({ var self = this; var container = E('div', { 'class': 'cyberpunk-mode' }, [ + // Load cyberpunk CSS + E('link', { 'rel': 'stylesheet', 'type': 'text/css', + 'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }), + // ASCII Art Header E('div', { 'class': 'cyber-header cyber-scanlines' }, [ E('pre', { 'class': 'cyber-ascii-art' }, diff --git a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/dashboard.js b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/dashboard.js index 2e7ee763..b342a9ce 100644 --- a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/dashboard.js +++ b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/dashboard.js @@ -29,7 +29,9 @@ return view.extend({ var stats = DataUtils.buildAppStats(apps, modules, alerts, updateInfo, API.getAppStatus); var healthSnapshot = DataUtils.normalizeHealth(health); - var container = E('div', { 'class': 'secubox-admin-dashboard' }, [ + var container = E('div', { 'class': 'cyberpunk-mode secubox-admin-dashboard' }, [ + E('link', { 'rel': 'stylesheet', 'type': 'text/css', + 'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }), E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-admin/common.css') }), E('link', { 'rel': 'stylesheet', @@ -37,7 +39,10 @@ return view.extend({ E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-admin/widgets.css') }), - E('h2', {}, 'Admin Control Panel'), + E('div', { 'class': 'cyber-header' }, [ + E('div', { 'class': 'cyber-header-title' }, '🎛️ ADMIN CONTROL PANEL'), + E('div', { 'class': 'cyber-header-subtitle' }, 'System Overview · Applications · Health Monitoring') + ]), // Stats grid E('div', { 'class': 'stats-grid' }, [ diff --git a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/health.js b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/health.js index 9acebe98..0f8b332a 100644 --- a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/health.js +++ b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/health.js @@ -15,14 +15,18 @@ return view.extend({ var snapshot = DataUtils.normalizeHealth(health); this.currentHealth = snapshot; - var container = E('div', { 'class': 'secubox-health' }, [ + var container = E('div', { 'class': 'cyberpunk-mode secubox-health' }, [ + E('link', { 'rel': 'stylesheet', 'type': 'text/css', + 'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }), E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-admin/common.css') }), E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-admin/admin.css') }), - E('h2', {}, 'System Health'), - E('p', {}, 'Monitor system resources and performance'), + E('div', { 'class': 'cyber-header' }, [ + E('div', { 'class': 'cyber-header-title' }, '💊 SYSTEM HEALTH'), + E('div', { 'class': 'cyber-header-subtitle' }, 'Monitor system resources and performance') + ]), E('div', { 'class': 'health-cards' }, [ this.renderMetricCard('CPU Usage', snapshot.cpuUsage || 0, '%', 'cpu'), diff --git a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/logs.js b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/logs.js index 809025a8..29cda8a6 100644 --- a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/logs.js +++ b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/logs.js @@ -14,14 +14,18 @@ return view.extend({ var self = this; var logs = logsData.logs || ''; - var container = E('div', { 'class': 'secubox-logs' }, [ + var container = E('div', { 'class': 'cyberpunk-mode secubox-logs' }, [ + E('link', { 'rel': 'stylesheet', 'type': 'text/css', + 'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }), E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-admin/common.css') }), E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-admin/admin.css') }), - E('h2', {}, 'System Logs'), - E('p', {}, 'View logs from system services and applications'), + E('div', { 'class': 'cyber-header' }, [ + E('div', { 'class': 'cyber-header-title' }, '📋 SYSTEM LOGS'), + E('div', { 'class': 'cyber-header-subtitle' }, 'View logs from system services and applications') + ]), E('div', { 'class': 'logs-controls' }, [ E('select', { diff --git a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/settings.js b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/settings.js index f73f1e48..446d16c3 100644 --- a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/settings.js +++ b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/settings.js @@ -24,14 +24,18 @@ return view.extend({ return status.installed; }); - var container = E('div', { 'class': 'secubox-settings' }, [ + var container = E('div', { 'class': 'cyberpunk-mode secubox-settings' }, [ + E('link', { 'rel': 'stylesheet', 'type': 'text/css', + 'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }), E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-admin/common.css') }), E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-admin/admin.css') }), - E('h2', {}, 'App Settings'), - E('p', {}, 'Configure installed applications'), + E('div', { 'class': 'cyber-header' }, [ + E('div', { 'class': 'cyber-header-title' }, '⚙️ APP SETTINGS'), + E('div', { 'class': 'cyber-header-subtitle' }, 'Configure installed applications') + ]), installedApps.length === 0 ? E('div', { 'class': 'alert alert-info' }, 'No installed apps') : diff --git a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/updates.js b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/updates.js index 97bd003e..3881b188 100644 --- a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/updates.js +++ b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/updates.js @@ -86,6 +86,8 @@ return view.extend({ console.log('[UPDATES-DEBUG] ========== RENDER PROCESSING =========='); var container = E('div', { 'class': 'cyberpunk-mode secubox-updates' }, [ + E('link', { 'rel': 'stylesheet', 'type': 'text/css', + 'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }), E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-admin/common.css') }), E('link', { 'rel': 'stylesheet', diff --git a/package/secubox/luci-app-secubox-security-threats/Makefile b/package/secubox/luci-app-secubox-security-threats/Makefile new file mode 100644 index 00000000..40651e8a --- /dev/null +++ b/package/secubox/luci-app-secubox-security-threats/Makefile @@ -0,0 +1,27 @@ +# Copyright (C) 2026 CyberMind.fr +# Licensed under Apache-2.0 + +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-secubox-security-threats +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 +PKG_ARCH:=all +PKG_LICENSE:=Apache-2.0 +PKG_MAINTAINER:=CyberMind + +LUCI_TITLE:=SecuBox Security Threats Dashboard +LUCI_DESCRIPTION:=Unified dashboard integrating netifyd DPI threats with CrowdSec intelligence for real-time threat monitoring and automated blocking +LUCI_DEPENDS:=+luci-base +rpcd +netifyd +crowdsec +jq +jsonfilter +LUCI_PKGARCH:=all + +# File permissions (CRITICAL: RPCD scripts MUST be executable 755) +# Format: path:owner:group:mode +# - RPCD scripts: 755 (executable by root, required for ubus calls) +# - Config files: 644 (readable by all, writable by root) +# - CSS/JS files: 644 (set automatically by luci.mk) +PKG_FILE_MODES:=/usr/libexec/rpcd/luci.secubox-security-threats:root:root:755 + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/package/secubox/luci-app-secubox-security-threats/README.md b/package/secubox/luci-app-secubox-security-threats/README.md new file mode 100644 index 00000000..419c717b --- /dev/null +++ b/package/secubox/luci-app-secubox-security-threats/README.md @@ -0,0 +1,191 @@ +# SecuBox Security Threats Dashboard + +## Overview + +A unified LuCI dashboard that integrates **netifyd DPI security risks** with **CrowdSec threat intelligence** for comprehensive network threat monitoring and automated blocking. + +## Features + +- **Real-time Threat Detection**: Monitors netifyd's 52 security risk types +- **CrowdSec Integration**: Correlates with CrowdSec alerts and decisions +- **Risk Scoring**: Calculates 0-100 risk scores based on multiple factors +- **Auto-blocking**: Configurable rules for automatic threat blocking +- **Per-host Analysis**: Track threats by IP address +- **Visual Dashboard**: Stats, charts, and real-time threat table + +## Architecture + +``` +netifyd DPI Engine → RPCD Backend → ubus API → LuCI Dashboard + ↓ + CrowdSec LAPI + ↓ + nftables (blocking) +``` + +## Dependencies + +- `luci-base`: LuCI framework +- `rpcd`: Remote Procedure Call daemon +- `netifyd`: Deep Packet Inspection engine +- `crowdsec`: Threat intelligence and blocking +- `jq`: JSON processing +- `jsonfilter`: UCI-compatible JSON filtering + +## Installation + +1. Build the package: +```bash +cd /path/to/openwrt +make package/secubox/luci-app-secubox-security-threats/compile +``` + +2. Install on router: +```bash +opkg install luci-app-secubox-security-threats_*.ipk +``` + +3. Restart services: +```bash +/etc/init.d/rpcd restart +/etc/init.d/uhttpd restart +``` + +## Usage + +### Access Dashboard + +Navigate to: **Admin → SecuBox → Security → Threat Monitor → Dashboard** + +### Configure Auto-block Rules + +Edit `/etc/config/secubox_security_threats`: + +```uci +config block_rule 'my_rule' + option name 'Block Malware' + option enabled '1' + option threat_types 'malware' + option duration '24h' + option threshold '60' +``` + +Apply changes: +```bash +uci commit secubox_security_threats +``` + +### Manual Blocking + +Via dashboard: +1. Click "Block" button next to threat +2. Confirm action +3. IP will be blocked via CrowdSec + +Via CLI: +```bash +ubus call luci.secubox-security-threats block_threat '{"ip":"192.168.1.100","duration":"4h","reason":"Test"}' +``` + +### Whitelist Host + +```bash +ubus call luci.secubox-security-threats whitelist_host '{"ip":"192.168.1.100","reason":"Admin workstation"}' +``` + +## Risk Scoring Algorithm + +**Base Score (0-50):** risk_count × 10 (capped) + +**Severity Weights:** +- Malware indicators (MALICIOUS_JA3, DGA): +20 +- Web attacks (SQL injection, XSS): +15 +- Network anomalies (RISKY_ASN, DNS tunneling): +10 +- Protocol threats (BitTorrent, Mining): +5 + +**CrowdSec Correlation:** +- Active decision: +30 + +**Severity Levels:** +- Critical: ≥80 +- High: 60-79 +- Medium: 40-59 +- Low: <40 + +## Threat Categories + +- **malware**: Malicious JA3, DGA domains, suspicious entropy +- **web_attack**: SQL injection, XSS, RCE attempts +- **anomaly**: DNS tunneling, risky ASNs, unidirectional traffic +- **protocol**: BitTorrent, mining, Tor, unauthorized protocols +- **tls_issue**: Certificate problems, weak ciphers + +## Testing + +### Backend (ubus CLI) +```bash +# Test status +ubus call luci.secubox-security-threats status + +# Get active threats +ubus call luci.secubox-security-threats get_active_threats + +# Test blocking +ubus call luci.secubox-security-threats block_threat '{"ip":"192.168.1.100","duration":"4h","reason":"Test"}' + +# Verify in CrowdSec +cscli decisions list +``` + +### Frontend +1. Navigate to dashboard in LuCI +2. Verify stats cards display +3. Verify threats table populates +4. Test "Block" button +5. Check real-time polling (10s refresh) + +## Troubleshooting + +### No threats detected +- Check if netifyd is running: `ps | grep netifyd` +- Verify netifyd data: `cat /var/run/netifyd/status.json` +- Enable netifyd risk detection in config + +### Auto-blocking not working +- Check if auto-blocking is enabled: `uci get secubox_security_threats.global.auto_block_enabled` +- Verify block rules are enabled: `uci show secubox_security_threats` +- Check logs: `logread | grep security-threats` + +### CrowdSec integration issues +- Check if CrowdSec is running: `ps | grep crowdsec` +- Test cscli: `cscli version` +- Verify permissions: `ls -l /usr/bin/cscli` + +## Files + +**Backend:** +- `/usr/libexec/rpcd/luci.secubox-security-threats` - RPCD backend (mode 755) +- `/etc/config/secubox_security_threats` - UCI configuration + +**Frontend:** +- `/www/luci-static/resources/secubox-security-threats/api.js` - API wrapper +- `/www/luci-static/resources/view/secubox-security-threats/dashboard.js` - Dashboard view + +**Configuration:** +- `/usr/share/luci/menu.d/luci-app-secubox-security-threats.json` - Menu +- `/usr/share/rpcd/acl.d/luci-app-secubox-security-threats.json` - Permissions + +**Runtime:** +- `/tmp/secubox-threats-history.json` - Threat history (volatile) + +## License + +Apache-2.0 + +## Authors + +CyberMind.fr - Gandalf + +## Version + +1.0.0 (2026-01-07) diff --git a/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/secubox-security-threats/api.js b/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/secubox-security-threats/api.js new file mode 100644 index 00000000..c65c94b2 --- /dev/null +++ b/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/secubox-security-threats/api.js @@ -0,0 +1,255 @@ +'use strict'; +'require baseclass'; +'require rpc'; + +// ============================================================================== +// RPC Method Declarations +// ============================================================================== + +var callStatus = rpc.declare({ + object: 'luci.secubox-security-threats', + method: 'status', + expect: { } +}); + +var callGetActiveThreats = rpc.declare({ + object: 'luci.secubox-security-threats', + method: 'get_active_threats', + expect: { threats: [] } +}); + +var callGetThreatHistory = rpc.declare({ + object: 'luci.secubox-security-threats', + method: 'get_threat_history', + params: ['hours'], + expect: { threats: [] } +}); + +var callGetStatsByType = rpc.declare({ + object: 'luci.secubox-security-threats', + method: 'get_stats_by_type', + expect: { } +}); + +var callGetStatsByHost = rpc.declare({ + object: 'luci.secubox-security-threats', + method: 'get_stats_by_host', + expect: { hosts: [] } +}); + +var callGetBlockedIPs = rpc.declare({ + object: 'luci.secubox-security-threats', + method: 'get_blocked_ips', + expect: { blocked: [] } +}); + +var callBlockThreat = rpc.declare({ + object: 'luci.secubox-security-threats', + method: 'block_threat', + params: ['ip', 'duration', 'reason'], + expect: { } +}); + +var callWhitelistHost = rpc.declare({ + object: 'luci.secubox-security-threats', + method: 'whitelist_host', + params: ['ip', 'reason'], + expect: { } +}); + +var callRemoveWhitelist = rpc.declare({ + object: 'luci.secubox-security-threats', + method: 'remove_whitelist', + params: ['ip'], + expect: { } +}); + +// ============================================================================== +// Utility Functions +// ============================================================================== + +/** + * Get color for severity level + * @param {string} severity - Severity level (critical, high, medium, low) + * @returns {string} Hex color code + */ +function getSeverityColor(severity) { + var colors = { + 'critical': '#d32f2f', // Red + 'high': '#ff5722', // Deep Orange + 'medium': '#ff9800', // Orange + 'low': '#ffc107' // Amber + }; + return colors[severity] || '#666'; +} + +/** + * Get icon for threat category + * @param {string} category - Threat category + * @returns {string} Unicode emoji icon + */ +function getThreatIcon(category) { + var icons = { + 'malware': '🦠', + 'web_attack': '⚔️', + 'anomaly': '⚠️', + 'protocol': '🚫', + 'tls_issue': '🔒', + 'other': '❓' + }; + return icons[category] || '❓'; +} + +/** + * Format risk flags for display + * @param {Array} risks - Array of risk flag names + * @returns {string} Formatted risk flags + */ +function formatRiskFlags(risks) { + if (!risks || !Array.isArray(risks)) return 'N/A'; + + return risks.map(function(risk) { + // Convert MALICIOUS_JA3 to "Malicious JA3" + return risk.toString().split('_').map(function(word) { + return word.charAt(0) + word.slice(1).toLowerCase(); + }).join(' '); + }).join(', '); +} + +/** + * Get human-readable category label + * @param {string} category - Category code + * @returns {string} Display label + */ +function getCategoryLabel(category) { + var labels = { + 'malware': 'Malware', + 'web_attack': 'Web Attack', + 'anomaly': 'Network Anomaly', + 'protocol': 'Protocol Threat', + 'tls_issue': 'TLS/Certificate', + 'other': 'Other' + }; + return labels[category] || 'Unknown'; +} + +/** + * Format duration string (4h, 24h, etc.) + * @param {string} duration - Duration string + * @returns {string} Formatted duration + */ +function formatDuration(duration) { + if (!duration) return 'N/A'; + return duration; +} + +/** + * Format timestamp to localized string + * @param {string} timestamp - ISO 8601 timestamp + * @returns {string} Formatted timestamp + */ +function formatTimestamp(timestamp) { + if (!timestamp) return 'N/A'; + try { + var date = new Date(timestamp); + return date.toLocaleString(); + } catch(e) { + return timestamp; + } +} + +/** + * Format relative time (e.g., "5 minutes ago") + * @param {string} timestamp - ISO 8601 timestamp + * @returns {string} Relative time string + */ +function formatRelativeTime(timestamp) { + if (!timestamp) return 'N/A'; + try { + var date = new Date(timestamp); + var now = new Date(); + var seconds = Math.floor((now - date) / 1000); + + if (seconds < 60) return seconds + 's ago'; + if (seconds < 3600) return Math.floor(seconds / 60) + 'm ago'; + if (seconds < 86400) return Math.floor(seconds / 3600) + 'h ago'; + return Math.floor(seconds / 86400) + 'd ago'; + } catch(e) { + return timestamp; + } +} + +/** + * Format bytes to human-readable size + * @param {number} bytes - Byte count + * @returns {string} Formatted size (e.g., "1.5 MB") + */ +function formatBytes(bytes) { + if (!bytes || bytes === 0) return '0 B'; + var k = 1024; + var sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + var i = Math.floor(Math.log(bytes) / Math.log(k)); + return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]; +} + +/** + * Get badge HTML for severity + * @param {string} severity - Severity level + * @returns {string} HTML string + */ +function getSeverityBadge(severity) { + var color = getSeverityColor(severity); + var label = severity.charAt(0).toUpperCase() + severity.slice(1); + return '' + label + ''; +} + +/** + * Composite data fetcher for dashboard + * @returns {Promise} Promise resolving to dashboard data + */ +function getDashboardData() { + return Promise.all([ + callStatus(), + callGetActiveThreats(), + callGetStatsByType(), + callGetBlockedIPs() + ]).then(function(results) { + return { + status: results[0] || {}, + threats: results[1].threats || [], + stats: results[2] || {}, + blocked: results[3].blocked || [] + }; + }); +} + +// ============================================================================== +// Exports +// ============================================================================== + +return baseclass.extend({ + // RPC Methods + getStatus: callStatus, + getActiveThreats: callGetActiveThreats, + getThreatHistory: callGetThreatHistory, + getStatsByType: callGetStatsByType, + getStatsByHost: callGetStatsByHost, + getBlockedIPs: callGetBlockedIPs, + blockThreat: callBlockThreat, + whitelistHost: callWhitelistHost, + removeWhitelist: callRemoveWhitelist, + + // Utility Functions + getSeverityColor: getSeverityColor, + getThreatIcon: getThreatIcon, + formatRiskFlags: formatRiskFlags, + getCategoryLabel: getCategoryLabel, + formatDuration: formatDuration, + formatTimestamp: formatTimestamp, + formatRelativeTime: formatRelativeTime, + formatBytes: formatBytes, + getSeverityBadge: getSeverityBadge, + + // Composite Fetchers + getDashboardData: getDashboardData +}); diff --git a/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/view/secubox-security-threats/dashboard.js b/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/view/secubox-security-threats/dashboard.js new file mode 100644 index 00000000..b8a77119 --- /dev/null +++ b/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/view/secubox-security-threats/dashboard.js @@ -0,0 +1,306 @@ +'use strict'; +'require view'; +'require poll'; +'require ui'; +'require dom'; +'require secubox-security-threats/api as API'; + +return L.view.extend({ + load: function() { + return API.getDashboardData(); + }, + + render: function(data) { + data = data || {}; + var threats = data.threats || []; + var status = data.status || {}; + var stats = data.stats || {}; + var blocked = data.blocked || []; + + // Calculate statistics + var threatStats = { + total: threats.length, + critical: threats.filter(function(t) { return t.severity === 'critical'; }).length, + high: threats.filter(function(t) { return t.severity === 'high'; }).length, + medium: threats.filter(function(t) { return t.severity === 'medium'; }).length, + low: threats.filter(function(t) { return t.severity === 'low'; }).length, + avg_score: threats.length > 0 ? + Math.round(threats.reduce(function(sum, t) { return sum + t.risk_score; }, 0) / threats.length) : 0 + }; + + // Build view elements + var statusBanner = this.renderStatusBanner(status); + var statsGrid = this.renderStatsGrid(threatStats, blocked.length); + var threatDist = this.renderThreatDistribution(stats); + var riskGauge = this.renderRiskGauge(threatStats.avg_score); + var threatsTable = this.renderThreatsTable(threats.slice(0, 10)); + + // Setup auto-refresh polling (every 10 seconds) + poll.add(L.bind(function() { + this.handleRefresh(); + }, this), 10); + + // Return the complete view + return E('div', { 'class': 'cbi-map' }, [ + E('h2', {}, _('Security Threats Dashboard')), + E('div', { 'class': 'cbi-map-descr' }, _('Real-time threat detection integrating netifyd DPI and CrowdSec intelligence')), + statusBanner, + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Overview')), + statsGrid + ]), + E('div', { 'class': 'cbi-section', 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;' }, [ + threatDist, + riskGauge + ]), + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Recent Threats')), + threatsTable + ]) + ]); + }, + + renderStatusBanner: function(status) { + var services = []; + var hasIssue = false; + + if (!status.netifyd_running) { + services.push('netifyd is not running'); + hasIssue = true; + } + if (!status.crowdsec_running) { + services.push('CrowdSec is not running'); + hasIssue = true; + } + + if (!hasIssue) { + return E('div', { + 'class': 'alert-message', + 'style': 'background: #4caf50; color: white; padding: 10px; border-radius: 4px; margin-bottom: 1rem;' + }, [ + E('strong', {}, '✓ All systems operational'), + E('span', { 'style': 'margin-left: 1rem;' }, 'netifyd + CrowdSec integration active') + ]); + } + + return E('div', { + 'class': 'alert-message', + 'style': 'background: #ff9800; color: white; padding: 10px; border-radius: 4px; margin-bottom: 1rem;' + }, [ + E('strong', {}, '⚠ Service Issues: '), + E('span', {}, services.join(', ')) + ]); + }, + + renderStatsGrid: function(stats, blockedCount) { + return E('div', { + 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;' + }, [ + this.renderStatCard(_('Active Threats'), stats.total, '#2196f3', ''), + this.renderStatCard(_('Critical'), stats.critical, '#d32f2f', 'Immediate attention required'), + this.renderStatCard(_('High Risk'), stats.high, '#ff5722', 'Review recommended'), + this.renderStatCard(_('Avg Risk Score'), stats.avg_score + '/100', '#ff9800', 'Overall threat level'), + this.renderStatCard(_('Blocked IPs'), blockedCount, '#9c27b0', 'Via CrowdSec') + ]); + }, + + renderStatCard: function(label, value, color, description) { + var children = [ + E('div', { 'style': 'font-size: 0.85rem; color: #666; margin-bottom: 0.5rem;' }, label), + E('div', { 'style': 'font-size: 2rem; font-weight: bold; color: ' + color + ';' }, value) + ]; + + if (description) { + children.push(E('div', { 'style': 'font-size: 0.75rem; color: #999; margin-top: 0.25rem;' }, description)); + } + + return E('div', { + 'style': 'background: #f5f5f5; padding: 1rem; border-left: 4px solid ' + color + '; border-radius: 4px;' + }, children); + }, + + renderThreatDistribution: function(stats) { + var categories = [ + { label: 'Malware', value: stats.malware || 0, color: '#d32f2f', icon: '🦠' }, + { label: 'Web Attack', value: stats.web_attack || 0, color: '#ff5722', icon: '⚔️' }, + { label: 'Anomaly', value: stats.anomaly || 0, color: '#ff9800', icon: '⚠️' }, + { label: 'Protocol', value: stats.protocol || 0, color: '#9c27b0', icon: '🚫' }, + { label: 'TLS Issue', value: stats.tls_issue || 0, color: '#3f51b5', icon: '🔒' } + ]; + + var total = categories.reduce(function(sum, cat) { return sum + cat.value; }, 0); + + return E('div', {}, [ + E('h4', {}, _('Threat Distribution')), + E('div', { 'style': 'padding: 1rem; background: white; border-radius: 4px;' }, [ + total === 0 ? + E('div', { 'style': 'text-align: center; color: #999; padding: 2rem;' }, _('No threats detected')) : + E('div', {}, categories.filter(function(cat) { + return cat.value > 0; + }).map(L.bind(function(cat) { + var percentage = total > 0 ? Math.round((cat.value / total) * 100) : 0; + return E('div', { 'style': 'margin-bottom: 1rem;' }, [ + E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 0.25rem;' }, [ + E('span', {}, cat.icon + ' ' + cat.label), + E('span', { 'style': 'font-weight: bold;' }, cat.value + ' (' + percentage + '%)') + ]), + E('div', { + 'style': 'background: #e0e0e0; height: 20px; border-radius: 10px; overflow: hidden;' + }, [ + E('div', { + 'style': 'background: ' + cat.color + '; height: 100%; width: ' + percentage + '%;' + }) + ]) + ]); + }, this))) + ]) + ]); + }, + + renderRiskGauge: function(avgScore) { + var level, color, description; + if (avgScore >= 80) { + level = 'CRITICAL'; + color = '#d32f2f'; + description = 'Immediate action required'; + } else if (avgScore >= 60) { + level = 'HIGH'; + color = '#ff5722'; + description = 'Review threats promptly'; + } else if (avgScore >= 40) { + level = 'MEDIUM'; + color = '#ff9800'; + description = 'Monitor situation'; + } else { + level = 'LOW'; + color = '#4caf50'; + description = 'Normal security posture'; + } + + return E('div', {}, [ + E('h4', {}, _('Risk Level')), + E('div', { 'style': 'padding: 1rem; background: white; border-radius: 4px; text-align: center;' }, [ + E('div', { 'style': 'font-size: 3rem; font-weight: bold; color: ' + color + '; margin: 1rem 0;' }, avgScore), + E('div', { 'style': 'font-size: 1.2rem; font-weight: bold; color: ' + color + '; margin-bottom: 0.5rem;' }, level), + E('div', { 'style': 'color: #666; font-size: 0.9rem;' }, description), + E('div', { + 'style': 'margin-top: 1rem; height: 10px; background: linear-gradient(to right, #4caf50, #ff9800, #ff5722, #d32f2f); border-radius: 5px; position: relative;' + }, [ + E('div', { + 'style': 'position: absolute; top: -5px; left: ' + avgScore + '%; width: 2px; height: 20px; background: #000;' + }) + ]) + ]) + ]); + }, + + renderThreatsTable: function(threats) { + if (threats.length === 0) { + return E('div', { + 'style': 'text-align: center; padding: 2rem; color: #999; background: #f5f5f5; border-radius: 4px;' + }, _('No threats detected. Your network is secure.')); + } + + var rows = threats.map(L.bind(function(threat) { + return E('tr', {}, [ + E('td', {}, [ + E('div', {}, threat.ip), + E('div', { 'style': 'font-size: 0.85em; color: #666;' }, API.formatRelativeTime(threat.timestamp)) + ]), + E('td', {}, threat.mac), + E('td', {}, [ + E('div', {}, API.getThreatIcon(threat.category) + ' ' + API.getCategoryLabel(threat.category)), + E('div', { 'style': 'font-size: 0.85em; color: #666;' }, threat.netifyd.application || 'unknown') + ]), + E('td', { 'innerHTML': API.getSeverityBadge(threat.severity) }), + E('td', { 'style': 'font-weight: bold;' }, threat.risk_score), + E('td', {}, [ + E('div', { 'style': 'font-size: 0.85em; max-width: 200px; overflow: hidden; text-overflow: ellipsis;' }, + API.formatRiskFlags(threat.netifyd.risks)) + ]), + E('td', {}, threat.crowdsec.has_decision ? + E('span', { 'style': 'color: #d32f2f; font-weight: bold;' }, '✓ Blocked') : + E('span', { 'style': 'color: #999;' }, '-')), + E('td', {}, [ + threat.crowdsec.has_decision ? + E('button', { + 'class': 'cbi-button cbi-button-neutral', + 'disabled': 'disabled' + }, _('Blocked')) : + E('button', { + 'class': 'cbi-button cbi-button-negative', + 'click': L.bind(function(ev) { + this.handleBlock(threat.ip); + }, this) + }, _('Block')) + ]) + ]); + }, this)); + + return E('div', { 'class': 'table-wrapper' }, [ + E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, _('IP Address')), + E('th', { 'class': 'th' }, _('MAC')), + E('th', { 'class': 'th' }, _('Category / App')), + E('th', { 'class': 'th' }, _('Severity')), + E('th', { 'class': 'th' }, _('Risk Score')), + E('th', { 'class': 'th' }, _('netifyd Risks')), + E('th', { 'class': 'th' }, _('CrowdSec')), + E('th', { 'class': 'th' }, _('Actions')) + ]) + ].concat(rows)) + ]); + }, + + handleBlock: function(ip) { + ui.showModal(_('Block IP Address'), [ + E('p', {}, _('Are you sure you want to block %s?').format(ip)), + E('p', {}, _('This will add a CrowdSec decision and block all traffic from this IP.')), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'cbi-button cbi-button-neutral', + 'click': ui.hideModal + }, _('Cancel')), + ' ', + E('button', { + 'class': 'cbi-button cbi-button-negative', + 'click': L.bind(function() { + ui.hideModal(); + ui.showModal(_('Blocking IP...'), E('p', { 'class': 'spinning' }, _('Please wait...'))); + + API.blockThreat(ip, '4h', 'Manual block from Security Threats Dashboard').then(L.bind(function(result) { + ui.hideModal(); + if (result.success) { + ui.addNotification(null, E('p', _('IP %s blocked successfully').format(ip)), 'success'); + this.handleRefresh(); + } else { + ui.addNotification(null, E('p', _('Failed to block IP: %s').format(result.error || 'Unknown error')), 'error'); + } + }, this)).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', _('Error: %s').format(err.message)), 'error'); + }); + }, this) + }, _('Block for 4 hours')) + ]) + ]); + }, + + handleRefresh: function() { + return API.getDashboardData().then(L.bind(function(data) { + // Update view with new data + var container = document.querySelector('.cbi-map'); + if (container) { + var newView = this.render(data); + dom.content(container, newView); + } + }, this)).catch(function(err) { + console.error('Failed to refresh dashboard:', err); + }); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-secubox-security-threats/root/etc/config/secubox_security_threats b/package/secubox/luci-app-secubox-security-threats/root/etc/config/secubox_security_threats new file mode 100644 index 00000000..62f5c75a --- /dev/null +++ b/package/secubox/luci-app-secubox-security-threats/root/etc/config/secubox_security_threats @@ -0,0 +1,59 @@ +# SecuBox Security Threats Dashboard Configuration +# Auto-blocking rules and whitelist configuration + +config global 'global' + option enabled '1' + option history_retention_days '7' + option refresh_interval '10' + option auto_block_enabled '1' + option log_threats '1' + +# High-priority: Block malware indicators +config block_rule 'malware_high' + option name 'Block Malware Indicators' + option enabled '1' + option threat_types 'malware' + option risk_flags 'MALICIOUS_JA3,SUSPICIOUS_DGA_DOMAIN,SUSPICIOUS_ENTROPY,POSSIBLE_EXPLOIT' + option action 'ban' + option duration '24h' + option threshold '60' + option description 'Automatically block hosts with malware signatures (JA3, DGA domains, suspicious entropy)' + +# Medium-priority: Block web attacks +config block_rule 'web_attacks' + option name 'Block Web Attacks' + option enabled '1' + option threat_types 'web_attack' + option risk_flags 'URL_POSSIBLE_SQL_INJECTION,URL_POSSIBLE_XSS,URL_POSSIBLE_RCE_INJECTION' + option action 'ban' + option duration '12h' + option threshold '40' + option description 'Block SQL injection, XSS, and RCE attempts' + +# Low-priority: Block protocol threats (disabled by default) +config block_rule 'protocol_threats' + option name 'Block Unauthorized Protocols' + option enabled '0' + option threat_types 'protocol' + option risk_flags '' + option action 'ban' + option duration '4h' + option threshold '20' + option description 'Block unauthorized protocols like BitTorrent, Mining, Tor (disabled by default)' + +# Network anomalies (disabled by default - may generate false positives) +config block_rule 'network_anomalies' + option name 'Block Network Anomalies' + option enabled '0' + option threat_types 'anomaly' + option risk_flags 'RISKY_ASN,RISKY_DOMAIN,DNS_SUSPICIOUS_TRAFFIC' + option action 'ban' + option duration '6h' + option threshold '50' + option description 'Block connections from risky ASNs/domains and suspicious DNS traffic' + +# Example whitelist entry (commented out) +# config whitelist 'admin_workstation' +# option ip '192.168.1.100' +# option reason 'Admin workstation - never block' +# option added_at '2026-01-07T15:00:00Z' diff --git a/package/secubox/luci-app-secubox-security-threats/root/usr/libexec/rpcd/luci.secubox-security-threats b/package/secubox/luci-app-secubox-security-threats/root/usr/libexec/rpcd/luci.secubox-security-threats new file mode 100644 index 00000000..fcd3e773 --- /dev/null +++ b/package/secubox/luci-app-secubox-security-threats/root/usr/libexec/rpcd/luci.secubox-security-threats @@ -0,0 +1,536 @@ +#!/bin/sh +# SPDX-License-Identifier: Apache-2.0 +# SecuBox Security Threats Dashboard RPCD backend +# Copyright (C) 2026 CyberMind.fr - Gandalf +# +# Integrates netifyd DPI security risks with CrowdSec threat intelligence +# for comprehensive network threat monitoring and automated blocking + +. /lib/functions.sh +. /usr/share/libubox/jshn.sh + +HISTORY_FILE="/tmp/secubox-threats-history.json" +CSCLI="/usr/bin/cscli" +SECCUBOX_LOG="/usr/sbin/secubox-log" + +secubox_log() { + [ -x "$SECCUBOX_LOG" ] || return + "$SECCUBOX_LOG" --tag "security-threats" --message "$1" >/dev/null 2>&1 +} + +# Initialize storage +init_storage() { + [ ! -f "$HISTORY_FILE" ] && echo '[]' > "$HISTORY_FILE" +} + +# ============================================================================== +# DATA COLLECTION +# ============================================================================== + +# Get netifyd flows (socket first, fallback to file) +get_netifyd_flows() { + if [ -S /var/run/netifyd/netifyd.sock ]; then + echo "status" | nc -U /var/run/netifyd/netifyd.sock 2>/dev/null + elif [ -f /var/run/netifyd/status.json ]; then + cat /var/run/netifyd/status.json + else + echo '{}' + fi +} + +# Filter flows with security risks +filter_risky_flows() { + local flows="$1" + # Extract flows with risks[] array (length > 0) + echo "$flows" | jq -c '.flows[]? | select(.risks != null and (.risks | length) > 0)' 2>/dev/null +} + +# Get CrowdSec decisions (active bans) +get_crowdsec_decisions() { + [ ! -x "$CSCLI" ] && echo '[]' && return + $CSCLI decisions list -o json 2>/dev/null || echo '[]' +} + +# Get CrowdSec alerts (recent detections) +get_crowdsec_alerts() { + [ ! -x "$CSCLI" ] && echo '[]' && return + $CSCLI alerts list -o json --limit 100 2>/dev/null || echo '[]' +} + +# ============================================================================== +# CLASSIFICATION +# ============================================================================== + +# Classify netifyd risk by category +classify_netifyd_risk() { + local risk_name="$1" + + # Map ND_RISK_* to categories + case "$risk_name" in + *MALICIOUS_JA3*|*SUSPICIOUS_DGA*|*SUSPICIOUS_ENTROPY*|*POSSIBLE_EXPLOIT*|*PERIODIC_FLOW*) + echo "malware";; + *SQL_INJECTION*|*XSS*|*RCE_INJECTION*|*HTTP_SUSPICIOUS*) + echo "web_attack";; + *DNS_FRAGMENTED*|*DNS_LARGE_PACKET*|*DNS_SUSPICIOUS*|*RISKY_ASN*|*RISKY_DOMAIN*|*UNIDIRECTIONAL*|*MALFORMED_PACKET*) + echo "anomaly";; + *BitTorrent*|*Mining*|*Tor*|*PROXY*|*SOCKS*) + echo "protocol";; + *TLS_*|*CERTIFICATE_*) + echo "tls_issue";; + *) + echo "other";; + esac +} + +# Calculate risk score (0-100) +calculate_risk_score() { + local risk_count="$1" + local has_crowdsec="$2" + local risk_types="$3" # comma-separated + + local score=$((risk_count * 10)) # Base: 10 per risk + [ "$score" -gt 50 ] && score=50 # Cap base at 50 + + # Severity weights + echo "$risk_types" | grep -q "MALICIOUS_JA3\|SUSPICIOUS_DGA\|POSSIBLE_EXPLOIT" && score=$((score + 20)) + echo "$risk_types" | grep -q "SQL_INJECTION\|XSS\|RCE" && score=$((score + 15)) + echo "$risk_types" | grep -q "RISKY_ASN\|RISKY_DOMAIN" && score=$((score + 10)) + echo "$risk_types" | grep -q "BitTorrent\|Mining\|Tor" && score=$((score + 5)) + + # CrowdSec correlation bonus + [ "$has_crowdsec" = "1" ] && score=$((score + 30)) + + # Cap at 100 + [ "$score" -gt 100 ] && score=100 + echo "$score" +} + +# Determine severity level +get_threat_severity() { + local score="$1" + if [ "$score" -ge 80 ]; then + echo "critical" + elif [ "$score" -ge 60 ]; then + echo "high" + elif [ "$score" -ge 40 ]; then + echo "medium" + else + echo "low" + fi +} + +# ============================================================================== +# CORRELATION ENGINE +# ============================================================================== + +# Correlate netifyd risks with CrowdSec data +correlate_threats() { + local netifyd_flows="$1" + local crowdsec_decisions="$2" + local crowdsec_alerts="$3" + + # Create lookup tables with jq + local decisions_by_ip=$(echo "$crowdsec_decisions" | jq -c 'INDEX(.value)' 2>/dev/null || echo '{}') + local alerts_by_ip=$(echo "$crowdsec_alerts" | jq -c 'group_by(.source.ip) | map({(.[0].source.ip): .}) | add // {}' 2>/dev/null || echo '{}') + + # Process each risky flow + echo "$netifyd_flows" | while read -r flow; do + [ -z "$flow" ] && continue + + local ip=$(echo "$flow" | jq -r '.src_ip // "unknown"') + [ "$ip" = "unknown" ] && continue + + local mac=$(echo "$flow" | jq -r '.src_mac // "N/A"') + local risks=$(echo "$flow" | jq -r '.risks | map(tostring) | join(",")' 2>/dev/null || echo "") + local risk_count=$(echo "$flow" | jq '.risks | length' 2>/dev/null || echo 0) + + # Lookup CrowdSec data + local decision=$(echo "$decisions_by_ip" | jq -c ".[\"$ip\"] // null") + local has_decision=$([[ "$decision" != "null" ]] && echo 1 || echo 0) + local alert=$(echo "$alerts_by_ip" | jq -c ".[\"$ip\"] // null") + + # Calculate metrics + local risk_score=$(calculate_risk_score "$risk_count" "$has_decision" "$risks") + local severity=$(get_threat_severity "$risk_score") + local first_risk=$(echo "$risks" | cut -d, -f1) + local category=$(classify_netifyd_risk "$first_risk") + + # Build unified threat JSON + jq -n \ + --arg ip "$ip" \ + --arg mac "$mac" \ + --arg timestamp "$(date -Iseconds)" \ + --argjson score "$risk_score" \ + --arg severity "$severity" \ + --arg category "$category" \ + --argjson netifyd "$(echo "$flow" | jq '{ + application: .detected_application // "unknown", + protocol: .detected_protocol // "unknown", + risks: [.risks[] | tostring], + risk_count: (.risks | length), + bytes: .total_bytes // 0, + packets: .total_packets // 0 + }')" \ + --argjson crowdsec "$(jq -n \ + --argjson decision "$decision" \ + --argjson alert "$alert" \ + '{ + has_decision: ($decision != null), + decision: $decision, + has_alert: ($alert != null), + alert_count: (if $alert != null then ($alert | length) else 0 end), + scenarios: (if $alert != null then ($alert | map(.scenario) | join(",")) else "" end) + }')" \ + '{ + ip: $ip, + mac: $mac, + timestamp: $timestamp, + risk_score: $score, + severity: $severity, + category: $category, + netifyd: $netifyd, + crowdsec: $crowdsec + }' 2>/dev/null + done +} + +# ============================================================================== +# AUTO-BLOCKING +# ============================================================================== + +# Execute block via CrowdSec +execute_block() { + local ip="$1" + local duration="$2" + local reason="$3" + + [ ! -x "$CSCLI" ] && return 1 + + # Call cscli to add decision + if $CSCLI decisions add --ip "$ip" --duration "$duration" --reason "$reason" >/dev/null 2>&1; then + secubox_log "Auto-blocked $ip for $duration ($reason)" + return 0 + else + return 1 + fi +} + +# Check single rule match +check_rule_match() { + local section="$1" + local threat_category="$2" + local threat_risks="$3" + local threat_score="$4" + local threat_ip="$5" + + local enabled=$(uci -q get "secubox_security_threats.${section}.enabled") + [ "$enabled" != "1" ] && return 1 + + local rule_types=$(uci -q get "secubox_security_threats.${section}.threat_types") + echo "$rule_types" | grep -qw "$threat_category" || return 1 + + local threshold=$(uci -q get "secubox_security_threats.${section}.threshold" 2>/dev/null || echo 1) + [ "$threat_score" -lt "$threshold" ] && return 1 + + # Rule matched - execute block + local duration=$(uci -q get "secubox_security_threats.${section}.duration" || echo "4h") + local name=$(uci -q get "secubox_security_threats.${section}.name" || echo "Security threat") + + execute_block "$threat_ip" "$duration" "Auto-blocked: $name" + return $? +} + +# Check if threat should be auto-blocked +check_block_rules() { + local threat="$1" + + local ip=$(echo "$threat" | jq -r '.ip') + local category=$(echo "$threat" | jq -r '.category') + local score=$(echo "$threat" | jq -r '.risk_score') + local risks=$(echo "$threat" | jq -r '.netifyd.risks | join(",")') + + # Check whitelist first + local whitelist_section="whitelist_${ip//./_}" + uci -q get "secubox_security_threats.${whitelist_section}" >/dev/null 2>&1 && return 1 + + # Check if auto-blocking is enabled globally + local auto_block_enabled=$(uci -q get secubox_security_threats.global.auto_block_enabled 2>/dev/null || echo 1) + [ "$auto_block_enabled" != "1" ] && return 1 + + # Iterate block rules from UCI + config_load secubox_security_threats + config_foreach check_rule_match block_rule "$category" "$risks" "$score" "$ip" +} + +# ============================================================================== +# STATISTICS +# ============================================================================== + +# Get stats by type (category) +get_stats_by_type() { + local threats="$1" + + echo "$threats" | jq -s '{ + malware: [.[] | select(.category == "malware")] | length, + web_attack: [.[] | select(.category == "web_attack")] | length, + anomaly: [.[] | select(.category == "anomaly")] | length, + protocol: [.[] | select(.category == "protocol")] | length, + tls_issue: [.[] | select(.category == "tls_issue")] | length, + other: [.[] | select(.category == "other")] | length + }' 2>/dev/null +} + +# Get stats by host (IP) +get_stats_by_host() { + local threats="$1" + + echo "$threats" | jq -s 'group_by(.ip) | map({ + ip: .[0].ip, + mac: .[0].mac, + threat_count: length, + avg_risk_score: (map(.risk_score) | add / length | floor), + highest_severity: (map(.severity) | sort | reverse | .[0]), + first_seen: (map(.timestamp) | sort | .[0]), + last_seen: (map(.timestamp) | sort | reverse | .[0]), + categories: (map(.category) | unique | join(",")) + })' 2>/dev/null +} + +# ============================================================================== +# UBUS INTERFACE +# ============================================================================== + +case "$1" in + list) + # List available methods + json_init + json_add_object "status" + json_close_object + json_add_object "get_active_threats" + json_close_object + json_add_object "get_threat_history" + json_add_string "hours" "int" + json_close_object + json_add_object "get_stats_by_type" + json_close_object + json_add_object "get_stats_by_host" + json_close_object + json_add_object "get_blocked_ips" + json_close_object + json_add_object "block_threat" + json_add_string "ip" "string" + json_add_string "duration" "string" + json_add_string "reason" "string" + json_close_object + json_add_object "whitelist_host" + json_add_string "ip" "string" + json_add_string "reason" "string" + json_close_object + json_add_object "remove_whitelist" + json_add_string "ip" "string" + json_close_object + json_dump + ;; + + call) + case "$2" in + status) + json_init + json_add_boolean "enabled" 1 + json_add_string "module" "secubox-security-threats" + json_add_string "version" "1.0.0" + json_add_boolean "netifyd_running" $(pgrep -x netifyd >/dev/null && echo 1 || echo 0) + json_add_boolean "crowdsec_running" $(pgrep crowdsec >/dev/null && echo 1 || echo 0) + json_add_boolean "cscli_available" $([ -x "$CSCLI" ] && echo 1 || echo 0) + json_dump + ;; + + get_active_threats) + # Main correlation workflow + local netifyd_data=$(get_netifyd_flows) + local risky_flows=$(filter_risky_flows "$netifyd_data") + + # Only fetch CrowdSec data if available + local decisions='[]' + local alerts='[]' + if [ -x "$CSCLI" ]; then + decisions=$(get_crowdsec_decisions) + alerts=$(get_crowdsec_alerts) + fi + + # Correlate threats + local threats=$(correlate_threats "$risky_flows" "$decisions" "$alerts") + + # Check auto-block rules for each threat + if [ -n "$threats" ]; then + echo "$threats" | while read -r threat; do + [ -z "$threat" ] && continue + check_block_rules "$threat" >/dev/null 2>&1 || true + done + fi + + # Output as JSON array + json_init + json_add_array "threats" + if [ -n "$threats" ]; then + echo "$threats" | jq -s 'sort_by(.risk_score) | reverse' | jq -c '.[]' | while read -r threat; do + echo "$threat" + done + fi + json_close_array + json_dump + ;; + + get_threat_history) + read -r input + json_load "$input" + json_get_var hours hours + hours=${hours:-24} + + init_storage + + # Filter history by time + local cutoff_time=$(date -d "$hours hours ago" -Iseconds 2>/dev/null || date -Iseconds) + + json_init + json_add_array "threats" + if [ -f "$HISTORY_FILE" ]; then + jq -c --arg cutoff "$cutoff_time" '.[] | select(.timestamp >= $cutoff)' "$HISTORY_FILE" 2>/dev/null | while read -r threat; do + echo "$threat" + done + fi + json_close_array + json_dump + ;; + + get_stats_by_type) + local netifyd_data=$(get_netifyd_flows) + local risky_flows=$(filter_risky_flows "$netifyd_data") + local decisions=$(get_crowdsec_decisions) + local alerts=$(get_crowdsec_alerts) + local threats=$(correlate_threats "$risky_flows" "$decisions" "$alerts") + + local stats=$(get_stats_by_type "$threats") + + echo "$stats" + ;; + + get_stats_by_host) + local netifyd_data=$(get_netifyd_flows) + local risky_flows=$(filter_risky_flows "$netifyd_data") + local decisions=$(get_crowdsec_decisions) + local alerts=$(get_crowdsec_alerts) + local threats=$(correlate_threats "$risky_flows" "$decisions" "$alerts") + + json_init + json_add_array "hosts" + if [ -n "$threats" ]; then + get_stats_by_host "$threats" | jq -c '.[]' | while read -r host; do + echo "$host" + done + fi + json_close_array + json_dump + ;; + + get_blocked_ips) + if [ -x "$CSCLI" ]; then + local decisions=$(get_crowdsec_decisions) + echo "{\"blocked\":$decisions}" + else + echo '{"blocked":[]}' + fi + ;; + + block_threat) + read -r input + json_load "$input" + json_get_var ip ip + json_get_var duration duration + json_get_var reason reason + + if [ -z "$ip" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "IP address required" + json_dump + exit 0 + fi + + duration=${duration:-4h} + reason=${reason:-"Manual block from Security Threats Dashboard"} + + if execute_block "$ip" "$duration" "$reason"; then + json_init + json_add_boolean "success" 1 + json_add_string "message" "IP $ip blocked for $duration" + json_dump + else + json_init + json_add_boolean "success" 0 + json_add_string "error" "Failed to block IP (check if CrowdSec is running)" + json_dump + fi + ;; + + whitelist_host) + read -r input + json_load "$input" + json_get_var ip ip + json_get_var reason reason + + if [ -z "$ip" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "IP address required" + json_dump + exit 0 + fi + + reason=${reason:-"Whitelisted from Security Threats Dashboard"} + local section="whitelist_${ip//./_}" + + uci set "secubox_security_threats.${section}=whitelist" + uci set "secubox_security_threats.${section}.ip=$ip" + uci set "secubox_security_threats.${section}.reason=$reason" + uci set "secubox_security_threats.${section}.added_at=$(date -Iseconds)" + uci commit secubox_security_threats + + json_init + json_add_boolean "success" 1 + json_add_string "message" "IP $ip added to whitelist" + json_dump + ;; + + remove_whitelist) + read -r input + json_load "$input" + json_get_var ip ip + + if [ -z "$ip" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "IP address required" + json_dump + exit 0 + fi + + local section="whitelist_${ip//./_}" + uci delete "secubox_security_threats.${section}" 2>/dev/null + uci commit secubox_security_threats + + json_init + json_add_boolean "success" 1 + json_add_string "message" "IP $ip removed from whitelist" + json_dump + ;; + + *) + json_init + json_add_boolean "error" 1 + json_add_string "message" "Unknown method: $2" + json_dump + ;; + esac + ;; +esac diff --git a/package/secubox/luci-app-secubox-security-threats/root/usr/share/luci/menu.d/luci-app-secubox-security-threats.json b/package/secubox/luci-app-secubox-security-threats/root/usr/share/luci/menu.d/luci-app-secubox-security-threats.json new file mode 100644 index 00000000..ff979f7d --- /dev/null +++ b/package/secubox/luci-app-secubox-security-threats/root/usr/share/luci/menu.d/luci-app-secubox-security-threats.json @@ -0,0 +1,20 @@ +{ + "admin/secubox/security/threats": { + "title": "Threat Monitor", + "order": 15, + "action": { + "type": "firstchild" + }, + "depends": { + "acl": ["luci-app-secubox-security-threats"] + } + }, + "admin/secubox/security/threats/dashboard": { + "title": "Dashboard", + "order": 10, + "action": { + "type": "view", + "path": "secubox-security-threats/dashboard" + } + } +} diff --git a/package/secubox/luci-app-secubox-security-threats/root/usr/share/rpcd/acl.d/luci-app-secubox-security-threats.json b/package/secubox/luci-app-secubox-security-threats/root/usr/share/rpcd/acl.d/luci-app-secubox-security-threats.json new file mode 100644 index 00000000..bf2adada --- /dev/null +++ b/package/secubox/luci-app-secubox-security-threats/root/usr/share/rpcd/acl.d/luci-app-secubox-security-threats.json @@ -0,0 +1,42 @@ +{ + "luci-app-secubox-security-threats": { + "description": "Grant access to SecuBox Security Threats Dashboard", + "read": { + "ubus": { + "luci.secubox-security-threats": [ + "status", + "get_active_threats", + "get_threat_history", + "get_stats_by_type", + "get_stats_by_host", + "get_blocked_ips" + ], + "luci.crowdsec-dashboard": [ + "decisions", + "alerts", + "status" + ], + "luci.netifyd-dashboard": [ + "status", + "get_flows", + "get_devices" + ] + }, + "uci": ["secubox_security_threats", "netifyd", "crowdsec"] + }, + "write": { + "ubus": { + "luci.secubox-security-threats": [ + "block_threat", + "whitelist_host", + "remove_whitelist" + ], + "luci.crowdsec-dashboard": [ + "ban", + "unban" + ] + }, + "uci": ["secubox_security_threats"] + } + } +} diff --git a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/dev-status-widget.js b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/dev-status-widget.js index 9533d640..055f95aa 100644 --- a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/dev-status-widget.js +++ b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/dev-status-widget.js @@ -82,23 +82,26 @@ const DevStatusWidget = { } }, - // Per-module status overview - moduleStatus: [ - { name: 'SecuBox Central Hub', version: '0.7.0-r6', note: 'Dashboard central + Appstore (5 apps)' }, - { name: 'System Hub', version: '0.5.1-r2', note: 'Centre de contrôle' }, - { name: 'Traffic Shaper', version: '0.4.0-r1', note: 'CAKE / fq_codel / HTB' }, - { name: 'CrowdSec Dashboard', version: '0.5.0-r1', note: 'Détection d\'intrusions' }, - { name: 'Netdata Dashboard', version: '0.5.0-r1', note: 'Monitoring temps réel' }, - { name: 'Netifyd Dashboard', version: '0.4.0-r1', note: 'Intelligence applicative' }, - { name: 'Network Modes', version: '0.5.0-r1', note: '5 topologies réseau' }, - { name: 'WireGuard Dashboard', version: '0.4.0-r1', note: 'VPN + QR codes' }, - { name: 'Auth Guardian', version: '0.4.0-r1', note: 'OAuth / vouchers' }, - { name: 'Client Guardian', version: '0.4.0-r1', note: 'Portail captif + contrôle d\'accès' }, - { name: 'Bandwidth Manager', version: '0.4.0-r1', note: 'QoS + quotas' }, - { name: 'Media Flow', version: '0.4.0-r1', note: 'DPI streaming' }, - { name: 'CDN Cache', version: '0.5.0-r1', note: 'Cache contenu local' }, - { name: 'VHost Manager', version: '0.4.1-r3', note: 'Reverse proxy / SSL' }, - { name: 'KSM Manager', version: '0.4.0-r1', note: 'Gestion clés / HSM' } + // Per-module status overview (will be populated dynamically) + moduleStatus: [], + + // Static module definitions (fallback if API fails) + staticModuleStatus: [ + { name: 'SecuBox Central Hub', version: '0.7.0-r6', note: 'Dashboard central + Appstore (5 apps)', id: 'secubox-admin' }, + { name: 'System Hub', version: '0.5.1-r2', note: 'Centre de contrôle', id: 'system-hub' }, + { name: 'Traffic Shaper', version: '0.4.0-r1', note: 'CAKE / fq_codel / HTB', id: 'traffic-shaper' }, + { name: 'CrowdSec Dashboard', version: '0.5.0-r1', note: 'Détection d\'intrusions', id: 'crowdsec' }, + { name: 'Netdata Dashboard', version: '0.5.0-r1', note: 'Monitoring temps réel', id: 'netdata' }, + { name: 'Netifyd Dashboard', version: '0.4.0-r1', note: 'Intelligence applicative', id: 'netifyd' }, + { name: 'Network Modes', version: '0.5.0-r1', note: '5 topologies réseau', id: 'network-modes' }, + { name: 'WireGuard Dashboard', version: '0.4.0-r1', note: 'VPN + QR codes', id: 'wireguard' }, + { name: 'Auth Guardian', version: '0.4.0-r1', note: 'OAuth / vouchers', id: 'auth-guardian' }, + { name: 'Client Guardian', version: '0.4.0-r1', note: 'Portail captif + contrôle d\'accès', id: 'client-guardian' }, + { name: 'Bandwidth Manager', version: '0.4.0-r1', note: 'QoS + quotas', id: 'bandwidth-manager' }, + { name: 'Media Flow', version: '0.4.0-r1', note: 'DPI streaming', id: 'media-flow' }, + { name: 'CDN Cache', version: '0.5.0-r1', note: 'Cache contenu local', id: 'cdn-cache' }, + { name: 'VHost Manager', version: '0.4.1-r3', note: 'Reverse proxy / SSL', id: 'vhost-manager' }, + { name: 'KSM Manager', version: '0.4.0-r1', note: 'Gestion clés / HSM', id: 'ksm-manager' } ], // Overall project statistics @@ -159,6 +162,92 @@ const DevStatusWidget = { } ], + /** + * Fetch and synchronize module versions from system + */ + async syncModuleVersions() { + try { + // Try to fetch from secubox via ubus + const appsData = await L.resolveDefault( + L.Request.post(L.env.ubus_rpc_session ? '/ubus/' : '/ubus', { + 'jsonrpc': '2.0', + 'id': 1, + 'method': 'call', + 'params': [ + L.env.ubus_rpc_session, + 'luci.secubox', + 'get_apps', + {} + ] + }), + null + ); + + if (!appsData || !appsData.json || !appsData.json().result || !appsData.json().result[1]) { + console.warn('[DevStatus] API not available, using static data'); + this.moduleStatus = this.staticModuleStatus; + return; + } + + const result = appsData.json().result[1]; + const apps = result.apps || []; + + // Also get modules status + const modulesData = await L.resolveDefault( + L.Request.post(L.env.ubus_rpc_session ? '/ubus/' : '/ubus', { + 'jsonrpc': '2.0', + 'id': 2, + 'method': 'call', + 'params': [ + L.env.ubus_rpc_session, + 'luci.secubox', + 'get_modules', + {} + ] + }), + null + ); + + const modules = modulesData && modulesData.json() && modulesData.json().result && modulesData.json().result[1] ? + modulesData.json().result[1].modules || {} : {}; + + // Map apps to module status + this.moduleStatus = this.staticModuleStatus.map(staticModule => { + const app = apps.find(a => a.id === staticModule.id || a.name === staticModule.name); + + let installed = false; + let running = false; + + if (app && app.packages && app.packages.required && app.packages.required[0]) { + const pkgName = app.packages.required[0]; + const moduleInfo = modules[pkgName]; + if (moduleInfo) { + installed = moduleInfo.enabled || false; + running = moduleInfo.running || false; + } + } + + if (app) { + return { + name: staticModule.name, + version: app.pkg_version || app.version || staticModule.version, + note: staticModule.note, + id: staticModule.id, + installed: installed, + running: running + }; + } + + return staticModule; + }); + + console.log('[DevStatus] Module versions synchronized:', this.moduleStatus.length, 'modules'); + } catch (error) { + console.error('[DevStatus] Failed to sync module versions:', error); + this.moduleStatus = this.staticModuleStatus; + } + }, + /** * Calculate overall progress */ @@ -184,13 +273,27 @@ const DevStatusWidget = { /** * Render the widget */ - render(containerId) { + async render(containerId) { const container = document.getElementById(containerId); if (!container) { console.error(`Container #${containerId} not found`); return; } + // Show loading state + container.innerHTML = ` +
+
+
+

Loading development status...

+
+
+ `; + + // Fetch and sync module versions + await this.syncModuleVersions(); + + // Render with fresh data const overallProgress = this.getModulesOverallProgress(); const currentPhase = this.getCurrentPhase(); @@ -351,11 +454,22 @@ const DevStatusWidget = { const statusLabel = status === 'completed' ? `Prêt pour v${this.targetVersion}` : `Progression vers v${this.targetVersion}`; + + // Runtime status indicators + const runtimeStatus = module.running ? 'running' : (module.installed ? 'stopped' : 'not-installed'); + const runtimeIcon = module.running ? '🟢' : (module.installed ? '🟠' : '⚫'); + const runtimeLabel = module.running ? 'Running' : (module.installed ? 'Installed' : 'Not Installed'); + return `
${module.name} - ${this.formatVersion(module.version)} +
+ ${this.formatVersion(module.version)} + + ${runtimeIcon} + +
${status === 'completed' ? '✅' : '🔄'} @@ -577,6 +691,33 @@ const DevStatusWidget = { color: var(--sb-text, #f1f5f9); } + .dsw-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 200px; + gap: 16px; + } + + .dsw-loading .spinner { + width: 50px; + height: 50px; + border: 4px solid rgba(99, 102, 241, 0.1); + border-top-color: #6366f1; + border-radius: 50%; + animation: spin 1s linear infinite; + } + + .dsw-loading p { + color: var(--sb-text-muted, #94a3b8); + font-size: 14px; + } + + @keyframes spin { + to { transform: rotate(360deg); } + } + .dsw-header { display: flex; justify-content: space-between; @@ -983,16 +1124,60 @@ const DevStatusWidget = { .dsw-module-header { display: flex; justify-content: space-between; + align-items: center; gap: 12px; font-weight: 600; } + .dsw-module-badges { + display: flex; + align-items: center; + gap: 8px; + } + .dsw-module-version { font-family: 'JetBrains Mono', monospace; font-size: 13px; color: var(--sb-text-muted, #94a3b8); } + .dsw-module-runtime-badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + font-size: 12px; + border-radius: 50%; + cursor: help; + transition: transform 0.2s; + } + + .dsw-module-runtime-badge:hover { + transform: scale(1.2); + } + + .dsw-runtime-running { + background: rgba(16, 185, 129, 0.15); + box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.3); + animation: pulse-green 2s ease-in-out infinite; + } + + .dsw-runtime-stopped { + background: rgba(245, 158, 11, 0.15); + box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.3); + } + + .dsw-runtime-not-installed { + background: rgba(107, 116, 128, 0.15); + box-shadow: 0 0 0 2px rgba(107, 116, 128, 0.2); + } + + @keyframes pulse-green { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } + } + .dsw-module-status-row { display: flex; align-items: center; diff --git a/package/secubox/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub b/package/secubox/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub index 76c1b9e7..22b7b47f 100755 --- a/package/secubox/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub +++ b/package/secubox/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub @@ -1406,18 +1406,36 @@ save_settings() { # Get components (leverages secubox module detection) get_components() { - # Call secubox backend to get modules, which are the system components - local result=$(ubus call luci.secubox getModules 2>/dev/null) + # Call secubox backend to get apps list + local apps_result=$(ubus call luci.secubox list_apps 2>/dev/null) - if [ -n "$result" ]; then - # Pass through the secubox modules as components - echo "$result" + if [ -n "$apps_result" ]; then + # Transform apps to components format + echo "$apps_result" | jq '{ + modules: [ + .apps[] | { + id: .id, + name: .name, + version: (.pkg_version // .version // "1.0.0"), + category: (.category // "system"), + description: (.description // "No description"), + icon: (.icon // "📦"), + package: (.packages.required[0] // ""), + installed: false, + running: false, + color: ( + if .category == "security" then "#ef4444" + elif .category == "monitoring" then "#10b981" + elif .category == "network" then "#3b82f6" + else "#64748b" + end + ) + } + ] + }' else # Fallback if secubox is not available - json_init - json_add_array "modules" - json_close_array - json_dump + echo '{"modules":[]}' fi } diff --git a/package/secubox/secubox-core/root/usr/libexec/rpcd/luci.secubox b/package/secubox/secubox-core/root/usr/libexec/rpcd/luci.secubox index 8e955d0a..d584a2ea 100755 --- a/package/secubox/secubox-core/root/usr/libexec/rpcd/luci.secubox +++ b/package/secubox/secubox-core/root/usr/libexec/rpcd/luci.secubox @@ -864,6 +864,52 @@ case "$1" in if [ "$widget_enabled" = "true" ]; then json_add_boolean "widget_enabled" true + # Get version information from catalog + catalog_version=$(jsonfilter -i "$CATALOG_FILE" -e "@.plugins[@.id='$app_id'].version" 2>/dev/null) + pkg_version=$(jsonfilter -i "$CATALOG_FILE" -e "@.plugins[@.id='$app_id'].pkg_version" 2>/dev/null) + + [ -n "$catalog_version" ] && json_add_string "catalog_version" "$catalog_version" + [ -n "$pkg_version" ] && json_add_string "pkg_version" "$pkg_version" + + # Get installed version from opkg + installed_version="" + if [ -n "$pkg_version" ]; then + package_name=$(jsonfilter -i "$CATALOG_FILE" -e "@.plugins[@.id='$app_id'].packages.required[0]" 2>/dev/null) + if [ -n "$package_name" ]; then + installed_version=$(opkg info "$package_name" 2>/dev/null | awk '/^Version:/ {print $2}') + fi + fi + [ -n "$installed_version" ] && json_add_string "installed_version" "$installed_version" + + # Check if installed and running + json_add_boolean "installed" false + json_add_boolean "running" false + json_add_string "status" "unknown" + + # Check installation status via ubus + if command -v ubus >/dev/null 2>&1; then + modules_json=$(ubus call luci.secubox get_modules 2>/dev/null) + if [ -n "$modules_json" ]; then + package_name=$(jsonfilter -i "$CATALOG_FILE" -e "@.plugins[@.id='$app_id'].packages.required[0]" 2>/dev/null) + if [ -n "$package_name" ]; then + module_enabled=$(echo "$modules_json" | jsonfilter -e "@.modules['$package_name'].enabled" 2>/dev/null) + module_running=$(echo "$modules_json" | jsonfilter -e "@.modules['$package_name'].running" 2>/dev/null) + + [ "$module_enabled" = "true" ] && json_add_boolean "installed" true + [ "$module_running" = "true" ] && json_add_boolean "running" true + + # Set status based on state + if [ "$module_running" = "true" ]; then + json_add_string "status" "running" + elif [ "$module_enabled" = "true" ]; then + json_add_string "status" "stopped" + else + json_add_string "status" "not_installed" + fi + fi + fi + fi + # Get metrics from catalog definition # This would call app-specific data sources (ubus, files, etc.) # For now, return placeholder structure diff --git a/package/secubox/secubox-core/root/usr/sbin/secubox-appstore b/package/secubox/secubox-core/root/usr/sbin/secubox-appstore index 4425548b..007d3aeb 100755 --- a/package/secubox/secubox-core/root/usr/sbin/secubox-appstore +++ b/package/secubox/secubox-core/root/usr/sbin/secubox-appstore @@ -92,7 +92,6 @@ get_active_catalog() { # List all modules list_modules() { local format="${1:-table}" - local modules=() local cache_ready=0 mkdir -p "$CATALOG_DIR"