secubox-openwrt/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js
CyberMind-FR cec4893db9 feat(security): Implement SysWarden Evolution #1-3 security enhancements
Evolution #1 - IP Blocklist (secubox-app-ipblocklist, luci-app-ipblocklist):
- Pre-emptive blocking layer with ipset (~100k IPs)
- Default sources: Data-Shield, Firehol Level 1
- Supports nftables (fw4) and iptables backends
- LuCI KISS dashboard with sources/whitelist management

Evolution #2 - AbuseIPDB Reporter (luci-app-crowdsec-dashboard v0.8.0):
- New "AbuseIPDB" tab in CrowdSec Dashboard
- crowdsec-reporter.sh CLI for reporting blocked IPs
- RPCD handler luci.crowdsec-abuseipdb with 9 methods
- Cron job for automatic reporting every 15 minutes
- IP reputation checker in dashboard

Evolution #3 - Log Denoising (luci-app-system-hub v0.5.2):
- Three modes: RAW, SMART (noise ratio), SIGNAL_ONLY (filter known IPs)
- Integrates with IP Blocklist ipset + CrowdSec decisions
- RPCD methods: get_denoised_logs, get_denoise_stats
- Denoise mode selector panel with noise ratio indicator

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-20 20:11:24 +01:00

474 lines
16 KiB
JavaScript

'use strict';
'require view';
'require ui';
'require dom';
'require poll';
'require system-hub/api as API';
'require secubox-theme/theme as Theme';
'require system-hub/theme-assets as ThemeAssets';
'require system-hub/nav as HubNav';
'require secubox-portal/header as SbHeader';
'require secubox/kiss-theme';
var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
(navigator.language ? navigator.language.split('-')[0] : 'en');
Theme.init({ language: shLang });
return view.extend({
logs: [],
lineCount: 200,
autoRefresh: true,
autoScroll: true,
searchQuery: '',
severityFilter: 'all',
lastLogCount: 0,
pollInterval: 2,
// Denoising mode: RAW, SMART, SIGNAL_ONLY
denoiseMode: 'RAW',
noiseRatio: 0,
filteredLines: 0,
totalLines: 0,
denoiseStats: null,
load: function() {
var self = this;
return Promise.all([
API.getDenoisedLogs(this.lineCount, '', this.denoiseMode),
API.getDenoiseStats()
]).then(function(results) {
return {
logsData: results[0],
denoiseStats: results[1]
};
});
},
render: function(data) {
// Extract logs and denoising info from new data structure
var logsData = data.logsData || {};
this.logs = logsData.logs || [];
this.noiseRatio = logsData.noise_ratio || 0;
this.filteredLines = logsData.filtered_lines || 0;
this.totalLines = logsData.total_lines || this.logs.length;
this.denoiseStats = data.denoiseStats || {};
this.lastLogCount = this.logs.length;
var content = [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
ThemeAssets.stylesheet('common.css'),
ThemeAssets.stylesheet('dashboard.css'),
ThemeAssets.stylesheet('logs.css'),
HubNav.renderTabs('logs'),
this.renderHero(),
this.renderDenoisePanel(),
this.renderControls(),
this.renderBody()
];
this.updateLogStream();
this.updateStats();
var self = this;
poll.add(function() {
if (!self.autoRefresh) return;
self.updateLiveIndicator(true);
return API.getDenoisedLogs(self.lineCount, '', self.denoiseMode).then(function(result) {
var newLogs = result.logs || [];
var hasNewLogs = newLogs.length !== self.lastLogCount;
self.logs = newLogs;
self.noiseRatio = result.noise_ratio || 0;
self.filteredLines = result.filtered_lines || 0;
self.totalLines = result.total_lines || newLogs.length;
self.lastLogCount = newLogs.length;
self.updateStats();
self.updateLogStream(hasNewLogs);
self.updateDenoiseIndicator();
self.updateLiveIndicator(false);
});
}, this.pollInterval);
return KissTheme.wrap(content, 'admin/system/hub/logs');
},
renderHero: function() {
return E('section', { 'class': 'sh-logs-hero' }, [
E('div', {}, [
E('h1', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
_('System Logs'),
E('span', {
'id': 'sh-live-indicator',
'class': 'sh-live-badge',
'style': 'display: inline-flex; align-items: center; gap: 0.3em; font-size: 0.5em; padding: 0.3em 0.6em; background: #22c55e; color: #fff; border-radius: 4px; animation: pulse 2s infinite;'
}, [
E('span', { 'class': 'sh-live-dot', 'style': 'width: 8px; height: 8px; background: #fff; border-radius: 50%;' }),
'LIVE'
])
]),
E('p', {}, _('Real-time kernel, service, and security events'))
]),
E('div', { 'class': 'sh-log-stats', 'id': 'sh-log-stats' }, [
this.createStat('sh-log-total', _('Lines'), this.logs.length),
this.createStat('sh-log-errors', _('Errors'), this.countSeverity('error'), 'danger'),
this.createStat('sh-log-warnings', _('Warning'), this.countSeverity('warning'), 'warn')
])
]);
},
createStat: function(id, label, value, tone) {
var cls = 'sh-log-stat';
if (tone) cls += ' ' + tone;
return E('div', { 'class': cls, 'id': id }, [
E('span', { 'class': 'label' }, label),
E('span', { 'class': 'value' }, value)
]);
},
renderDenoisePanel: function() {
var self = this;
var stats = this.denoiseStats || {};
var knownThreats = stats.total_known_threats || 0;
var ipblocklistEnabled = stats.ipblocklist_enabled;
var modeDescriptions = {
'RAW': _('All logs displayed without filtering'),
'SMART': _('Known threat IPs highlighted, all logs visible'),
'SIGNAL_ONLY': _('Only new/unknown threats shown')
};
return E('div', { 'class': 'sh-denoise-panel', 'style': 'display: flex; align-items: center; gap: 1.5em; padding: 0.8em 1.2em; background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); border-radius: 8px; margin-bottom: 1em; border: 1px solid rgba(99, 102, 241, 0.3);' }, [
// Mode selector
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
E('span', { 'style': 'font-weight: 600; color: #94a3b8;' }, '🧹 ' + _('Denoise Mode') + ':'),
E('select', {
'id': 'sh-denoise-mode',
'style': 'background: #334155; color: #f1f5f9; border: 1px solid #475569; border-radius: 4px; padding: 0.4em 0.8em; cursor: pointer;',
'change': function(ev) {
self.denoiseMode = ev.target.value;
self.refreshLogs();
}
}, [
E('option', { 'value': 'RAW', 'selected': this.denoiseMode === 'RAW' ? 'selected' : null }, 'RAW'),
E('option', { 'value': 'SMART', 'selected': this.denoiseMode === 'SMART' ? 'selected' : null }, 'SMART'),
E('option', { 'value': 'SIGNAL_ONLY', 'selected': this.denoiseMode === 'SIGNAL_ONLY' ? 'selected' : null }, 'SIGNAL ONLY')
])
]),
// Mode description
E('span', { 'id': 'sh-denoise-desc', 'style': 'color: #64748b; font-size: 0.9em;' }, modeDescriptions[this.denoiseMode]),
// Spacer
E('div', { 'style': 'flex: 1;' }),
// Noise ratio indicator
E('div', { 'id': 'sh-noise-indicator', 'style': 'display: flex; align-items: center; gap: 0.8em;' }, [
this.denoiseMode !== 'RAW' ? E('div', { 'style': 'text-align: center;' }, [
E('div', { 'style': 'font-size: 0.75em; color: #64748b; text-transform: uppercase;' }, _('Noise Filtered')),
E('div', {
'id': 'sh-noise-ratio',
'style': 'font-size: 1.4em; font-weight: 700; color: ' + this.getNoiseColor(this.noiseRatio) + ';'
}, this.noiseRatio + '%')
]) : null,
E('div', { 'style': 'text-align: center;' }, [
E('div', { 'style': 'font-size: 0.75em; color: #64748b; text-transform: uppercase;' }, _('Known Threats')),
E('div', { 'style': 'font-size: 1.4em; font-weight: 700; color: #f59e0b;' }, knownThreats.toLocaleString())
]),
!ipblocklistEnabled ? E('span', {
'style': 'padding: 0.3em 0.6em; background: #7c3aed; color: #fff; border-radius: 4px; font-size: 0.8em;',
'title': _('Enable IP Blocklist for better noise reduction')
}, '⚠ ' + _('Blocklist Off')) : null
])
]);
},
getNoiseColor: function(ratio) {
if (ratio >= 70) return '#22c55e'; // Green - lots of noise filtered
if (ratio >= 40) return '#f59e0b'; // Orange - moderate
return '#94a3b8'; // Gray - low noise
},
updateDenoiseIndicator: function() {
var ratioEl = document.getElementById('sh-noise-ratio');
if (ratioEl) {
ratioEl.textContent = this.noiseRatio + '%';
ratioEl.style.color = this.getNoiseColor(this.noiseRatio);
}
},
renderControls: function() {
var self = this;
return E('div', { 'class': 'sh-log-controls' }, [
E('div', { 'class': 'sh-log-search' }, [
E('input', {
'type': 'text',
'placeholder': _('Search keywords, services, IPs...'),
'input': function(ev) {
self.searchQuery = (ev.target.value || '').toLowerCase();
self.updateLogStream();
}
})
]),
E('div', { 'class': 'sh-log-selectors' }, [
E('button', {
'id': 'sh-play-pause',
'class': 'sh-btn ' + (this.autoRefresh ? 'sh-btn-danger' : 'sh-btn-success'),
'type': 'button',
'style': 'min-width: 100px; font-size: 1.1em;',
'click': function(ev) {
self.autoRefresh = !self.autoRefresh;
self.updatePlayPauseButton();
self.updateLiveBadge();
}
}, this.autoRefresh ? '⏸ ' + _('Pause') : '▶ ' + _('Play')),
E('select', {
'change': function(ev) {
self.lineCount = parseInt(ev.target.value, 10);
self.refreshLogs();
}
}, [
E('option', { 'value': '100' }, '100 lines'),
E('option', { 'value': '200', 'selected': 'selected' }, '200 lines'),
E('option', { 'value': '500' }, '500 lines'),
E('option', { 'value': '1000' }, '1000 lines')
]),
E('label', { 'class': 'sh-toggle', 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
E('input', {
'type': 'checkbox',
'checked': this.autoScroll ? 'checked' : null,
'change': function(ev) {
self.autoScroll = ev.target.checked;
}
}),
E('span', {}, _('Auto Scroll'))
]),
E('button', {
'class': 'sh-btn sh-btn-primary',
'type': 'button',
'click': ui.createHandlerFn(this, 'downloadLogs')
}, '⬇ ' + _('Export'))
])
]);
},
updatePlayPauseButton: function() {
var btn = document.getElementById('sh-play-pause');
if (btn) {
btn.textContent = this.autoRefresh ? '⏸ ' + _('Pause') : '▶ ' + _('Play');
btn.className = 'sh-btn ' + (this.autoRefresh ? 'sh-btn-danger' : 'sh-btn-success');
}
},
updateLiveBadge: function() {
var badge = document.getElementById('sh-live-indicator');
if (badge) {
if (this.autoRefresh) {
badge.style.background = '#22c55e';
badge.style.animation = 'pulse 2s infinite';
badge.innerHTML = '<span class="sh-live-dot" style="width: 8px; height: 8px; background: #fff; border-radius: 50%;"></span>LIVE';
} else {
badge.style.background = '#6b7280';
badge.style.animation = 'none';
badge.innerHTML = '⏸ PAUSED';
}
}
},
updateLiveIndicator: function(fetching) {
var badge = document.getElementById('sh-live-indicator');
if (badge && this.autoRefresh) {
if (fetching) {
badge.style.background = '#f59e0b';
} else {
badge.style.background = '#22c55e';
}
}
},
renderToggle: function(label, state, handler) {
return E('label', { 'class': 'sh-toggle' }, [
E('input', {
'type': 'checkbox',
'checked': state ? 'checked' : null,
'change': function(ev) {
handler(ev.target.checked);
}
}),
E('span', {}, label)
]);
},
renderBody: function() {
return E('div', { 'class': 'sh-logs-body' }, [
E('div', { 'class': 'sh-log-panel' }, [
this.renderSeverityTabs(),
E('div', { 'class': 'sh-log-stream', 'id': 'sh-log-stream' })
]),
E('div', { 'class': 'sh-log-side' }, [
E('h3', {}, _('Statistics (recent)')),
E('ul', { 'id': 'sh-log-metrics' },
this.buildMetrics().map(function(metric) {
return E('li', {}, [
E('span', {}, metric.label),
E('strong', {}, metric.value)
]);
}))
])
]);
},
renderSeverityTabs: function() {
var self = this;
var filters = [
{ id: 'all', label: _('All') },
{ id: 'error', label: _('Errors') },
{ id: 'warning', label: _('Warnings') },
{ id: 'info', label: _('Info') }
];
return E('div', { 'class': 'sh-log-filters cyber-tablist cyber-tablist--pills', 'id': 'sh-log-filters' },
filters.map(function(filter) {
return E('button', {
'type': 'button',
'class': 'cyber-tab cyber-tab--pill' + (self.severityFilter === filter.id ? ' is-active' : ''),
'data-filter': filter.id,
'click': function() {
self.severityFilter = filter.id;
self.updateLogStream();
self.refreshSeverityTabs();
}
}, filter.label);
}));
},
refreshSeverityTabs: function() {
var tabs = document.querySelectorAll('#sh-log-filters .cyber-tab');
tabs.forEach(function(tab) {
var match = tab.getAttribute('data-filter') === this.severityFilter;
tab.classList.toggle('is-active', match);
}, this);
},
getFilteredLogs: function() {
return this.logs.filter(function(line) {
if (!line) return false;
var severity = this.detectSeverity(line);
var matchesSeverity = this.severityFilter === 'all' || severity === this.severityFilter;
var matchesSearch = !this.searchQuery || line.toLowerCase().indexOf(this.searchQuery) !== -1;
return matchesSeverity && matchesSearch;
}, this);
},
updateLogStream: function(hasNewLogs) {
var container = document.getElementById('sh-log-stream');
if (!container) return;
var filtered = this.getFilteredLogs();
var totalLines = filtered.length;
var frag = filtered.map(function(line, idx) {
var severity = this.detectSeverity(line);
var isNew = hasNewLogs && idx >= totalLines - 5;
return E('div', {
'class': 'sh-log-line ' + severity + (isNew ? ' sh-log-new' : ''),
'style': isNew ? 'animation: fadeIn 0.5s ease;' : ''
}, [
E('span', { 'class': 'sh-log-index' }, idx + 1),
E('span', { 'class': 'sh-log-message' }, line)
]);
}, this);
dom.content(container, frag);
if (this.autoScroll) {
container.scrollTop = container.scrollHeight;
}
var badge = document.getElementById('sh-log-total');
if (badge) badge.querySelector('.value').textContent = filtered.length;
},
updateStats: function() {
var stats = {
'sh-log-total': this.logs.length,
'sh-log-errors': this.countSeverity('error'),
'sh-log-warnings': this.countSeverity('warning')
};
Object.keys(stats).forEach(function(id) {
var node = document.getElementById(id);
if (node) {
var span = node.querySelector('.value');
if (span) span.textContent = stats[id];
}
});
var list = document.getElementById('sh-log-metrics');
if (list) {
dom.content(list, this.buildMetrics().map(function(metric) {
return E('li', {}, [E('span', {}, metric.label), E('strong', {}, metric.value)]);
}));
}
},
buildMetrics: function() {
var metrics = [
{ label: _('Critical events (last refresh)'), value: this.countSeverity('error') },
{ label: _('Warnings'), value: this.countSeverity('warning') },
{ label: _('Info/Debug'), value: this.countSeverity('info') },
{ label: _('Matched search'), value: this.getFilteredLogs().length }
];
// Add denoising metrics when not in RAW mode
if (this.denoiseMode !== 'RAW') {
metrics.push({ label: _('Noise ratio'), value: this.noiseRatio + '%' });
metrics.push({ label: _('Filtered entries'), value: this.filteredLines });
}
return metrics;
},
refreshLogs: function() {
var self = this;
ui.showModal(_('Loading logs...'), [
E('p', { 'class': 'spinning' }, _('Fetching system logs'))
]);
return API.getDenoisedLogs(this.lineCount, '', this.denoiseMode).then(function(result) {
ui.hideModal();
self.logs = result.logs || [];
self.noiseRatio = result.noise_ratio || 0;
self.filteredLines = result.filtered_lines || 0;
self.totalLines = result.total_lines || self.logs.length;
self.updateStats();
self.updateLogStream();
self.updateDenoiseIndicator();
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, err.message || err), 'error');
});
},
detectSeverity: function(line) {
if (!line) return 'info';
var lc = line.toLowerCase();
if (lc.indexOf('error') !== -1 || lc.indexOf('fatal') !== -1 || lc.indexOf('crit') !== -1) return 'error';
if (lc.indexOf('warn') !== -1 || lc.indexOf('notice') !== -1) return 'warning';
return 'info';
},
countSeverity: function(level) {
return this.logs.filter(function(line) {
return this.detectSeverity(line) === level;
}, this).length;
},
downloadLogs: function() {
var blob = new Blob([this.getFilteredLogs().join('\n')], { type: 'text/plain' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'system-logs.txt';
a.click();
URL.revokeObjectURL(url);
},
updateLogStreamDebounced: function() {
clearTimeout(this._updateTimer);
var self = this;
this._updateTimer = setTimeout(function() {
self.updateLogStream();
}, 200);
}
});