secubox-openwrt/package/secubox/luci-app-wazuh/htdocs/luci-static/resources/view/wazuh/fim.js
CyberMind-FR b1c34021db 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 <noreply@anthropic.com>
2026-02-14 14:45:05 +01:00

198 lines
7.8 KiB
JavaScript

'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 = '<div style="text-align: center; padding: 2rem;">Loading...</div>';
}
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();
}
});