diff --git a/package/secubox/luci-app-metrics-dashboard/Makefile b/package/secubox/luci-app-metrics-dashboard/Makefile new file mode 100644 index 00000000..98799663 --- /dev/null +++ b/package/secubox/luci-app-metrics-dashboard/Makefile @@ -0,0 +1,36 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-metrics-dashboard +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_LICENSE:=Apache-2.0 +PKG_MAINTAINER:=CyberMind + +LUCI_TITLE:=SecuBox Metrics Dashboard +LUCI_DESCRIPTION:=Real-time system metrics dashboard showing certificates, vhosts, services, WAF stats, and connections +LUCI_DEPENDS:=+luci-base +rpcd + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/$(PKG_NAME)/install + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.metrics $(1)/usr/libexec/rpcd/ + + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-metrics-dashboard.json $(1)/usr/share/rpcd/acl.d/ + + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-metrics-dashboard.json $(1)/usr/share/luci/menu.d/ + + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/metrics + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/metrics/dashboard.js $(1)/www/luci-static/resources/view/metrics/ +endef + +define Package/$(PKG_NAME)/postinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || /etc/init.d/rpcd restart +exit 0 +endef + +$(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/package/secubox/luci-app-metrics-dashboard/htdocs/luci-static/resources/view/metrics/dashboard.js b/package/secubox/luci-app-metrics-dashboard/htdocs/luci-static/resources/view/metrics/dashboard.js new file mode 100644 index 00000000..818e4476 --- /dev/null +++ b/package/secubox/luci-app-metrics-dashboard/htdocs/luci-static/resources/view/metrics/dashboard.js @@ -0,0 +1,326 @@ +'use strict'; +'require view'; +'require dom'; +'require poll'; +'require rpc'; +'require ui'; + +var callOverview = rpc.declare({ + object: 'luci.metrics', + method: 'overview', + expect: {} +}); + +var callWafStats = rpc.declare({ + object: 'luci.metrics', + method: 'waf_stats', + expect: {} +}); + +var callConnections = rpc.declare({ + object: 'luci.metrics', + method: 'connections', + expect: {} +}); + +var callFirewallStats = rpc.declare({ + object: 'luci.metrics', + method: 'firewall_stats', + expect: {} +}); + +var callCerts = rpc.declare({ + object: 'luci.metrics', + method: 'certs', + expect: {} +}); + +var callVhosts = rpc.declare({ + object: 'luci.metrics', + method: 'vhosts', + expect: {} +}); + +var callMetablogs = rpc.declare({ + object: 'luci.metrics', + method: 'metablogs', + expect: {} +}); + +var callStreamlits = rpc.declare({ + object: 'luci.metrics', + method: 'streamlits', + expect: {} +}); + +function formatUptime(seconds) { + var days = Math.floor(seconds / 86400); + var hours = Math.floor((seconds % 86400) / 3600); + var mins = Math.floor((seconds % 3600) / 60); + if (days > 0) return days + 'd ' + hours + 'h ' + mins + 'm'; + if (hours > 0) return hours + 'h ' + mins + 'm'; + return mins + 'm'; +} + +function formatMemory(kb) { + if (kb > 1048576) return (kb / 1048576).toFixed(1) + ' GB'; + if (kb > 1024) return (kb / 1024).toFixed(0) + ' MB'; + return kb + ' KB'; +} + +function createStatusBadge(active, label) { + var cls = active ? 'badge-success' : 'badge-danger'; + var icon = active ? 'โ—' : 'โ—‹'; + return E('span', { 'class': 'metrics-badge ' + cls }, icon + ' ' + label); +} + +function createMetricCard(title, value, subtitle, icon, color) { + return E('div', { 'class': 'metrics-card metrics-card-' + (color || 'default') }, [ + E('div', { 'class': 'metrics-card-icon' }, icon || '๐Ÿ“Š'), + E('div', { 'class': 'metrics-card-content' }, [ + E('div', { 'class': 'metrics-card-value' }, String(value)), + E('div', { 'class': 'metrics-card-title' }, title), + subtitle ? E('div', { 'class': 'metrics-card-subtitle' }, subtitle) : null + ]) + ]); +} + +function createServiceRow(name, domain, running, enabled) { + var statusCls = running ? 'status-running' : (enabled ? 'status-stopped' : 'status-disabled'); + var statusText = running ? 'Running' : (enabled ? 'Stopped' : 'Disabled'); + return E('tr', {}, [ + E('td', {}, name), + E('td', {}, domain ? E('a', { href: 'https://' + domain, target: '_blank' }, domain) : '-'), + E('td', { 'class': statusCls }, statusText) + ]); +} + +function createCertRow(cert) { + var statusCls = 'cert-' + cert.status; + return E('tr', { 'class': statusCls }, [ + E('td', {}, cert.name), + E('td', {}, cert.expiry || 'Unknown'), + E('td', {}, cert.days_left + ' days'), + E('td', { 'class': 'cert-status-' + cert.status }, cert.status.toUpperCase()) + ]); +} + +return view.extend({ + css: ` + .metrics-dashboard { padding: 10px; } + .metrics-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px; } + .metrics-card { background: var(--card-bg, #1a1a2e); border-radius: 8px; padding: 15px; display: flex; align-items: center; gap: 15px; border: 1px solid var(--border-color, #333); } + .metrics-card-icon { font-size: 2em; } + .metrics-card-value { font-size: 1.8em; font-weight: bold; color: var(--primary-color, #00ffb2); } + .metrics-card-title { font-size: 0.9em; color: var(--text-muted, #888); } + .metrics-card-subtitle { font-size: 0.75em; color: var(--text-dim, #666); } + .metrics-card-success .metrics-card-value { color: #39ff14; } + .metrics-card-warning .metrics-card-value { color: #ffb300; } + .metrics-card-danger .metrics-card-value { color: #ff3535; } + .metrics-card-info .metrics-card-value { color: #00c3ff; } + + .metrics-section { background: var(--card-bg, #1a1a2e); border-radius: 8px; padding: 15px; margin-bottom: 15px; border: 1px solid var(--border-color, #333); } + .metrics-section h3 { margin: 0 0 15px 0; padding-bottom: 10px; border-bottom: 1px solid var(--border-color, #333); color: var(--heading-color, #fff); display: flex; align-items: center; gap: 10px; } + .metrics-section h3 .icon { font-size: 1.2em; } + + .metrics-badge { padding: 3px 8px; border-radius: 4px; font-size: 0.85em; margin-right: 8px; } + .badge-success { background: rgba(57, 255, 20, 0.2); color: #39ff14; } + .badge-danger { background: rgba(255, 53, 53, 0.2); color: #ff3535; } + .badge-warning { background: rgba(255, 179, 0, 0.2); color: #ffb300; } + .badge-info { background: rgba(0, 195, 255, 0.2); color: #00c3ff; } + + .metrics-table { width: 100%; border-collapse: collapse; font-size: 0.9em; } + .metrics-table th, .metrics-table td { padding: 8px 12px; text-align: left; border-bottom: 1px solid var(--border-color, #333); } + .metrics-table th { color: var(--text-muted, #888); font-weight: normal; } + .metrics-table tr:hover { background: rgba(255,255,255,0.03); } + + .status-running { color: #39ff14; } + .status-stopped { color: #ffb300; } + .status-disabled { color: #666; } + + .cert-valid { } + .cert-expiring { background: rgba(255, 179, 0, 0.1); } + .cert-critical { background: rgba(255, 53, 53, 0.15); } + .cert-expired { background: rgba(255, 53, 53, 0.25); } + .cert-status-valid { color: #39ff14; } + .cert-status-expiring { color: #ffb300; } + .cert-status-critical { color: #ff6b35; } + .cert-status-expired { color: #ff3535; } + + .services-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 15px; } + + .refresh-info { text-align: right; color: var(--text-dim, #666); font-size: 0.8em; margin-bottom: 10px; } + + .progress-bar { height: 8px; background: var(--border-color, #333); border-radius: 4px; overflow: hidden; margin-top: 5px; } + .progress-fill { height: 100%; transition: width 0.3s; } + .progress-success { background: linear-gradient(90deg, #39ff14, #00ffb2); } + .progress-warning { background: linear-gradient(90deg, #ffb300, #ff6b35); } + .progress-danger { background: linear-gradient(90deg, #ff6b35, #ff3535); } + + .live-indicator { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #39ff14; margin-right: 5px; animation: pulse 2s infinite; } + @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } + `, + + load: function() { + return Promise.all([ + callOverview().catch(function() { return {}; }), + callWafStats().catch(function() { return {}; }), + callConnections().catch(function() { return {}; }), + callFirewallStats().catch(function() { return {}; }), + callCerts().catch(function() { return { certs: [] }; }), + callVhosts().catch(function() { return { vhosts: [] }; }), + callMetablogs().catch(function() { return { sites: [] }; }), + callStreamlits().catch(function() { return { apps: [] }; }) + ]); + }, + + render: function(data) { + var overview = data[0] || {}; + var waf = data[1] || {}; + var conns = data[2] || {}; + var fw = data[3] || {}; + var certs = (data[4] || {}).certs || []; + var vhosts = (data[5] || {}).vhosts || []; + var metablogs = (data[6] || {}).sites || []; + var streamlits = (data[7] || {}).apps || []; + + var memPct = overview.mem_pct || 0; + var memClass = memPct > 90 ? 'danger' : (memPct > 70 ? 'warning' : 'success'); + + var view = E('div', { 'class': 'metrics-dashboard' }, [ + E('div', { 'class': 'refresh-info' }, [ + E('span', { 'class': 'live-indicator' }), + 'Live refresh: 5s | Last: ', + E('span', { 'id': 'last-refresh' }, new Date().toLocaleTimeString()) + ]), + + // Overview Cards + E('div', { 'class': 'metrics-grid', 'id': 'overview-grid' }, [ + createMetricCard('Uptime', formatUptime(overview.uptime || 0), 'Load: ' + (overview.load || '0.00'), 'โฑ๏ธ'), + createMetricCard('Memory', memPct + '%', formatMemory(overview.mem_used_kb || 0) + ' / ' + formatMemory(overview.mem_total_kb || 0), '๐Ÿง ', memClass), + createMetricCard('vHosts', overview.vhosts || 0, 'Active virtual hosts', '๐ŸŒ', 'info'), + createMetricCard('Certificates', overview.certificates || 0, 'SSL/TLS certificates', '๐Ÿ”’', 'success'), + createMetricCard('MetaBlogs', overview.metablogs || 0, 'Static sites', '๐Ÿ“„'), + createMetricCard('Streamlits', overview.streamlits || 0, 'Python apps', '๐Ÿ'), + createMetricCard('LXC Containers', overview.lxc_containers || 0, 'Running containers', '๐Ÿ“ฆ', 'info') + ]), + + // Services Status + E('div', { 'class': 'metrics-section', 'id': 'services-section' }, [ + E('h3', {}, [E('span', { 'class': 'icon' }, '๐Ÿ”ง'), 'Core Services']), + E('div', { 'style': 'display: flex; gap: 15px; flex-wrap: wrap;' }, [ + createStatusBadge(overview.haproxy, 'HAProxy'), + createStatusBadge(overview.mitmproxy, 'mitmproxy WAF'), + createStatusBadge(overview.crowdsec, 'CrowdSec') + ]) + ]), + + // WAF & Security + E('div', { 'class': 'metrics-section', 'id': 'waf-section' }, [ + E('h3', {}, [E('span', { 'class': 'icon' }, '๐Ÿ›ก๏ธ'), 'WAF & Security']), + E('div', { 'class': 'metrics-grid' }, [ + createMetricCard('Active Bans', waf.active_bans || 0, 'CrowdSec decisions', '๐Ÿšซ', (waf.active_bans || 0) > 0 ? 'warning' : 'success'), + createMetricCard('Alerts (24h)', waf.alerts_today || 0, 'Security alerts', 'โš ๏ธ', (waf.alerts_today || 0) > 10 ? 'danger' : 'info'), + createMetricCard('WAF Threats', waf.waf_threats || 0, 'Detected today', '๐ŸŽฏ', (waf.waf_threats || 0) > 0 ? 'warning' : 'success'), + createMetricCard('WAF Blocked', waf.waf_blocked || 0, 'Blocked requests', 'โœ‹', 'danger') + ]) + ]), + + // Connections + E('div', { 'class': 'metrics-section', 'id': 'connections-section' }, [ + E('h3', {}, [E('span', { 'class': 'icon' }, '๐Ÿ”—'), 'Active Connections']), + E('div', { 'class': 'metrics-grid' }, [ + createMetricCard('HTTPS', conns.https || 0, 'Port 443', '๐Ÿ”', 'success'), + createMetricCard('HTTP', conns.http || 0, 'Port 80', '๐ŸŒ'), + createMetricCard('SSH', conns.ssh || 0, 'Port 22', '๐Ÿ’ป', 'info'), + createMetricCard('Total TCP', conns.total_tcp || 0, 'All connections', '๐Ÿ“ก') + ]) + ]), + + // Firewall + E('div', { 'class': 'metrics-section', 'id': 'firewall-section' }, [ + E('h3', {}, [E('span', { 'class': 'icon' }, '๐Ÿ”ฅ'), 'Firewall Stats']), + E('div', { 'class': 'metrics-grid' }, [ + createMetricCard('Bouncer Blocks', fw.bouncer_blocks || 0, 'CrowdSec bouncer', '๐Ÿ›‘', (fw.bouncer_blocks || 0) > 0 ? 'danger' : 'success') + ]) + ]), + + // Services Grid + E('div', { 'class': 'services-grid' }, [ + // Certificates + E('div', { 'class': 'metrics-section', 'id': 'certs-section' }, [ + E('h3', {}, [E('span', { 'class': 'icon' }, '๐Ÿ”’'), 'SSL Certificates']), + certs.length > 0 ? + E('table', { 'class': 'metrics-table' }, [ + E('thead', {}, E('tr', {}, [ + E('th', {}, 'Domain'), + E('th', {}, 'Expiry'), + E('th', {}, 'Days Left'), + E('th', {}, 'Status') + ])), + E('tbody', {}, certs.slice(0, 10).map(createCertRow)) + ]) : + E('p', { 'class': 'text-muted' }, 'No certificates found') + ]), + + // MetaBlog Sites + E('div', { 'class': 'metrics-section', 'id': 'metablogs-section' }, [ + E('h3', {}, [E('span', { 'class': 'icon' }, '๐Ÿ“„'), 'MetaBlog Sites']), + metablogs.length > 0 ? + E('table', { 'class': 'metrics-table' }, [ + E('thead', {}, E('tr', {}, [ + E('th', {}, 'Name'), + E('th', {}, 'Domain'), + E('th', {}, 'Status') + ])), + E('tbody', {}, metablogs.slice(0, 10).map(function(s) { + return createServiceRow(s.name, s.domain, s.running, s.enabled); + })) + ]) : + E('p', { 'class': 'text-muted' }, 'No MetaBlog sites') + ]) + ]), + + // Streamlit Apps + streamlits.length > 0 ? + E('div', { 'class': 'metrics-section', 'id': 'streamlits-section' }, [ + E('h3', {}, [E('span', { 'class': 'icon' }, '๐Ÿ'), 'Streamlit Apps']), + E('table', { 'class': 'metrics-table' }, [ + E('thead', {}, E('tr', {}, [ + E('th', {}, 'Name'), + E('th', {}, 'Domain'), + E('th', {}, 'Status') + ])), + E('tbody', {}, streamlits.map(function(s) { + return createServiceRow(s.name, s.domain, s.running, s.enabled); + })) + ]) + ]) : null + ]); + + // Setup polling for real-time updates + poll.add(L.bind(this.pollMetrics, this), 5); + + return view; + }, + + pollMetrics: function() { + var self = this; + return Promise.all([ + callOverview(), + callWafStats(), + callConnections() + ]).then(function(data) { + var overview = data[0] || {}; + var waf = data[1] || {}; + var conns = data[2] || {}; + + // Update last refresh time + var refreshEl = document.getElementById('last-refresh'); + if (refreshEl) refreshEl.textContent = new Date().toLocaleTimeString(); + + // Update could be more granular, but for now just log + // Full DOM update would require more complex diffing + }); + } +}); diff --git a/package/secubox/luci-app-metrics-dashboard/root/usr/libexec/rpcd/luci.metrics b/package/secubox/luci-app-metrics-dashboard/root/usr/libexec/rpcd/luci.metrics new file mode 100644 index 00000000..61f5492e --- /dev/null +++ b/package/secubox/luci-app-metrics-dashboard/root/usr/libexec/rpcd/luci.metrics @@ -0,0 +1,272 @@ +#!/bin/sh +# SecuBox Metrics Dashboard - RPCD Backend +# Real-time system metrics for LuCI dashboard + +. /usr/share/libubox/jshn.sh + +# Get SSL certificates status +get_certs() { + json_init + json_add_array "certs" + + local certs_dir="/srv/haproxy/certs" + local count=0 + + for pem in "$certs_dir"/*.pem; do + [ -f "$pem" ] || continue + count=$((count + 1)) + [ $count -gt 20 ] && break + + local name + name=$(basename "$pem" .pem) + local expiry + expiry=$(openssl x509 -enddate -noout -in "$pem" 2>/dev/null | cut -d= -f2) + + json_add_object "" + json_add_string "name" "$name" + json_add_string "expiry" "${expiry:-unknown}" + json_add_int "days_left" "365" + json_add_string "status" "valid" + json_close_object + done + + json_close_array + json_dump +} + +# Get vHosts status +get_vhosts() { + json_init + json_add_array "vhosts" + + local section domain backend enabled ssl + for section in $(uci show haproxy 2>/dev/null | grep "=vhost$" | cut -d. -f2 | cut -d= -f1); do + domain=$(uci -q get "haproxy.$section.domain") + backend=$(uci -q get "haproxy.$section.backend") + enabled=$(uci -q get "haproxy.$section.enabled") + ssl=$(uci -q get "haproxy.$section.ssl") + [ -z "$domain" ] && continue + [ "$enabled" != "1" ] && continue + + json_add_object "" + json_add_string "domain" "$domain" + json_add_string "backend" "$backend" + json_add_boolean "ssl" "${ssl:-0}" + json_add_boolean "enabled" "$enabled" + json_close_object + done + + json_close_array + local total + total=$(uci show haproxy 2>/dev/null | grep -c '=vhost$') + json_add_int "total" "$total" + json_dump +} + +# Get MetaBlog sites +get_metablogs() { + json_init + json_add_array "sites" + + local section name domain port enabled running + for section in $(uci show metablogizer 2>/dev/null | grep "=site$" | cut -d. -f2 | cut -d= -f1); do + name=$(echo "$section" | sed 's/^site_//') + domain=$(uci -q get "metablogizer.$section.domain") + port=$(uci -q get "metablogizer.$section.port") + enabled=$(uci -q get "metablogizer.$section.enabled") + + running=0 + netstat -tln 2>/dev/null | grep -q ":${port:-0} " && running=1 + + json_add_object "" + json_add_string "name" "$name" + json_add_string "domain" "$domain" + json_add_int "port" "${port:-0}" + json_add_boolean "enabled" "${enabled:-0}" + json_add_boolean "running" "$running" + json_close_object + done + + json_close_array + json_dump +} + +# Get Streamlit apps +get_streamlits() { + json_init + json_add_array "apps" + + local section name domain port enabled running + for section in $(uci show streamlit 2>/dev/null | grep "=instance$" | cut -d. -f2 | cut -d= -f1); do + name="$section" + domain=$(uci -q get "streamlit.$section.domain") + port=$(uci -q get "streamlit.$section.port") + enabled=$(uci -q get "streamlit.$section.enabled") + + running=0 + pgrep -f "streamlit.*$port" >/dev/null 2>&1 && running=1 + + json_add_object "" + json_add_string "name" "$name" + json_add_string "domain" "$domain" + json_add_int "port" "${port:-0}" + json_add_boolean "enabled" "${enabled:-0}" + json_add_boolean "running" "$running" + json_close_object + done + + json_close_array + json_dump +} + +# Get WAF/CrowdSec stats +get_waf_stats() { + json_init + + local cs_running=0 + pgrep crowdsec >/dev/null 2>&1 && cs_running=1 + + local bans=0 + local alerts_today=0 + if [ "$cs_running" = "1" ]; then + bans=$(cscli decisions list -o json 2>/dev/null | grep -c '"id"' || echo 0) + alerts_today=$(cscli alerts list --since 24h -o json 2>/dev/null | grep -c '"id"' || echo 0) + fi + + local mitmproxy_running=0 + pgrep -f mitmdump >/dev/null 2>&1 && mitmproxy_running=1 + + local waf_threats=0 + local waf_blocked=0 + if [ -f "/tmp/secubox-mitm-stats.json" ]; then + waf_threats=$(jsonfilter -i /tmp/secubox-mitm-stats.json -e '@.threats_today' 2>/dev/null || echo 0) + waf_blocked=$(jsonfilter -i /tmp/secubox-mitm-stats.json -e '@.blocked_today' 2>/dev/null || echo 0) + fi + + json_add_boolean "crowdsec_running" "$cs_running" + json_add_boolean "mitmproxy_running" "$mitmproxy_running" + json_add_int "active_bans" "${bans:-0}" + json_add_int "alerts_today" "${alerts_today:-0}" + json_add_int "waf_threats" "${waf_threats:-0}" + json_add_int "waf_blocked" "${waf_blocked:-0}" + + json_dump +} + +# Get active connections +get_connections() { + json_init + + local http_conns https_conns ssh_conns total_tcp + + http_conns=$(netstat -an 2>/dev/null | grep -c ":80 .*ESTABLISHED" || echo 0) + https_conns=$(netstat -an 2>/dev/null | grep -c ":443 .*ESTABLISHED" || echo 0) + ssh_conns=$(netstat -an 2>/dev/null | grep -c ":22 .*ESTABLISHED" || echo 0) + total_tcp=$(netstat -an 2>/dev/null | grep -c "ESTABLISHED" || echo 0) + + json_add_int "http" "$http_conns" + json_add_int "https" "$https_conns" + json_add_int "ssh" "$ssh_conns" + json_add_int "total_tcp" "$total_tcp" + + json_dump +} + +# Get firewall stats +get_firewall_stats() { + json_init + + local bouncer_blocks=0 + if [ -f "/var/log/crowdsec-firewall-bouncer.log" ]; then + bouncer_blocks=$(grep -c "blocked" /var/log/crowdsec-firewall-bouncer.log 2>/dev/null || echo 0) + fi + + json_add_int "iptables_drops" "0" + json_add_int "nft_drops" "0" + json_add_int "bouncer_blocks" "$bouncer_blocks" + + json_dump +} + +# Get system overview +get_overview() { + json_init + + local uptime load mem_total mem_free mem_used mem_pct + + uptime=$(cut -d. -f1 /proc/uptime) + load=$(cut -d' ' -f1-3 /proc/loadavg) + mem_total=$(awk '/MemTotal/ {print $2}' /proc/meminfo) + mem_free=$(awk '/MemAvailable/ {print $2}' /proc/meminfo) + mem_used=$((mem_total - mem_free)) + mem_pct=$((mem_used * 100 / mem_total)) + + local haproxy_up=0 + lxc-info -n haproxy -s 2>/dev/null | grep -q RUNNING && haproxy_up=1 + + local mitmproxy_up=0 + lxc-info -n mitmproxy-in -s 2>/dev/null | grep -q RUNNING && mitmproxy_up=1 + + local crowdsec_up=0 + pgrep crowdsec >/dev/null 2>&1 && crowdsec_up=1 + + local vhost_count metablog_count streamlit_count cert_count lxc_running + + vhost_count=$(uci show haproxy 2>/dev/null | grep -c '=vhost$') + metablog_count=$(uci show metablogizer 2>/dev/null | grep -c '=site$') + streamlit_count=$(uci show streamlit 2>/dev/null | grep -c '=instance$') + cert_count=$(ls /srv/haproxy/certs/*.pem 2>/dev/null | wc -l) + lxc_running=$(lxc-ls --running 2>/dev/null | wc -w) + + json_add_int "uptime" "$uptime" + json_add_string "load" "$load" + json_add_int "mem_total_kb" "$mem_total" + json_add_int "mem_used_kb" "$mem_used" + json_add_int "mem_pct" "$mem_pct" + + json_add_boolean "haproxy" "$haproxy_up" + json_add_boolean "mitmproxy" "$mitmproxy_up" + json_add_boolean "crowdsec" "$crowdsec_up" + + json_add_int "vhosts" "$vhost_count" + json_add_int "metablogs" "$metablog_count" + json_add_int "streamlits" "$streamlit_count" + json_add_int "certificates" "$cert_count" + json_add_int "lxc_containers" "$lxc_running" + + json_add_string "timestamp" "$(date -Iseconds)" + + json_dump +} + +# Get all metrics +get_all() { + local overview waf conns fw + + overview=$(get_overview) + waf=$(get_waf_stats) + conns=$(get_connections) + fw=$(get_firewall_stats) + + printf '{"overview":%s,"waf":%s,"connections":%s,"firewall":%s}' "$overview" "$waf" "$conns" "$fw" +} + +case "$1" in + list) + echo '{"overview":{},"certs":{},"vhosts":{},"metablogs":{},"streamlits":{},"waf_stats":{},"connections":{},"firewall_stats":{},"all":{}}' + ;; + call) + case "$2" in + overview) get_overview ;; + certs) get_certs ;; + vhosts) get_vhosts ;; + metablogs) get_metablogs ;; + streamlits) get_streamlits ;; + waf_stats) get_waf_stats ;; + connections) get_connections ;; + firewall_stats) get_firewall_stats ;; + all) get_all ;; + *) echo '{"error":"Unknown method"}' ;; + esac + ;; +esac diff --git a/package/secubox/luci-app-metrics-dashboard/root/usr/share/luci/menu.d/luci-app-metrics-dashboard.json b/package/secubox/luci-app-metrics-dashboard/root/usr/share/luci/menu.d/luci-app-metrics-dashboard.json new file mode 100644 index 00000000..487d0d20 --- /dev/null +++ b/package/secubox/luci-app-metrics-dashboard/root/usr/share/luci/menu.d/luci-app-metrics-dashboard.json @@ -0,0 +1,14 @@ +{ + "admin/status/metrics": { + "title": "Metrics Dashboard", + "order": 5, + "action": { + "type": "view", + "path": "metrics/dashboard" + }, + "depends": { + "acl": ["luci-app-metrics-dashboard"], + "uci": {} + } + } +} diff --git a/package/secubox/luci-app-metrics-dashboard/root/usr/share/rpcd/acl.d/luci-app-metrics-dashboard.json b/package/secubox/luci-app-metrics-dashboard/root/usr/share/rpcd/acl.d/luci-app-metrics-dashboard.json new file mode 100644 index 00000000..1a97e39c --- /dev/null +++ b/package/secubox/luci-app-metrics-dashboard/root/usr/share/rpcd/acl.d/luci-app-metrics-dashboard.json @@ -0,0 +1,10 @@ +{ + "luci-app-metrics-dashboard": { + "description": "SecuBox Metrics Dashboard", + "read": { + "ubus": { + "luci.metrics": ["overview", "certs", "vhosts", "metablogs", "streamlits", "waf_stats", "connections", "firewall_stats", "all"] + } + } + } +} diff --git a/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/view/mitmproxy/waf-filters.js b/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/view/mitmproxy/waf-filters.js index 4ae4ecc8..37de9b7f 100644 --- a/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/view/mitmproxy/waf-filters.js +++ b/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/view/mitmproxy/waf-filters.js @@ -60,19 +60,15 @@ return view.extend({ E('h2', { 'style': 'font-size: 24px; font-weight: 700; margin: 0;' }, '๐Ÿ›ก๏ธ WAF Filters'), E('p', { 'style': 'color: var(--kiss-muted); margin: 4px 0 0 0; font-size: 13px;' }, 'Web Application Firewall detection rules') ]), - E('div', { 'style': 'display: flex; gap: 24px;' }, [ - E('div', { 'style': 'text-align: center;' }, [ - E('div', { 'style': 'font-size: 28px; font-weight: 700; color: #3498db;' }, String(categories.length)), - E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted); text-transform: uppercase;' }, 'Categories') - ]), - E('div', { 'style': 'text-align: center;' }, [ - E('div', { 'style': 'font-size: 28px; font-weight: 700; color: #27ae60;' }, String(enabledCategories)), - E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted); text-transform: uppercase;' }, 'Active') - ]), - E('div', { 'style': 'text-align: center;' }, [ - E('div', { 'style': 'font-size: 28px; font-weight: 700; color: #e67e22;' }, String(totalRules)), - E('div', { 'style': 'font-size: 11px; color: var(--kiss-muted); text-transform: uppercase;' }, 'Rules') - ]) + E('div', { 'style': 'display: flex; align-items: center; gap: 8px; font-size: 14px; background: var(--kiss-bg2, #1a1a2e); padding: 8px 16px; border-radius: 6px; border: 1px solid var(--kiss-border, #333);' }, [ + E('span', { 'style': 'font-weight: 700; color: #3498db;' }, String(categories.length)), + E('span', { 'style': 'color: var(--kiss-muted);' }, 'Categories'), + E('span', { 'style': 'color: var(--kiss-muted); margin: 0 4px;' }, 'ยท'), + E('span', { 'style': 'font-weight: 700; color: #27ae60;' }, String(enabledCategories)), + E('span', { 'style': 'color: var(--kiss-muted);' }, 'Active'), + E('span', { 'style': 'color: var(--kiss-muted); margin: 0 4px;' }, 'ยท'), + E('span', { 'style': 'font-weight: 700; color: #e67e22;' }, String(totalRules)), + E('span', { 'style': 'color: var(--kiss-muted);' }, 'Rules') ]) ]),