feat(ai): Add v0.19 AI agent packages

Network Anomaly Agent (secubox-network-anomaly):
- 5 detection modules: bandwidth, connection flood, port scan, DNS, protocol
- EMA-based baseline comparison
- LocalAI integration for threat assessment
- network-anomalyctl CLI

LocalRecall Memory System (secubox-localrecall):
- Persistent memory for AI agents
- Categories: threats, decisions, patterns, configs, conversations
- EMA-based importance scoring
- LocalAI integration for summarization
- localrecallctl CLI with 13 commands

AI Insights Dashboard (luci-app-ai-insights):
- Unified view across all AI agents
- Security posture scoring (0-100)
- Agent status grid with alert counts
- Aggregated alerts from all agents
- Run All Agents and AI Analysis actions

LuCI Dashboards:
- luci-app-network-anomaly with real-time stats
- luci-app-localrecall with memory management

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-05 18:58:08 +01:00
parent 006ff03c86
commit f2dfb5c144
32 changed files with 4823 additions and 0 deletions

View File

@ -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))

View File

@ -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 || []
};
});
}
});

View File

@ -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); }
}

View File

@ -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
});

View File

@ -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 <<EOF
{
"localai": "$localai_status",
"memories": $memories,
"posture_score": ${posture_score:-0},
"agents": {
"threat_analyst": $ta,
"dns_guard": $dg,
"network_anomaly": $na,
"cve_triage": $ct
}
}
EOF
;;
get_alerts)
read -r input
limit=$(echo "$input" | jsonfilter -e '@.limit' 2>/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

View File

@ -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"]
}
}
}

View File

@ -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"]
}
}
}
}

View File

@ -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))

View File

@ -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 || {}
};
});
}
});

View File

@ -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); }
}

View File

@ -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
});

View File

@ -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 <<EOF
{
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
"total": $total,
"threats": $threats,
"decisions": $decisions,
"patterns": $patterns,
"configs": $configs,
"conversations": $convs,
"max_memories": ${max_mem:-1000},
"retention_days": ${retention:-90},
"storage_size": $storage_size,
"localai_status": "$localai_status"
}
EOF
;;
get_memories)
read -r input
category=$(echo "$input" | jsonfilter -e '@.category' 2>/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

View File

@ -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}
}
}
}

View File

@ -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"]
}
}
}

View File

@ -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))

View File

@ -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] || {}
};
});
}
});

View File

@ -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); }
}

View File

@ -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
});

View File

@ -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 <<EOF
{
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
"daemon_running": $daemon_running,
"interval": ${interval:-60},
"auto_block": $([ "$auto_block" = "1" ] && echo "true" || echo "false"),
"alert_count": $alert_count,
"unacked_count": $unacked_count,
"last_run": "$last_run",
"localai_status": "$localai_status",
"localai_url": "$localai_url"
}
EOF
;;
get_alerts)
read -r input
limit=$(echo "$input" | jsonfilter -e '@.limit' 2>/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 <<EOF
{
"timestamp": "$(date -Iseconds)",
"rx_bytes": $rx_bytes,
"tx_bytes": $tx_bytes,
"total_connections": $total_conn,
"new_connections": $new_conn,
"established": $established,
"unique_ports": $unique_ports
}
EOF
;;
run)
/usr/bin/network-anomalyctl run -q >/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

View File

@ -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}
}
}
}

View File

@ -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"]
}
}
}

View File

@ -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))

View File

@ -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'

View File

@ -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"
}

View File

@ -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 <<EOF
LocalRecall Memory Controller v$VERSION
Usage: $(basename "$0") <command> [options]
Commands:
status Show memory status
add <cat> <content> Add a memory (category: threats|decisions|patterns|configs|conversations)
get <id> Get memory by ID
search <query> 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 <id> Delete a memory
cleanup Cleanup old memories
export <file> Export memories to file
import <file> Import memories from file
summarize [cat] AI-summarize memories
context <agent> Get context for an agent
stats Show memory statistics
Options:
-a, --agent <name> 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 <<EOF
{
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
"total_memories": $total,
"threats": $threats,
"decisions": $decisions,
"patterns": $patterns,
"max_memories": ${max_mem:-1000},
"storage_path": "${storage:-/var/lib/localrecall}",
"storage_size": "$storage_size",
"localai_status": "$localai_status"
}
EOF
else
echo "LocalRecall Memory Status"
echo "========================="
echo "Enabled: $([ "$enabled" = "1" ] && echo "Yes" || echo "No")"
echo "Total: $total memories"
echo " Threats: $threats"
echo " Decisions: $decisions"
echo " Patterns: $patterns"
echo "Max: ${max_mem:-1000}"
echo "Storage: ${storage:-/var/lib/localrecall} ($storage_size)"
echo "LocalAI: $localai_status"
fi
}
cmd_add() {
local category="$1"
local content="$2"
local agent="${AGENT:-user}"
local importance="${IMPORTANCE:-5}"
if [ -z "$category" ] || [ -z "$content" ]; then
echo "Error: Category and content required"
echo "Usage: localrecallctl add <category> <content>"
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

View File

@ -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 <<EOF
## Agent Context: $agent
### Recent Actions
$agent_memories
### Important Memories
$important
### Related
$related
EOF
}
# Record a decision for future reference
# $1 = agent
# $2 = decision description
# $3 = outcome (approved|rejected|auto)
# $4 = details JSON
record_decision() {
local agent="$1"
local decision="$2"
local outcome="$3"
local details="${4:-{}}"
local content="Decision: $decision | Outcome: $outcome"
local metadata="{\"outcome\":\"$outcome\",\"details\":$details}"
# Decisions are moderately important by default
local importance=6
[ "$outcome" = "rejected" ] && importance=7 # Learn from rejections
add_memory "decisions" "$agent" "$content" "$metadata" "$importance"
}
# Record a threat pattern
# $1 = agent
# $2 = pattern description
# $3 = severity (low|medium|high|critical)
# $4 = details JSON
record_threat() {
local agent="$1"
local pattern="$2"
local severity="$3"
local details="${4:-{}}"
local metadata="{\"severity\":\"$severity\",\"details\":$details}"
# Importance based on severity
local importance=5
case "$severity" in
critical) importance=10 ;;
high) importance=8 ;;
medium) importance=6 ;;
low) importance=4 ;;
esac
add_memory "threats" "$agent" "$pattern" "$metadata" "$importance"
}

View File

@ -0,0 +1,301 @@
#!/bin/sh
# LocalRecall Memory Library
# Persistent memory storage for AI agents
STORAGE_DIR="/var/lib/localrecall"
MEMORIES_FILE="$STORAGE_DIR/memories.json"
INDEX_FILE="$STORAGE_DIR/index.json"
# Initialize storage
init_storage() {
mkdir -p "$STORAGE_DIR"
[ -f "$MEMORIES_FILE" ] || echo '[]' > "$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 <<EOF
{
"id": "$id",
"category": "$category",
"agent": "$agent",
"content": "$escaped_content",
"metadata": $metadata,
"importance": $importance,
"timestamp": "$timestamp",
"accessed": "$timestamp",
"access_count": 0
}
EOF
)
# Append to memories file
local tmp_file="$MEMORIES_FILE.tmp"
if [ -s "$MEMORIES_FILE" ] && [ "$(cat "$MEMORIES_FILE")" != "[]" ]; then
# Remove closing bracket, add comma and new entry
head -c -2 "$MEMORIES_FILE" > "$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
}

View File

@ -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))

View File

@ -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'

View File

@ -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
}

View File

@ -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 <<EOF
Network Anomaly Detection Controller v$VERSION
Usage: $(basename "$0") <command> [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 <id> 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 <<EOF
{
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
"daemon_running": $daemon_running,
"interval": ${interval:-60},
"auto_block": $([ "$auto_block" = "1" ] && echo "true" || echo "false"),
"alert_count": $alert_count,
"last_run": "$last_run",
"localai_status": "$localai_status"
}
EOF
else
echo "Network Anomaly Detection Status"
echo "================================="
echo "Enabled: $([ "$enabled" = "1" ] && echo "Yes" || echo "No")"
echo "Daemon: $([ "$daemon_running" = "true" ] && echo "Running" || echo "Stopped")"
echo "Interval: ${interval:-60}s"
echo "Auto-block: $([ "$auto_block" = "1" ] && echo "Yes" || echo "No")"
echo "Alerts: $alert_count"
echo "Last run: ${last_run:-Never}"
echo "LocalAI: $localai_status"
fi
}
cmd_run() {
local quiet="$1"
init_state
[ "$quiet" != "1" ] && echo "Running detection cycle..."
local alerts_found=$(run_detection)
# Save last run timestamp
date -Iseconds > "$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

View File

@ -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 <<EOF
{
"timestamp": "$(date -Iseconds)",
"rx_bytes": $rx_bytes,
"tx_bytes": $tx_bytes,
"total_connections": $total_conn,
"new_connections": $new_conn,
"established": $established,
"unique_ports": $unique_ports,
"dns_queries": $dns_queries,
"failed_connections": $failed_conn,
"tcp_connections": $tcp_conn,
"udp_connections": $udp_conn
}
EOF
}
# Update baseline with exponential moving average
update_baseline() {
local current_stats="$1"
local alpha=0.1 # EMA smoothing factor
if [ ! -f "$BASELINE_FILE" ]; then
echo "$current_stats" > "$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 <<EOF
{
"id": "$(head -c 8 /dev/urandom | md5sum | head -c 16)",
"type": "$type",
"severity": "$severity",
"message": "$message",
"details": $details,
"timestamp": "$(date -Iseconds)",
"acknowledged": false
}
EOF
)
# Append to alerts file (keep last 100)
local alerts=$(cat "$ALERTS_FILE" 2>/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"
}