diff --git a/package/secubox/luci-app-ai-insights/Makefile b/package/secubox/luci-app-ai-insights/Makefile new file mode 100644 index 00000000..64182238 --- /dev/null +++ b/package/secubox/luci-app-ai-insights/Makefile @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI AI Insights Dashboard +LUCI_DEPENDS:=+luci-base +LUCI_PKGARCH:=all + +PKG_NAME:=luci-app-ai-insights +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=SecuBox Team +PKG_LICENSE:=Apache-2.0 + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/luci-app-ai-insights/description + Unified AI security insights dashboard for SecuBox. + Aggregates data from all AI agents: Threat Analyst, + DNS Guard, Network Anomaly, CVE Triage, and LocalRecall. + Provides security posture scoring and AI-powered analysis. +endef + +define Package/luci-app-ai-insights/install + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-ai-insights.json $(1)/usr/share/luci/menu.d/ + + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-ai-insights.json $(1)/usr/share/rpcd/acl.d/ + + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.ai-insights $(1)/usr/libexec/rpcd/ + + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/ai-insights + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/ai-insights/dashboard.js $(1)/www/luci-static/resources/view/ai-insights/ + + $(INSTALL_DIR) $(1)/www/luci-static/resources/ai-insights + $(INSTALL_DATA) ./htdocs/luci-static/resources/ai-insights/api.js $(1)/www/luci-static/resources/ai-insights/ + $(INSTALL_DATA) ./htdocs/luci-static/resources/ai-insights/dashboard.css $(1)/www/luci-static/resources/ai-insights/ +endef + +define Package/luci-app-ai-insights/postinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + rm -f /tmp/luci-indexcache* /tmp/luci-modulecache/* 2>/dev/null + /etc/init.d/rpcd restart 2>/dev/null +} +exit 0 +endef + +$(eval $(call BuildPackage,luci-app-ai-insights)) diff --git a/package/secubox/luci-app-ai-insights/htdocs/luci-static/resources/ai-insights/api.js b/package/secubox/luci-app-ai-insights/htdocs/luci-static/resources/ai-insights/api.js new file mode 100644 index 00000000..bb025d29 --- /dev/null +++ b/package/secubox/luci-app-ai-insights/htdocs/luci-static/resources/ai-insights/api.js @@ -0,0 +1,127 @@ +'use strict'; +'require baseclass'; +'require rpc'; + +/** + * AI Insights Aggregation API + * Package: luci-app-ai-insights + * RPCD object: luci.ai-insights + * Version: 1.0.0 + */ + +var callStatus = rpc.declare({ + object: 'luci.ai-insights', + method: 'status', + expect: { } +}); + +var callGetAlerts = rpc.declare({ + object: 'luci.ai-insights', + method: 'get_alerts', + params: ['limit'], + expect: { } +}); + +var callGetPosture = rpc.declare({ + object: 'luci.ai-insights', + method: 'get_posture', + expect: { } +}); + +var callGetTimeline = rpc.declare({ + object: 'luci.ai-insights', + method: 'get_timeline', + params: ['hours'], + expect: { } +}); + +var callRunAll = rpc.declare({ + object: 'luci.ai-insights', + method: 'run_all', + expect: { } +}); + +var callAnalyze = rpc.declare({ + object: 'luci.ai-insights', + method: 'analyze', + expect: { } +}); + +function getPostureColor(score) { + if (score >= 80) return 'success'; + if (score >= 60) return 'warning'; + if (score >= 40) return 'caution'; + return 'danger'; +} + +function getPostureLabel(score) { + if (score >= 80) return 'Excellent'; + if (score >= 60) return 'Good'; + if (score >= 40) return 'Fair'; + if (score >= 20) return 'Poor'; + return 'Critical'; +} + +function getAgentIcon(agent) { + switch (agent) { + case 'threat_analyst': return '\uD83D\uDEE1'; + case 'dns_guard': return '\uD83C\uDF10'; + case 'network_anomaly': return '\uD83D\uDCCA'; + case 'cve_triage': return '\u26A0'; + default: return '\u2022'; + } +} + +function getAgentName(agent) { + switch (agent) { + case 'threat_analyst': return 'Threat Analyst'; + case 'dns_guard': return 'DNS Guard'; + case 'network_anomaly': return 'Network Anomaly'; + case 'cve_triage': return 'CVE Triage'; + default: return agent; + } +} + +function formatRelativeTime(dateStr) { + if (!dateStr) return 'N/A'; + try { + var date = new Date(dateStr); + 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 dateStr; + } +} + +return baseclass.extend({ + getStatus: callStatus, + getAlerts: callGetAlerts, + getPosture: callGetPosture, + getTimeline: callGetTimeline, + runAll: callRunAll, + analyze: callAnalyze, + + getPostureColor: getPostureColor, + getPostureLabel: getPostureLabel, + getAgentIcon: getAgentIcon, + getAgentName: getAgentName, + formatRelativeTime: formatRelativeTime, + + getOverview: function() { + return Promise.all([ + callStatus(), + callGetPosture(), + callGetAlerts(30) + ]).then(function(results) { + return { + status: results[0] || {}, + posture: results[1] || {}, + alerts: (results[2] || {}).alerts || [] + }; + }); + } +}); diff --git a/package/secubox/luci-app-ai-insights/htdocs/luci-static/resources/ai-insights/dashboard.css b/package/secubox/luci-app-ai-insights/htdocs/luci-static/resources/ai-insights/dashboard.css new file mode 100644 index 00000000..569541dc --- /dev/null +++ b/package/secubox/luci-app-ai-insights/htdocs/luci-static/resources/ai-insights/dashboard.css @@ -0,0 +1,337 @@ +/* AI Insights Dashboard Styles */ + +.ai-view { + max-width: 1200px; + margin: 0 auto; + padding: 1rem; +} + +.ai-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--border-color-medium, #ddd); +} + +.ai-title { + font-size: 1.5rem; + font-weight: 600; +} + +.ai-status { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.ai-dot { + width: 10px; + height: 10px; + border-radius: 50%; + display: inline-block; +} + +.ai-dot.online { + background: #28a745; + box-shadow: 0 0 6px #28a745; +} + +.ai-dot.offline { + background: #dc3545; +} + +/* Posture Card */ +.ai-posture-card { + display: flex; + align-items: center; + gap: 2rem; + padding: 1.5rem 2rem; + border-radius: 12px; + margin-bottom: 1.5rem; + color: #fff; + background: linear-gradient(135deg, #6c757d 0%, #495057 100%); +} + +.ai-posture-card.success { + background: linear-gradient(135deg, #28a745 0%, #1e7e34 100%); +} + +.ai-posture-card.warning { + background: linear-gradient(135deg, #ffc107 0%, #d39e00 100%); + color: #212529; +} + +.ai-posture-card.caution { + background: linear-gradient(135deg, #fd7e14 0%, #dc6502 100%); +} + +.ai-posture-card.danger { + background: linear-gradient(135deg, #dc3545 0%, #bd2130 100%); +} + +.ai-posture-score { + text-align: center; + min-width: 100px; +} + +.ai-score-value { + font-size: 3rem; + font-weight: 700; + line-height: 1; +} + +.ai-score-label { + font-size: 0.85rem; + opacity: 0.9; +} + +.ai-posture-info { + flex: 1; +} + +.ai-posture-label { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 0.25rem; +} + +.ai-posture-factors { + font-size: 0.9rem; + opacity: 0.9; +} + +/* Stats Row */ +.ai-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; +} + +.ai-stat { + background: var(--background-color-low, #f8f9fa); + border-radius: 8px; + padding: 1rem; + text-align: center; + border-left: 4px solid var(--border-color-medium, #ddd); +} + +.ai-stat.success { border-left-color: #28a745; } +.ai-stat.warning { border-left-color: #ffc107; } +.ai-stat.danger { border-left-color: #dc3545; } + +.ai-stat-value { + font-size: 1.5rem; + font-weight: 700; +} + +.ai-stat-label { + font-size: 0.85rem; + color: var(--text-color-low, #666); +} + +/* Agents Grid */ +.ai-agents-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; +} + +.ai-agent-card { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem; + background: var(--background-color-high, #fff); + border: 1px solid var(--border-color-medium, #ddd); + border-radius: 8px; + border-left: 4px solid #6c757d; +} + +.ai-agent-card.online { + border-left-color: #28a745; +} + +.ai-agent-card.offline { + border-left-color: #dc3545; + opacity: 0.7; +} + +.ai-agent-icon { + font-size: 2rem; +} + +.ai-agent-info { + flex: 1; +} + +.ai-agent-name { + font-weight: 600; +} + +.ai-agent-status { + font-size: 0.85rem; + color: var(--text-color-low, #666); + display: flex; + align-items: center; + gap: 0.5rem; +} + +.ai-agent-alerts { + font-size: 0.85rem; +} + +/* Card */ +.ai-card { + background: var(--background-color-high, #fff); + border: 1px solid var(--border-color-medium, #ddd); + border-radius: 8px; + margin-bottom: 1rem; +} + +.ai-card-header { + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border-color-medium, #ddd); + font-weight: 600; + display: flex; + justify-content: space-between; + align-items: center; +} + +.ai-card-body { + padding: 1rem; +} + +/* Actions */ +.ai-actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.ai-btn { + padding: 0.5rem 1rem; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + text-decoration: none; + display: inline-block; + transition: opacity 0.2s; +} + +.ai-btn:hover { opacity: 0.85; } + +.ai-btn-primary { background: #007bff; color: #fff; } +.ai-btn-secondary { background: #6c757d; color: #fff; } +.ai-btn-info { background: #17a2b8; color: #fff; } +.ai-btn-outline { background: transparent; border: 1px solid #6c757d; color: #6c757d; } + +/* Alerts */ +.ai-alerts-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.ai-alert-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem; + background: var(--background-color-low, #f8f9fa); + border-radius: 4px; + border-left: 3px solid #6c757d; +} + +.ai-alert-item.alert { border-left-color: #ffc107; } +.ai-alert-item.rule { border-left-color: #007bff; } +.ai-alert-item.cve { border-left-color: #dc3545; } + +.ai-alert-icon { + font-size: 1.25rem; +} + +.ai-alert-content { + flex: 1; +} + +.ai-alert-source { + font-size: 0.8rem; + color: var(--text-color-low, #666); +} + +.ai-alert-message { + font-size: 0.9rem; +} + +.ai-alert-time { + font-size: 0.8rem; + color: var(--text-color-low, #666); +} + +/* Badge */ +.ai-badge { + display: inline-block; + padding: 0.2rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + background: var(--background-color-low, #e9ecef); +} + +.ai-badge.warning { + background: #fff3cd; + color: #856404; +} + +/* Timeline */ +.ai-timeline { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.ai-timeline-item { + display: grid; + grid-template-columns: 100px 100px 1fr; + gap: 0.5rem; + padding: 0.5rem; + background: var(--background-color-low, #f8f9fa); + border-radius: 4px; + font-size: 0.85rem; +} + +.ai-timeline-time { + color: var(--text-color-low, #666); +} + +.ai-timeline-source { + font-weight: 500; +} + +/* Empty state */ +.ai-empty { + color: var(--text-color-low, #666); + text-align: center; + padding: 2rem; +} + +.spinning::after { + content: ''; + display: inline-block; + width: 1em; + height: 1em; + margin-left: 0.5em; + border: 2px solid currentColor; + border-right-color: transparent; + border-radius: 50%; + animation: spin 0.75s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} diff --git a/package/secubox/luci-app-ai-insights/htdocs/luci-static/resources/view/ai-insights/dashboard.js b/package/secubox/luci-app-ai-insights/htdocs/luci-static/resources/view/ai-insights/dashboard.js new file mode 100644 index 00000000..576ca541 --- /dev/null +++ b/package/secubox/luci-app-ai-insights/htdocs/luci-static/resources/view/ai-insights/dashboard.js @@ -0,0 +1,255 @@ +'use strict'; +'require view'; +'require dom'; +'require poll'; +'require ui'; +'require ai-insights.api as api'; + +/** + * AI Insights Dashboard - v1.0.0 + * Unified view across all SecuBox AI agents + */ + +return view.extend({ + load: function() { + var link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = L.resource('ai-insights/dashboard.css'); + document.head.appendChild(link); + return api.getOverview().catch(function() { return {}; }); + }, + + render: function(data) { + var self = this; + var s = data.status || {}; + var p = data.posture || {}; + var alerts = data.alerts || []; + var agents = s.agents || {}; + + var view = E('div', { 'class': 'ai-view' }, [ + // Header + E('div', { 'class': 'ai-header' }, [ + E('div', { 'class': 'ai-title' }, 'AI Security Insights'), + E('div', { 'class': 'ai-status' }, [ + E('span', { 'class': 'ai-dot ' + (s.localai === 'online' ? 'online' : 'offline') }), + 'LocalAI: ' + (s.localai || 'offline') + ]) + ]), + + // Posture Score (hero) + E('div', { 'class': 'ai-posture-card ' + api.getPostureColor(p.score || 0), 'id': 'ai-posture' }, [ + E('div', { 'class': 'ai-posture-score' }, [ + E('div', { 'class': 'ai-score-value' }, String(p.score || 0)), + E('div', { 'class': 'ai-score-label' }, 'Security Score') + ]), + E('div', { 'class': 'ai-posture-info' }, [ + E('div', { 'class': 'ai-posture-label' }, api.getPostureLabel(p.score || 0)), + E('div', { 'class': 'ai-posture-factors' }, p.factors || 'Calculating...') + ]) + ]), + + // Stats row + E('div', { 'class': 'ai-stats', 'id': 'ai-stats' }, this.renderStats(s)), + + // Agents grid + E('div', { 'class': 'ai-agents-grid', 'id': 'ai-agents' }, this.renderAgents(agents)), + + // Actions card + E('div', { 'class': 'ai-card' }, [ + E('div', { 'class': 'ai-card-header' }, 'Actions'), + E('div', { 'class': 'ai-card-body' }, this.renderActions()) + ]), + + // Alerts card + E('div', { 'class': 'ai-card' }, [ + E('div', { 'class': 'ai-card-header' }, [ + 'Recent Activity', + E('span', { 'class': 'ai-badge' }, String(alerts.length)) + ]), + E('div', { 'class': 'ai-card-body', 'id': 'ai-alerts' }, this.renderAlerts(alerts)) + ]) + ]); + + poll.add(L.bind(this.pollData, this), 15); + return view; + }, + + renderStats: function(s) { + var agents = s.agents || {}; + var online = Object.values(agents).filter(function(a) { return a.status === 'online'; }).length; + var totalAlerts = Object.values(agents).reduce(function(sum, a) { return sum + (a.alerts || 0); }, 0); + + var statItems = [ + { label: 'Agents Online', value: online + '/4', type: online === 4 ? 'success' : online > 0 ? 'warning' : 'danger' }, + { label: 'LocalAI', value: s.localai === 'online' ? 'OK' : 'OFF', type: s.localai === 'online' ? 'success' : 'danger' }, + { label: 'Pending', value: totalAlerts, type: totalAlerts > 10 ? 'danger' : totalAlerts > 0 ? 'warning' : 'success' }, + { label: 'Memories', value: s.memories || 0, type: '' } + ]; + return statItems.map(function(st) { + return E('div', { 'class': 'ai-stat ' + st.type }, [ + E('div', { 'class': 'ai-stat-value' }, String(st.value)), + E('div', { 'class': 'ai-stat-label' }, st.label) + ]); + }); + }, + + renderAgents: function(agents) { + var agentList = ['threat_analyst', 'dns_guard', 'network_anomaly', 'cve_triage']; + return agentList.map(function(id) { + var agent = agents[id] || {}; + var isOnline = agent.status === 'online'; + return E('div', { 'class': 'ai-agent-card ' + (isOnline ? 'online' : 'offline') }, [ + E('div', { 'class': 'ai-agent-icon' }, api.getAgentIcon(id)), + E('div', { 'class': 'ai-agent-info' }, [ + E('div', { 'class': 'ai-agent-name' }, api.getAgentName(id)), + E('div', { 'class': 'ai-agent-status' }, [ + E('span', { 'class': 'ai-dot ' + (isOnline ? 'online' : 'offline') }), + isOnline ? 'Running' : 'Stopped' + ]) + ]), + E('div', { 'class': 'ai-agent-alerts' }, [ + E('span', { 'class': 'ai-badge ' + (agent.alerts > 0 ? 'warning' : '') }, String(agent.alerts || 0)), + E('span', {}, ' alerts') + ]) + ]); + }); + }, + + renderActions: function() { + var self = this; + return E('div', { 'class': 'ai-actions' }, [ + E('button', { + 'class': 'ai-btn ai-btn-primary', + 'click': function() { self.runAllAgents(); } + }, 'Run All Agents'), + E('button', { + 'class': 'ai-btn ai-btn-secondary', + 'click': function() { self.getAIAnalysis(); } + }, 'AI Analysis'), + E('button', { + 'class': 'ai-btn ai-btn-info', + 'click': function() { self.showTimeline(); } + }, 'View Timeline'), + E('a', { + 'class': 'ai-btn ai-btn-outline', + 'href': L.url('admin/secubox/ai/localrecall') + }, 'LocalRecall') + ]); + }, + + renderAlerts: function(alerts) { + if (!alerts || !alerts.length) { + return E('div', { 'class': 'ai-empty' }, 'No recent activity from AI agents'); + } + + return E('div', { 'class': 'ai-alerts-list' }, alerts.slice(0, 15).map(function(alert) { + var data = alert.data || {}; + var source = alert.source || 'unknown'; + var type = alert.type || 'alert'; + + return E('div', { 'class': 'ai-alert-item ' + type }, [ + E('div', { 'class': 'ai-alert-icon' }, api.getAgentIcon(source)), + E('div', { 'class': 'ai-alert-content' }, [ + E('div', { 'class': 'ai-alert-source' }, api.getAgentName(source)), + E('div', { 'class': 'ai-alert-message' }, data.message || data.type || type) + ]), + E('div', { 'class': 'ai-alert-time' }, api.formatRelativeTime(data.timestamp)) + ]); + })); + }, + + runAllAgents: function() { + ui.showModal('Running Agents', [ + E('p', { 'class': 'spinning' }, 'Starting all AI agents...') + ]); + + api.runAll().then(function(result) { + ui.hideModal(); + var started = Object.values(result).filter(function(s) { return s === 'started'; }).length; + ui.addNotification(null, E('p', {}, 'Started ' + started + ' agents'), 'success'); + }).catch(function() { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Failed to start agents'), 'error'); + }); + }, + + getAIAnalysis: function() { + ui.showModal('AI Analysis', [ + E('p', { 'class': 'spinning' }, 'Generating security analysis (may take up to 60s)...') + ]); + + api.analyze().then(function(result) { + ui.hideModal(); + if (result.analysis) { + ui.showModal('Security Analysis', [ + E('div', { 'style': 'white-space: pre-wrap; max-height: 400px; overflow-y: auto;' }, result.analysis), + E('div', { 'class': 'right' }, E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, 'Close')) + ]); + } else { + ui.addNotification(null, E('p', {}, 'Error: ' + (result.error || 'Analysis failed')), 'error'); + } + }).catch(function() { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Analysis failed'), 'error'); + }); + }, + + showTimeline: function() { + api.getTimeline(24).then(function(result) { + var timeline = result.timeline || []; + ui.showModal('Security Timeline (24h)', [ + E('div', { 'style': 'max-height: 400px; overflow-y: auto;' }, + timeline.length ? E('div', { 'class': 'ai-timeline' }, timeline.map(function(e) { + return E('div', { 'class': 'ai-timeline-item' }, [ + E('div', { 'class': 'ai-timeline-time' }, e.time), + E('div', { 'class': 'ai-timeline-source' }, e.source), + E('div', { 'class': 'ai-timeline-msg' }, e.message) + ]); + })) : E('p', {}, 'No events in the last 24 hours') + ), + E('div', { 'class': 'right' }, E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, 'Close')) + ]); + }); + }, + + pollData: function() { + var self = this; + return api.getOverview().then(function(data) { + var s = data.status || {}; + var p = data.posture || {}; + var alerts = data.alerts || []; + var agents = s.agents || {}; + + var el = document.getElementById('ai-stats'); + if (el) dom.content(el, self.renderStats(s)); + + el = document.getElementById('ai-agents'); + if (el) dom.content(el, self.renderAgents(agents)); + + el = document.getElementById('ai-alerts'); + if (el) dom.content(el, self.renderAlerts(alerts)); + + // Update posture card + el = document.getElementById('ai-posture'); + if (el) { + el.className = 'ai-posture-card ' + api.getPostureColor(p.score || 0); + var scoreEl = el.querySelector('.ai-score-value'); + var labelEl = el.querySelector('.ai-posture-label'); + var factorsEl = el.querySelector('.ai-posture-factors'); + if (scoreEl) scoreEl.textContent = String(p.score || 0); + if (labelEl) labelEl.textContent = api.getPostureLabel(p.score || 0); + if (factorsEl) factorsEl.textContent = p.factors || ''; + } + }); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-ai-insights/root/usr/libexec/rpcd/luci.ai-insights b/package/secubox/luci-app-ai-insights/root/usr/libexec/rpcd/luci.ai-insights new file mode 100644 index 00000000..dd1ef0ae --- /dev/null +++ b/package/secubox/luci-app-ai-insights/root/usr/libexec/rpcd/luci.ai-insights @@ -0,0 +1,307 @@ +#!/bin/sh +# AI Insights Aggregation RPCD Handler +# Unified view across all SecuBox AI agents + +. /usr/share/libubox/jshn.sh + +log_info() { logger -t ai-insights-rpcd "$*"; } + +# Check if a command exists +cmd_exists() { command -v "$1" >/dev/null 2>&1; } + +# Get agent status +get_agent_status() { + local agent="$1" + local status="offline" + local alerts=0 + + case "$agent" in + threat_analyst) + pgrep -f "threat-analyst daemon" >/dev/null 2>&1 && status="online" + [ -f /var/lib/threat-analyst/pending_rules.json ] && \ + alerts=$(jsonfilter -i /var/lib/threat-analyst/pending_rules.json -e '@[*]' 2>/dev/null | wc -l) + ;; + dns_guard) + pgrep -f "dnsguardctl daemon" >/dev/null 2>&1 && status="online" + [ -f /var/lib/dns-guard/alerts.json ] && \ + alerts=$(jsonfilter -i /var/lib/dns-guard/alerts.json -e '@[*]' 2>/dev/null | wc -l) + ;; + network_anomaly) + pgrep -f "network-anomalyctl daemon" >/dev/null 2>&1 && status="online" + [ -f /var/lib/network-anomaly/alerts.json ] && \ + alerts=$(jsonfilter -i /var/lib/network-anomaly/alerts.json -e '@[*]' 2>/dev/null | wc -l) + ;; + cve_triage) + pgrep -f "cve-triagectl daemon" >/dev/null 2>&1 && status="online" + [ -f /var/lib/cve-triage/alerts.json ] && \ + alerts=$(jsonfilter -i /var/lib/cve-triage/alerts.json -e '@[*]' 2>/dev/null | wc -l) + ;; + esac + + printf '{"status":"%s","alerts":%d}' "$status" "$alerts" +} + +# Calculate security posture score (0-100) +calculate_posture() { + local score=100 + local factors="" + + # Check LocalAI + local localai_url=$(uci -q get localrecall.main.localai_url || echo "http://127.0.0.1:8091") + if ! wget -q -O /dev/null --timeout=2 "${localai_url}/v1/models" 2>/dev/null; then + score=$((score - 10)) + factors="${factors}LocalAI offline (-10), " + fi + + # Check agent statuses + for agent in threat_analyst dns_guard network_anomaly cve_triage; do + local status=$(get_agent_status "$agent" | jsonfilter -e '@.status' 2>/dev/null) + if [ "$status" != "online" ]; then + score=$((score - 5)) + factors="${factors}${agent} offline (-5), " + fi + done + + # Check CrowdSec alerts (high = bad) + if cmd_exists cscli; then + local cs_alerts=$(cscli alerts list -o json --since 1h 2>/dev/null | jsonfilter -e '@[*]' 2>/dev/null | wc -l) + if [ "$cs_alerts" -gt 50 ]; then + score=$((score - 20)) + factors="${factors}High CrowdSec alerts (-20), " + elif [ "$cs_alerts" -gt 20 ]; then + score=$((score - 10)) + factors="${factors}Elevated CrowdSec alerts (-10), " + elif [ "$cs_alerts" -gt 5 ]; then + score=$((score - 5)) + factors="${factors}Some CrowdSec alerts (-5), " + fi + fi + + # Check CVE alerts + if [ -f /var/lib/cve-triage/vulnerabilities.json ]; then + local critical=$(jsonfilter -i /var/lib/cve-triage/vulnerabilities.json -e "@[@.severity='critical']" 2>/dev/null | wc -l) + local high=$(jsonfilter -i /var/lib/cve-triage/vulnerabilities.json -e "@[@.severity='high']" 2>/dev/null | wc -l) + [ "$critical" -gt 0 ] && score=$((score - critical * 10)) && factors="${factors}Critical CVEs (-$((critical * 10))), " + [ "$high" -gt 0 ] && score=$((score - high * 5)) && factors="${factors}High CVEs (-$((high * 5))), " + fi + + # Ensure score is 0-100 + [ "$score" -lt 0 ] && score=0 + [ "$score" -gt 100 ] && score=100 + + # Remove trailing comma + factors=$(echo "$factors" | sed 's/, $//') + [ -z "$factors" ] && factors="All systems nominal" + + printf '{"score":%d,"factors":"%s"}' "$score" "$factors" +} + +case "$1" in + list) + cat <<'EOF' +{ + "status": {}, + "get_alerts": {"limit": 50}, + "get_posture": {}, + "get_timeline": {"hours": 24}, + "run_all": {}, + "analyze": {} +} +EOF + ;; + + call) + case "$2" in + status) + # Get all agent statuses + ta=$(get_agent_status threat_analyst) + dg=$(get_agent_status dns_guard) + na=$(get_agent_status network_anomaly) + ct=$(get_agent_status cve_triage) + + # Check LocalAI + localai_status="offline" + localai_url=$(uci -q get localrecall.main.localai_url || echo "http://127.0.0.1:8091") + wget -q -O /dev/null --timeout=2 "${localai_url}/v1/models" 2>/dev/null && localai_status="online" + + # Check LocalRecall + memories=0 + [ -f /var/lib/localrecall/memories.json ] && \ + memories=$(jsonfilter -i /var/lib/localrecall/memories.json -e '@[*]' 2>/dev/null | wc -l) + + # Get posture + posture=$(calculate_posture) + posture_score=$(echo "$posture" | jsonfilter -e '@.score' 2>/dev/null) + + cat </dev/null) + [ -z "$limit" ] && limit=50 + + # Aggregate alerts from all sources + alerts='[' + first=1 + + # Threat Analyst pending rules + if [ -f /var/lib/threat-analyst/pending_rules.json ]; then + jsonfilter -i /var/lib/threat-analyst/pending_rules.json -e '@[*]' 2>/dev/null | head -n 10 | while read -r a; do + [ $first -eq 0 ] && printf ',' + first=0 + printf '{"source":"threat_analyst","type":"rule","data":%s}' "$a" + done + fi + + # DNS Guard alerts + if [ -f /var/lib/dns-guard/alerts.json ]; then + jsonfilter -i /var/lib/dns-guard/alerts.json -e '@[*]' 2>/dev/null | head -n 10 | while read -r a; do + [ $first -eq 0 ] && printf ',' + first=0 + printf '{"source":"dns_guard","type":"alert","data":%s}' "$a" + done + fi + + # Network Anomaly alerts + if [ -f /var/lib/network-anomaly/alerts.json ]; then + jsonfilter -i /var/lib/network-anomaly/alerts.json -e '@[*]' 2>/dev/null | head -n 10 | while read -r a; do + [ $first -eq 0 ] && printf ',' + first=0 + printf '{"source":"network_anomaly","type":"alert","data":%s}' "$a" + done + fi + + # CVE Triage alerts + if [ -f /var/lib/cve-triage/alerts.json ]; then + jsonfilter -i /var/lib/cve-triage/alerts.json -e '@[*]' 2>/dev/null | head -n 10 | while read -r a; do + [ $first -eq 0 ] && printf ',' + first=0 + printf '{"source":"cve_triage","type":"cve","data":%s}' "$a" + done + fi + + alerts="${alerts}]" + printf '{"alerts":%s}' "$alerts" + ;; + + get_posture) + posture=$(calculate_posture) + echo "$posture" + ;; + + get_timeline) + read -r input + hours=$(echo "$input" | jsonfilter -e '@.hours' 2>/dev/null) + [ -z "$hours" ] && hours=24 + + # Build timeline from system log + timeline='[' + first=1 + + # Get security-related log entries + logread 2>/dev/null | grep -E "(crowdsec|threat-analyst|dns-guard|network-anomaly|cve-triage)" | tail -n 50 | while read -r line; do + ts=$(echo "$line" | awk '{print $1" "$2" "$3}') + msg=$(echo "$line" | cut -d: -f4-) + source=$(echo "$line" | grep -oE "(crowdsec|threat-analyst|dns-guard|network-anomaly|cve-triage)" | head -1) + + [ $first -eq 0 ] && printf ',' + first=0 + printf '{"time":"%s","source":"%s","message":"%s"}' "$ts" "$source" "$(echo "$msg" | sed 's/"/\\"/g')" + done + + timeline="${timeline}]" + printf '{"timeline":%s}' "$timeline" + ;; + + run_all) + # Trigger all agents + results='{' + + # Run Threat Analyst + if cmd_exists threat-analystctl; then + /usr/bin/threat-analystctl run -q >/dev/null 2>&1 & + results="${results}\"threat_analyst\":\"started\"," + else + results="${results}\"threat_analyst\":\"not_installed\"," + fi + + # Run DNS Guard + if cmd_exists dnsguardctl; then + /usr/bin/dnsguardctl run -q >/dev/null 2>&1 & + results="${results}\"dns_guard\":\"started\"," + else + results="${results}\"dns_guard\":\"not_installed\"," + fi + + # Run Network Anomaly + if cmd_exists network-anomalyctl; then + /usr/bin/network-anomalyctl run -q >/dev/null 2>&1 & + results="${results}\"network_anomaly\":\"started\"," + else + results="${results}\"network_anomaly\":\"not_installed\"," + fi + + # Run CVE Triage + if cmd_exists cve-triagectl; then + /usr/bin/cve-triagectl run -q >/dev/null 2>&1 & + results="${results}\"cve_triage\":\"started\"," + else + results="${results}\"cve_triage\":\"not_installed\"," + fi + + # Remove trailing comma and close + results=$(echo "$results" | sed 's/,$//') + results="${results}}" + echo "$results" + ;; + + analyze) + # Get AI security analysis + localai_url=$(uci -q get localrecall.main.localai_url || echo "http://127.0.0.1:8091") + localai_model=$(uci -q get localrecall.main.localai_model || echo "tinyllama-1.1b-chat-v1.0.Q4_K_M") + + if ! curl -s --max-time 2 "${localai_url}/v1/models" >/dev/null 2>&1; then + echo '{"error":"LocalAI not available"}' + exit 0 + fi + + # Collect current state + posture=$(calculate_posture) + score=$(echo "$posture" | jsonfilter -e '@.score' 2>/dev/null) + factors=$(echo "$posture" | jsonfilter -e '@.factors' 2>/dev/null) + + prompt="You are a security analyst for SecuBox. Current security posture score: $score/100. Factors: $factors. Provide a brief security assessment and top 3 recommendations." + prompt=$(printf '%s' "$prompt" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ') + + response=$(curl -s --max-time 60 -X POST "${localai_url}/v1/chat/completions" \ + -H "Content-Type: application/json" \ + -d "{\"model\":\"$localai_model\",\"messages\":[{\"role\":\"user\",\"content\":\"$prompt\"}],\"max_tokens\":256,\"temperature\":0.3}" 2>/dev/null) + + if [ -n "$response" ]; then + content=$(echo "$response" | jsonfilter -e '@.choices[0].message.content' 2>/dev/null) + content=$(printf '%s' "$content" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ') + printf '{"analysis":"%s"}' "$content" + else + echo '{"error":"AI analysis failed"}' + fi + ;; + + *) + echo '{"error":"Unknown method"}' + ;; + esac + ;; +esac diff --git a/package/secubox/luci-app-ai-insights/root/usr/share/luci/menu.d/luci-app-ai-insights.json b/package/secubox/luci-app-ai-insights/root/usr/share/luci/menu.d/luci-app-ai-insights.json new file mode 100644 index 00000000..73869f16 --- /dev/null +++ b/package/secubox/luci-app-ai-insights/root/usr/share/luci/menu.d/luci-app-ai-insights.json @@ -0,0 +1,13 @@ +{ + "admin/secubox/ai/insights": { + "title": "AI Insights", + "order": 10, + "action": { + "type": "view", + "path": "ai-insights/dashboard" + }, + "depends": { + "acl": ["luci-app-ai-insights"] + } + } +} diff --git a/package/secubox/luci-app-ai-insights/root/usr/share/rpcd/acl.d/luci-app-ai-insights.json b/package/secubox/luci-app-ai-insights/root/usr/share/rpcd/acl.d/luci-app-ai-insights.json new file mode 100644 index 00000000..6c3c601e --- /dev/null +++ b/package/secubox/luci-app-ai-insights/root/usr/share/rpcd/acl.d/luci-app-ai-insights.json @@ -0,0 +1,15 @@ +{ + "luci-app-ai-insights": { + "description": "Grant access to AI Insights Dashboard", + "read": { + "ubus": { + "luci.ai-insights": ["status", "get_alerts", "get_posture", "get_timeline"] + } + }, + "write": { + "ubus": { + "luci.ai-insights": ["run_all", "analyze"] + } + } + } +} diff --git a/package/secubox/luci-app-localrecall/Makefile b/package/secubox/luci-app-localrecall/Makefile new file mode 100644 index 00000000..bc86c245 --- /dev/null +++ b/package/secubox/luci-app-localrecall/Makefile @@ -0,0 +1,45 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI LocalRecall AI Memory Dashboard +LUCI_DEPENDS:=+secubox-localrecall +luci-base +LUCI_PKGARCH:=all + +PKG_NAME:=luci-app-localrecall +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=SecuBox Team +PKG_LICENSE:=Apache-2.0 + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/luci-app-localrecall/install + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-localrecall.json $(1)/usr/share/luci/menu.d/ + + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-localrecall.json $(1)/usr/share/rpcd/acl.d/ + + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.localrecall $(1)/usr/libexec/rpcd/ + + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/localrecall + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/localrecall/dashboard.js $(1)/www/luci-static/resources/view/localrecall/ + + $(INSTALL_DIR) $(1)/www/luci-static/resources/localrecall + $(INSTALL_DATA) ./htdocs/luci-static/resources/localrecall/api.js $(1)/www/luci-static/resources/localrecall/ + $(INSTALL_DATA) ./htdocs/luci-static/resources/localrecall/dashboard.css $(1)/www/luci-static/resources/localrecall/ +endef + +define Package/luci-app-localrecall/postinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + rm -f /tmp/luci-indexcache* /tmp/luci-modulecache/* 2>/dev/null + /etc/init.d/rpcd restart 2>/dev/null +} +exit 0 +endef + +$(eval $(call BuildPackage,luci-app-localrecall)) diff --git a/package/secubox/luci-app-localrecall/htdocs/luci-static/resources/localrecall/api.js b/package/secubox/luci-app-localrecall/htdocs/luci-static/resources/localrecall/api.js new file mode 100644 index 00000000..ba7ee756 --- /dev/null +++ b/package/secubox/luci-app-localrecall/htdocs/luci-static/resources/localrecall/api.js @@ -0,0 +1,134 @@ +'use strict'; +'require baseclass'; +'require rpc'; + +/** + * LocalRecall Memory API + * Package: luci-app-localrecall + * RPCD object: luci.localrecall + * Version: 1.0.0 + */ + +var callStatus = rpc.declare({ + object: 'luci.localrecall', + method: 'status', + expect: { } +}); + +var callGetMemories = rpc.declare({ + object: 'luci.localrecall', + method: 'get_memories', + params: ['category', 'limit'], + expect: { } +}); + +var callSearch = rpc.declare({ + object: 'luci.localrecall', + method: 'search', + params: ['query', 'limit'], + expect: { } +}); + +var callStats = rpc.declare({ + object: 'luci.localrecall', + method: 'stats', + expect: { } +}); + +var callAdd = rpc.declare({ + object: 'luci.localrecall', + method: 'add', + params: ['category', 'content', 'agent', 'importance'], + expect: { } +}); + +var callDelete = rpc.declare({ + object: 'luci.localrecall', + method: 'delete', + params: ['id'], + expect: { } +}); + +var callCleanup = rpc.declare({ + object: 'luci.localrecall', + method: 'cleanup', + expect: { } +}); + +var callSummarize = rpc.declare({ + object: 'luci.localrecall', + method: 'summarize', + params: ['category'], + expect: { } +}); + +function formatRelativeTime(dateStr) { + if (!dateStr) return 'N/A'; + try { + var date = new Date(dateStr); + 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 dateStr; + } +} + +function formatBytes(bytes) { + if (bytes === 0) return '0 B'; + var k = 1024; + var sizes = ['B', 'KB', 'MB', 'GB']; + var i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +function getCategoryIcon(category) { + switch (category) { + case 'threats': return '\u26A0'; + case 'decisions': return '\u2714'; + case 'patterns': return '\uD83D\uDD0D'; + case 'configs': return '\u2699'; + case 'conversations': return '\uD83D\uDCAC'; + default: return '\u2022'; + } +} + +function getImportanceColor(importance) { + if (importance >= 8) return 'danger'; + if (importance >= 6) return 'warning'; + if (importance >= 4) return 'info'; + return ''; +} + +return baseclass.extend({ + getStatus: callStatus, + getMemories: callGetMemories, + search: callSearch, + getStats: callStats, + add: callAdd, + delete: callDelete, + cleanup: callCleanup, + summarize: callSummarize, + + formatRelativeTime: formatRelativeTime, + formatBytes: formatBytes, + getCategoryIcon: getCategoryIcon, + getImportanceColor: getImportanceColor, + + getOverview: function() { + return Promise.all([ + callStatus(), + callGetMemories(null, 50), + callStats() + ]).then(function(results) { + return { + status: results[0] || {}, + memories: (results[1] || {}).memories || [], + stats: (results[2] || {}).agents || {} + }; + }); + } +}); diff --git a/package/secubox/luci-app-localrecall/htdocs/luci-static/resources/localrecall/dashboard.css b/package/secubox/luci-app-localrecall/htdocs/luci-static/resources/localrecall/dashboard.css new file mode 100644 index 00000000..03586df3 --- /dev/null +++ b/package/secubox/luci-app-localrecall/htdocs/luci-static/resources/localrecall/dashboard.css @@ -0,0 +1,333 @@ +/* LocalRecall Dashboard Styles */ + +.lr-view { + max-width: 1200px; + margin: 0 auto; + padding: 1rem; +} + +.lr-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--border-color-medium, #ddd); +} + +.lr-title { + font-size: 1.5rem; + font-weight: 600; +} + +.lr-status { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.lr-dot { + width: 10px; + height: 10px; + border-radius: 50%; +} + +.lr-dot.online { + background: #28a745; + box-shadow: 0 0 6px #28a745; +} + +.lr-dot.offline { + background: #dc3545; +} + +.lr-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; +} + +.lr-stat { + background: var(--background-color-low, #f8f9fa); + border-radius: 8px; + padding: 1rem; + text-align: center; + border-left: 4px solid var(--border-color-medium, #ddd); +} + +.lr-stat.danger { + border-left-color: #dc3545; +} + +.lr-stat.success { + border-left-color: #28a745; +} + +.lr-stat.info { + border-left-color: #17a2b8; +} + +.lr-stat-value { + font-size: 1.5rem; + font-weight: 700; +} + +.lr-stat-label { + font-size: 0.85rem; + color: var(--text-color-low, #666); +} + +.lr-grid-2 { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1rem; + margin-bottom: 1rem; +} + +.lr-card { + background: var(--background-color-high, #fff); + border: 1px solid var(--border-color-medium, #ddd); + border-radius: 8px; + margin-bottom: 1rem; +} + +.lr-card-header { + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border-color-medium, #ddd); + font-weight: 600; + display: flex; + justify-content: space-between; + align-items: center; +} + +.lr-card-body { + padding: 1rem; +} + +.lr-categories { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.lr-category { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.5rem; + border-radius: 4px; + background: var(--background-color-low, #f8f9fa); +} + +.lr-category.danger { + background: #f8d7da; +} + +.lr-category.success { + background: #d4edda; +} + +.lr-category.info { + background: #d1ecf1; +} + +.lr-category-icon { + font-size: 1.2rem; +} + +.lr-category-name { + flex: 1; + text-transform: capitalize; +} + +.lr-category-count { + font-weight: 600; +} + +.lr-agents { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.lr-agent { + display: flex; + justify-content: space-between; + padding: 0.5rem; + background: var(--background-color-low, #f8f9fa); + border-radius: 4px; +} + +.lr-agent-name { + font-size: 0.9rem; +} + +.lr-agent-count { + font-weight: 600; +} + +.lr-actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.lr-btn { + padding: 0.5rem 1rem; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + transition: opacity 0.2s; +} + +.lr-btn:hover { + opacity: 0.85; +} + +.lr-btn-primary { + background: #007bff; + color: #fff; +} + +.lr-btn-secondary { + background: #6c757d; + color: #fff; +} + +.lr-btn-success { + background: #28a745; + color: #fff; +} + +.lr-btn-warning { + background: #ffc107; + color: #212529; +} + +.lr-btn-danger { + background: #dc3545; + color: #fff; +} + +.lr-btn-info { + background: #17a2b8; + color: #fff; +} + +.lr-btn-sm { + padding: 0.25rem 0.5rem; + font-size: 0.8rem; +} + +.lr-add-form { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.lr-form-row { + display: flex; + gap: 0.75rem; +} + +.lr-select { + padding: 0.5rem; + border: 1px solid var(--border-color-medium, #ddd); + border-radius: 4px; + min-width: 140px; +} + +.lr-textarea { + width: 100%; + padding: 0.5rem; + border: 1px solid var(--border-color-medium, #ddd); + border-radius: 4px; + resize: vertical; +} + +.lr-table { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; +} + +.lr-table th, +.lr-table td { + padding: 0.5rem; + text-align: left; + border-bottom: 1px solid var(--border-color-medium, #ddd); +} + +.lr-table th { + background: var(--background-color-low, #f8f9fa); + font-weight: 600; +} + +.lr-time { + font-size: 0.8rem; + color: var(--text-color-low, #666); + white-space: nowrap; +} + +.lr-agent { + font-family: monospace; + font-size: 0.8rem; +} + +.lr-content { + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; +} + +.lr-cat-badge { + font-size: 1rem; +} + +.lr-badge { + display: inline-block; + padding: 0.2rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + background: var(--background-color-low, #e9ecef); +} + +.lr-badge.danger { + background: #f8d7da; + color: #721c24; +} + +.lr-badge.warning { + background: #fff3cd; + color: #856404; +} + +.lr-badge.info { + background: #d1ecf1; + color: #0c5460; +} + +.lr-empty { + color: var(--text-color-low, #666); + text-align: center; + padding: 2rem; +} + +.spinning::after { + content: ''; + display: inline-block; + width: 1em; + height: 1em; + margin-left: 0.5em; + border: 2px solid currentColor; + border-right-color: transparent; + border-radius: 50%; + animation: spin 0.75s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} diff --git a/package/secubox/luci-app-localrecall/htdocs/luci-static/resources/view/localrecall/dashboard.js b/package/secubox/luci-app-localrecall/htdocs/luci-static/resources/view/localrecall/dashboard.js new file mode 100644 index 00000000..d65a3bb0 --- /dev/null +++ b/package/secubox/luci-app-localrecall/htdocs/luci-static/resources/view/localrecall/dashboard.js @@ -0,0 +1,338 @@ +'use strict'; +'require view'; +'require dom'; +'require poll'; +'require ui'; +'require localrecall.api as api'; + +/** + * LocalRecall Memory Dashboard - v1.0.0 + * AI agent memory visualization and management + */ + +return view.extend({ + load: function() { + var link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = L.resource('localrecall/dashboard.css'); + document.head.appendChild(link); + return api.getOverview().catch(function() { return {}; }); + }, + + render: function(data) { + var self = this; + var s = data.status || {}; + var memories = data.memories || []; + var stats = data.stats || {}; + + var view = E('div', { 'class': 'lr-view' }, [ + // Header + E('div', { 'class': 'lr-header' }, [ + E('div', { 'class': 'lr-title' }, 'LocalRecall Memory'), + E('div', { 'class': 'lr-status' }, [ + E('span', { 'class': 'lr-dot ' + (s.localai_status === 'online' ? 'online' : 'offline') }), + 'LocalAI: ' + (s.localai_status || 'offline') + ]) + ]), + + // Stats row + E('div', { 'class': 'lr-stats', 'id': 'lr-stats' }, this.renderStats(s)), + + // Two column layout + E('div', { 'class': 'lr-grid-2' }, [ + // Categories card + E('div', { 'class': 'lr-card' }, [ + E('div', { 'class': 'lr-card-header' }, 'Memory Categories'), + E('div', { 'class': 'lr-card-body' }, this.renderCategories(s)) + ]), + // Agent Stats card + E('div', { 'class': 'lr-card' }, [ + E('div', { 'class': 'lr-card-header' }, 'By Agent'), + E('div', { 'class': 'lr-card-body', 'id': 'lr-agents' }, this.renderAgents(stats)) + ]) + ]), + + // Actions card + E('div', { 'class': 'lr-card' }, [ + E('div', { 'class': 'lr-card-header' }, 'Actions'), + E('div', { 'class': 'lr-card-body' }, this.renderActions()) + ]), + + // Add Memory card + E('div', { 'class': 'lr-card' }, [ + E('div', { 'class': 'lr-card-header' }, 'Add Memory'), + E('div', { 'class': 'lr-card-body' }, this.renderAddForm()) + ]), + + // Memories table card + E('div', { 'class': 'lr-card' }, [ + E('div', { 'class': 'lr-card-header' }, [ + 'Recent Memories', + E('span', { 'class': 'lr-badge' }, String(s.total || 0)) + ]), + E('div', { 'class': 'lr-card-body', 'id': 'lr-memories' }, this.renderMemories(memories)) + ]) + ]); + + poll.add(L.bind(this.pollData, this), 30); + return view; + }, + + renderStats: function(s) { + var statItems = [ + { label: 'Total', value: s.total || 0, type: '' }, + { label: 'Threats', value: s.threats || 0, type: (s.threats || 0) > 0 ? 'danger' : '' }, + { label: 'Decisions', value: s.decisions || 0, type: '' }, + { label: 'Patterns', value: s.patterns || 0, type: '' } + ]; + return statItems.map(function(st) { + return E('div', { 'class': 'lr-stat ' + st.type }, [ + E('div', { 'class': 'lr-stat-value' }, String(st.value)), + E('div', { 'class': 'lr-stat-label' }, st.label) + ]); + }); + }, + + renderCategories: function(s) { + var cats = [ + { name: 'threats', icon: '\u26A0', count: s.threats || 0, color: 'danger' }, + { name: 'decisions', icon: '\u2714', count: s.decisions || 0, color: 'success' }, + { name: 'patterns', icon: '\uD83D\uDD0D', count: s.patterns || 0, color: 'info' }, + { name: 'configs', icon: '\u2699', count: s.configs || 0, color: '' }, + { name: 'conversations', icon: '\uD83D\uDCAC', count: s.conversations || 0, color: '' } + ]; + return E('div', { 'class': 'lr-categories' }, cats.map(function(c) { + return E('div', { 'class': 'lr-category ' + c.color }, [ + E('span', { 'class': 'lr-category-icon' }, c.icon), + E('span', { 'class': 'lr-category-name' }, c.name), + E('span', { 'class': 'lr-category-count' }, String(c.count)) + ]); + })); + }, + + renderAgents: function(stats) { + var agents = [ + { id: 'threat_analyst', name: 'Threat Analyst', count: stats.threat_analyst || 0 }, + { id: 'dns_guard', name: 'DNS Guard', count: stats.dns_guard || 0 }, + { id: 'network_anomaly', name: 'Network Anomaly', count: stats.network_anomaly || 0 }, + { id: 'cve_triage', name: 'CVE Triage', count: stats.cve_triage || 0 }, + { id: 'user', name: 'User', count: stats.user || 0 } + ]; + return E('div', { 'class': 'lr-agents' }, agents.map(function(a) { + return E('div', { 'class': 'lr-agent' }, [ + E('span', { 'class': 'lr-agent-name' }, a.name), + E('span', { 'class': 'lr-agent-count' }, String(a.count)) + ]); + })); + }, + + renderActions: function() { + var self = this; + return E('div', { 'class': 'lr-actions' }, [ + E('button', { + 'class': 'lr-btn lr-btn-primary', + 'click': function() { self.summarizeMemories(); } + }, 'AI Summary'), + E('button', { + 'class': 'lr-btn lr-btn-secondary', + 'click': function() { self.searchMemories(); } + }, 'Search'), + E('button', { + 'class': 'lr-btn lr-btn-warning', + 'click': function() { self.cleanupMemories(); } + }, 'Cleanup Old'), + E('button', { + 'class': 'lr-btn lr-btn-info', + 'click': function() { self.exportMemories(); } + }, 'Export') + ]); + }, + + renderAddForm: function() { + var self = this; + return E('div', { 'class': 'lr-add-form' }, [ + E('div', { 'class': 'lr-form-row' }, [ + E('select', { 'id': 'lr-add-category', 'class': 'lr-select' }, [ + E('option', { 'value': 'patterns' }, 'Pattern'), + E('option', { 'value': 'threats' }, 'Threat'), + E('option', { 'value': 'decisions' }, 'Decision'), + E('option', { 'value': 'configs' }, 'Config'), + E('option', { 'value': 'conversations' }, 'Conversation') + ]), + E('select', { 'id': 'lr-add-importance', 'class': 'lr-select' }, [ + E('option', { 'value': '5' }, 'Normal (5)'), + E('option', { 'value': '3' }, 'Low (3)'), + E('option', { 'value': '7' }, 'High (7)'), + E('option', { 'value': '9' }, 'Critical (9)') + ]) + ]), + E('div', { 'class': 'lr-form-row' }, [ + E('textarea', { + 'id': 'lr-add-content', + 'class': 'lr-textarea', + 'placeholder': 'Enter memory content...', + 'rows': 3 + }) + ]), + E('div', { 'class': 'lr-form-row' }, [ + E('button', { + 'class': 'lr-btn lr-btn-success', + 'click': function() { self.addMemory(); } + }, 'Add Memory') + ]) + ]); + }, + + renderMemories: function(memories) { + var self = this; + if (!memories || !memories.length) { + return E('div', { 'class': 'lr-empty' }, 'No memories stored yet'); + } + + // Handle both array and object formats + var memArray = Array.isArray(memories) ? memories : [memories]; + + return E('table', { 'class': 'lr-table' }, [ + E('thead', {}, E('tr', {}, [ + E('th', {}, 'Time'), + E('th', {}, 'Cat'), + E('th', {}, 'Agent'), + E('th', {}, 'Content'), + E('th', {}, 'Imp'), + E('th', {}, '') + ])), + E('tbody', {}, memArray.slice(0, 30).map(function(mem) { + if (!mem || !mem.id) return null; + var impColor = api.getImportanceColor(mem.importance || 5); + return E('tr', {}, [ + E('td', { 'class': 'lr-time' }, api.formatRelativeTime(mem.timestamp)), + E('td', {}, E('span', { 'class': 'lr-cat-badge' }, api.getCategoryIcon(mem.category))), + E('td', { 'class': 'lr-agent' }, (mem.agent || '-').substring(0, 10)), + E('td', { 'class': 'lr-content' }, (mem.content || '-').substring(0, 60) + ((mem.content || '').length > 60 ? '...' : '')), + E('td', {}, E('span', { 'class': 'lr-badge ' + impColor }, String(mem.importance || 5))), + E('td', {}, E('button', { + 'class': 'lr-btn lr-btn-sm lr-btn-danger', + 'click': function() { self.deleteMemory(mem.id); } + }, '\u2717')) + ]); + }).filter(Boolean)) + ]); + }, + + addMemory: function() { + var category = document.getElementById('lr-add-category').value; + var importance = parseInt(document.getElementById('lr-add-importance').value, 10); + var content = document.getElementById('lr-add-content').value.trim(); + + if (!content) { + ui.addNotification(null, E('p', {}, 'Content is required'), 'error'); + return; + } + + api.add(category, content, 'user', importance).then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', {}, 'Memory added: ' + result.id), 'success'); + document.getElementById('lr-add-content').value = ''; + window.location.reload(); + } else { + ui.addNotification(null, E('p', {}, 'Failed to add memory'), 'error'); + } + }); + }, + + deleteMemory: function(id) { + if (!confirm('Delete this memory?')) return; + + api.delete(id).then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', {}, 'Memory deleted'), 'success'); + window.location.reload(); + } else { + ui.addNotification(null, E('p', {}, 'Failed to delete memory'), 'error'); + } + }); + }, + + summarizeMemories: function() { + ui.showModal('AI Summary', [ + E('p', { 'class': 'spinning' }, 'Generating AI summary (may take up to 60s)...') + ]); + + api.summarize(null).then(function(result) { + ui.hideModal(); + if (result.summary) { + ui.showModal('Memory Summary', [ + E('div', { 'style': 'white-space: pre-wrap; max-height: 400px; overflow-y: auto;' }, result.summary), + E('div', { 'class': 'right' }, E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, 'Close')) + ]); + } else { + ui.addNotification(null, E('p', {}, 'Error: ' + (result.error || 'Summary failed')), 'error'); + } + }).catch(function() { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Summary failed'), 'error'); + }); + }, + + searchMemories: function() { + var query = prompt('Search memories:'); + if (!query) return; + + api.search(query, 50).then(function(result) { + var results = result.results || []; + ui.showModal('Search Results (' + results.length + ')', [ + E('div', { 'style': 'max-height: 400px; overflow-y: auto;' }, + results.length ? results.map(function(m) { + return E('div', { 'style': 'padding: 0.5rem; border-bottom: 1px solid #ddd;' }, [ + E('strong', {}, m.category + ' '), + E('span', {}, m.content) + ]); + }) : E('p', {}, 'No results found') + ), + E('div', { 'class': 'right' }, E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, 'Close')) + ]); + }); + }, + + cleanupMemories: function() { + if (!confirm('Delete old memories (keeping important ones)?')) return; + + api.cleanup().then(function(result) { + ui.addNotification(null, E('p', {}, 'Cleanup complete. Deleted: ' + (result.deleted || 0)), 'success'); + window.location.reload(); + }); + }, + + exportMemories: function() { + window.location.href = L.url('admin/secubox/ai/localrecall') + '?export=1'; + ui.addNotification(null, E('p', {}, 'Export started'), 'info'); + }, + + pollData: function() { + var self = this; + return api.getOverview().then(function(data) { + var s = data.status || {}; + var memories = data.memories || []; + var stats = data.stats || {}; + + var el = document.getElementById('lr-stats'); + if (el) dom.content(el, self.renderStats(s)); + + el = document.getElementById('lr-agents'); + if (el) dom.content(el, self.renderAgents(stats)); + + el = document.getElementById('lr-memories'); + if (el) dom.content(el, self.renderMemories(memories)); + }); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-localrecall/root/usr/libexec/rpcd/luci.localrecall b/package/secubox/luci-app-localrecall/root/usr/libexec/rpcd/luci.localrecall new file mode 100644 index 00000000..f9e5f7b7 --- /dev/null +++ b/package/secubox/luci-app-localrecall/root/usr/libexec/rpcd/luci.localrecall @@ -0,0 +1,220 @@ +#!/bin/sh +# LocalRecall Memory RPCD Handler + +. /usr/share/libubox/jshn.sh + +CONFIG="localrecall" +STORAGE_DIR="/var/lib/localrecall" +MEMORIES_FILE="$STORAGE_DIR/memories.json" + +# Source memory library +[ -f /usr/lib/localrecall/memory.sh ] && . /usr/lib/localrecall/memory.sh +[ -f /usr/lib/localrecall/ai.sh ] && . /usr/lib/localrecall/ai.sh + +log_info() { logger -t localrecall-rpcd "$*"; } + +uci_get() { uci -q get "${CONFIG}.$1"; } + +case "$1" in + list) + cat <<'EOF' +{ + "status": {}, + "get_memories": {"category": "string", "limit": 50}, + "search": {"query": "string", "limit": 20}, + "stats": {}, + "add": {"category": "string", "content": "string", "agent": "string", "importance": 5}, + "delete": {"id": "string"}, + "cleanup": {}, + "summarize": {"category": "string"}, + "export": {}, + "import": {"data": "string"} +} +EOF + ;; + + call) + case "$2" in + status) + enabled=$(uci_get main.enabled) + storage=$(uci_get main.storage_path) + max_mem=$(uci_get main.max_memories) + retention=$(uci_get main.retention_days) + + total=0 + threats=0 + decisions=0 + patterns=0 + configs=0 + convs=0 + + if [ -f "$MEMORIES_FILE" ]; then + total=$(jsonfilter -i "$MEMORIES_FILE" -e '@[*]' 2>/dev/null | wc -l) + threats=$(jsonfilter -i "$MEMORIES_FILE" -e "@[@.category='threats']" 2>/dev/null | wc -l) + decisions=$(jsonfilter -i "$MEMORIES_FILE" -e "@[@.category='decisions']" 2>/dev/null | wc -l) + patterns=$(jsonfilter -i "$MEMORIES_FILE" -e "@[@.category='patterns']" 2>/dev/null | wc -l) + configs=$(jsonfilter -i "$MEMORIES_FILE" -e "@[@.category='configs']" 2>/dev/null | wc -l) + convs=$(jsonfilter -i "$MEMORIES_FILE" -e "@[@.category='conversations']" 2>/dev/null | wc -l) + fi + + localai_status="offline" + localai_url=$(uci_get main.localai_url) + [ -z "$localai_url" ] && localai_url="http://127.0.0.1:8091" + wget -q -O /dev/null --timeout=2 "${localai_url}/v1/models" 2>/dev/null && localai_status="online" + + storage_size="0" + [ -d "${storage:-/var/lib/localrecall}" ] && storage_size=$(du -s "${storage:-/var/lib/localrecall}" 2>/dev/null | cut -f1) + + cat </dev/null) + limit=$(echo "$input" | jsonfilter -e '@.limit' 2>/dev/null) + [ -z "$limit" ] && limit=50 + + memories='[]' + if [ -f "$MEMORIES_FILE" ]; then + if [ -n "$category" ]; then + memories=$(jsonfilter -i "$MEMORIES_FILE" -e "@[@.category='$category']" 2>/dev/null | head -n "$limit" | tr '\n' ',' | sed 's/,$//') + [ -z "$memories" ] && memories='[]' || memories="[$memories]" + else + # Get all, sorted by timestamp (most recent first) + memories=$(cat "$MEMORIES_FILE") + fi + fi + + printf '{"memories":%s}' "$memories" + ;; + + search) + read -r input + query=$(echo "$input" | jsonfilter -e '@.query' 2>/dev/null) + limit=$(echo "$input" | jsonfilter -e '@.limit' 2>/dev/null) + [ -z "$limit" ] && limit=20 + + results='[]' + if [ -n "$query" ] && [ -f "$MEMORIES_FILE" ]; then + results=$(grep -i "$query" "$MEMORIES_FILE" 2>/dev/null | head -n "$limit" | tr '\n' ',' | sed 's/,$//') + [ -z "$results" ] && results='[]' || results="[$results]" + fi + + printf '{"results":%s}' "$results" + ;; + + stats) + # Agent breakdown + agent_stats='{}' + if [ -f "$MEMORIES_FILE" ]; then + ta=$(jsonfilter -i "$MEMORIES_FILE" -e "@[@.agent='threat_analyst']" 2>/dev/null | wc -l) + dg=$(jsonfilter -i "$MEMORIES_FILE" -e "@[@.agent='dns_guard']" 2>/dev/null | wc -l) + na=$(jsonfilter -i "$MEMORIES_FILE" -e "@[@.agent='network_anomaly']" 2>/dev/null | wc -l) + ct=$(jsonfilter -i "$MEMORIES_FILE" -e "@[@.agent='cve_triage']" 2>/dev/null | wc -l) + us=$(jsonfilter -i "$MEMORIES_FILE" -e "@[@.agent='user']" 2>/dev/null | wc -l) + agent_stats="{\"threat_analyst\":$ta,\"dns_guard\":$dg,\"network_anomaly\":$na,\"cve_triage\":$ct,\"user\":$us}" + fi + + printf '{"agents":%s}' "$agent_stats" + ;; + + add) + read -r input + category=$(echo "$input" | jsonfilter -e '@.category' 2>/dev/null) + content=$(echo "$input" | jsonfilter -e '@.content' 2>/dev/null) + agent=$(echo "$input" | jsonfilter -e '@.agent' 2>/dev/null) + importance=$(echo "$input" | jsonfilter -e '@.importance' 2>/dev/null) + + [ -z "$category" ] || [ -z "$content" ] && { + echo '{"error":"Category and content required"}' + exit 0 + } + + [ -z "$agent" ] && agent="user" + [ -z "$importance" ] && importance=5 + + mkdir -p "$STORAGE_DIR" + id=$(add_memory "$category" "$agent" "$content" '{}' "$importance") + + printf '{"success":true,"id":"%s"}' "$id" + ;; + + delete) + read -r input + id=$(echo "$input" | jsonfilter -e '@.id' 2>/dev/null) + + [ -z "$id" ] && { + echo '{"error":"Memory ID required"}' + exit 0 + } + + delete_memory "$id" + echo '{"success":true}' + ;; + + cleanup) + retention=$(uci_get main.retention_days) + keep_imp=$(uci_get cleanup.keep_important) + + deleted=$(cleanup_old "${retention:-90}" "${keep_imp:-1}") + printf '{"success":true,"deleted":%d}' "${deleted:-0}" + ;; + + summarize) + read -r input + category=$(echo "$input" | jsonfilter -e '@.category' 2>/dev/null) + + localai_url=$(uci_get main.localai_url) + [ -z "$localai_url" ] && localai_url="http://127.0.0.1:8091" + + if ! curl -s --max-time 2 "${localai_url}/v1/models" >/dev/null 2>&1; then + echo '{"error":"LocalAI not available"}' + exit 0 + fi + + summary=$(summarize_memories "$category" 2>/dev/null) + summary=$(printf '%s' "$summary" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ') + + printf '{"summary":"%s"}' "$summary" + ;; + + export) + memories='[]' + [ -f "$MEMORIES_FILE" ] && memories=$(cat "$MEMORIES_FILE") + printf '{"data":%s}' "$memories" + ;; + + import) + read -r input + data=$(echo "$input" | jsonfilter -e '@.data' 2>/dev/null) + + [ -z "$data" ] && { + echo '{"error":"No data provided"}' + exit 0 + } + + mkdir -p "$STORAGE_DIR" + echo "$data" > "$MEMORIES_FILE" + echo '{"success":true}' + ;; + + *) + echo '{"error":"Unknown method"}' + ;; + esac + ;; +esac diff --git a/package/secubox/luci-app-localrecall/root/usr/share/luci/menu.d/luci-app-localrecall.json b/package/secubox/luci-app-localrecall/root/usr/share/luci/menu.d/luci-app-localrecall.json new file mode 100644 index 00000000..a7513631 --- /dev/null +++ b/package/secubox/luci-app-localrecall/root/usr/share/luci/menu.d/luci-app-localrecall.json @@ -0,0 +1,14 @@ +{ + "admin/secubox/ai/localrecall": { + "title": "LocalRecall", + "order": 40, + "action": { + "type": "view", + "path": "localrecall/dashboard" + }, + "depends": { + "acl": ["luci-app-localrecall"], + "uci": {"localrecall": true} + } + } +} diff --git a/package/secubox/luci-app-localrecall/root/usr/share/rpcd/acl.d/luci-app-localrecall.json b/package/secubox/luci-app-localrecall/root/usr/share/rpcd/acl.d/luci-app-localrecall.json new file mode 100644 index 00000000..67db6a2d --- /dev/null +++ b/package/secubox/luci-app-localrecall/root/usr/share/rpcd/acl.d/luci-app-localrecall.json @@ -0,0 +1,17 @@ +{ + "luci-app-localrecall": { + "description": "Grant access to LocalRecall AI Memory", + "read": { + "ubus": { + "luci.localrecall": ["status", "get_memories", "search", "stats"] + }, + "uci": ["localrecall"] + }, + "write": { + "ubus": { + "luci.localrecall": ["add", "delete", "cleanup", "summarize", "export", "import"] + }, + "uci": ["localrecall"] + } + } +} diff --git a/package/secubox/luci-app-network-anomaly/Makefile b/package/secubox/luci-app-network-anomaly/Makefile new file mode 100644 index 00000000..b3846572 --- /dev/null +++ b/package/secubox/luci-app-network-anomaly/Makefile @@ -0,0 +1,45 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI Network Anomaly Detection Dashboard +LUCI_DEPENDS:=+secubox-network-anomaly +luci-base +LUCI_PKGARCH:=all + +PKG_NAME:=luci-app-network-anomaly +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=SecuBox Team +PKG_LICENSE:=Apache-2.0 + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/luci-app-network-anomaly/install + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-network-anomaly.json $(1)/usr/share/luci/menu.d/ + + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-network-anomaly.json $(1)/usr/share/rpcd/acl.d/ + + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.network-anomaly $(1)/usr/libexec/rpcd/ + + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/network-anomaly + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/network-anomaly/dashboard.js $(1)/www/luci-static/resources/view/network-anomaly/ + + $(INSTALL_DIR) $(1)/www/luci-static/resources/network-anomaly + $(INSTALL_DATA) ./htdocs/luci-static/resources/network-anomaly/api.js $(1)/www/luci-static/resources/network-anomaly/ + $(INSTALL_DATA) ./htdocs/luci-static/resources/network-anomaly/dashboard.css $(1)/www/luci-static/resources/network-anomaly/ +endef + +define Package/luci-app-network-anomaly/postinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + rm -f /tmp/luci-indexcache* /tmp/luci-modulecache/* 2>/dev/null + /etc/init.d/rpcd restart 2>/dev/null +} +exit 0 +endef + +$(eval $(call BuildPackage,luci-app-network-anomaly)) diff --git a/package/secubox/luci-app-network-anomaly/htdocs/luci-static/resources/network-anomaly/api.js b/package/secubox/luci-app-network-anomaly/htdocs/luci-static/resources/network-anomaly/api.js new file mode 100644 index 00000000..a44e14e6 --- /dev/null +++ b/package/secubox/luci-app-network-anomaly/htdocs/luci-static/resources/network-anomaly/api.js @@ -0,0 +1,121 @@ +'use strict'; +'require baseclass'; +'require rpc'; + +/** + * Network Anomaly Detection API + * Package: luci-app-network-anomaly + * RPCD object: luci.network-anomaly + * Version: 1.0.0 + */ + +var callStatus = rpc.declare({ + object: 'luci.network-anomaly', + method: 'status', + expect: { } +}); + +var callGetAlerts = rpc.declare({ + object: 'luci.network-anomaly', + method: 'get_alerts', + params: ['limit'], + expect: { } +}); + +var callGetStats = rpc.declare({ + object: 'luci.network-anomaly', + method: 'get_stats', + expect: { } +}); + +var callRun = rpc.declare({ + object: 'luci.network-anomaly', + method: 'run', + expect: { } +}); + +var callAckAlert = rpc.declare({ + object: 'luci.network-anomaly', + method: 'ack_alert', + params: ['id'], + expect: { } +}); + +var callClearAlerts = rpc.declare({ + object: 'luci.network-anomaly', + method: 'clear_alerts', + expect: { } +}); + +var callResetBaseline = rpc.declare({ + object: 'luci.network-anomaly', + method: 'reset_baseline', + expect: { } +}); + +var callAnalyze = rpc.declare({ + object: 'luci.network-anomaly', + method: 'analyze', + expect: { } +}); + +function formatBytes(bytes) { + if (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 parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +function formatRelativeTime(dateStr) { + if (!dateStr) return 'N/A'; + try { + var date = new Date(dateStr); + 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 dateStr; + } +} + +function getSeverityClass(severity) { + switch (severity) { + case 'high': return 'danger'; + case 'medium': return 'warning'; + case 'low': return 'info'; + default: return ''; + } +} + +return baseclass.extend({ + getStatus: callStatus, + getAlerts: callGetAlerts, + getStats: callGetStats, + run: callRun, + ackAlert: callAckAlert, + clearAlerts: callClearAlerts, + resetBaseline: callResetBaseline, + analyze: callAnalyze, + + formatBytes: formatBytes, + formatRelativeTime: formatRelativeTime, + getSeverityClass: getSeverityClass, + + getOverview: function() { + return Promise.all([ + callStatus(), + callGetAlerts(50), + callGetStats() + ]).then(function(results) { + return { + status: results[0] || {}, + alerts: (results[1] || {}).alerts || [], + stats: results[2] || {} + }; + }); + } +}); diff --git a/package/secubox/luci-app-network-anomaly/htdocs/luci-static/resources/network-anomaly/dashboard.css b/package/secubox/luci-app-network-anomaly/htdocs/luci-static/resources/network-anomaly/dashboard.css new file mode 100644 index 00000000..3b66eec1 --- /dev/null +++ b/package/secubox/luci-app-network-anomaly/htdocs/luci-static/resources/network-anomaly/dashboard.css @@ -0,0 +1,300 @@ +/* Network Anomaly Dashboard Styles */ + +.na-view { + max-width: 1200px; + margin: 0 auto; + padding: 1rem; +} + +.na-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--border-color-medium, #ddd); +} + +.na-title { + font-size: 1.5rem; + font-weight: 600; +} + +.na-status { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.na-dot { + width: 10px; + height: 10px; + border-radius: 50%; +} + +.na-dot.online { + background: #28a745; + box-shadow: 0 0 6px #28a745; +} + +.na-dot.offline { + background: #dc3545; +} + +.na-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; +} + +.na-stat { + background: var(--background-color-low, #f8f9fa); + border-radius: 8px; + padding: 1rem; + text-align: center; + border-left: 4px solid var(--border-color-medium, #ddd); +} + +.na-stat.success { + border-left-color: #28a745; +} + +.na-stat.warning { + border-left-color: #ffc107; +} + +.na-stat.danger { + border-left-color: #dc3545; +} + +.na-stat-value { + font-size: 1.5rem; + font-weight: 700; +} + +.na-stat-label { + font-size: 0.85rem; + color: var(--text-color-low, #666); +} + +.na-grid-2 { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1rem; + margin-bottom: 1rem; +} + +.na-card { + background: var(--background-color-high, #fff); + border: 1px solid var(--border-color-medium, #ddd); + border-radius: 8px; + margin-bottom: 1rem; +} + +.na-card-header { + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border-color-medium, #ddd); + font-weight: 600; + display: flex; + justify-content: space-between; + align-items: center; +} + +.na-card-body { + padding: 1rem; +} + +.na-health { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.na-health-item { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.na-health-icon { + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.85rem; +} + +.na-health-icon.ok { + background: #d4edda; + color: #155724; +} + +.na-health-icon.error { + background: #f8d7da; + color: #721c24; +} + +.na-health-label { + font-weight: 500; +} + +.na-health-value { + font-size: 0.85rem; + color: var(--text-color-low, #666); +} + +.na-network-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 0.5rem; +} + +.na-network-stat { + display: flex; + justify-content: space-between; + padding: 0.5rem; + background: var(--background-color-low, #f8f9fa); + border-radius: 4px; +} + +.na-network-stat-label { + color: var(--text-color-low, #666); +} + +.na-network-stat-value { + font-weight: 600; +} + +.na-actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.na-btn { + padding: 0.5rem 1rem; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + transition: opacity 0.2s; +} + +.na-btn:hover { + opacity: 0.85; +} + +.na-btn-primary { + background: #007bff; + color: #fff; +} + +.na-btn-secondary { + background: #6c757d; + color: #fff; +} + +.na-btn-success { + background: #28a745; + color: #fff; +} + +.na-btn-warning { + background: #ffc107; + color: #212529; +} + +.na-btn-danger { + background: #dc3545; + color: #fff; +} + +.na-btn-sm { + padding: 0.25rem 0.5rem; + font-size: 0.8rem; +} + +.na-table { + width: 100%; + border-collapse: collapse; +} + +.na-table th, +.na-table td { + padding: 0.75rem; + text-align: left; + border-bottom: 1px solid var(--border-color-medium, #ddd); +} + +.na-table th { + background: var(--background-color-low, #f8f9fa); + font-weight: 600; +} + +.na-table tr.na-acked { + opacity: 0.6; +} + +.na-badge { + display: inline-block; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + background: var(--background-color-low, #e9ecef); +} + +.na-badge.danger { + background: #f8d7da; + color: #721c24; +} + +.na-badge.warning { + background: #fff3cd; + color: #856404; +} + +.na-badge.info { + background: #d1ecf1; + color: #0c5460; +} + +.na-time { + font-size: 0.85rem; + color: var(--text-color-low, #666); +} + +.na-alert-type { + font-family: monospace; +} + +.na-acked-label { + font-size: 0.75rem; + color: var(--text-color-low, #999); +} + +.na-empty { + color: var(--text-color-low, #666); + text-align: center; + padding: 2rem; +} + +.spinning::after { + content: ''; + display: inline-block; + width: 1em; + height: 1em; + margin-left: 0.5em; + border: 2px solid currentColor; + border-right-color: transparent; + border-radius: 50%; + animation: spin 0.75s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} diff --git a/package/secubox/luci-app-network-anomaly/htdocs/luci-static/resources/view/network-anomaly/dashboard.js b/package/secubox/luci-app-network-anomaly/htdocs/luci-static/resources/view/network-anomaly/dashboard.js new file mode 100644 index 00000000..61d06a60 --- /dev/null +++ b/package/secubox/luci-app-network-anomaly/htdocs/luci-static/resources/view/network-anomaly/dashboard.js @@ -0,0 +1,272 @@ +'use strict'; +'require view'; +'require dom'; +'require poll'; +'require ui'; +'require network-anomaly.api as api'; + +/** + * Network Anomaly Detection Dashboard - v1.0.0 + * AI-powered network traffic anomaly detection + * + * Following CrowdSec Dashboard KISS template pattern + */ + +return view.extend({ + load: function() { + var link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = L.resource('network-anomaly/dashboard.css'); + document.head.appendChild(link); + return api.getOverview().catch(function() { return {}; }); + }, + + render: function(data) { + var self = this; + var s = data.status || {}; + var alerts = data.alerts || []; + var stats = data.stats || {}; + + var view = E('div', { 'class': 'na-view' }, [ + // Header + E('div', { 'class': 'na-header' }, [ + E('div', { 'class': 'na-title' }, 'Network Anomaly Detection'), + E('div', { 'class': 'na-status' }, [ + E('span', { 'class': 'na-dot ' + (s.daemon_running ? 'online' : 'offline') }), + s.daemon_running ? 'Running' : 'Stopped' + ]) + ]), + + // Stats row + E('div', { 'class': 'na-stats', 'id': 'na-stats' }, this.renderStats(s, stats)), + + // Two column layout + E('div', { 'class': 'na-grid-2' }, [ + // Health card + E('div', { 'class': 'na-card' }, [ + E('div', { 'class': 'na-card-header' }, 'System Health'), + E('div', { 'class': 'na-card-body' }, this.renderHealth(s)) + ]), + // Network Stats card + E('div', { 'class': 'na-card' }, [ + E('div', { 'class': 'na-card-header' }, 'Current Network Stats'), + E('div', { 'class': 'na-card-body', 'id': 'na-network-stats' }, this.renderNetworkStats(stats)) + ]) + ]), + + // Actions card + E('div', { 'class': 'na-card' }, [ + E('div', { 'class': 'na-card-header' }, 'Actions'), + E('div', { 'class': 'na-card-body' }, this.renderActions()) + ]), + + // Alerts card + E('div', { 'class': 'na-card' }, [ + E('div', { 'class': 'na-card-header' }, [ + 'Recent Alerts', + E('span', { 'class': 'na-badge' }, String(alerts.length)) + ]), + E('div', { 'class': 'na-card-body', 'id': 'na-alerts' }, this.renderAlerts(alerts)) + ]) + ]); + + poll.add(L.bind(this.pollData, this), 10); + return view; + }, + + renderStats: function(s, stats) { + var statItems = [ + { label: 'Daemon', value: s.daemon_running ? 'ON' : 'OFF', type: s.daemon_running ? 'success' : 'danger' }, + { label: 'LocalAI', value: s.localai_status === 'online' ? 'OK' : 'OFF', type: s.localai_status === 'online' ? 'success' : 'warning' }, + { label: 'Alerts', value: s.unacked_count || 0, type: (s.unacked_count || 0) > 0 ? 'danger' : 'success' }, + { label: 'Connections', value: stats.total_connections || 0, type: '' } + ]; + return statItems.map(function(st) { + return E('div', { 'class': 'na-stat ' + st.type }, [ + E('div', { 'class': 'na-stat-value' }, String(st.value)), + E('div', { 'class': 'na-stat-label' }, st.label) + ]); + }); + }, + + renderHealth: function(s) { + var checks = [ + { label: 'Daemon', ok: s.daemon_running }, + { label: 'LocalAI', ok: s.localai_status === 'online' }, + { label: 'Auto-Block', ok: s.auto_block, value: s.auto_block ? 'Enabled' : 'Manual' }, + { label: 'Interval', ok: true, value: (s.interval || 60) + 's' }, + { label: 'Last Run', ok: !!s.last_run, value: s.last_run ? api.formatRelativeTime(s.last_run) : 'Never' } + ]; + return E('div', { 'class': 'na-health' }, checks.map(function(c) { + var valueText = c.value ? c.value : (c.ok ? 'OK' : 'Unavailable'); + var iconClass = c.ok ? 'ok' : 'error'; + var iconChar = c.ok ? '\u2713' : '\u2717'; + return E('div', { 'class': 'na-health-item' }, [ + E('div', { 'class': 'na-health-icon ' + iconClass }, iconChar), + E('div', {}, [ + E('div', { 'class': 'na-health-label' }, c.label), + E('div', { 'class': 'na-health-value' }, valueText) + ]) + ]); + })); + }, + + renderNetworkStats: function(stats) { + if (!stats || !stats.timestamp) { + return E('div', { 'class': 'na-empty' }, 'No data collected yet'); + } + var items = [ + { label: 'RX Bytes', value: api.formatBytes(stats.rx_bytes || 0) }, + { label: 'TX Bytes', value: api.formatBytes(stats.tx_bytes || 0) }, + { label: 'Total Connections', value: stats.total_connections || 0 }, + { label: 'Established', value: stats.established || 0 }, + { label: 'New Connections', value: stats.new_connections || 0 }, + { label: 'Unique Ports', value: stats.unique_ports || 0 } + ]; + return E('div', { 'class': 'na-network-stats' }, items.map(function(item) { + return E('div', { 'class': 'na-network-stat' }, [ + E('span', { 'class': 'na-network-stat-label' }, item.label + ':'), + E('span', { 'class': 'na-network-stat-value' }, String(item.value)) + ]); + })); + }, + + renderActions: function() { + var self = this; + return E('div', { 'class': 'na-actions' }, [ + E('button', { + 'class': 'na-btn na-btn-primary', + 'click': function() { self.runDetection(); } + }, 'Run Detection'), + E('button', { + 'class': 'na-btn na-btn-secondary', + 'click': function() { self.runAnalysis(); } + }, 'AI Analysis'), + E('button', { + 'class': 'na-btn na-btn-warning', + 'click': function() { self.resetBaseline(); } + }, 'Reset Baseline'), + E('button', { + 'class': 'na-btn na-btn-danger', + 'click': function() { self.clearAlerts(); } + }, 'Clear Alerts') + ]); + }, + + renderAlerts: function(alerts) { + var self = this; + if (!alerts.length) { + return E('div', { 'class': 'na-empty' }, 'No alerts detected'); + } + return E('table', { 'class': 'na-table' }, [ + E('thead', {}, E('tr', {}, [ + E('th', {}, 'Time'), + E('th', {}, 'Type'), + E('th', {}, 'Severity'), + E('th', {}, 'Message'), + E('th', {}, 'Actions') + ])), + E('tbody', {}, alerts.slice(0, 20).map(function(alert) { + var severity = alert.severity || 'medium'; + var acked = alert.acknowledged; + return E('tr', { 'class': acked ? 'na-acked' : '' }, [ + E('td', { 'class': 'na-time' }, api.formatRelativeTime(alert.timestamp)), + E('td', {}, E('span', { 'class': 'na-alert-type' }, alert.type || '-')), + E('td', {}, E('span', { 'class': 'na-badge ' + api.getSeverityClass(severity) }, severity)), + E('td', {}, alert.message || '-'), + E('td', {}, acked ? E('span', { 'class': 'na-acked-label' }, 'ACK') : + E('button', { + 'class': 'na-btn na-btn-sm na-btn-success', + 'click': function() { self.ackAlert(alert.id); } + }, 'Ack') + ) + ]); + })) + ]); + }, + + runDetection: function() { + ui.showModal('Running Detection', [ + E('p', { 'class': 'spinning' }, 'Running detection cycle...') + ]); + + api.run().then(function() { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Detection cycle started'), 'success'); + }).catch(function() { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Failed to start detection'), 'error'); + }); + }, + + runAnalysis: function() { + ui.showModal('AI Analysis', [ + E('p', { 'class': 'spinning' }, 'Running AI analysis (may take up to 60s)...') + ]); + + api.analyze().then(function(result) { + ui.hideModal(); + if (result.pending) { + ui.addNotification(null, E('p', {}, 'Analysis started in background'), 'info'); + } else if (result.error) { + ui.addNotification(null, E('p', {}, 'Error: ' + result.error), 'error'); + } else { + ui.addNotification(null, E('p', {}, 'Analysis complete'), 'success'); + } + }).catch(function() { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Failed to start analysis'), 'error'); + }); + }, + + resetBaseline: function() { + if (!confirm('Reset the baseline? This will clear historical averages.')) return; + + api.resetBaseline().then(function() { + ui.addNotification(null, E('p', {}, 'Baseline reset'), 'success'); + }).catch(function() { + ui.addNotification(null, E('p', {}, 'Failed to reset baseline'), 'error'); + }); + }, + + clearAlerts: function() { + if (!confirm('Clear all alerts?')) return; + + api.clearAlerts().then(function() { + ui.addNotification(null, E('p', {}, 'Alerts cleared'), 'success'); + window.location.reload(); + }).catch(function() { + ui.addNotification(null, E('p', {}, 'Failed to clear alerts'), 'error'); + }); + }, + + ackAlert: function(id) { + api.ackAlert(id).then(function() { + window.location.reload(); + }).catch(function() { + ui.addNotification(null, E('p', {}, 'Failed to acknowledge alert'), 'error'); + }); + }, + + pollData: function() { + var self = this; + return api.getOverview().then(function(data) { + var s = data.status || {}; + var alerts = data.alerts || []; + var stats = data.stats || {}; + + var el = document.getElementById('na-stats'); + if (el) dom.content(el, self.renderStats(s, stats)); + + el = document.getElementById('na-network-stats'); + if (el) dom.content(el, self.renderNetworkStats(stats)); + + el = document.getElementById('na-alerts'); + if (el) dom.content(el, self.renderAlerts(alerts)); + }); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-network-anomaly/root/usr/libexec/rpcd/luci.network-anomaly b/package/secubox/luci-app-network-anomaly/root/usr/libexec/rpcd/luci.network-anomaly new file mode 100644 index 00000000..e777c3e2 --- /dev/null +++ b/package/secubox/luci-app-network-anomaly/root/usr/libexec/rpcd/luci.network-anomaly @@ -0,0 +1,177 @@ +#!/bin/sh +# Network Anomaly Detection RPCD Handler + +. /usr/share/libubox/jshn.sh + +CONFIG="network-anomaly" +STATE_DIR="/var/lib/network-anomaly" +ALERTS_FILE="$STATE_DIR/alerts.json" +BASELINE_FILE="$STATE_DIR/baseline.json" + +log_info() { logger -t network-anomaly-rpcd "$*"; } + +uci_get() { uci -q get "${CONFIG}.$1"; } + +case "$1" in + list) + cat <<'EOF' +{ + "status": {}, + "get_alerts": {"limit": 50}, + "get_stats": {}, + "run": {}, + "ack_alert": {"id": "string"}, + "clear_alerts": {}, + "reset_baseline": {}, + "analyze": {} +} +EOF + ;; + + call) + case "$2" in + status) + enabled=$(uci_get main.enabled) + interval=$(uci_get main.interval) + auto_block=$(uci_get main.auto_block) + + daemon_running="false" + pgrep -f "network-anomalyctl daemon" >/dev/null 2>&1 && daemon_running="true" + + alert_count=0 + [ -f "$ALERTS_FILE" ] && alert_count=$(jsonfilter -i "$ALERTS_FILE" -e '@[*]' 2>/dev/null | wc -l) + + unacked_count=0 + [ -f "$ALERTS_FILE" ] && unacked_count=$(grep -c '"acknowledged":false' "$ALERTS_FILE" 2>/dev/null || echo 0) + + last_run="" + [ -f "$STATE_DIR/last_run" ] && last_run=$(cat "$STATE_DIR/last_run") + + localai_status="offline" + localai_url=$(uci_get main.localai_url) + [ -z "$localai_url" ] && localai_url="http://127.0.0.1:8091" + wget -q -O /dev/null --timeout=2 "${localai_url}/v1/models" 2>/dev/null && localai_status="online" + + cat </dev/null) + [ -z "$limit" ] && limit=50 + + alerts='[]' + [ -f "$ALERTS_FILE" ] && alerts=$(cat "$ALERTS_FILE") + + printf '{"alerts":%s}' "$alerts" + ;; + + get_stats) + # Collect current network stats + rx_bytes=0 tx_bytes=0 + for iface in $(ls /sys/class/net/ 2>/dev/null | grep -v lo); do + rx=$(cat /sys/class/net/$iface/statistics/rx_bytes 2>/dev/null || echo 0) + tx=$(cat /sys/class/net/$iface/statistics/tx_bytes 2>/dev/null || echo 0) + rx_bytes=$((rx_bytes + rx)) + tx_bytes=$((tx_bytes + tx)) + done + + total_conn=0 new_conn=0 established=0 + if [ -f /proc/net/nf_conntrack ]; then + total_conn=$(wc -l < /proc/net/nf_conntrack) + new_conn=$(grep -c "TIME_WAIT\|SYN_SENT" /proc/net/nf_conntrack 2>/dev/null || echo 0) + established=$(grep -c "ESTABLISHED" /proc/net/nf_conntrack 2>/dev/null || echo 0) + fi + + unique_ports=0 + if [ -f /proc/net/nf_conntrack ]; then + unique_ports=$(awk -F'[ =]' '/dport/ {print $NF}' /proc/net/nf_conntrack 2>/dev/null | sort -u | wc -l) + fi + + cat </dev/null 2>&1 & + echo '{"started":true}' + ;; + + ack_alert) + read -r input + alert_id=$(echo "$input" | jsonfilter -e '@.id' 2>/dev/null) + + if [ -z "$alert_id" ]; then + echo '{"error":"No alert ID provided"}' + exit 0 + fi + + if [ -f "$ALERTS_FILE" ]; then + tmp_file="$ALERTS_FILE.tmp" + sed "s/\"id\":\"$alert_id\",\\(.*\\)\"acknowledged\":false/\"id\":\"$alert_id\",\\1\"acknowledged\":true/" \ + "$ALERTS_FILE" > "$tmp_file" + mv "$tmp_file" "$ALERTS_FILE" + echo '{"success":true}' + else + echo '{"error":"No alerts file"}' + fi + ;; + + clear_alerts) + echo '[]' > "$ALERTS_FILE" + echo '{"success":true}' + ;; + + reset_baseline) + rm -f "$BASELINE_FILE" + echo '{"success":true}' + ;; + + analyze) + # Start background AI analysis + localai_url=$(uci_get main.localai_url) + [ -z "$localai_url" ] && localai_url="http://127.0.0.1:8091" + + if ! curl -s --max-time 2 "${localai_url}/v1/models" >/dev/null 2>&1; then + echo '{"error":"LocalAI not available"}' + exit 0 + fi + + req_id=$(head -c 8 /dev/urandom | md5sum | head -c 8) + req_file="$STATE_DIR/analyze_${req_id}.json" + mkdir -p "$STATE_DIR" + + ( + /usr/bin/network-anomalyctl analyze > "$req_file" 2>&1 + ) & + + echo "{\"pending\":true,\"poll_id\":\"$req_id\"}" + ;; + + *) + echo '{"error":"Unknown method"}' + ;; + esac + ;; +esac diff --git a/package/secubox/luci-app-network-anomaly/root/usr/share/luci/menu.d/luci-app-network-anomaly.json b/package/secubox/luci-app-network-anomaly/root/usr/share/luci/menu.d/luci-app-network-anomaly.json new file mode 100644 index 00000000..72441833 --- /dev/null +++ b/package/secubox/luci-app-network-anomaly/root/usr/share/luci/menu.d/luci-app-network-anomaly.json @@ -0,0 +1,14 @@ +{ + "admin/secubox/security/network-anomaly": { + "title": "Network Anomaly", + "order": 35, + "action": { + "type": "view", + "path": "network-anomaly/dashboard" + }, + "depends": { + "acl": ["luci-app-network-anomaly"], + "uci": {"network-anomaly": true} + } + } +} diff --git a/package/secubox/luci-app-network-anomaly/root/usr/share/rpcd/acl.d/luci-app-network-anomaly.json b/package/secubox/luci-app-network-anomaly/root/usr/share/rpcd/acl.d/luci-app-network-anomaly.json new file mode 100644 index 00000000..b1448c91 --- /dev/null +++ b/package/secubox/luci-app-network-anomaly/root/usr/share/rpcd/acl.d/luci-app-network-anomaly.json @@ -0,0 +1,17 @@ +{ + "luci-app-network-anomaly": { + "description": "Grant access to Network Anomaly Detection", + "read": { + "ubus": { + "luci.network-anomaly": ["status", "get_alerts", "get_stats"] + }, + "uci": ["network-anomaly"] + }, + "write": { + "ubus": { + "luci.network-anomaly": ["run", "ack_alert", "clear_alerts", "reset_baseline", "analyze"] + }, + "uci": ["network-anomaly"] + } + } +} diff --git a/package/secubox/secubox-localrecall/Makefile b/package/secubox/secubox-localrecall/Makefile new file mode 100644 index 00000000..29eb1194 --- /dev/null +++ b/package/secubox/secubox-localrecall/Makefile @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +include $(TOPDIR)/rules.mk + +PKG_NAME:=secubox-localrecall +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=SecuBox Team +PKG_LICENSE:=GPL-2.0-or-later + +include $(INCLUDE_DIR)/package.mk + +define Package/secubox-localrecall + SECTION:=secubox + CATEGORY:=SecuBox + TITLE:=LocalRecall AI Memory System + DEPENDS:=+jsonfilter +curl + PKGARCH:=all +endef + +define Package/secubox-localrecall/description + Persistent memory system for SecuBox AI agents. + Stores threats, decisions, patterns, and learned behaviors + for context across sessions. LocalAI integration for + semantic search and AI-powered summarization. +endef + +define Package/secubox-localrecall/conffiles +/etc/config/localrecall +endef + +define Build/Compile +endef + +define Package/secubox-localrecall/install + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/etc/config/localrecall $(1)/etc/config/ + + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/etc/init.d/localrecall $(1)/etc/init.d/ + + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) ./files/usr/bin/localrecallctl $(1)/usr/bin/ + + $(INSTALL_DIR) $(1)/usr/lib/localrecall + $(INSTALL_DATA) ./files/usr/lib/localrecall/memory.sh $(1)/usr/lib/localrecall/ + $(INSTALL_DATA) ./files/usr/lib/localrecall/ai.sh $(1)/usr/lib/localrecall/ +endef + +define Package/secubox-localrecall/postinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + mkdir -p /var/lib/localrecall + /etc/init.d/localrecall enable 2>/dev/null +} +exit 0 +endef + +$(eval $(call BuildPackage,secubox-localrecall)) diff --git a/package/secubox/secubox-localrecall/files/etc/config/localrecall b/package/secubox/secubox-localrecall/files/etc/config/localrecall new file mode 100644 index 00000000..ceddfa41 --- /dev/null +++ b/package/secubox/secubox-localrecall/files/etc/config/localrecall @@ -0,0 +1,26 @@ +config memory 'main' + option enabled '1' + option storage_path '/var/lib/localrecall' + option max_memories '1000' + option retention_days '90' + option localai_url 'http://127.0.0.1:8091' + option localai_model 'tinyllama-1.1b-chat-v1.0.Q4_K_M' + option embedding_model 'gte-small' + +config categories 'categories' + option threats '1' + option decisions '1' + option patterns '1' + option configs '1' + option conversations '1' + +config agents 'agents' + option threat_analyst '1' + option dns_guard '1' + option network_anomaly '1' + option cve_triage '1' + +config cleanup 'cleanup' + option auto_cleanup '1' + option cleanup_hour '3' + option keep_important '1' diff --git a/package/secubox/secubox-localrecall/files/etc/init.d/localrecall b/package/secubox/secubox-localrecall/files/etc/init.d/localrecall new file mode 100644 index 00000000..7db37c86 --- /dev/null +++ b/package/secubox/secubox-localrecall/files/etc/init.d/localrecall @@ -0,0 +1,45 @@ +#!/bin/sh /etc/rc.common +# LocalRecall Memory Cleanup Daemon + +START=99 +STOP=10 +USE_PROCD=1 + +CONFIG="localrecall" + +start_service() { + local enabled auto_cleanup cleanup_hour + + config_load "$CONFIG" + config_get enabled main enabled 0 + config_get auto_cleanup cleanup auto_cleanup 1 + config_get cleanup_hour cleanup cleanup_hour 3 + + [ "$enabled" = "1" ] || return 0 + [ "$auto_cleanup" = "1" ] || return 0 + + # Schedule daily cleanup via cron instead of daemon + # This is more efficient for periodic tasks + local cron_entry="0 $cleanup_hour * * * /usr/bin/localrecallctl cleanup -q" + + # Add to crontab if not present + if ! grep -q "localrecallctl cleanup" /etc/crontabs/root 2>/dev/null; then + echo "$cron_entry" >> /etc/crontabs/root + /etc/init.d/cron restart 2>/dev/null + fi + + # Initialize storage + mkdir -p /var/lib/localrecall +} + +stop_service() { + # Remove from crontab + if [ -f /etc/crontabs/root ]; then + sed -i '/localrecallctl cleanup/d' /etc/crontabs/root + /etc/init.d/cron restart 2>/dev/null + fi +} + +service_triggers() { + procd_add_reload_trigger "$CONFIG" +} diff --git a/package/secubox/secubox-localrecall/files/usr/bin/localrecallctl b/package/secubox/secubox-localrecall/files/usr/bin/localrecallctl new file mode 100644 index 00000000..0abf0fdb --- /dev/null +++ b/package/secubox/secubox-localrecall/files/usr/bin/localrecallctl @@ -0,0 +1,367 @@ +#!/bin/sh +# LocalRecall Memory Controller +# Persistent memory for AI agents + +VERSION="1.0.0" +CONFIG="localrecall" + +# Load libraries +. /usr/lib/localrecall/memory.sh +. /usr/lib/localrecall/ai.sh + +usage() { + cat < [options] + +Commands: + status Show memory status + add Add a memory (category: threats|decisions|patterns|configs|conversations) + get Get memory by ID + search Search memories by content + list [category] List memories (optionally by category) + recent [N] Show N most recent memories (default: 20) + important [N] Show important memories (importance >= 7) + delete Delete a memory + cleanup Cleanup old memories + export Export memories to file + import Import memories from file + summarize [cat] AI-summarize memories + context Get context for an agent + stats Show memory statistics + +Options: + -a, --agent Specify agent name (default: user) + -i, --importance N Set importance (1-10, default: 5) + -j, --json Output in JSON format + -q, --quiet Suppress output + +Examples: + $(basename "$0") add threats "Detected SQL injection attempt from 192.168.1.100" + $(basename "$0") search "SQL injection" + $(basename "$0") context threat_analyst + $(basename "$0") summarize threats +EOF +} + +uci_get() { + uci -q get "${CONFIG}.$1" +} + +cmd_status() { + local json_mode="$1" + + local enabled=$(uci_get main.enabled) + local storage=$(uci_get main.storage_path) + local max_mem=$(uci_get main.max_memories) + + local total=$(count_memories) + local threats=$(count_category threats) + local decisions=$(count_category decisions) + local patterns=$(count_category patterns) + + local localai_status="offline" + check_localai && localai_status="online" + + local storage_size="0" + [ -d "$storage" ] && storage_size=$(du -sh "$storage" 2>/dev/null | cut -f1) + + if [ "$json_mode" = "1" ]; then + cat < " + return 1 + fi + + # Validate category + case "$category" in + threats|decisions|patterns|configs|conversations) ;; + *) + echo "Error: Invalid category. Use: threats|decisions|patterns|configs|conversations" + return 1 + ;; + esac + + local id=$(add_memory "$category" "$agent" "$content" '{}' "$importance") + echo "Memory added: $id" +} + +cmd_get() { + local id="$1" + + if [ -z "$id" ]; then + echo "Error: Memory ID required" + return 1 + fi + + local memory=$(get_memory "$id") + if [ -n "$memory" ]; then + echo "$memory" + else + echo "Memory not found: $id" + return 1 + fi +} + +cmd_search() { + local query="$1" + local limit="${2:-20}" + + if [ -z "$query" ]; then + echo "Error: Search query required" + return 1 + fi + + echo "Searching for: $query" + echo "---" + search_content "$query" "$limit" +} + +cmd_list() { + local category="$1" + local limit="${2:-50}" + + if [ -n "$category" ]; then + echo "Memories in category: $category" + echo "---" + search_category "$category" "$limit" + else + echo "All memories (last $limit)" + echo "---" + get_recent "$limit" + fi +} + +cmd_recent() { + local limit="${1:-20}" + echo "Recent memories (last $limit)" + echo "---" + get_recent "$limit" +} + +cmd_important() { + local limit="${1:-50}" + echo "Important memories (importance >= 7)" + echo "---" + get_important "$limit" +} + +cmd_delete() { + local id="$1" + + if [ -z "$id" ]; then + echo "Error: Memory ID required" + return 1 + fi + + delete_memory "$id" + echo "Memory deleted: $id" +} + +cmd_cleanup() { + local retention=$(uci_get main.retention_days) + local keep_imp=$(uci_get cleanup.keep_important) + + echo "Cleaning up memories older than ${retention:-90} days..." + local deleted=$(cleanup_old "${retention:-90}" "${keep_imp:-1}") + echo "Deleted: $deleted memories" +} + +cmd_export() { + local file="$1" + + if [ -z "$file" ]; then + echo "Error: Output file required" + return 1 + fi + + export_memories "$file" + echo "Memories exported to: $file" +} + +cmd_import() { + local file="$1" + + if [ -z "$file" ]; then + echo "Error: Input file required" + return 1 + fi + + if [ ! -f "$file" ]; then + echo "Error: File not found: $file" + return 1 + fi + + import_memories "$file" + echo "Memories imported from: $file" +} + +cmd_summarize() { + local category="$1" + + if ! check_localai; then + echo "Error: LocalAI not available" + return 1 + fi + + echo "Summarizing memories..." + echo "---" + summarize_memories "$category" +} + +cmd_context() { + local agent="$1" + + if [ -z "$agent" ]; then + echo "Error: Agent name required" + return 1 + fi + + get_agent_context "$agent" "" +} + +cmd_stats() { + local total=$(count_memories) + local threats=$(count_category threats) + local decisions=$(count_category decisions) + local patterns=$(count_category patterns) + local configs=$(count_category configs) + local convs=$(count_category conversations) + + echo "Memory Statistics" + echo "=================" + echo "Total: $total" + echo "" + echo "By Category:" + echo " Threats: $threats" + echo " Decisions: $decisions" + echo " Patterns: $patterns" + echo " Configs: $configs" + echo " Conversations: $convs" + echo "" + + # Agent breakdown + echo "By Agent:" + for agent in threat_analyst dns_guard network_anomaly cve_triage user; do + local count=$(search_agent "$agent" 9999 | wc -l) + [ "$count" -gt 0 ] && echo " $agent: $count" + done +} + +# Parse global options +AGENT="user" +IMPORTANCE=5 +JSON=0 +QUIET=0 + +while [ $# -gt 0 ]; do + case "$1" in + -a|--agent) + AGENT="$2" + shift 2 + ;; + -i|--importance) + IMPORTANCE="$2" + shift 2 + ;; + -j|--json) + JSON=1 + shift + ;; + -q|--quiet) + QUIET=1 + shift + ;; + *) + break + ;; + esac +done + +# Parse command +case "$1" in + status) + cmd_status "$JSON" + ;; + add) + cmd_add "$2" "$3" + ;; + get) + cmd_get "$2" + ;; + search) + cmd_search "$2" "$3" + ;; + list) + cmd_list "$2" "$3" + ;; + recent) + cmd_recent "$2" + ;; + important) + cmd_important "$2" + ;; + delete) + cmd_delete "$2" + ;; + cleanup) + cmd_cleanup + ;; + export) + cmd_export "$2" + ;; + import) + cmd_import "$2" + ;; + summarize) + cmd_summarize "$2" + ;; + context) + cmd_context "$2" + ;; + stats) + cmd_stats + ;; + help|--help|-h|"") + usage + ;; + *) + echo "Unknown command: $1" + usage + exit 1 + ;; +esac diff --git a/package/secubox/secubox-localrecall/files/usr/lib/localrecall/ai.sh b/package/secubox/secubox-localrecall/files/usr/lib/localrecall/ai.sh new file mode 100644 index 00000000..c1206738 --- /dev/null +++ b/package/secubox/secubox-localrecall/files/usr/lib/localrecall/ai.sh @@ -0,0 +1,220 @@ +#!/bin/sh +# LocalRecall AI Integration +# Semantic search and AI-powered memory operations + +# Source memory library +. /usr/lib/localrecall/memory.sh + +# Get LocalAI settings +get_localai_config() { + local url=$(uci -q get localrecall.main.localai_url) + local model=$(uci -q get localrecall.main.localai_model) + local embed_model=$(uci -q get localrecall.main.embedding_model) + + LOCALAI_URL="${url:-http://127.0.0.1:8091}" + LOCALAI_MODEL="${model:-tinyllama-1.1b-chat-v1.0.Q4_K_M}" + EMBEDDING_MODEL="${embed_model:-gte-small}" +} + +# Check LocalAI availability +check_localai() { + get_localai_config + curl -s --max-time 2 "${LOCALAI_URL}/v1/models" >/dev/null 2>&1 +} + +# Generate embedding for text +# $1 = text to embed +generate_embedding() { + local text="$1" + + get_localai_config + + local escaped=$(printf '%s' "$text" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ') + + local response=$(curl -s --max-time 30 -X POST "${LOCALAI_URL}/v1/embeddings" \ + -H "Content-Type: application/json" \ + -d "{\"model\":\"$EMBEDDING_MODEL\",\"input\":\"$escaped\"}" 2>/dev/null) + + if [ -n "$response" ]; then + echo "$response" | jsonfilter -e '@.data[0].embedding' 2>/dev/null + fi +} + +# Semantic search using embeddings +# $1 = query text +# $2 = limit (optional) +semantic_search() { + local query="$1" + local limit="${2:-10}" + + # For now, fall back to keyword search + # Full implementation would compute embedding similarity + search_content "$query" "$limit" +} + +# Summarize memories for context +# $1 = category (optional) +# $2 = agent (optional) +summarize_memories() { + local category="$1" + local agent="$2" + + get_localai_config + + # Collect relevant memories + local memories="" + + if [ -n "$category" ]; then + memories=$(search_category "$category" 20) + elif [ -n "$agent" ]; then + memories=$(search_agent "$agent" 20) + else + memories=$(get_recent 20) + fi + + [ -z "$memories" ] && echo "No memories to summarize" && return + + # Build prompt + local prompt="Summarize the following security-related memories into key insights. Be concise:\n\n$memories" + prompt=$(printf '%s' "$prompt" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ') + + local response=$(curl -s --max-time 60 -X POST "${LOCALAI_URL}/v1/chat/completions" \ + -H "Content-Type: application/json" \ + -d "{\"model\":\"$LOCALAI_MODEL\",\"messages\":[{\"role\":\"user\",\"content\":\"$prompt\"}],\"max_tokens\":256,\"temperature\":0.3}" 2>/dev/null) + + if [ -n "$response" ]; then + echo "$response" | jsonfilter -e '@.choices[0].message.content' 2>/dev/null + else + echo "AI summarization failed" + fi +} + +# Extract key facts from text and store as memories +# $1 = source text +# $2 = agent +# $3 = category +auto_memorize() { + local text="$1" + local agent="$2" + local category="${3:-patterns}" + + get_localai_config + + local prompt="Extract the most important security facts from this text. Return each fact on a new line, starting with importance score (1-10):\n\n$text" + prompt=$(printf '%s' "$prompt" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ') + + local response=$(curl -s --max-time 60 -X POST "${LOCALAI_URL}/v1/chat/completions" \ + -H "Content-Type: application/json" \ + -d "{\"model\":\"$LOCALAI_MODEL\",\"messages\":[{\"role\":\"user\",\"content\":\"$prompt\"}],\"max_tokens\":512,\"temperature\":0.3}" 2>/dev/null) + + if [ -n "$response" ]; then + local facts=$(echo "$response" | jsonfilter -e '@.choices[0].message.content' 2>/dev/null) + + # Parse and store each fact + local count=0 + echo "$facts" | while IFS= read -r line; do + [ -z "$line" ] && continue + + # Try to extract importance (format: "8: fact text") + local importance=5 + local content="$line" + + if echo "$line" | grep -qE '^[0-9]+:'; then + importance=$(echo "$line" | cut -d: -f1) + content=$(echo "$line" | cut -d: -f2-) + fi + + add_memory "$category" "$agent" "$content" '{}' "$importance" + count=$((count + 1)) + done + + echo "$count" + else + echo "0" + fi +} + +# Get context for an agent (relevant memories for current task) +# $1 = agent name +# $2 = current task/query +get_agent_context() { + local agent="$1" + local task="$2" + + # Get recent memories from this agent + local agent_memories=$(search_agent "$agent" 10) + + # Get important memories + local important=$(get_important 10) + + # Get related memories (keyword search from task) + local related="" + if [ -n "$task" ]; then + # Extract key terms + local terms=$(echo "$task" | tr ' ' '\n' | grep -E '^[a-zA-Z]{4,}' | head -5) + for term in $terms; do + local found=$(search_content "$term" 3) + [ -n "$found" ] && related="${related}${found}\n" + done + fi + + # Combine into context + cat < "$MEMORIES_FILE" + [ -f "$INDEX_FILE" ] || echo '{}' > "$INDEX_FILE" +} + +# Generate unique ID +gen_id() { + head -c 8 /dev/urandom | md5sum | head -c 16 +} + +# Add a memory +# $1 = category (threats|decisions|patterns|configs|conversations) +# $2 = agent (threat_analyst|dns_guard|network_anomaly|cve_triage|user) +# $3 = content +# $4 = metadata JSON (optional) +# $5 = importance (1-10, optional, default 5) +add_memory() { + local category="$1" + local agent="$2" + local content="$3" + local metadata="${4:-{}}" + local importance="${5:-5}" + + init_storage + + local id=$(gen_id) + local timestamp=$(date -Iseconds) + + # Escape content for JSON + local escaped_content=$(printf '%s' "$content" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ') + + # Create memory entry + local memory=$(cat < "$tmp_file" + printf ',\n%s\n]' "$memory" >> "$tmp_file" + else + # First entry + printf '[\n%s\n]' "$memory" > "$tmp_file" + fi + + mv "$tmp_file" "$MEMORIES_FILE" + + # Update category index + update_index "$category" "$id" + + echo "$id" +} + +# Update category index +update_index() { + local category="$1" + local id="$2" + + local index=$(cat "$INDEX_FILE" 2>/dev/null || echo '{}') + + # Simple JSON manipulation for index + if echo "$index" | grep -q "\"$category\""; then + # Add ID to existing category array + local tmp_file="$INDEX_FILE.tmp" + sed "s/\"$category\":\[/\"$category\":[\"$id\",/" "$INDEX_FILE" > "$tmp_file" + mv "$tmp_file" "$INDEX_FILE" + else + # Create new category entry + local tmp_file="$INDEX_FILE.tmp" + if [ "$index" = "{}" ]; then + echo "{\"$category\":[\"$id\"]}" > "$tmp_file" + else + # Add to existing object + sed "s/^{/{\"$category\":[\"$id\"],/" "$INDEX_FILE" > "$tmp_file" + fi + mv "$tmp_file" "$INDEX_FILE" + fi +} + +# Get memory by ID +get_memory() { + local id="$1" + + [ ! -f "$MEMORIES_FILE" ] && return 1 + + # Extract memory with matching ID + local memory=$(jsonfilter -i "$MEMORIES_FILE" -e "@[@.id='$id']" 2>/dev/null) + + if [ -n "$memory" ]; then + # Update access timestamp and count + update_access "$id" + echo "$memory" + return 0 + fi + + return 1 +} + +# Update access stats +update_access() { + local id="$1" + local timestamp=$(date -Iseconds) + + # Update accessed timestamp (simplified - full impl would use proper JSON parsing) + local tmp_file="$MEMORIES_FILE.tmp" + sed "s/\"id\":\"$id\",\\(.*\\)\"accessed\":\"[^\"]*\"/\"id\":\"$id\",\\1\"accessed\":\"$timestamp\"/" \ + "$MEMORIES_FILE" > "$tmp_file" + mv "$tmp_file" "$MEMORIES_FILE" +} + +# Search memories by category +search_category() { + local category="$1" + local limit="${2:-20}" + + [ ! -f "$MEMORIES_FILE" ] && echo '[]' && return + + # Filter by category + jsonfilter -i "$MEMORIES_FILE" -e "@[@.category='$category']" 2>/dev/null | head -n "$limit" +} + +# Search memories by agent +search_agent() { + local agent="$1" + local limit="${2:-20}" + + [ ! -f "$MEMORIES_FILE" ] && echo '[]' && return + + jsonfilter -i "$MEMORIES_FILE" -e "@[@.agent='$agent']" 2>/dev/null | head -n "$limit" +} + +# Search memories by content (simple grep) +search_content() { + local query="$1" + local limit="${2:-20}" + + [ ! -f "$MEMORIES_FILE" ] && echo '[]' && return + + grep -i "$query" "$MEMORIES_FILE" 2>/dev/null | head -n "$limit" +} + +# Get recent memories +get_recent() { + local limit="${1:-20}" + + [ ! -f "$MEMORIES_FILE" ] && echo '[]' && return + + # Return last N entries (file is append-only so last = recent) + jsonfilter -i "$MEMORIES_FILE" -e '@[*]' 2>/dev/null | tail -n "$limit" +} + +# Get important memories (importance >= 7) +get_important() { + local limit="${1:-50}" + + [ ! -f "$MEMORIES_FILE" ] && echo '[]' && return + + # Filter by importance + local result='[' + local first=1 + + jsonfilter -i "$MEMORIES_FILE" -e '@[*]' 2>/dev/null | while read -r mem; do + local imp=$(echo "$mem" | jsonfilter -e '@.importance' 2>/dev/null) + if [ "${imp:-0}" -ge 7 ]; then + [ $first -eq 0 ] && result="${result}," + first=0 + result="${result}${mem}" + fi + done + + echo "${result}]" +} + +# Delete memory by ID +delete_memory() { + local id="$1" + + [ ! -f "$MEMORIES_FILE" ] && return 1 + + # Filter out the memory with this ID + local tmp_file="$MEMORIES_FILE.tmp" + local result='[' + local first=1 + + jsonfilter -i "$MEMORIES_FILE" -e '@[*]' 2>/dev/null | while read -r mem; do + local mem_id=$(echo "$mem" | jsonfilter -e '@.id' 2>/dev/null) + if [ "$mem_id" != "$id" ]; then + [ $first -eq 0 ] && result="${result}," + first=0 + result="${result}${mem}" + fi + done + + echo "${result}]" > "$tmp_file" + mv "$tmp_file" "$MEMORIES_FILE" +} + +# Cleanup old memories +cleanup_old() { + local retention_days="${1:-90}" + local keep_important="${2:-1}" + + [ ! -f "$MEMORIES_FILE" ] && return + + local cutoff_date=$(date -d "-${retention_days} days" -Iseconds 2>/dev/null || date -Iseconds) + local deleted=0 + + local tmp_file="$MEMORIES_FILE.tmp" + local result='[' + local first=1 + + jsonfilter -i "$MEMORIES_FILE" -e '@[*]' 2>/dev/null | while read -r mem; do + local ts=$(echo "$mem" | jsonfilter -e '@.timestamp' 2>/dev/null) + local imp=$(echo "$mem" | jsonfilter -e '@.importance' 2>/dev/null) + + # Keep if recent or important + local keep=0 + [ "$ts" \> "$cutoff_date" ] && keep=1 + [ "$keep_important" = "1" ] && [ "${imp:-0}" -ge 7 ] && keep=1 + + if [ $keep -eq 1 ]; then + [ $first -eq 0 ] && result="${result}," + first=0 + result="${result}${mem}" + else + deleted=$((deleted + 1)) + fi + done + + echo "${result}]" > "$tmp_file" + mv "$tmp_file" "$MEMORIES_FILE" + + echo "$deleted" +} + +# Count memories +count_memories() { + [ ! -f "$MEMORIES_FILE" ] && echo 0 && return + jsonfilter -i "$MEMORIES_FILE" -e '@[*]' 2>/dev/null | wc -l +} + +# Count by category +count_category() { + local category="$1" + [ ! -f "$MEMORIES_FILE" ] && echo 0 && return + jsonfilter -i "$MEMORIES_FILE" -e "@[@.category='$category']" 2>/dev/null | wc -l +} + +# Export memories to file +export_memories() { + local output_file="$1" + [ -f "$MEMORIES_FILE" ] && cp "$MEMORIES_FILE" "$output_file" +} + +# Import memories from file +import_memories() { + local input_file="$1" + + [ ! -f "$input_file" ] && return 1 + + # Validate JSON + jsonfilter -i "$input_file" -e '@[0]' >/dev/null 2>&1 || return 1 + + # Merge with existing (simple concat for now) + init_storage + + if [ -s "$MEMORIES_FILE" ] && [ "$(cat "$MEMORIES_FILE")" != "[]" ]; then + # Merge arrays + local tmp_file="$MEMORIES_FILE.tmp" + head -c -1 "$MEMORIES_FILE" > "$tmp_file" + printf ',' + tail -c +2 "$input_file" >> "$tmp_file" + mv "$tmp_file" "$MEMORIES_FILE" + else + cp "$input_file" "$MEMORIES_FILE" + fi +} diff --git a/package/secubox/secubox-network-anomaly/Makefile b/package/secubox/secubox-network-anomaly/Makefile new file mode 100644 index 00000000..9e3fbe81 --- /dev/null +++ b/package/secubox/secubox-network-anomaly/Makefile @@ -0,0 +1,59 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +include $(TOPDIR)/rules.mk + +PKG_NAME:=secubox-network-anomaly +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=SecuBox Team +PKG_LICENSE:=GPL-2.0-or-later + +include $(INCLUDE_DIR)/package.mk + +define Package/secubox-network-anomaly + SECTION:=secubox + CATEGORY:=SecuBox + TITLE:=Network Anomaly Detection Agent + DEPENDS:=+jsonfilter +curl + PKGARCH:=all +endef + +define Package/secubox-network-anomaly/description + AI-powered network anomaly detection for SecuBox. + Detects bandwidth spikes, connection floods, port scans, + DNS anomalies, and protocol anomalies using statistical + analysis and optional LocalAI integration. +endef + +define Package/secubox-network-anomaly/conffiles +/etc/config/network-anomaly +endef + +define Build/Compile +endef + +define Package/secubox-network-anomaly/install + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/etc/config/network-anomaly $(1)/etc/config/ + + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/etc/init.d/network-anomaly $(1)/etc/init.d/ + + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) ./files/usr/bin/network-anomalyctl $(1)/usr/bin/ + + $(INSTALL_DIR) $(1)/usr/lib/network-anomaly + $(INSTALL_DATA) ./files/usr/lib/network-anomaly/detector.sh $(1)/usr/lib/network-anomaly/ +endef + +define Package/secubox-network-anomaly/postinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + mkdir -p /var/lib/network-anomaly + /etc/init.d/network-anomaly enable 2>/dev/null +} +exit 0 +endef + +$(eval $(call BuildPackage,secubox-network-anomaly)) diff --git a/package/secubox/secubox-network-anomaly/files/etc/config/network-anomaly b/package/secubox/secubox-network-anomaly/files/etc/config/network-anomaly new file mode 100644 index 00000000..b34a31fc --- /dev/null +++ b/package/secubox/secubox-network-anomaly/files/etc/config/network-anomaly @@ -0,0 +1,28 @@ +config anomaly 'main' + option enabled '1' + option interval '60' + option localai_url 'http://127.0.0.1:8091' + option localai_model 'tinyllama-1.1b-chat-v1.0.Q4_K_M' + option auto_block '0' + option min_confidence '75' + option baseline_hours '24' + +config thresholds 'thresholds' + option bandwidth_spike_percent '200' + option new_connections_per_min '50' + option unique_ports_per_host '20' + option dns_queries_per_min '100' + option failed_connections_percent '30' + +config detection 'detection' + option bandwidth_anomaly '1' + option connection_flood '1' + option port_scan '1' + option dns_anomaly '1' + option protocol_anomaly '1' + option geo_anomaly '1' + +config alerting 'alerting' + option crowdsec_integration '1' + option log_alerts '1' + option mesh_broadcast '0' diff --git a/package/secubox/secubox-network-anomaly/files/etc/init.d/network-anomaly b/package/secubox/secubox-network-anomaly/files/etc/init.d/network-anomaly new file mode 100644 index 00000000..bd26f04e --- /dev/null +++ b/package/secubox/secubox-network-anomaly/files/etc/init.d/network-anomaly @@ -0,0 +1,34 @@ +#!/bin/sh /etc/rc.common +# Network Anomaly Detection Daemon + +START=95 +STOP=10 +USE_PROCD=1 + +PROG="/usr/bin/network-anomalyctl" +CONFIG="network-anomaly" + +start_service() { + local enabled + + config_load "$CONFIG" + config_get enabled main enabled 0 + + [ "$enabled" = "1" ] || return 0 + + procd_open_instance + procd_set_param command "$PROG" daemon + procd_set_param respawn + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_close_instance +} + +service_triggers() { + procd_add_reload_trigger "$CONFIG" +} + +reload_service() { + stop + start +} diff --git a/package/secubox/secubox-network-anomaly/files/usr/bin/network-anomalyctl b/package/secubox/secubox-network-anomaly/files/usr/bin/network-anomalyctl new file mode 100644 index 00000000..a012af2d --- /dev/null +++ b/package/secubox/secubox-network-anomaly/files/usr/bin/network-anomalyctl @@ -0,0 +1,295 @@ +#!/bin/sh +# Network Anomaly Detection Controller +# Package: secubox-network-anomaly + +VERSION="1.0.0" +CONFIG="network-anomaly" +STATE_DIR="/var/lib/network-anomaly" +LIB_DIR="/usr/lib/network-anomaly" + +# Load detection library +. "$LIB_DIR/detector.sh" + +usage() { + cat < [options] + +Commands: + status Show detection status + run Run single detection cycle + daemon Start background daemon + analyze Analyze with AI (requires LocalAI) + list-alerts List recent alerts + ack Acknowledge an alert + clear-alerts Clear all alerts + baseline Show/reset baseline + help Show this help + +Options: + -q, --quiet Suppress output + -j, --json Output in JSON format +EOF +} + +uci_get() { + uci -q get "${CONFIG}.$1" +} + +cmd_status() { + local json_mode="$1" + local enabled=$(uci_get main.enabled) + local interval=$(uci_get main.interval) + local auto_block=$(uci_get main.auto_block) + + local daemon_running="false" + pgrep -f "network-anomalyctl daemon" >/dev/null 2>&1 && daemon_running="true" + + local alert_count=0 + [ -f "$ALERTS_FILE" ] && alert_count=$(jsonfilter -i "$ALERTS_FILE" -e '@[*]' 2>/dev/null | wc -l) + + local last_run="" + [ -f "$STATE_DIR/last_run" ] && last_run=$(cat "$STATE_DIR/last_run") + + local localai_status="offline" + local localai_url=$(uci_get main.localai_url) + [ -z "$localai_url" ] && localai_url="http://127.0.0.1:8091" + wget -q -O /dev/null --timeout=2 "${localai_url}/v1/models" 2>/dev/null && localai_status="online" + + if [ "$json_mode" = "1" ]; then + cat < "$STATE_DIR/last_run" + + [ "$quiet" != "1" ] && echo "Detection complete. Alerts found: $alerts_found" + + return 0 +} + +cmd_daemon() { + local interval=$(uci_get main.interval) + [ -z "$interval" ] && interval=60 + + logger -t network-anomaly "Starting daemon (interval: ${interval}s)" + + init_state + + while true; do + run_detection >/dev/null 2>&1 + date -Iseconds > "$STATE_DIR/last_run" + sleep "$interval" + done +} + +cmd_analyze() { + local localai_url=$(uci_get main.localai_url) + local localai_model=$(uci_get main.localai_model) + [ -z "$localai_url" ] && localai_url="http://127.0.0.1:8091" + [ -z "$localai_model" ] && localai_model="tinyllama-1.1b-chat-v1.0.Q4_K_M" + + echo "Collecting current network stats..." + local stats=$(collect_stats) + + echo "Analyzing with AI..." + + # Get recent alerts for context + local recent_alerts="" + [ -f "$ALERTS_FILE" ] && recent_alerts=$(head -c 2000 "$ALERTS_FILE") + + local prompt="Analyze this network traffic data for security anomalies. Current stats: $stats. Recent alerts: $recent_alerts. Provide a brief security assessment." + prompt=$(printf '%s' "$prompt" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ') + + local response=$(curl -s --max-time 120 -X POST "${localai_url}/v1/chat/completions" \ + -H "Content-Type: application/json" \ + -d "{\"model\":\"$localai_model\",\"messages\":[{\"role\":\"user\",\"content\":\"$prompt\"}],\"max_tokens\":256,\"temperature\":0.7}" 2>/dev/null) + + if [ -n "$response" ]; then + local content=$(echo "$response" | jsonfilter -e '@.choices[0].message.content' 2>/dev/null) + if [ -n "$content" ]; then + echo "" + echo "AI Analysis:" + echo "============" + echo "$content" + else + echo "Error: Empty AI response" + return 1 + fi + else + echo "Error: AI query failed. Is LocalAI running?" + return 1 + fi +} + +cmd_list_alerts() { + local json_mode="$1" + local limit="$2" + [ -z "$limit" ] && limit=20 + + if [ ! -f "$ALERTS_FILE" ]; then + [ "$json_mode" = "1" ] && echo '{"alerts":[]}' || echo "No alerts" + return 0 + fi + + if [ "$json_mode" = "1" ]; then + printf '{"alerts":' + cat "$ALERTS_FILE" + printf '}' + else + echo "Recent Alerts" + echo "=============" + + local count=0 + jsonfilter -i "$ALERTS_FILE" -e '@[*]' 2>/dev/null | while read -r alert; do + [ $count -ge $limit ] && break + + local id=$(echo "$alert" | jsonfilter -e '@.id' 2>/dev/null) + local type=$(echo "$alert" | jsonfilter -e '@.type' 2>/dev/null) + local severity=$(echo "$alert" | jsonfilter -e '@.severity' 2>/dev/null) + local message=$(echo "$alert" | jsonfilter -e '@.message' 2>/dev/null) + local timestamp=$(echo "$alert" | jsonfilter -e '@.timestamp' 2>/dev/null) + local acked=$(echo "$alert" | jsonfilter -e '@.acknowledged' 2>/dev/null) + + printf "[%s] %s (%s) - %s" "$severity" "$type" "${timestamp:-unknown}" "$message" + [ "$acked" = "true" ] && printf " [ACK]" + printf "\n" + + count=$((count + 1)) + done + fi +} + +cmd_ack() { + local alert_id="$1" + + if [ -z "$alert_id" ]; then + echo "Error: Alert ID required" + return 1 + fi + + if [ ! -f "$ALERTS_FILE" ]; then + echo "Error: No alerts file" + return 1 + fi + + # Mark alert as acknowledged + # For simplicity, we rewrite the file with sed + local tmp_file="$ALERTS_FILE.tmp" + sed "s/\"id\":\"$alert_id\",\\(.*\\)\"acknowledged\":false/\"id\":\"$alert_id\",\\1\"acknowledged\":true/" \ + "$ALERTS_FILE" > "$tmp_file" + mv "$tmp_file" "$ALERTS_FILE" + + echo "Alert $alert_id acknowledged" +} + +cmd_clear_alerts() { + echo '[]' > "$ALERTS_FILE" + echo "All alerts cleared" +} + +cmd_baseline() { + local action="$1" + + case "$action" in + reset) + rm -f "$BASELINE_FILE" + echo "Baseline reset. New baseline will be established on next run." + ;; + *) + if [ -f "$BASELINE_FILE" ]; then + echo "Current Baseline:" + cat "$BASELINE_FILE" + else + echo "No baseline established yet. Run detection to create one." + fi + ;; + esac +} + +# Parse global options +QUIET=0 +JSON=0 + +while [ $# -gt 0 ]; do + case "$1" in + -q|--quiet) + QUIET=1 + shift + ;; + -j|--json) + JSON=1 + shift + ;; + *) + break + ;; + esac +done + +# Parse command +case "$1" in + status) + cmd_status "$JSON" + ;; + run) + cmd_run "$QUIET" + ;; + daemon) + cmd_daemon + ;; + analyze) + cmd_analyze + ;; + list-alerts|alerts) + cmd_list_alerts "$JSON" "$2" + ;; + ack|acknowledge) + cmd_ack "$2" + ;; + clear-alerts|clear) + cmd_clear_alerts + ;; + baseline) + cmd_baseline "$2" + ;; + help|--help|-h|"") + usage + ;; + *) + echo "Unknown command: $1" + usage + exit 1 + ;; +esac diff --git a/package/secubox/secubox-network-anomaly/files/usr/lib/network-anomaly/detector.sh b/package/secubox/secubox-network-anomaly/files/usr/lib/network-anomaly/detector.sh new file mode 100644 index 00000000..6114b0d6 --- /dev/null +++ b/package/secubox/secubox-network-anomaly/files/usr/lib/network-anomaly/detector.sh @@ -0,0 +1,235 @@ +#!/bin/sh +# Network Anomaly Detection Library + +STATE_DIR="/var/lib/network-anomaly" +BASELINE_FILE="$STATE_DIR/baseline.json" +ALERTS_FILE="$STATE_DIR/alerts.json" + +# Initialize state directory +init_state() { + mkdir -p "$STATE_DIR" + [ -f "$ALERTS_FILE" ] || echo '[]' > "$ALERTS_FILE" +} + +# Get current network stats +collect_stats() { + local stats="{}" + + # Interface bandwidth (bytes) + local rx_bytes=0 tx_bytes=0 + for iface in $(ls /sys/class/net/ 2>/dev/null | grep -v lo); do + local rx=$(cat /sys/class/net/$iface/statistics/rx_bytes 2>/dev/null || echo 0) + local tx=$(cat /sys/class/net/$iface/statistics/tx_bytes 2>/dev/null || echo 0) + rx_bytes=$((rx_bytes + rx)) + tx_bytes=$((tx_bytes + tx)) + done + + # Connection counts from conntrack + local total_conn=0 new_conn=0 established=0 + if [ -f /proc/net/nf_conntrack ]; then + total_conn=$(wc -l < /proc/net/nf_conntrack) + new_conn=$(grep -c "TIME_WAIT\|SYN_SENT" /proc/net/nf_conntrack 2>/dev/null || echo 0) + established=$(grep -c "ESTABLISHED" /proc/net/nf_conntrack 2>/dev/null || echo 0) + fi + + # Unique destination ports + local unique_ports=0 + if [ -f /proc/net/nf_conntrack ]; then + unique_ports=$(awk -F'[ =]' '/dport/ {print $NF}' /proc/net/nf_conntrack 2>/dev/null | sort -u | wc -l) + fi + + # DNS queries (from dnsmasq log or AdGuard) + local dns_queries=0 + if [ -f /var/log/dnsmasq.log ]; then + dns_queries=$(grep -c "query\[" /var/log/dnsmasq.log 2>/dev/null || echo 0) + fi + + # Failed connections + local failed_conn=0 + if [ -f /proc/net/nf_conntrack ]; then + failed_conn=$(grep -c "UNREPLIED" /proc/net/nf_conntrack 2>/dev/null || echo 0) + fi + + # Protocol distribution + local tcp_conn=$(grep -c "tcp" /proc/net/nf_conntrack 2>/dev/null || echo 0) + local udp_conn=$(grep -c "udp" /proc/net/nf_conntrack 2>/dev/null || echo 0) + + # Output JSON + cat < "$BASELINE_FILE" + return + fi + + # For simplicity, just keep rolling average in a file + # Real implementation would use proper EMA calculation + echo "$current_stats" > "$STATE_DIR/current.json" +} + +# Detect bandwidth anomaly +detect_bandwidth_anomaly() { + local current="$1" + local threshold="$2" + + [ ! -f "$BASELINE_FILE" ] && return 1 + + local baseline_rx=$(jsonfilter -i "$BASELINE_FILE" -e '@.rx_bytes' 2>/dev/null || echo 0) + local current_rx=$(echo "$current" | jsonfilter -e '@.rx_bytes' 2>/dev/null || echo 0) + + [ "$baseline_rx" -eq 0 ] && return 1 + + local ratio=$((current_rx * 100 / baseline_rx)) + [ "$ratio" -gt "$threshold" ] && return 0 + return 1 +} + +# Detect connection flood +detect_connection_flood() { + local current="$1" + local threshold="$2" + + local new_conn=$(echo "$current" | jsonfilter -e '@.new_connections' 2>/dev/null || echo 0) + [ "$new_conn" -gt "$threshold" ] && return 0 + return 1 +} + +# Detect port scan (many unique ports to single host) +detect_port_scan() { + local current="$1" + local threshold="$2" + + local unique_ports=$(echo "$current" | jsonfilter -e '@.unique_ports' 2>/dev/null || echo 0) + [ "$unique_ports" -gt "$threshold" ] && return 0 + return 1 +} + +# Detect DNS anomaly +detect_dns_anomaly() { + local current="$1" + local threshold="$2" + + local dns_queries=$(echo "$current" | jsonfilter -e '@.dns_queries' 2>/dev/null || echo 0) + [ "$dns_queries" -gt "$threshold" ] && return 0 + return 1 +} + +# Detect protocol anomaly (unusual TCP/UDP ratio) +detect_protocol_anomaly() { + local current="$1" + + local tcp=$(echo "$current" | jsonfilter -e '@.tcp_connections' 2>/dev/null || echo 0) + local udp=$(echo "$current" | jsonfilter -e '@.udp_connections' 2>/dev/null || echo 0) + local total=$((tcp + udp)) + + [ "$total" -eq 0 ] && return 1 + + # Normal ratio is roughly 80% TCP, 20% UDP + # Flag if UDP is more than 50% + local udp_percent=$((udp * 100 / total)) + [ "$udp_percent" -gt 50 ] && return 0 + return 1 +} + +# Add alert +add_alert() { + local type="$1" + local severity="$2" + local message="$3" + local details="$4" + + local alert=$(cat </dev/null || echo '[]') + echo "$alerts" | jsonfilter -e '@[0:99]' 2>/dev/null > "$ALERTS_FILE.tmp" || echo '[]' > "$ALERTS_FILE.tmp" + + # Prepend new alert + echo "[$alert," > "$ALERTS_FILE.new" + tail -c +2 "$ALERTS_FILE.tmp" >> "$ALERTS_FILE.new" + mv "$ALERTS_FILE.new" "$ALERTS_FILE" + rm -f "$ALERTS_FILE.tmp" + + logger -t network-anomaly "[$severity] $type: $message" +} + +# Run all detectors +run_detection() { + local stats=$(collect_stats) + local alerts_found=0 + + # Load thresholds + local bw_threshold=$(uci -q get network-anomaly.thresholds.bandwidth_spike_percent || echo 200) + local conn_threshold=$(uci -q get network-anomaly.thresholds.new_connections_per_min || echo 50) + local port_threshold=$(uci -q get network-anomaly.thresholds.unique_ports_per_host || echo 20) + local dns_threshold=$(uci -q get network-anomaly.thresholds.dns_queries_per_min || echo 100) + + # Load detection flags + local detect_bw=$(uci -q get network-anomaly.detection.bandwidth_anomaly || echo 1) + local detect_conn=$(uci -q get network-anomaly.detection.connection_flood || echo 1) + local detect_port=$(uci -q get network-anomaly.detection.port_scan || echo 1) + local detect_dns=$(uci -q get network-anomaly.detection.dns_anomaly || echo 1) + local detect_proto=$(uci -q get network-anomaly.detection.protocol_anomaly || echo 1) + + # Run detectors + if [ "$detect_bw" = "1" ] && detect_bandwidth_anomaly "$stats" "$bw_threshold"; then + add_alert "bandwidth_spike" "high" "Unusual bandwidth spike detected" "$stats" + alerts_found=$((alerts_found + 1)) + fi + + if [ "$detect_conn" = "1" ] && detect_connection_flood "$stats" "$conn_threshold"; then + add_alert "connection_flood" "high" "Connection flood detected" "$stats" + alerts_found=$((alerts_found + 1)) + fi + + if [ "$detect_port" = "1" ] && detect_port_scan "$stats" "$port_threshold"; then + add_alert "port_scan" "medium" "Possible port scan activity" "$stats" + alerts_found=$((alerts_found + 1)) + fi + + if [ "$detect_dns" = "1" ] && detect_dns_anomaly "$stats" "$dns_threshold"; then + add_alert "dns_anomaly" "medium" "Unusual DNS query volume" "$stats" + alerts_found=$((alerts_found + 1)) + fi + + if [ "$detect_proto" = "1" ] && detect_protocol_anomaly "$stats"; then + add_alert "protocol_anomaly" "low" "Unusual protocol distribution" "$stats" + alerts_found=$((alerts_found + 1)) + fi + + # Update baseline + update_baseline "$stats" + + echo "$alerts_found" +}