diff --git a/package/secubox/luci-app-vortex-firewall/Makefile b/package/secubox/luci-app-vortex-firewall/Makefile new file mode 100644 index 00000000..b83b64e0 --- /dev/null +++ b/package/secubox/luci-app-vortex-firewall/Makefile @@ -0,0 +1,26 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-vortex-firewall +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 +PKG_MAINTAINER:=SecuBox Team +PKG_LICENSE:=GPL-3.0 + +LUCI_TITLE:=LuCI Vortex DNS Firewall Dashboard +LUCI_DEPENDS:=+secubox-vortex-firewall +LUCI_PKGARCH:=all + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/luci-app-vortex-firewall/install + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/vortex-firewall + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/vortex-firewall/*.js $(1)/www/luci-static/resources/view/vortex-firewall/ + + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/*.json $(1)/usr/share/luci/menu.d/ + + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/*.json $(1)/usr/share/rpcd/acl.d/ +endef + +$(eval $(call BuildPackage,luci-app-vortex-firewall)) diff --git a/package/secubox/luci-app-vortex-firewall/htdocs/luci-static/resources/view/vortex-firewall/overview.js b/package/secubox/luci-app-vortex-firewall/htdocs/luci-static/resources/view/vortex-firewall/overview.js new file mode 100644 index 00000000..3d7fa794 --- /dev/null +++ b/package/secubox/luci-app-vortex-firewall/htdocs/luci-static/resources/view/vortex-firewall/overview.js @@ -0,0 +1,437 @@ +'use strict'; +'require view'; +'require rpc'; +'require ui'; +'require poll'; + +var callStatus = rpc.declare({ + object: 'luci.vortex-firewall', + method: 'status', + expect: {} +}); + +var callGetStats = rpc.declare({ + object: 'luci.vortex-firewall', + method: 'get_stats', + expect: {} +}); + +var callGetFeeds = rpc.declare({ + object: 'luci.vortex-firewall', + method: 'get_feeds', + expect: {} +}); + +var callGetBlocked = rpc.declare({ + object: 'luci.vortex-firewall', + method: 'get_blocked', + params: ['limit'], + expect: {} +}); + +var callSearch = rpc.declare({ + object: 'luci.vortex-firewall', + method: 'search', + params: ['domain'], + expect: {} +}); + +var callUpdateFeeds = rpc.declare({ + object: 'luci.vortex-firewall', + method: 'update_feeds', + expect: {} +}); + +var callBlockDomain = rpc.declare({ + object: 'luci.vortex-firewall', + method: 'block_domain', + params: ['domain', 'reason'], + expect: {} +}); + +var callUnblockDomain = rpc.declare({ + object: 'luci.vortex-firewall', + method: 'unblock_domain', + params: ['domain'], + expect: {} +}); + +function formatNumber(n) { + if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'; + if (n >= 1000) return (n / 1000).toFixed(1) + 'K'; + return String(n || 0); +} + +function getThreatBadge(threat) { + var colors = { + 'malware': '#e74c3c', + 'phishing': '#f39c12', + 'c2': '#9b59b6', + 'spam': '#95a5a6', + 'manual': '#3498db', + 'dnsguard': '#1abc9c' + }; + var color = colors[threat] || '#7f8c8d'; + return E('span', { + 'style': 'background:' + color + ';color:#fff;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;' + }, threat || 'unknown'); +} + +return view.extend({ + load: function() { + return Promise.all([ + callStatus(), + callGetStats(), + callGetFeeds(), + callGetBlocked(50) + ]); + }, + + renderStatusCards: function(status, stats) { + var active = status.active; + var domains = stats.domains || 0; + var hits = stats.hits || 0; + var x47 = stats.x47_impact || 0; + + return E('div', { 'class': 'vf-cards' }, [ + E('div', { 'class': 'vf-card' }, [ + E('div', { 'class': 'vf-card-icon', 'style': 'background:' + (active ? '#27ae60' : '#e74c3c') }, + active ? '\u2713' : '\u2717'), + E('div', { 'class': 'vf-card-content' }, [ + E('div', { 'class': 'vf-card-value' }, active ? 'Active' : 'Inactive'), + E('div', { 'class': 'vf-card-label' }, 'Firewall Status') + ]) + ]), + E('div', { 'class': 'vf-card' }, [ + E('div', { 'class': 'vf-card-icon', 'style': 'background:#3498db' }, '\uD83D\uDEE1'), + E('div', { 'class': 'vf-card-content' }, [ + E('div', { 'class': 'vf-card-value', 'data-stat': 'domains' }, formatNumber(domains)), + E('div', { 'class': 'vf-card-label' }, 'Blocked Domains') + ]) + ]), + E('div', { 'class': 'vf-card' }, [ + E('div', { 'class': 'vf-card-icon', 'style': 'background:#e74c3c' }, '\uD83D\uDEAB'), + E('div', { 'class': 'vf-card-content' }, [ + E('div', { 'class': 'vf-card-value', 'data-stat': 'hits' }, formatNumber(hits)), + E('div', { 'class': 'vf-card-label' }, 'Total Blocks') + ]) + ]), + E('div', { 'class': 'vf-card vf-card-highlight' }, [ + E('div', { 'class': 'vf-card-icon', 'style': 'background:#9b59b6' }, '\u00D747'), + E('div', { 'class': 'vf-card-content' }, [ + E('div', { 'class': 'vf-card-value', 'data-stat': 'x47' }, formatNumber(x47)), + E('div', { 'class': 'vf-card-label' }, 'Connections Prevented') + ]) + ]) + ]); + }, + + renderQuickActions: function() { + var self = this; + + return E('div', { 'class': 'vf-section' }, [ + E('h3', {}, 'Quick Actions'), + E('div', { 'class': 'vf-actions' }, [ + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': function() { self.handleUpdateFeeds(); } + }, '\uD83D\uDD04 Update Feeds'), + E('button', { + 'class': 'cbi-button cbi-button-add', + 'click': function() { self.handleBlockDomain(); } + }, '\u2795 Block Domain'), + E('button', { + 'class': 'cbi-button', + 'click': function() { self.handleSearchDomain(); } + }, '\uD83D\uDD0D Search Domain') + ]) + ]); + }, + + renderFeedsTable: function(feeds) { + var feedList = feeds.feeds || []; + + var rows = feedList.map(function(feed) { + return E('tr', {}, [ + E('td', {}, feed.name || '-'), + E('td', { 'style': 'text-align:right' }, formatNumber(feed.domains)), + E('td', {}, feed.updated || '-'), + E('td', {}, E('span', { + 'style': 'color:' + (feed.enabled ? '#27ae60' : '#e74c3c') + }, feed.enabled ? '\u2713 Enabled' : '\u2717 Disabled')) + ]); + }); + + if (rows.length === 0) { + rows.push(E('tr', {}, [ + E('td', { 'colspan': '4', 'style': 'text-align:center;color:#999' }, 'No feeds configured') + ])); + } + + return E('div', { 'class': 'vf-section' }, [ + E('h3', {}, 'Threat Intelligence Feeds'), + E('table', { 'class': 'table' }, [ + E('thead', {}, E('tr', {}, [ + E('th', {}, 'Feed'), + E('th', { 'style': 'text-align:right' }, 'Domains'), + E('th', {}, 'Last Update'), + E('th', {}, 'Status') + ])), + E('tbody', {}, rows) + ]) + ]); + }, + + renderBlockedTable: function(blocked) { + var domainList = blocked.domains || []; + + var rows = domainList.slice(0, 25).map(function(d) { + return E('tr', {}, [ + E('td', { 'style': 'font-family:monospace;font-size:12px' }, d.domain || '-'), + E('td', {}, getThreatBadge(d.threat)), + E('td', { 'style': 'text-align:center' }, String(d.confidence || 0) + '%'), + E('td', { 'style': 'text-align:right' }, formatNumber(d.hits)), + E('td', {}, d.source || '-') + ]); + }); + + if (rows.length === 0) { + rows.push(E('tr', {}, [ + E('td', { 'colspan': '5', 'style': 'text-align:center;color:#999' }, 'No blocked domains yet') + ])); + } + + return E('div', { 'class': 'vf-section' }, [ + E('h3', {}, 'Top Blocked Domains'), + E('table', { 'class': 'table' }, [ + E('thead', {}, E('tr', {}, [ + E('th', {}, 'Domain'), + E('th', {}, 'Threat'), + E('th', { 'style': 'text-align:center' }, 'Confidence'), + E('th', { 'style': 'text-align:right' }, 'Hits'), + E('th', {}, 'Source') + ])), + E('tbody', { 'id': 'blocked-tbody' }, rows) + ]) + ]); + }, + + renderThreatDistribution: function(stats) { + var threats = stats.threats || {}; + var total = Object.values(threats).reduce(function(a, b) { return a + b; }, 0) || 1; + + var items = Object.entries(threats).map(function(entry) { + var pct = Math.round((entry[1] / total) * 100); + return E('div', { 'class': 'vf-dist-item' }, [ + E('div', { 'class': 'vf-dist-label' }, [ + getThreatBadge(entry[0]), + E('span', { 'style': 'margin-left:8px' }, formatNumber(entry[1])) + ]), + E('div', { 'class': 'vf-dist-bar' }, [ + E('div', { + 'class': 'vf-dist-fill', + 'style': 'width:' + pct + '%;background:' + (entry[0] === 'malware' ? '#e74c3c' : entry[0] === 'phishing' ? '#f39c12' : '#3498db') + }) + ]) + ]); + }); + + if (items.length === 0) { + items.push(E('div', { 'style': 'color:#999;text-align:center;padding:20px' }, 'No threat data available')); + } + + return E('div', { 'class': 'vf-section' }, [ + E('h3', {}, 'Threat Distribution'), + E('div', { 'class': 'vf-distribution' }, items) + ]); + }, + + handleUpdateFeeds: function() { + var self = this; + ui.showModal('Updating Feeds', [ + E('p', { 'class': 'spinning' }, 'Downloading threat intelligence feeds...') + ]); + + callUpdateFeeds().then(function(result) { + ui.hideModal(); + if (result.success) { + ui.addNotification(null, E('p', {}, result.message || 'Feed update started'), 'success'); + } else { + ui.addNotification(null, E('p', {}, result.message || 'Failed to update feeds'), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Error: ' + err.message), 'error'); + }); + }, + + handleBlockDomain: function() { + var self = this; + + ui.showModal('Block Domain', [ + E('div', { 'class': 'cbi-section' }, [ + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, 'Domain'), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { 'type': 'text', 'id': 'block-domain-input', 'class': 'cbi-input-text', 'placeholder': 'malware.example.com' }) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, 'Reason'), + E('div', { 'class': 'cbi-value-field' }, [ + E('select', { 'id': 'block-reason-input', 'class': 'cbi-input-select' }, [ + E('option', { 'value': 'manual' }, 'Manual Block'), + E('option', { 'value': 'malware' }, 'Malware'), + E('option', { 'value': 'phishing' }, 'Phishing'), + E('option', { 'value': 'c2' }, 'C2 Server'), + E('option', { 'value': 'spam' }, 'Spam') + ]) + ]) + ]) + ]), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'), + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'click': function() { + var domain = document.getElementById('block-domain-input').value.trim(); + var reason = document.getElementById('block-reason-input').value; + if (!domain) { + ui.addNotification(null, E('p', {}, 'Please enter a domain'), 'warning'); + return; + } + ui.hideModal(); + callBlockDomain(domain, reason).then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', {}, result.message), 'success'); + } else { + ui.addNotification(null, E('p', {}, result.message || 'Failed to block domain'), 'error'); + } + }); + } + }, 'Block') + ]) + ]); + }, + + handleSearchDomain: function() { + var self = this; + + ui.showModal('Search Domain', [ + E('div', { 'class': 'cbi-section' }, [ + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, 'Domain'), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { 'type': 'text', 'id': 'search-domain-input', 'class': 'cbi-input-text', 'placeholder': 'example.com' }) + ]) + ]), + E('div', { 'id': 'search-result', 'style': 'padding:10px;display:none' }) + ]), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Close'), + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': function() { + var domain = document.getElementById('search-domain-input').value.trim(); + var resultDiv = document.getElementById('search-result'); + if (!domain) { + ui.addNotification(null, E('p', {}, 'Please enter a domain'), 'warning'); + return; + } + resultDiv.style.display = 'block'; + resultDiv.innerHTML = '

Searching...

'; + callSearch(domain).then(function(result) { + if (result.found && result.blocked) { + resultDiv.innerHTML = '
' + + '\u26A0 BLOCKED
' + + 'Domain: ' + (result.domain || domain) + '
' + + 'Threat: ' + (result.threat || 'unknown') + '
' + + 'Confidence: ' + (result.confidence || 0) + '%
' + + 'Source: ' + (result.source || 'unknown') + + '
'; + } else { + resultDiv.innerHTML = '
' + + '\u2713 NOT BLOCKED
' + + 'Domain ' + domain + ' is not in the blocklist.' + + '
'; + } + }); + } + }, 'Search') + ]) + ]); + }, + + render: function(data) { + var status = data[0] || {}; + var stats = data[1] || {}; + var feeds = data[2] || {}; + var blocked = data[3] || {}; + + var self = this; + + // Start polling for live updates + poll.add(function() { + return Promise.all([callStatus(), callGetStats()]).then(function(results) { + var s = results[0] || {}; + var st = results[1] || {}; + var domainsEl = document.querySelector('[data-stat="domains"]'); + var hitsEl = document.querySelector('[data-stat="hits"]'); + var x47El = document.querySelector('[data-stat="x47"]'); + if (domainsEl) domainsEl.textContent = formatNumber(st.domains || 0); + if (hitsEl) hitsEl.textContent = formatNumber(st.hits || 0); + if (x47El) x47El.textContent = formatNumber(st.x47_impact || 0); + }); + }, 10); + + return E('div', { 'class': 'vf-dashboard' }, [ + E('style', {}, [ + '.vf-dashboard { max-width: 1200px; }', + '.vf-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }', + '.vf-card { background: #fff; border-radius: 8px; padding: 16px; display: flex; align-items: center; gap: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }', + '.vf-card-highlight { background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%); color: #fff; }', + '.vf-card-highlight .vf-card-label { color: rgba(255,255,255,0.8); }', + '.vf-card-icon { width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 20px; color: #fff; }', + '.vf-card-value { font-size: 24px; font-weight: 700; }', + '.vf-card-label { font-size: 12px; color: #666; text-transform: uppercase; letter-spacing: 0.5px; }', + '.vf-section { background: #fff; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }', + '.vf-section h3 { margin: 0 0 16px 0; font-size: 16px; font-weight: 600; color: #333; }', + '.vf-actions { display: flex; gap: 10px; flex-wrap: wrap; }', + '.vf-distribution { display: flex; flex-direction: column; gap: 12px; }', + '.vf-dist-item { display: flex; align-items: center; gap: 12px; }', + '.vf-dist-label { min-width: 150px; display: flex; align-items: center; }', + '.vf-dist-bar { flex: 1; height: 20px; background: #eee; border-radius: 4px; overflow: hidden; }', + '.vf-dist-fill { height: 100%; transition: width 0.3s; }', + '.table { width: 100%; border-collapse: collapse; }', + '.table th, .table td { padding: 10px 12px; text-align: left; border-bottom: 1px solid #eee; }', + '.table th { background: #f8f9fa; font-weight: 600; font-size: 12px; text-transform: uppercase; color: #666; }', + '.table tbody tr:hover { background: #f8f9fa; }', + '@media (prefers-color-scheme: dark) {', + ' .vf-card { background: #2d2d2d; }', + ' .vf-card-label { color: #aaa; }', + ' .vf-section { background: #2d2d2d; }', + ' .vf-section h3 { color: #eee; }', + ' .table th { background: #333; color: #aaa; }', + ' .table td { border-color: #444; }', + ' .table tbody tr:hover { background: #333; }', + '}' + ].join('\n')), + E('h2', { 'style': 'margin-bottom: 20px' }, [ + '\uD83C\uDF00 Vortex DNS Firewall' + ]), + E('p', { 'style': 'color: #666; margin-bottom: 24px' }, + 'DNS-level threat blocking with \u00D747 vitality multiplier. Each blocked DNS query prevents approximately 47 malicious connections.'), + this.renderStatusCards(status, stats), + E('div', { 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 20px;' }, [ + this.renderQuickActions(), + this.renderThreatDistribution(stats) + ]), + this.renderFeedsTable(feeds), + this.renderBlockedTable(blocked) + ]); + }, + + handleSave: null, + handleSaveApply: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-vortex-firewall/root/usr/share/luci/menu.d/luci-app-vortex-firewall.json b/package/secubox/luci-app-vortex-firewall/root/usr/share/luci/menu.d/luci-app-vortex-firewall.json new file mode 100644 index 00000000..29755329 --- /dev/null +++ b/package/secubox/luci-app-vortex-firewall/root/usr/share/luci/menu.d/luci-app-vortex-firewall.json @@ -0,0 +1,14 @@ +{ + "admin/services/vortex-firewall": { + "title": "Vortex DNS Firewall", + "order": 85, + "action": { + "type": "view", + "path": "vortex-firewall/overview" + }, + "depends": { + "acl": ["luci-app-vortex-firewall"], + "uci": { "vortex-firewall": true } + } + } +} diff --git a/package/secubox/luci-app-vortex-firewall/root/usr/share/rpcd/acl.d/luci-app-vortex-firewall.json b/package/secubox/luci-app-vortex-firewall/root/usr/share/rpcd/acl.d/luci-app-vortex-firewall.json new file mode 100644 index 00000000..bc785585 --- /dev/null +++ b/package/secubox/luci-app-vortex-firewall/root/usr/share/rpcd/acl.d/luci-app-vortex-firewall.json @@ -0,0 +1,15 @@ +{ + "luci-app-vortex-firewall": { + "description": "Grant access to Vortex DNS Firewall LuCI app", + "read": { + "ubus": { + "luci.vortex-firewall": ["status", "get_stats", "get_feeds", "get_blocked", "search"] + } + }, + "write": { + "ubus": { + "luci.vortex-firewall": ["update_feeds", "block_domain", "unblock_domain"] + } + } + } +}