From b1c34021db210dbaf9feae2be97dfd2bfdc0fd02 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sat, 14 Feb 2026 14:45:05 +0100 Subject: [PATCH] feat(wazuh): Add LuCI dashboard for Wazuh SIEM integration Create luci-app-wazuh package with unified security monitoring dashboard inspired by SysWarden's layered defense model: - 4 views: Overview, Alerts, File Integrity, Agents - RPCD handler with 12 API methods for status, alerts, FIM, agent control - SysWarden-style 4-layer security visualization: - Layer 1: Vortex Firewall + nftables (kernel-level) - Layer 2: CrowdSec + Bouncer (IPS) - Layer 3: Wazuh Manager (SIEM/XDR) - Layer 4: mitmproxy + HAProxy (WAF) - CrowdSec integration for threat correlation - Real-time polling and auto-refresh - Simplified printf-based JSON output (avoids jshn segfault) Co-Authored-By: Claude Opus 4.5 --- .claude/HISTORY.md | 18 + .claude/WIP.md | 8 + package/secubox/luci-app-wazuh/Makefile | 30 ++ .../resources/view/wazuh/agents.js | 224 ++++++++++ .../resources/view/wazuh/alerts.js | 156 +++++++ .../luci-static/resources/view/wazuh/fim.js | 197 +++++++++ .../resources/view/wazuh/overview.js | 199 +++++++++ .../htdocs/luci-static/resources/wazuh/api.js | 165 ++++++++ .../root/usr/libexec/rpcd/luci.wazuh | 387 ++++++++++++++++++ .../usr/share/luci/menu.d/luci-app-wazuh.json | 46 +++ .../usr/share/rpcd/acl.d/luci-app-wazuh.json | 31 ++ 11 files changed, 1461 insertions(+) create mode 100644 package/secubox/luci-app-wazuh/Makefile create mode 100644 package/secubox/luci-app-wazuh/htdocs/luci-static/resources/view/wazuh/agents.js create mode 100644 package/secubox/luci-app-wazuh/htdocs/luci-static/resources/view/wazuh/alerts.js create mode 100644 package/secubox/luci-app-wazuh/htdocs/luci-static/resources/view/wazuh/fim.js create mode 100644 package/secubox/luci-app-wazuh/htdocs/luci-static/resources/view/wazuh/overview.js create mode 100644 package/secubox/luci-app-wazuh/htdocs/luci-static/resources/wazuh/api.js create mode 100644 package/secubox/luci-app-wazuh/root/usr/libexec/rpcd/luci.wazuh create mode 100644 package/secubox/luci-app-wazuh/root/usr/share/luci/menu.d/luci-app-wazuh.json create mode 100644 package/secubox/luci-app-wazuh/root/usr/share/rpcd/acl.d/luci-app-wazuh.json diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md index e8729c76..7c49f2fe 100644 --- a/.claude/HISTORY.md +++ b/.claude/HISTORY.md @@ -1679,3 +1679,21 @@ git checkout HEAD -- index.html - Site now displays medieval/renaissance cinematic presentation - Title: "Les Seigneurs de La Chambre - Présentation Cinématique" - Description: "seigneurs de la Chambre" (from UCI config) + +### 2026-02-14: Wazuh SIEM LuCI Dashboard Integration +- **Created luci-app-wazuh package** - unified Wazuh SIEM monitoring dashboard + - 4 views: Overview, Alerts, FIM (File Integrity), Agents + - SysWarden-inspired 4-layer security visualization: + - Layer 1: Vortex Firewall + nftables (kernel-level) + - Layer 2: CrowdSec + Bouncer (IPS) + - Layer 3: Wazuh Manager (SIEM/XDR) + - Layer 4: mitmproxy + HAProxy (WAF) +- **RPCD handler (luci.wazuh)** with 12 API methods: + - get_overview, get_agent_status, get_manager_status + - get_alerts, get_alert_summary + - get_fim_events, get_fim_config + - list_agents, get_crowdsec_correlation + - start_agent, stop_agent, restart_agent +- **API wrapper (wazuh/api.js)** with helper functions for alert levels and timestamps +- **Fixed jshn segfault issue** - simplified to printf-based JSON output +- Tested all RPCD methods via ubus calls diff --git a/.claude/WIP.md b/.claude/WIP.md index 132f6d5b..ef1e279f 100644 --- a/.claude/WIP.md +++ b/.claude/WIP.md @@ -64,6 +64,14 @@ _Last updated: 2026-02-14 (WAF architecture configured)_ ### Just Completed (2026-02-14) +- **Wazuh SIEM LuCI Dashboard** — DONE (2026-02-14) + - Created `luci-app-wazuh` package for unified Wazuh security monitoring + - 4 views: Overview, Alerts, File Integrity, Agents + - SysWarden-inspired 4-layer security visualization + - RPCD handler (luci.wazuh) with 12 API methods + - CrowdSec integration for threat correlation display + - Full RPCD testing verified via ubus calls + - **MetaBlogizer SDLC Content Restoration** — DONE (2026-02-14) - sdlc.gk2.secubox.in was showing GK2 Hub template instead of original content - GK2 Hub generator had overwritten local index.html diff --git a/package/secubox/luci-app-wazuh/Makefile b/package/secubox/luci-app-wazuh/Makefile new file mode 100644 index 00000000..7c95b4c3 --- /dev/null +++ b/package/secubox/luci-app-wazuh/Makefile @@ -0,0 +1,30 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-wazuh +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=SecuBox + +LUCI_TITLE:=LuCI Wazuh SIEM Dashboard +LUCI_DESCRIPTION:=Unified security monitoring dashboard for Wazuh SIEM/XDR integration +LUCI_DEPENDS:=+luci-base +secubox-app-wazuh +LUCI_PKGARCH:=all + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/$(PKG_NAME)/install + $(INSTALL_DIR) $(1)/www/luci-static/resources/wazuh + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/wazuh + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + + $(INSTALL_DATA) ./htdocs/luci-static/resources/wazuh/*.js $(1)/www/luci-static/resources/wazuh/ + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/wazuh/*.js $(1)/www/luci-static/resources/view/wazuh/ + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.wazuh $(1)/usr/libexec/rpcd/ + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/*.json $(1)/usr/share/luci/menu.d/ + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/*.json $(1)/usr/share/rpcd/acl.d/ +endef + +$(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/package/secubox/luci-app-wazuh/htdocs/luci-static/resources/view/wazuh/agents.js b/package/secubox/luci-app-wazuh/htdocs/luci-static/resources/view/wazuh/agents.js new file mode 100644 index 00000000..38786428 --- /dev/null +++ b/package/secubox/luci-app-wazuh/htdocs/luci-static/resources/view/wazuh/agents.js @@ -0,0 +1,224 @@ +'use strict'; +'require view'; +'require dom'; +'require poll'; +'require wazuh.api as api'; + +return view.extend({ + handleSaveApply: null, + handleSave: null, + handleReset: null, + + load: function() { + return api.listAgents(); + }, + + render: function(data) { + var agents = data.agents || []; + + // Calculate statistics + var connected = agents.filter(function(a) { return a.status === 'active' || a.status === 'connected'; }).length; + var disconnected = agents.filter(function(a) { return a.status === 'disconnected'; }).length; + var pending = agents.filter(function(a) { return a.status === 'pending' || a.status === 'never_connected'; }).length; + + var view = E('div', { 'class': 'cbi-map' }, [ + E('h2', {}, _('Wazuh Agents')), + E('div', { 'class': 'cbi-map-descr' }, + _('Manage security agents across your infrastructure') + ), + + // Agent Statistics + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Agent Status Summary')), + E('div', { 'style': 'display: flex; gap: 1rem; flex-wrap: wrap;' }, [ + this.renderStatCard('Connected', connected, 'success'), + this.renderStatCard('Disconnected', disconnected, 'danger'), + this.renderStatCard('Pending', pending, 'warning'), + this.renderStatCard('Total', agents.length, 'info') + ]) + ]), + + // Actions + E('div', { 'class': 'cbi-section' }, [ + E('div', { 'style': 'display: flex; gap: 1rem; flex-wrap: wrap;' }, [ + E('button', { + 'class': 'btn cbi-button cbi-button-action', + 'click': L.bind(this.handleRefresh, this) + }, _('Refresh')), + E('a', { + 'href': 'https://wazuh.gk2.secubox.in/app/wazuh#/agents-preview', + 'target': '_blank', + 'class': 'btn cbi-button' + }, _('View in Wazuh Dashboard')) + ]) + ]), + + // Agents Table + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Registered Agents')), + E('div', { 'id': 'agents-container' }, [ + this.renderAgentsTable(agents) + ]) + ]), + + // Local Agent Quick Actions + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Local Agent Control')), + E('div', { 'class': 'cbi-value', 'style': 'background: var(--background-color-high); padding: 1rem; border-radius: 8px;' }, [ + E('p', { 'style': 'margin-bottom: 1rem;' }, + _('Control the Wazuh agent running on this SecuBox device') + ), + E('div', { 'style': 'display: flex; gap: 0.5rem; flex-wrap: wrap;' }, [ + E('button', { + 'class': 'btn cbi-button cbi-button-apply', + 'click': L.bind(this.handleStartAgent, this) + }, _('Start Agent')), + E('button', { + 'class': 'btn cbi-button cbi-button-remove', + 'click': L.bind(this.handleStopAgent, this) + }, _('Stop Agent')), + E('button', { + 'class': 'btn cbi-button cbi-button-action', + 'click': L.bind(this.handleRestartAgent, this) + }, _('Restart Agent')) + ]) + ]) + ]), + + // Agent Installation Guide + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Deploy New Agent')), + E('div', { 'style': 'background: var(--background-color-high); padding: 1rem; border-radius: 8px;' }, [ + E('p', {}, _('To register a new agent with the Wazuh Manager:')), + E('ol', { 'style': 'margin: 1rem 0; padding-left: 1.5rem;' }, [ + E('li', {}, _('Install Wazuh agent on the target system')), + E('li', {}, _('Configure agent to connect to manager: 192.168.255.50')), + E('li', {}, _('Register with: /var/ossec/bin/agent-auth -m 192.168.255.50')), + E('li', {}, _('Start the agent service')) + ]), + E('div', { 'style': 'margin-top: 1rem;' }, [ + E('a', { + 'href': 'https://documentation.wazuh.com/current/installation-guide/wazuh-agent/index.html', + 'target': '_blank', + 'class': 'btn cbi-button' + }, _('Agent Installation Guide')) + ]) + ]) + ]) + ]); + + poll.add(L.bind(this.pollAgents, this), 30); + + return view; + }, + + renderStatCard: function(label, count, badgeClass) { + return E('div', { + 'style': 'text-align: center; padding: 1rem; background: var(--background-color-high); border-radius: 8px; min-width: 120px;' + }, [ + E('div', { 'style': 'font-size: 2.5em; font-weight: bold;' }, String(count)), + E('div', { 'class': 'badge ' + badgeClass, 'style': 'font-size: 0.9em;' }, label) + ]); + }, + + renderAgentsTable: function(agents) { + if (!agents || agents.length === 0) { + return E('div', { 'class': 'cbi-value', 'style': 'text-align: center; padding: 2rem;' }, + _('No agents registered') + ); + } + + var rows = [ + E('tr', { 'class': 'tr' }, [ + E('th', { 'class': 'th', 'style': 'width: 60px;' }, _('ID')), + E('th', { 'class': 'th' }, _('Name')), + E('th', { 'class': 'th', 'style': 'width: 120px;' }, _('IP Address')), + E('th', { 'class': 'th', 'style': 'width: 100px;' }, _('Status')), + E('th', { 'class': 'th', 'style': 'width: 100px;' }, _('OS')), + E('th', { 'class': 'th', 'style': 'width: 100px;' }, _('Version')), + E('th', { 'class': 'th', 'style': 'width: 150px;' }, _('Last Keep Alive')) + ]) + ]; + + agents.forEach(function(agent) { + var statusClass = (agent.status === 'active' || agent.status === 'connected') ? 'success' : + (agent.status === 'disconnected' ? 'danger' : 'warning'); + var statusText = agent.status === 'active' ? 'Connected' : + (agent.status === 'connected' ? 'Connected' : + (agent.status === 'disconnected' ? 'Disconnected' : + (agent.status === 'pending' ? 'Pending' : + (agent.status === 'never_connected' ? 'Never Connected' : agent.status || 'Unknown')))); + + rows.push(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, [ + E('code', {}, agent.id || '-') + ]), + E('td', { 'class': 'td', 'style': 'font-weight: bold;' }, agent.name || '-'), + E('td', { 'class': 'td', 'style': 'font-family: monospace;' }, agent.ip || '-'), + E('td', { 'class': 'td' }, [ + E('span', { 'class': 'badge ' + statusClass }, statusText) + ]), + E('td', { 'class': 'td', 'style': 'font-size: 0.85em;' }, + agent.os_name || agent.os || '-' + ), + E('td', { 'class': 'td', 'style': 'font-family: monospace; font-size: 0.85em;' }, + agent.version || '-' + ), + E('td', { 'class': 'td', 'style': 'font-size: 0.85em;' }, + api.formatTime(agent.lastKeepAlive || agent.last_keepalive) + ) + ])); + }); + + return E('table', { 'class': 'table' }, rows); + }, + + handleRefresh: function() { + var container = document.getElementById('agents-container'); + if (container) { + container.innerHTML = '
Loading...
'; + } + + var self = this; + return api.listAgents().then(function(data) { + var agents = data.agents || []; + if (container) { + dom.content(container, self.renderAgentsTable(agents)); + } + }); + }, + + handleStartAgent: function() { + return api.startAgent().then(function(res) { + if (res.success) { + L.ui.addNotification(null, E('p', _('Wazuh agent started successfully')), 'info'); + } else { + L.ui.addNotification(null, E('p', _('Failed to start agent: %s').format(res.error || 'Unknown error')), 'error'); + } + }); + }, + + handleStopAgent: function() { + return api.stopAgent().then(function(res) { + if (res.success) { + L.ui.addNotification(null, E('p', _('Wazuh agent stopped')), 'info'); + } else { + L.ui.addNotification(null, E('p', _('Failed to stop agent: %s').format(res.error || 'Unknown error')), 'error'); + } + }); + }, + + handleRestartAgent: function() { + return api.restartAgent().then(function(res) { + if (res.success) { + L.ui.addNotification(null, E('p', _('Wazuh agent restarted successfully')), 'info'); + } else { + L.ui.addNotification(null, E('p', _('Failed to restart agent: %s').format(res.error || 'Unknown error')), 'error'); + } + }); + }, + + pollAgents: function() { + return this.handleRefresh(); + } +}); diff --git a/package/secubox/luci-app-wazuh/htdocs/luci-static/resources/view/wazuh/alerts.js b/package/secubox/luci-app-wazuh/htdocs/luci-static/resources/view/wazuh/alerts.js new file mode 100644 index 00000000..7905e76d --- /dev/null +++ b/package/secubox/luci-app-wazuh/htdocs/luci-static/resources/view/wazuh/alerts.js @@ -0,0 +1,156 @@ +'use strict'; +'require view'; +'require dom'; +'require poll'; +'require wazuh.api as api'; + +return view.extend({ + handleSaveApply: null, + handleSave: null, + handleReset: null, + + load: function() { + return api.getAlerts(50, 0); + }, + + render: function(data) { + var alerts = data.alerts || []; + var self = this; + + var view = E('div', { 'class': 'cbi-map' }, [ + E('h2', {}, _('Wazuh Security Alerts')), + E('div', { 'class': 'cbi-map-descr' }, + _('Real-time security events from Wazuh SIEM/XDR') + ), + + // Filter Controls + E('div', { 'class': 'cbi-section' }, [ + E('div', { 'style': 'display: flex; gap: 1rem; align-items: center; flex-wrap: wrap;' }, [ + E('label', {}, _('Filter by Level:')), + E('select', { + 'id': 'level-filter', + 'class': 'cbi-input-select', + 'change': L.bind(this.handleFilterChange, this) + }, [ + E('option', { 'value': '0' }, _('All Levels')), + E('option', { 'value': '12' }, _('Critical (12+)')), + E('option', { 'value': '9' }, _('High (9+)')), + E('option', { 'value': '5' }, _('Medium (5+)')), + E('option', { 'value': '1' }, _('Low (1+)')) + ]), + + E('label', { 'style': 'margin-left: 1rem;' }, _('Count:')), + E('select', { + 'id': 'count-filter', + 'class': 'cbi-input-select', + 'change': L.bind(this.handleFilterChange, this) + }, [ + E('option', { 'value': '20' }, '20'), + E('option', { 'value': '50', 'selected': 'selected' }, '50'), + E('option', { 'value': '100' }, '100'), + E('option', { 'value': '200' }, '200') + ]), + + E('button', { + 'class': 'btn cbi-button cbi-button-action', + 'style': 'margin-left: 1rem;', + 'click': L.bind(this.handleRefresh, this) + }, _('Refresh')) + ]) + ]), + + // Alerts Table + E('div', { 'class': 'cbi-section' }, [ + E('div', { 'id': 'alerts-container' }, [ + this.renderAlertsTable(alerts) + ]) + ]), + + // Legend + E('div', { 'class': 'cbi-section' }, [ + E('h4', {}, _('Severity Legend')), + E('div', { 'style': 'display: flex; gap: 1rem; flex-wrap: wrap;' }, [ + E('span', { 'class': 'badge danger', 'style': 'padding: 0.3rem 0.6rem;' }, _('Critical (12+)')), + E('span', { 'class': 'badge warning', 'style': 'padding: 0.3rem 0.6rem;' }, _('High (9-11)')), + E('span', { 'class': 'badge notice', 'style': 'padding: 0.3rem 0.6rem;' }, _('Medium (5-8)')), + E('span', { 'class': 'badge info', 'style': 'padding: 0.3rem 0.6rem;' }, _('Low (1-4)')) + ]) + ]) + ]); + + // Setup auto-refresh + poll.add(L.bind(this.pollAlerts, this), 60); + + return view; + }, + + renderAlertsTable: function(alerts) { + if (!alerts || alerts.length === 0) { + return E('div', { 'class': 'cbi-value', 'style': 'text-align: center; padding: 2rem;' }, + _('No alerts found') + ); + } + + var rows = [ + E('tr', { 'class': 'tr' }, [ + E('th', { 'class': 'th', 'style': 'width: 60px;' }, _('Level')), + E('th', { 'class': 'th', 'style': 'width: 150px;' }, _('Time')), + E('th', { 'class': 'th', 'style': 'width: 100px;' }, _('Agent')), + E('th', { 'class': 'th', 'style': 'width: 120px;' }, _('Rule ID')), + E('th', { 'class': 'th' }, _('Description')), + E('th', { 'class': 'th', 'style': 'width: 150px;' }, _('Source')) + ]) + ]; + + var self = this; + alerts.forEach(function(alert) { + var levelInfo = api.formatLevel(alert.rule_level || 0); + rows.push(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, [ + E('span', { 'class': 'badge ' + levelInfo.class }, String(alert.rule_level || 0)) + ]), + E('td', { 'class': 'td', 'style': 'font-size: 0.85em;' }, + api.formatTime(alert.timestamp) + ), + E('td', { 'class': 'td' }, alert.agent_name || '-'), + E('td', { 'class': 'td' }, [ + E('code', {}, alert.rule_id || '-') + ]), + E('td', { 'class': 'td' }, alert.rule_description || '-'), + E('td', { 'class': 'td', 'style': 'font-family: monospace; font-size: 0.85em;' }, + alert.src_ip || alert.data_srcip || '-' + ) + ])); + }); + + return E('table', { 'class': 'table' }, rows); + }, + + handleFilterChange: function() { + this.handleRefresh(); + }, + + handleRefresh: function() { + var levelEl = document.getElementById('level-filter'); + var countEl = document.getElementById('count-filter'); + var level = levelEl ? parseInt(levelEl.value) : 0; + var count = countEl ? parseInt(countEl.value) : 50; + + var container = document.getElementById('alerts-container'); + if (container) { + container.innerHTML = '
Loading...
'; + } + + var self = this; + return api.getAlerts(count, level).then(function(data) { + var alerts = data.alerts || []; + if (container) { + dom.content(container, self.renderAlertsTable(alerts)); + } + }); + }, + + pollAlerts: function() { + return this.handleRefresh(); + } +}); diff --git a/package/secubox/luci-app-wazuh/htdocs/luci-static/resources/view/wazuh/fim.js b/package/secubox/luci-app-wazuh/htdocs/luci-static/resources/view/wazuh/fim.js new file mode 100644 index 00000000..0ef33237 --- /dev/null +++ b/package/secubox/luci-app-wazuh/htdocs/luci-static/resources/view/wazuh/fim.js @@ -0,0 +1,197 @@ +'use strict'; +'require view'; +'require dom'; +'require poll'; +'require wazuh.api as api'; + +return view.extend({ + handleSaveApply: null, + handleSave: null, + handleReset: null, + + load: function() { + return Promise.all([ + api.getFIMEvents(50), + api.getFIMConfig() + ]); + }, + + render: function(data) { + var events = data[0].events || []; + var config = data[1] || {}; + + var view = E('div', { 'class': 'cbi-map' }, [ + E('h2', {}, _('File Integrity Monitoring')), + E('div', { 'class': 'cbi-map-descr' }, + _('Track changes to critical files and directories') + ), + + // FIM Configuration Summary + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Monitored Paths')), + E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 1rem;' }, [ + this.renderConfigCard('Directories', config.directories || [], 'folder'), + this.renderConfigCard('Registry Keys', config.registry || [], 'key'), + this.renderConfigCard('Ignored Paths', config.ignore || [], 'blocked') + ]) + ]), + + // Statistics + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Event Statistics')), + E('div', { 'style': 'display: flex; gap: 1rem; flex-wrap: wrap;' }, [ + this.renderStatCard('Added', this.countByType(events, 'added'), 'success'), + this.renderStatCard('Modified', this.countByType(events, 'modified'), 'warning'), + this.renderStatCard('Deleted', this.countByType(events, 'deleted'), 'danger'), + this.renderStatCard('Total Events', events.length, 'info') + ]) + ]), + + // FIM Events Table + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Recent File Changes')), + E('div', { 'style': 'margin-bottom: 1rem;' }, [ + E('button', { + 'class': 'btn cbi-button cbi-button-action', + 'click': L.bind(this.handleRefresh, this) + }, _('Refresh')) + ]), + E('div', { 'id': 'fim-events-container' }, [ + this.renderEventsTable(events) + ]) + ]), + + // Event Type Legend + E('div', { 'class': 'cbi-section' }, [ + E('h4', {}, _('Event Types')), + E('div', { 'style': 'display: flex; gap: 1rem; flex-wrap: wrap;' }, [ + E('span', {}, [ + E('span', { 'class': 'badge success', 'style': 'margin-right: 0.5rem;' }, '+'), + _('Added') + ]), + E('span', {}, [ + E('span', { 'class': 'badge warning', 'style': 'margin-right: 0.5rem;' }, '~'), + _('Modified') + ]), + E('span', {}, [ + E('span', { 'class': 'badge danger', 'style': 'margin-right: 0.5rem;' }, '-'), + _('Deleted') + ]) + ]) + ]) + ]); + + poll.add(L.bind(this.pollEvents, this), 60); + + return view; + }, + + renderConfigCard: function(title, items, icon) { + var content = items.length > 0 + ? items.slice(0, 5).map(function(item) { + return E('div', { 'style': 'font-family: monospace; font-size: 0.85em; padding: 0.2rem 0;' }, + typeof item === 'string' ? item : item.path || JSON.stringify(item) + ); + }) + : [E('div', { 'style': 'color: var(--text-color-low);' }, _('None configured'))]; + + if (items.length > 5) { + content.push(E('div', { 'style': 'color: var(--text-color-low); font-style: italic;' }, + _('... and %d more').format(items.length - 5) + )); + } + + return E('div', { + 'style': 'flex: 1; min-width: 250px; background: var(--background-color-high); padding: 1rem; border-radius: 8px;' + }, [ + E('div', { 'style': 'font-weight: bold; margin-bottom: 0.5rem;' }, title), + E('div', {}, content) + ]); + }, + + renderStatCard: function(label, count, badgeClass) { + return E('div', { + 'style': 'text-align: center; padding: 1rem; background: var(--background-color-high); border-radius: 8px; min-width: 100px;' + }, [ + E('div', { 'style': 'font-size: 2em; font-weight: bold;' }, String(count)), + E('div', { 'class': 'badge ' + badgeClass }, label) + ]); + }, + + countByType: function(events, type) { + return events.filter(function(e) { return e.event_type === type; }).length; + }, + + renderEventsTable: function(events) { + if (!events || events.length === 0) { + return E('div', { 'class': 'cbi-value', 'style': 'text-align: center; padding: 2rem;' }, + _('No file integrity events found') + ); + } + + var rows = [ + E('tr', { 'class': 'tr' }, [ + E('th', { 'class': 'th', 'style': 'width: 60px;' }, _('Type')), + E('th', { 'class': 'th', 'style': 'width: 150px;' }, _('Time')), + E('th', { 'class': 'th' }, _('Path')), + E('th', { 'class': 'th', 'style': 'width: 100px;' }, _('Mode')), + E('th', { 'class': 'th', 'style': 'width: 100px;' }, _('User')), + E('th', { 'class': 'th', 'style': 'width: 120px;' }, _('Size')) + ]) + ]; + + events.forEach(function(event) { + var typeClass = event.event_type === 'added' ? 'success' : + (event.event_type === 'deleted' ? 'danger' : 'warning'); + var typeSymbol = event.event_type === 'added' ? '+' : + (event.event_type === 'deleted' ? '-' : '~'); + + rows.push(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, [ + E('span', { 'class': 'badge ' + typeClass }, typeSymbol) + ]), + E('td', { 'class': 'td', 'style': 'font-size: 0.85em;' }, + api.formatTime(event.timestamp) + ), + E('td', { 'class': 'td', 'style': 'font-family: monospace; word-break: break-all;' }, + event.path || '-' + ), + E('td', { 'class': 'td', 'style': 'font-family: monospace;' }, + event.perm || event.mode || '-' + ), + E('td', { 'class': 'td' }, event.uname || event.user || '-'), + E('td', { 'class': 'td', 'style': 'font-family: monospace;' }, + event.size ? this.formatSize(event.size) : '-' + ) + ])); + }, this); + + return E('table', { 'class': 'table' }, rows); + }, + + formatSize: function(bytes) { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; + return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB'; + }, + + handleRefresh: function() { + var container = document.getElementById('fim-events-container'); + if (container) { + container.innerHTML = '
Loading...
'; + } + + var self = this; + return api.getFIMEvents(50).then(function(data) { + var events = data.events || []; + if (container) { + dom.content(container, self.renderEventsTable(events)); + } + }); + }, + + pollEvents: function() { + return this.handleRefresh(); + } +}); diff --git a/package/secubox/luci-app-wazuh/htdocs/luci-static/resources/view/wazuh/overview.js b/package/secubox/luci-app-wazuh/htdocs/luci-static/resources/view/wazuh/overview.js new file mode 100644 index 00000000..24914268 --- /dev/null +++ b/package/secubox/luci-app-wazuh/htdocs/luci-static/resources/view/wazuh/overview.js @@ -0,0 +1,199 @@ +'use strict'; +'require view'; +'require dom'; +'require poll'; +'require wazuh.api as api'; + +return view.extend({ + handleSaveApply: null, + handleSave: null, + handleReset: null, + + load: function() { + return Promise.all([ + api.getOverview(), + api.getAlertSummary(), + api.getCrowdSecCorrelation() + ]); + }, + + render: function(data) { + var overview = data[0] || {}; + var alerts = data[1] || {}; + var crowdsec = data[2] || {}; + + var view = E('div', { 'class': 'cbi-map' }, [ + E('h2', {}, _('Wazuh SIEM Dashboard')), + E('div', { 'class': 'cbi-map-descr' }, + _('Security Information and Event Management powered by Wazuh XDR') + ), + + // Status Cards Row + E('div', { 'class': 'cbi-section', 'style': 'display: flex; flex-wrap: wrap; gap: 1rem;' }, [ + // Agent Status Card + this.renderStatusCard( + 'Agent Status', + overview.agent ? (overview.agent.connected ? 'Connected' : (overview.agent.running ? 'Running' : 'Stopped')) : 'Unknown', + overview.agent && overview.agent.connected ? 'success' : (overview.agent && overview.agent.running ? 'warning' : 'danger'), + 'Local security agent monitoring this device' + ), + + // Manager Status Card + this.renderStatusCard( + 'Manager Status', + overview.manager ? (overview.manager.running ? 'Running' : 'Stopped') : 'Unknown', + overview.manager && overview.manager.running ? 'success' : 'danger', + 'Central SIEM manager in LXC container' + ), + + // Indexer Status Card + this.renderStatusCard( + 'Indexer Health', + overview.manager ? (overview.manager.indexer_status || 'Unknown') : 'Unknown', + overview.manager && overview.manager.indexer_status === 'green' ? 'success' : + (overview.manager && overview.manager.indexer_status === 'yellow' ? 'warning' : 'danger'), + 'OpenSearch cluster for alert storage' + ), + + // CrowdSec Integration Card + this.renderStatusCard( + 'CrowdSec Integration', + crowdsec.crowdsec_running ? 'Active' : 'Inactive', + crowdsec.crowdsec_running ? 'success' : 'notice', + crowdsec.active_decisions + ' active ban decisions' + ) + ]), + + // Alert Summary Section + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Alert Summary')), + E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 1rem;' }, [ + this.renderAlertBadge('Critical', alerts.critical || 0, 'danger'), + this.renderAlertBadge('High', alerts.high || 0, 'warning'), + this.renderAlertBadge('Medium', alerts.medium || 0, 'notice'), + this.renderAlertBadge('Low', alerts.low || 0, 'info'), + this.renderAlertBadge('Total', alerts.total || 0, 'secondary') + ]) + ]), + + // Quick Actions Section + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Quick Actions')), + E('div', { 'style': 'display: flex; gap: 1rem; flex-wrap: wrap;' }, [ + E('a', { + 'href': 'https://wazuh.gk2.secubox.in', + 'target': '_blank', + 'class': 'btn cbi-button cbi-button-action' + }, _('Open Wazuh Dashboard')), + + E('button', { + 'class': 'btn cbi-button cbi-button-apply', + 'click': L.bind(this.handleRestartAgent, this) + }, _('Restart Agent')), + + E('a', { + 'href': L.url('admin/services/wazuh/alerts'), + 'class': 'btn cbi-button' + }, _('View Alerts')), + + E('a', { + 'href': L.url('admin/services/wazuh/fim'), + 'class': 'btn cbi-button' + }, _('File Integrity')) + ]) + ]), + + // Security Layers Info + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, _('Security Layers (SysWarden-Inspired)')), + E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr' }, [ + E('th', { 'class': 'th' }, _('Layer')), + E('th', { 'class': 'th' }, _('Component')), + E('th', { 'class': 'th' }, _('Function')), + E('th', { 'class': 'th' }, _('Status')) + ]), + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, _('Layer 1: Firewall')), + E('td', { 'class': 'td' }, _('Vortex Firewall + nftables')), + E('td', { 'class': 'td' }, _('Kernel-level IP blocking')), + E('td', { 'class': 'td' }, E('span', { 'class': 'badge success' }, _('Active'))) + ]), + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, _('Layer 2: IPS')), + E('td', { 'class': 'td' }, _('CrowdSec + Bouncer')), + E('td', { 'class': 'td' }, _('Behavior-based threat detection')), + E('td', { 'class': 'td' }, E('span', { + 'class': 'badge ' + (crowdsec.crowdsec_running ? 'success' : 'danger') + }, crowdsec.crowdsec_running ? _('Active') : _('Inactive'))) + ]), + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, _('Layer 3: SIEM/XDR')), + E('td', { 'class': 'td' }, _('Wazuh Manager')), + E('td', { 'class': 'td' }, _('Log analysis, FIM, threat correlation')), + E('td', { 'class': 'td' }, E('span', { + 'class': 'badge ' + (overview.manager && overview.manager.running ? 'success' : 'danger') + }, overview.manager && overview.manager.running ? _('Active') : _('Inactive'))) + ]), + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, _('Layer 4: WAF')), + E('td', { 'class': 'td' }, _('mitmproxy + HAProxy')), + E('td', { 'class': 'td' }, _('Web application firewall')), + E('td', { 'class': 'td' }, E('span', { 'class': 'badge success' }, _('Active'))) + ]) + ]) + ]) + ]); + + // Setup polling for real-time updates + poll.add(L.bind(this.pollStatus, this), 30); + + return view; + }, + + renderStatusCard: function(title, status, statusClass, description) { + return E('div', { + 'class': 'cbi-value', + 'style': 'flex: 1; min-width: 200px; background: var(--background-color-high); padding: 1rem; border-radius: 8px; border-left: 4px solid var(--' + statusClass + '-color, #666);' + }, [ + E('div', { 'style': 'font-weight: bold; margin-bottom: 0.5rem;' }, title), + E('div', { + 'class': 'badge ' + statusClass, + 'style': 'font-size: 1.2em; padding: 0.3rem 0.6rem;' + }, status), + E('div', { 'style': 'font-size: 0.85em; color: var(--text-color-low); margin-top: 0.5rem;' }, description) + ]); + }, + + renderAlertBadge: function(label, count, badgeClass) { + return E('div', { + 'style': 'text-align: center; padding: 0.5rem 1rem; background: var(--background-color-high); border-radius: 8px; min-width: 80px;' + }, [ + E('div', { 'style': 'font-size: 1.5em; font-weight: bold;' }, String(count)), + E('div', { 'class': 'badge ' + badgeClass }, label) + ]); + }, + + handleRestartAgent: function() { + var self = this; + return api.restartAgent().then(function(res) { + if (res.success) { + L.ui.addNotification(null, E('p', _('Wazuh agent restarted successfully')), 'info'); + } else { + L.ui.addNotification(null, E('p', _('Failed to restart agent')), 'error'); + } + return self.load().then(L.bind(self.render, self)); + }); + }, + + pollStatus: function() { + return api.getOverview().then(L.bind(function(overview) { + // Update status indicators + var agentBadge = document.querySelector('.cbi-section .badge'); + if (agentBadge && overview.agent) { + agentBadge.textContent = overview.agent.connected ? 'Connected' : + (overview.agent.running ? 'Running' : 'Stopped'); + } + }, this)); + } +}); diff --git a/package/secubox/luci-app-wazuh/htdocs/luci-static/resources/wazuh/api.js b/package/secubox/luci-app-wazuh/htdocs/luci-static/resources/wazuh/api.js new file mode 100644 index 00000000..a7b7dda8 --- /dev/null +++ b/package/secubox/luci-app-wazuh/htdocs/luci-static/resources/wazuh/api.js @@ -0,0 +1,165 @@ +'use strict'; +'require rpc'; + +var callWazuh = rpc.declare({ + object: 'luci.wazuh', + method: 'get_overview', + expect: {} +}); + +var callAgentStatus = rpc.declare({ + object: 'luci.wazuh', + method: 'get_agent_status', + expect: {} +}); + +var callManagerStatus = rpc.declare({ + object: 'luci.wazuh', + method: 'get_manager_status', + expect: {} +}); + +var callGetAlerts = rpc.declare({ + object: 'luci.wazuh', + method: 'get_alerts', + params: ['count', 'level'], + expect: {} +}); + +var callAlertSummary = rpc.declare({ + object: 'luci.wazuh', + method: 'get_alert_summary', + expect: {} +}); + +var callFIMEvents = rpc.declare({ + object: 'luci.wazuh', + method: 'get_fim_events', + params: ['count'], + expect: {} +}); + +var callFIMConfig = rpc.declare({ + object: 'luci.wazuh', + method: 'get_fim_config', + expect: {} +}); + +var callListAgents = rpc.declare({ + object: 'luci.wazuh', + method: 'list_agents', + expect: {} +}); + +var callCrowdSecCorrelation = rpc.declare({ + object: 'luci.wazuh', + method: 'get_crowdsec_correlation', + expect: {} +}); + +var callStartAgent = rpc.declare({ + object: 'luci.wazuh', + method: 'start_agent', + expect: {} +}); + +var callStopAgent = rpc.declare({ + object: 'luci.wazuh', + method: 'stop_agent', + expect: {} +}); + +var callRestartAgent = rpc.declare({ + object: 'luci.wazuh', + method: 'restart_agent', + expect: {} +}); + +return L.Class.extend({ + getOverview: function() { + return callWazuh().then(function(res) { + return res.result || res; + }); + }, + + getAgentStatus: function() { + return callAgentStatus().then(function(res) { + return res.result || res; + }); + }, + + getManagerStatus: function() { + return callManagerStatus().then(function(res) { + return res.result || res; + }); + }, + + getAlerts: function(count, level) { + return callGetAlerts(count || 20, level || 0).then(function(res) { + return res.result || res; + }); + }, + + getAlertSummary: function() { + return callAlertSummary().then(function(res) { + return res.result || res; + }); + }, + + getFIMEvents: function(count) { + return callFIMEvents(count || 50).then(function(res) { + return res.result || res; + }); + }, + + getFIMConfig: function() { + return callFIMConfig().then(function(res) { + return res.result || res; + }); + }, + + listAgents: function() { + return callListAgents().then(function(res) { + return res.result || res; + }); + }, + + getCrowdSecCorrelation: function() { + return callCrowdSecCorrelation().then(function(res) { + return res.result || res; + }); + }, + + startAgent: function() { + return callStartAgent().then(function(res) { + return res.result || res; + }); + }, + + stopAgent: function() { + return callStopAgent().then(function(res) { + return res.result || res; + }); + }, + + restartAgent: function() { + return callRestartAgent().then(function(res) { + return res.result || res; + }); + }, + + // Helper to format alert level + formatLevel: function(level) { + if (level >= 12) return { text: 'Critical', class: 'danger' }; + if (level >= 9) return { text: 'High', class: 'warning' }; + if (level >= 5) return { text: 'Medium', class: 'notice' }; + return { text: 'Low', class: 'info' }; + }, + + // Helper to format timestamp + formatTime: function(timestamp) { + if (!timestamp) return '-'; + var d = new Date(timestamp); + return d.toLocaleString(); + } +}); diff --git a/package/secubox/luci-app-wazuh/root/usr/libexec/rpcd/luci.wazuh b/package/secubox/luci-app-wazuh/root/usr/libexec/rpcd/luci.wazuh new file mode 100644 index 00000000..25e0a78a --- /dev/null +++ b/package/secubox/luci-app-wazuh/root/usr/libexec/rpcd/luci.wazuh @@ -0,0 +1,387 @@ +#!/bin/sh +# SecuBox Wazuh LuCI RPCD Handler +# Provides API for Wazuh dashboard + +WAZUH_DIR="/var/ossec" +WAZUH_MANAGER_CONTAINER="wazuh" +WAZUH_AGENT_CONTAINER="wazuh-agent" +MANAGER_IP="192.168.255.50" + +# ============================================ +# CrowdSec Integration +# ============================================ + +get_crowdsec_correlation() { + local crowdsec_running="false" + local decisions=0 + local wazuh_parser="false" + + if pgrep crowdsec >/dev/null 2>&1; then + crowdsec_running="true" + decisions=$(cscli decisions list -o json 2>/dev/null | grep -c '"id"' 2>/dev/null || echo 0) + fi + + if [ -f "/etc/crowdsec/parsers/s01-parse/wazuh.yaml" ]; then + wazuh_parser="true" + fi + + printf '{"crowdsec_running":%s,"active_decisions":%d,"wazuh_parser_enabled":%s}\n' \ + "$crowdsec_running" "$decisions" "$wazuh_parser" +} + +# ============================================ +# Agent Status +# ============================================ + +get_agent_status() { + local agent_running="false" + local agent_connected="false" + local agent_id="" + local agent_name="" + local manager_ip="" + + if lxc-info -n "$WAZUH_AGENT_CONTAINER" -s 2>/dev/null | grep -q RUNNING; then + agent_running="true" + if lxc-attach -n "$WAZUH_AGENT_CONTAINER" -- pgrep -f wazuh-agentd >/dev/null 2>&1; then + agent_connected="true" + local client_keys=$(lxc-attach -n "$WAZUH_AGENT_CONTAINER" -- cat "$WAZUH_DIR/etc/client.keys" 2>/dev/null | head -1) + if [ -n "$client_keys" ]; then + agent_id=$(echo "$client_keys" | awk '{print $1}') + agent_name=$(echo "$client_keys" | awk '{print $2}') + fi + fi + fi + + manager_ip=$(uci -q get wazuh.main.manager_ip 2>/dev/null || echo "$MANAGER_IP") + + printf '{"running":%s,"connected":%s,"agent_id":"%s","agent_name":"%s","manager_ip":"%s","container":"%s"}\n' \ + "$agent_running" "$agent_connected" "$agent_id" "$agent_name" "$manager_ip" "$WAZUH_AGENT_CONTAINER" +} + +# ============================================ +# Manager Status +# ============================================ + +get_manager_status() { + local manager_running="false" + local indexer_running="false" + local dashboard_running="false" + local cluster_health="unknown" + local agents_total=0 + local agents_active=0 + + if lxc-info -n "$WAZUH_MANAGER_CONTAINER" -s 2>/dev/null | grep -q RUNNING; then + manager_running="true" + + if lxc-attach -n "$WAZUH_MANAGER_CONTAINER" -- netstat -tlnp 2>/dev/null | grep -q ":9200 "; then + indexer_running="true" + cluster_health=$(lxc-attach -n "$WAZUH_MANAGER_CONTAINER" -- curl -sk -u admin:admin "https://127.0.0.1:9200/_cluster/health" 2>/dev/null | grep -o '"status":"[^"]*"' | cut -d'"' -f4 || echo "unknown") + fi + + if lxc-attach -n "$WAZUH_MANAGER_CONTAINER" -- netstat -tlnp 2>/dev/null | grep -q ":5601 "; then + dashboard_running="true" + fi + + local agent_list=$(lxc-attach -n "$WAZUH_MANAGER_CONTAINER" -- /var/ossec/bin/agent_control -l 2>/dev/null) + agents_total=$(echo "$agent_list" | grep -c "ID:" 2>/dev/null || echo 0) + agents_active=$(echo "$agent_list" | grep -c "Active" 2>/dev/null || echo 0) + fi + + printf '{"running":%s,"indexer_running":%s,"dashboard_running":%s,"cluster_health":"%s","agents_total":%d,"agents_active":%d,"dashboard_url":"https://wazuh.gk2.secubox.in","container":"%s"}\n' \ + "$manager_running" "$indexer_running" "$dashboard_running" "$cluster_health" "$agents_total" "$agents_active" "$WAZUH_MANAGER_CONTAINER" +} + +# ============================================ +# Alert Summary +# ============================================ + +get_alert_summary() { + local critical=0 + local high=0 + local medium=0 + local low=0 + local total=0 + + if lxc-info -n "$WAZUH_MANAGER_CONTAINER" -s 2>/dev/null | grep -q RUNNING; then + # Count alerts by severity using simple grep + local alerts_file="/var/ossec/logs/alerts/alerts.json" + critical=$(lxc-attach -n "$WAZUH_MANAGER_CONTAINER" -- sh -c "tail -n 1000 $alerts_file 2>/dev/null | grep -c '\"level\":1[2-5]' 2>/dev/null" || echo 0) + high=$(lxc-attach -n "$WAZUH_MANAGER_CONTAINER" -- sh -c "tail -n 1000 $alerts_file 2>/dev/null | grep -c '\"level\":[9]' 2>/dev/null" || echo 0) + medium=$(lxc-attach -n "$WAZUH_MANAGER_CONTAINER" -- sh -c "tail -n 1000 $alerts_file 2>/dev/null | grep -c '\"level\":[5-8]' 2>/dev/null" || echo 0) + low=$(lxc-attach -n "$WAZUH_MANAGER_CONTAINER" -- sh -c "tail -n 1000 $alerts_file 2>/dev/null | grep -c '\"level\":[1-4]' 2>/dev/null" || echo 0) + # Ensure values are numeric + critical=${critical:-0} + high=${high:-0} + medium=${medium:-0} + low=${low:-0} + # Strip any non-numeric chars + critical=$(echo "$critical" | tr -cd '0-9') + high=$(echo "$high" | tr -cd '0-9') + medium=$(echo "$medium" | tr -cd '0-9') + low=$(echo "$low" | tr -cd '0-9') + [ -z "$critical" ] && critical=0 + [ -z "$high" ] && high=0 + [ -z "$medium" ] && medium=0 + [ -z "$low" ] && low=0 + total=$((critical + high + medium + low)) + fi + + printf '{"critical":%d,"high":%d,"medium":%d,"low":%d,"total":%d}\n' \ + "$critical" "$high" "$medium" "$low" "$total" +} + +# ============================================ +# Alerts List +# ============================================ + +get_alerts() { + local count="${1:-20}" + local level="${2:-0}" + + printf '{"alerts":[' + + if lxc-info -n "$WAZUH_MANAGER_CONTAINER" -s 2>/dev/null | grep -q RUNNING; then + local first=1 + lxc-attach -n "$WAZUH_MANAGER_CONTAINER" -- tail -n "$count" /var/ossec/logs/alerts/alerts.json 2>/dev/null | while read -r line; do + if [ -n "$line" ]; then + local rule_level=$(echo "$line" | grep -oE '"level":[0-9]+' | head -1 | cut -d':' -f2) + if [ -n "$rule_level" ] && [ "$rule_level" -ge "$level" ] 2>/dev/null; then + local timestamp=$(echo "$line" | grep -oE '"timestamp":"[^"]*"' | head -1 | cut -d'"' -f4) + local rule_id=$(echo "$line" | grep -oE '"id":"[0-9]+"' | head -1 | cut -d'"' -f4) + local rule_desc=$(echo "$line" | grep -oE '"description":"[^"]*"' | head -1 | cut -d'"' -f4 | sed 's/"/\\"/g') + local agent_name=$(echo "$line" | grep -oE '"name":"[^"]*"' | head -1 | cut -d'"' -f4) + local src_ip=$(echo "$line" | grep -oE '"srcip":"[^"]*"' | head -1 | cut -d'"' -f4) + + [ $first -eq 0 ] && printf ',' + first=0 + printf '{"timestamp":"%s","rule_level":%s,"rule_id":"%s","rule_description":"%s","agent_name":"%s","src_ip":"%s"}' \ + "$timestamp" "$rule_level" "$rule_id" "$rule_desc" "$agent_name" "$src_ip" + fi + fi + done + fi + + printf ']}\n' +} + +# ============================================ +# FIM Functions +# ============================================ + +get_fim_events() { + local count="${1:-50}" + + printf '{"events":[' + + if lxc-info -n "$WAZUH_MANAGER_CONTAINER" -s 2>/dev/null | grep -q RUNNING; then + local first=1 + lxc-attach -n "$WAZUH_MANAGER_CONTAINER" -- grep -E 'syscheck' /var/ossec/logs/alerts/alerts.json 2>/dev/null | tail -n "$count" | while read -r line; do + if [ -n "$line" ]; then + local timestamp=$(echo "$line" | grep -oE '"timestamp":"[^"]*"' | head -1 | cut -d'"' -f4) + local path=$(echo "$line" | grep -oE '"path":"[^"]*"' | head -1 | cut -d'"' -f4 | sed 's/"/\\"/g') + local event_type=$(echo "$line" | grep -oE '"event":"[^"]*"' | head -1 | cut -d'"' -f4) + + [ $first -eq 0 ] && printf ',' + first=0 + printf '{"timestamp":"%s","path":"%s","event_type":"%s"}' \ + "$timestamp" "$path" "$event_type" + fi + done + fi + + printf ']}\n' +} + +get_fim_config() { + printf '{"directories":["/etc/config","/etc/init.d","/usr/sbin","/usr/libexec/rpcd","/srv/haproxy/config","/etc/passwd","/etc/shadow"]}\n' +} + +# ============================================ +# Agent Management +# ============================================ + +list_agents() { + printf '{"agents":[' + + if lxc-info -n "$WAZUH_MANAGER_CONTAINER" -s 2>/dev/null | grep -q RUNNING; then + local first=1 + lxc-attach -n "$WAZUH_MANAGER_CONTAINER" -- /var/ossec/bin/agent_control -l 2>/dev/null | grep "ID:" | while read -r line; do + local id=$(echo "$line" | sed 's/.*ID: \([0-9]*\),.*/\1/') + local name=$(echo "$line" | sed 's/.*Name: \([^,]*\),.*/\1/') + local ip=$(echo "$line" | sed 's/.*IP: \([^,]*\),.*/\1/') + local status=$(echo "$line" | sed 's/.*IP: [^,]*, \(.*\)/\1/' | tr -d ' ') + + [ $first -eq 0 ] && printf ',' + first=0 + printf '{"id":"%s","name":"%s","ip":"%s","status":"%s"}' \ + "$id" "$name" "$ip" "$status" + done + fi + + printf ']}\n' +} + +# ============================================ +# Service Control +# ============================================ + +start_agent() { + local success="false" + if lxc-info -n "$WAZUH_AGENT_CONTAINER" -s 2>/dev/null | grep -q RUNNING; then + lxc-attach -n "$WAZUH_AGENT_CONTAINER" -- /var/ossec/bin/wazuh-control start >/dev/null 2>&1 + success="true" + fi + printf '{"success":%s}\n' "$success" +} + +stop_agent() { + local success="false" + if lxc-info -n "$WAZUH_AGENT_CONTAINER" -s 2>/dev/null | grep -q RUNNING; then + lxc-attach -n "$WAZUH_AGENT_CONTAINER" -- /var/ossec/bin/wazuh-control stop >/dev/null 2>&1 + success="true" + fi + printf '{"success":%s}\n' "$success" +} + +restart_agent() { + local success="false" + if lxc-info -n "$WAZUH_AGENT_CONTAINER" -s 2>/dev/null | grep -q RUNNING; then + lxc-attach -n "$WAZUH_AGENT_CONTAINER" -- /var/ossec/bin/wazuh-control restart >/dev/null 2>&1 + success="true" + fi + printf '{"success":%s}\n' "$success" +} + +# ============================================ +# Overview / Dashboard Data +# ============================================ + +get_overview() { + local agent_running="false" + local agent_connected="false" + local manager_running="false" + local indexer_status="unknown" + local dashboard_accessible="false" + local critical=0 + local high=0 + local total=0 + local crowdsec_integrated="false" + + # Agent status + if lxc-info -n "$WAZUH_AGENT_CONTAINER" -s 2>/dev/null | grep -q RUNNING; then + agent_running="true" + if lxc-attach -n "$WAZUH_AGENT_CONTAINER" -- pgrep -f wazuh-agentd >/dev/null 2>&1; then + agent_connected="true" + fi + fi + + # Manager status + if lxc-info -n "$WAZUH_MANAGER_CONTAINER" -s 2>/dev/null | grep -q RUNNING; then + manager_running="true" + indexer_status=$(lxc-attach -n "$WAZUH_MANAGER_CONTAINER" -- curl -sk -u admin:admin "https://127.0.0.1:9200/_cluster/health" 2>/dev/null | grep -o '"status":"[^"]*"' | cut -d'"' -f4 || echo "unknown") + [ -z "$indexer_status" ] && indexer_status="unknown" + if lxc-attach -n "$WAZUH_MANAGER_CONTAINER" -- netstat -tlnp 2>/dev/null | grep -q ":5601 "; then + dashboard_accessible="true" + fi + + # Alert counts - run directly in container to avoid variable issues + local alerts_file="/var/ossec/logs/alerts/alerts.json" + critical=$(lxc-attach -n "$WAZUH_MANAGER_CONTAINER" -- sh -c "tail -n 500 $alerts_file 2>/dev/null | grep -c '\"level\":1[2-5]' 2>/dev/null" || echo 0) + high=$(lxc-attach -n "$WAZUH_MANAGER_CONTAINER" -- sh -c "tail -n 500 $alerts_file 2>/dev/null | grep -c '\"level\":[9]' 2>/dev/null" || echo 0) + total=$(lxc-attach -n "$WAZUH_MANAGER_CONTAINER" -- sh -c "tail -n 500 $alerts_file 2>/dev/null | wc -l 2>/dev/null" || echo 0) + # Strip non-numeric chars + critical=$(echo "$critical" | tr -cd '0-9') + high=$(echo "$high" | tr -cd '0-9') + total=$(echo "$total" | tr -cd '0-9') + [ -z "$critical" ] && critical=0 + [ -z "$high" ] && high=0 + [ -z "$total" ] && total=0 + fi + + # CrowdSec + if pgrep crowdsec >/dev/null 2>&1; then + crowdsec_integrated="true" + fi + + printf '{"agent":{"running":%s,"connected":%s},"manager":{"running":%s,"indexer_status":"%s","dashboard_accessible":%s},"alerts":{"critical":%d,"high":%d,"total":%d},"crowdsec_integrated":%s}\n' \ + "$agent_running" "$agent_connected" "$manager_running" "$indexer_status" "$dashboard_accessible" "$critical" "$high" "$total" "$crowdsec_integrated" +} + +# ============================================ +# RPCD Interface +# ============================================ + +case "$1" in + list) + cat <<'EOF' +{ + "get_overview": {}, + "get_agent_status": {}, + "get_manager_status": {}, + "get_alerts": {"count": 20, "level": 0}, + "get_alert_summary": {}, + "get_fim_events": {"count": 50}, + "get_fim_config": {}, + "list_agents": {}, + "get_crowdsec_correlation": {}, + "start_agent": {}, + "stop_agent": {}, + "restart_agent": {} +} +EOF + ;; + call) + case "$2" in + get_overview) + get_overview + ;; + get_agent_status) + get_agent_status + ;; + get_manager_status) + get_manager_status + ;; + get_alerts) + read -r input + count=$(echo "$input" | grep -oE '"count":[0-9]+' | cut -d':' -f2 || echo 20) + level=$(echo "$input" | grep -oE '"level":[0-9]+' | cut -d':' -f2 || echo 0) + [ -z "$count" ] && count=20 + [ -z "$level" ] && level=0 + get_alerts "$count" "$level" + ;; + get_alert_summary) + get_alert_summary + ;; + get_fim_events) + read -r input + count=$(echo "$input" | grep -oE '"count":[0-9]+' | cut -d':' -f2 || echo 50) + [ -z "$count" ] && count=50 + get_fim_events "$count" + ;; + get_fim_config) + get_fim_config + ;; + list_agents) + list_agents + ;; + get_crowdsec_correlation) + get_crowdsec_correlation + ;; + start_agent) + start_agent + ;; + stop_agent) + stop_agent + ;; + restart_agent) + restart_agent + ;; + *) + echo '{"error": "Unknown method"}' + ;; + esac + ;; + *) + echo '{"error": "Unknown command"}' + ;; +esac diff --git a/package/secubox/luci-app-wazuh/root/usr/share/luci/menu.d/luci-app-wazuh.json b/package/secubox/luci-app-wazuh/root/usr/share/luci/menu.d/luci-app-wazuh.json new file mode 100644 index 00000000..7997f882 --- /dev/null +++ b/package/secubox/luci-app-wazuh/root/usr/share/luci/menu.d/luci-app-wazuh.json @@ -0,0 +1,46 @@ +{ + "admin/services/wazuh": { + "title": "Wazuh SIEM", + "order": 15, + "action": { + "type": "view", + "path": "wazuh/overview" + }, + "depends": { + "acl": ["luci-app-wazuh"], + "uci": {"wazuh": true} + } + }, + "admin/services/wazuh/overview": { + "title": "Overview", + "order": 1, + "action": { + "type": "view", + "path": "wazuh/overview" + } + }, + "admin/services/wazuh/alerts": { + "title": "Alerts", + "order": 2, + "action": { + "type": "view", + "path": "wazuh/alerts" + } + }, + "admin/services/wazuh/fim": { + "title": "File Integrity", + "order": 3, + "action": { + "type": "view", + "path": "wazuh/fim" + } + }, + "admin/services/wazuh/agents": { + "title": "Agents", + "order": 4, + "action": { + "type": "view", + "path": "wazuh/agents" + } + } +} diff --git a/package/secubox/luci-app-wazuh/root/usr/share/rpcd/acl.d/luci-app-wazuh.json b/package/secubox/luci-app-wazuh/root/usr/share/rpcd/acl.d/luci-app-wazuh.json new file mode 100644 index 00000000..20194397 --- /dev/null +++ b/package/secubox/luci-app-wazuh/root/usr/share/rpcd/acl.d/luci-app-wazuh.json @@ -0,0 +1,31 @@ +{ + "luci-app-wazuh": { + "description": "Grant access to Wazuh SIEM dashboard", + "read": { + "ubus": { + "luci.wazuh": [ + "get_overview", + "get_agent_status", + "get_manager_status", + "get_alerts", + "get_alert_summary", + "get_fim_events", + "get_fim_config", + "list_agents", + "get_crowdsec_correlation" + ] + }, + "uci": ["wazuh", "wazuh-manager"] + }, + "write": { + "ubus": { + "luci.wazuh": [ + "start_agent", + "stop_agent", + "restart_agent" + ] + }, + "uci": ["wazuh", "wazuh-manager"] + } + } +}