secubox-openwrt/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/decisions.js
CyberMind-FR b314cae528 feat(crowdsec): Add notification settings, interface config, and advanced filtering
Settings View:
- Add notification settings section with SMTP configuration
- Add notification type checkboxes (new bans, alerts, service down, mass events)
- Add firewall bouncer interface configuration (WAN/WAN6/LAN selection)
- Add firewall chain configuration (INPUT/FORWARD)
- Add deny action selector (DROP/REJECT)

Decisions View:
- Add advanced filtering panel with type, duration, and country filters
- Add export to CSV functionality
- Add filter badge showing active filter count
- Add clear filters button
- Enhanced duration parsing for better filtering

These changes align with the OpenWrt CrowdSec guide for proper
configuration management.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 13:56:11 +01:00

683 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
'require view';
'require secubox-theme/theme as Theme';
'require dom';
'require poll';
'require ui';
'require crowdsec-dashboard/api as api';
'require crowdsec-dashboard/nav as CsNav';
/**
* CrowdSec Dashboard - Decisions View
* Detailed view and management of all active decisions
* Copyright (C) 2024 CyberMind.fr - Gandalf
*/
return view.extend({
title: _('Decisions'),
csApi: null,
decisions: [],
filteredDecisions: [],
searchQuery: '',
sortField: 'value',
sortOrder: 'asc',
// Advanced filters
filterType: 'all', // all, ban, captcha
filterDuration: 'all', // all, short (<1h), medium (1-24h), long (>24h), permanent
filterCountry: 'all', // all, or specific country code
showFilters: false,
load: function() {
var cssLink = document.createElement('link');
cssLink.rel = 'stylesheet';
cssLink.href = L.resource('crowdsec-dashboard/dashboard.css');
document.head.appendChild(cssLink);
this.csApi = api;
return this.csApi.getDecisions();
},
parseDurationToSeconds: function(duration) {
if (!duration) return 0;
var match = duration.match(/^(\d+)(h|m|s)?$/);
if (!match) {
// Try ISO 8601 duration
var hours = 0;
var hoursMatch = duration.match(/(\d+)h/i);
if (hoursMatch) hours = parseInt(hoursMatch[1]);
var minsMatch = duration.match(/(\d+)m/i);
if (minsMatch) hours += parseInt(minsMatch[1]) / 60;
return hours * 3600;
}
var value = parseInt(match[1]);
var unit = match[2] || 's';
if (unit === 'h') return value * 3600;
if (unit === 'm') return value * 60;
return value;
},
filterDecisions: function() {
var self = this;
var query = this.searchQuery.toLowerCase();
this.filteredDecisions = this.decisions.filter(function(d) {
// Text search filter
if (query) {
var searchFields = [
d.value,
d.scenario,
d.country,
d.type,
d.origin
].filter(Boolean).join(' ').toLowerCase();
if (searchFields.indexOf(query) === -1) return false;
}
// Type filter
if (self.filterType !== 'all') {
if ((d.type || 'ban').toLowerCase() !== self.filterType) return false;
}
// Country filter
if (self.filterCountry !== 'all') {
if ((d.country || '').toUpperCase() !== self.filterCountry) return false;
}
// Duration filter
if (self.filterDuration !== 'all') {
var durationSecs = self.parseDurationToSeconds(d.duration);
switch (self.filterDuration) {
case 'short': // < 1 hour
if (durationSecs >= 3600) return false;
break;
case 'medium': // 1-24 hours
if (durationSecs < 3600 || durationSecs >= 86400) return false;
break;
case 'long': // > 24 hours
if (durationSecs < 86400) return false;
break;
case 'permanent': // > 7 days or explicit permanent
if (durationSecs < 604800 && d.duration !== 'permanent') return false;
break;
}
}
return true;
});
// Sort
this.filteredDecisions.sort(function(a, b) {
var aVal = a[self.sortField] || '';
var bVal = b[self.sortField] || '';
if (self.sortOrder === 'asc') {
return aVal.localeCompare(bVal);
} else {
return bVal.localeCompare(aVal);
}
});
},
getUniqueCountries: function() {
var countries = {};
this.decisions.forEach(function(d) {
if (d.country) {
countries[d.country.toUpperCase()] = true;
}
});
return Object.keys(countries).sort();
},
handleFilterChange: function(filterName, value, ev) {
this[filterName] = value;
this.filterDecisions();
this.updateTable();
this.updateFilterBadge();
},
toggleFilters: function() {
this.showFilters = !this.showFilters;
var panel = document.getElementById('advanced-filters');
if (panel) {
panel.style.display = this.showFilters ? 'block' : 'none';
}
},
clearFilters: function() {
this.filterType = 'all';
this.filterDuration = 'all';
this.filterCountry = 'all';
this.searchQuery = '';
var searchInput = document.querySelector('.cs-search-box input');
if (searchInput) searchInput.value = '';
this.filterDecisions();
this.updateTable();
this.updateFilterBadge();
this.updateFilterSelects();
},
updateFilterSelects: function() {
var typeSelect = document.getElementById('filter-type');
var durationSelect = document.getElementById('filter-duration');
var countrySelect = document.getElementById('filter-country');
if (typeSelect) typeSelect.value = this.filterType;
if (durationSelect) durationSelect.value = this.filterDuration;
if (countrySelect) countrySelect.value = this.filterCountry;
},
updateFilterBadge: function() {
var count = 0;
if (this.filterType !== 'all') count++;
if (this.filterDuration !== 'all') count++;
if (this.filterCountry !== 'all') count++;
var badge = document.getElementById('filter-badge');
if (badge) {
badge.textContent = count;
badge.style.display = count > 0 ? 'inline-block' : 'none';
}
},
exportToCSV: function() {
var self = this;
var csv = 'IP Address,Scenario,Country,Type,Duration,Origin,Created\n';
this.filteredDecisions.forEach(function(d) {
csv += [
d.value || '',
(d.scenario || '').replace(/,/g, ';'),
d.country || '',
d.type || 'ban',
d.duration || '',
d.origin || 'crowdsec',
d.created_at || ''
].join(',') + '\n';
});
var blob = new Blob([csv], { type: 'text/csv' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'crowdsec-decisions-' + new Date().toISOString().slice(0, 10) + '.csv';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.showToast('Exported ' + this.filteredDecisions.length + ' decisions to CSV', 'success');
},
handleSearch: function(ev) {
this.searchQuery = ev.target.value;
this.filterDecisions();
this.updateTable();
},
handleSort: function(field, ev) {
if (this.sortField === field) {
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
} else {
this.sortField = field;
this.sortOrder = 'asc';
}
this.filterDecisions();
this.updateTable();
},
handleUnban: function(ip, ev) {
var self = this;
if (!confirm('Remove ban for ' + ip + '?')) {
return;
}
this.csApi.unbanIP(ip).then(function(result) {
if (result.success) {
self.showToast('IP ' + ip + ' unbanned successfully', 'success');
return self.csApi.getDecisions();
} else {
self.showToast('Failed to unban: ' + (result.error || 'Unknown error'), 'error');
return null;
}
}).then(function(data) {
if (data) {
// Flatten alerts->decisions structure
self.decisions = [];
if (Array.isArray(data)) {
data.forEach(function(alert) {
if (alert.decisions && Array.isArray(alert.decisions)) {
self.decisions = self.decisions.concat(alert.decisions);
}
});
}
self.filterDecisions();
self.updateTable();
}
}).catch(function(err) {
self.showToast('Error: ' + err.message, 'error');
});
},
handleBulkUnban: function(ev) {
var self = this;
var checkboxes = document.querySelectorAll('.cs-decision-checkbox:checked');
if (checkboxes.length === 0) {
self.showToast('No decisions selected', 'error');
return;
}
if (!confirm('Remove ban for ' + checkboxes.length + ' IP(s)?')) {
return;
}
var promises = [];
checkboxes.forEach(function(cb) {
promises.push(self.csApi.unbanIP(cb.dataset.ip));
});
Promise.all(promises).then(function(results) {
var success = results.filter(function(r) { return r.success; }).length;
var failed = results.length - success;
if (success > 0) {
self.showToast(success + ' IP(s) unbanned' + (failed > 0 ? ', ' + failed + ' failed' : ''),
failed > 0 ? 'warning' : 'success');
} else {
self.showToast('Failed to unban IPs', 'error');
}
return self.csApi.getDecisions();
}).then(function(data) {
if (data) {
// Flatten alerts->decisions structure
self.decisions = [];
if (Array.isArray(data)) {
data.forEach(function(alert) {
if (alert.decisions && Array.isArray(alert.decisions)) {
self.decisions = self.decisions.concat(alert.decisions);
}
});
}
self.filterDecisions();
self.updateTable();
}
});
},
handleSelectAll: function(ev) {
var checked = ev.target.checked;
document.querySelectorAll('.cs-decision-checkbox').forEach(function(cb) {
cb.checked = checked;
});
},
showToast: function(message, type) {
var existing = document.querySelector('.cs-toast');
if (existing) existing.remove();
var toast = E('div', { 'class': 'cs-toast ' + (type || '') }, message);
document.body.appendChild(toast);
setTimeout(function() { toast.remove(); }, 4000);
},
updateTable: function() {
var container = document.getElementById('decisions-table-container');
if (container) {
dom.content(container, this.renderTable());
}
var countEl = document.getElementById('decisions-count');
if (countEl) {
countEl.textContent = this.filteredDecisions.length + ' of ' + this.decisions.length + ' decisions';
}
},
renderSortIcon: function(field) {
if (this.sortField !== field) return ' ↕';
return this.sortOrder === 'asc' ? ' ↑' : ' ↓';
},
renderTable: function() {
var self = this;
if (this.filteredDecisions.length === 0) {
return E('div', { 'class': 'cs-empty' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
E('div', { 'class': 'cs-empty-icon' }, this.searchQuery ? '🔍' : '✅'),
E('p', {}, this.searchQuery ? 'No matching decisions found' : 'No active decisions')
]);
}
var rows = this.filteredDecisions.map(function(d, i) {
return E('tr', {}, [
E('td', {}, E('input', {
'type': 'checkbox',
'class': 'cs-decision-checkbox',
'data-ip': d.value
})),
E('td', {}, E('span', { 'class': 'cs-ip' }, d.value || 'N/A')),
E('td', {}, E('span', { 'class': 'cs-scenario' }, self.csApi.parseScenario(d.scenario))),
E('td', {}, E('span', { 'class': 'cs-country' }, [
E('span', { 'class': 'cs-country-flag' }, self.csApi.getCountryFlag(d.country)),
' ',
d.country || 'N/A'
])),
E('td', {}, d.origin || 'crowdsec'),
E('td', {}, E('span', { 'class': 'cs-action ' + (d.type || 'ban') }, d.type || 'ban')),
E('td', {}, E('span', { 'class': 'cs-time' }, self.csApi.formatDuration(d.duration))),
E('td', {}, E('span', { 'class': 'cs-time' }, self.csApi.formatRelativeTime(d.created_at))),
E('td', {}, E('button', {
'class': 'cs-btn cs-btn-danger cs-btn-sm',
'click': ui.createHandlerFn(self, 'handleUnban', d.value)
}, 'Unban'))
]);
});
return E('table', { 'class': 'cs-table' }, [
E('thead', {}, E('tr', {}, [
E('th', { 'style': 'width: 40px' }, E('input', {
'type': 'checkbox',
'id': 'select-all',
'change': ui.createHandlerFn(this, 'handleSelectAll')
})),
E('th', {
'click': ui.createHandlerFn(this, 'handleSort', 'value'),
'style': 'cursor: pointer'
}, 'IP Address' + this.renderSortIcon('value')),
E('th', {
'click': ui.createHandlerFn(this, 'handleSort', 'scenario'),
'style': 'cursor: pointer'
}, 'Scenario' + this.renderSortIcon('scenario')),
E('th', {
'click': ui.createHandlerFn(this, 'handleSort', 'country'),
'style': 'cursor: pointer'
}, 'Country' + this.renderSortIcon('country')),
E('th', {}, 'Origin'),
E('th', {}, 'Action'),
E('th', {}, 'Expires'),
E('th', {}, 'Created'),
E('th', {}, 'Actions')
])),
E('tbody', {}, rows)
]);
},
renderBanModal: function() {
return E('div', { 'class': 'cs-modal-overlay', 'id': 'ban-modal', 'style': 'display: none' }, [
E('div', { 'class': 'cs-modal' }, [
E('div', { 'class': 'cs-modal-header' }, [
E('div', { 'class': 'cs-modal-title' }, 'Add IP Ban'),
E('button', {
'class': 'cs-modal-close',
'click': ui.createHandlerFn(this, 'closeBanModal')
}, '×')
]),
E('div', { 'class': 'cs-modal-body' }, [
E('div', { 'class': 'cs-form-group' }, [
E('label', { 'class': 'cs-form-label' }, 'IP Address or Range'),
E('input', {
'class': 'cs-input',
'id': 'ban-ip',
'type': 'text',
'placeholder': '192.168.1.100 or 10.0.0.0/24'
})
]),
E('div', { 'class': 'cs-form-group' }, [
E('label', { 'class': 'cs-form-label' }, 'Duration'),
E('input', {
'class': 'cs-input',
'id': 'ban-duration',
'type': 'text',
'placeholder': '4h, 24h, 7d...',
'value': '4h'
})
]),
E('div', { 'class': 'cs-form-group' }, [
E('label', { 'class': 'cs-form-label' }, 'Reason'),
E('input', {
'class': 'cs-input',
'id': 'ban-reason',
'type': 'text',
'placeholder': 'Manual ban from dashboard'
})
])
]),
E('div', { 'class': 'cs-modal-footer' }, [
E('button', {
'class': 'cs-btn',
'click': ui.createHandlerFn(this, 'closeBanModal')
}, 'Cancel'),
E('button', {
'class': 'cs-btn cs-btn-primary',
'click': ui.createHandlerFn(this, 'submitBan')
}, 'Add Ban')
])
])
]);
},
openBanModal: function(ev) {
document.getElementById('ban-modal').style.display = 'flex';
},
closeBanModal: function(ev) {
document.getElementById('ban-modal').style.display = 'none';
document.getElementById('ban-ip').value = '';
document.getElementById('ban-duration').value = '4h';
document.getElementById('ban-reason').value = '';
},
submitBan: function(ev) {
var self = this;
var ip = document.getElementById('ban-ip').value.trim();
var duration = document.getElementById('ban-duration').value.trim() || '4h';
var reason = document.getElementById('ban-reason').value.trim() || 'Manual ban from dashboard';
if (!ip) {
self.showToast('Please enter an IP address', 'error');
return;
}
if (!self.csApi.isValidIP(ip)) {
self.showToast('Invalid IP address format', 'error');
return;
}
console.log('[Decisions] Banning IP:', ip, 'Duration:', duration, 'Reason:', reason);
self.csApi.banIP(ip, duration, reason).then(function(result) {
console.log('[Decisions] Ban result:', result);
if (result.success) {
self.showToast('IP ' + ip + ' banned for ' + duration, 'success');
self.closeBanModal();
// Wait 1 second for CrowdSec to process the decision
console.log('[Decisions] Waiting 1 second before refreshing...');
return new Promise(function(resolve) {
setTimeout(function() {
console.log('[Decisions] Refreshing decisions list...');
resolve(self.csApi.getDecisions());
}, 1000);
});
} else {
self.showToast('Failed to ban: ' + (result.error || 'Unknown error'), 'error');
return null;
}
}).then(function(data) {
console.log('[Decisions] Updated decisions data:', data);
if (data) {
// Flatten alerts->decisions structure
self.decisions = [];
if (Array.isArray(data)) {
data.forEach(function(alert) {
if (alert.decisions && Array.isArray(alert.decisions)) {
self.decisions = self.decisions.concat(alert.decisions);
}
});
}
self.filterDecisions();
self.updateTable();
console.log('[Decisions] Table updated with', self.decisions.length, 'decisions');
}
}).catch(function(err) {
console.error('[Decisions] Ban error:', err);
self.showToast('Error: ' + err.message, 'error');
});
},
render: function(data) {
var self = this;
// Flatten alerts->decisions structure
// data is an array of alerts, each containing a decisions array
this.decisions = [];
if (Array.isArray(data)) {
data.forEach(function(alert) {
if (alert.decisions && Array.isArray(alert.decisions)) {
self.decisions = self.decisions.concat(alert.decisions);
}
});
}
console.log('[Decisions] Flattened', this.decisions.length, 'decisions from', data ? data.length : 0, 'alerts');
this.filterDecisions();
var countries = this.getUniqueCountries();
var view = E('div', { 'class': 'crowdsec-dashboard' }, [
CsNav.renderTabs('decisions'),
E('div', { 'class': 'cs-card' }, [
E('div', { 'class': 'cs-card-header' }, [
E('div', { 'class': 'cs-card-title' }, [
'Active Decisions',
E('span', {
'id': 'decisions-count',
'style': 'font-weight: normal; margin-left: 12px; font-size: 12px; color: var(--cs-text-muted)'
}, this.filteredDecisions.length + ' of ' + this.decisions.length + ' decisions')
]),
E('div', { 'class': 'cs-actions-bar' }, [
E('div', { 'class': 'cs-search-box' }, [
E('input', {
'class': 'cs-input',
'type': 'text',
'placeholder': 'Search IP, scenario, country...',
'input': ui.createHandlerFn(this, 'handleSearch')
})
]),
E('button', {
'class': 'cs-btn',
'style': 'position: relative;',
'click': ui.createHandlerFn(this, 'toggleFilters')
}, [
'Filters ',
E('span', {
'id': 'filter-badge',
'style': 'display: none; background: #dc3545; color: white; padding: 2px 6px; border-radius: 10px; font-size: 10px; position: absolute; top: -5px; right: -5px;'
}, '0')
]),
E('button', {
'class': 'cs-btn',
'click': ui.createHandlerFn(this, 'exportToCSV'),
'title': 'Export to CSV'
}, 'Export CSV'),
E('button', {
'class': 'cs-btn cs-btn-danger',
'click': ui.createHandlerFn(this, 'handleBulkUnban')
}, 'Unban Selected'),
E('button', {
'class': 'cs-btn cs-btn-primary',
'click': ui.createHandlerFn(this, 'openBanModal')
}, '+ Add Ban')
])
]),
// Advanced Filters Panel
E('div', {
'id': 'advanced-filters',
'style': 'display: none; padding: 1em; background: #f8f9fa; border-bottom: 1px solid #ddd;'
}, [
E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 1em; align-items: flex-end;' }, [
E('div', {}, [
E('label', { 'style': 'display: block; font-size: 0.85em; margin-bottom: 4px; color: #666;' }, _('Action Type')),
E('select', {
'id': 'filter-type',
'class': 'cs-input',
'style': 'min-width: 120px;',
'change': function(ev) {
self.handleFilterChange('filterType', ev.target.value);
}
}, [
E('option', { 'value': 'all' }, 'All Types'),
E('option', { 'value': 'ban' }, 'Ban'),
E('option', { 'value': 'captcha' }, 'Captcha')
])
]),
E('div', {}, [
E('label', { 'style': 'display: block; font-size: 0.85em; margin-bottom: 4px; color: #666;' }, _('Duration')),
E('select', {
'id': 'filter-duration',
'class': 'cs-input',
'style': 'min-width: 140px;',
'change': function(ev) {
self.handleFilterChange('filterDuration', ev.target.value);
}
}, [
E('option', { 'value': 'all' }, 'All Durations'),
E('option', { 'value': 'short' }, '< 1 hour'),
E('option', { 'value': 'medium' }, '1-24 hours'),
E('option', { 'value': 'long' }, '> 24 hours'),
E('option', { 'value': 'permanent' }, 'Permanent (>7d)')
])
]),
E('div', {}, [
E('label', { 'style': 'display: block; font-size: 0.85em; margin-bottom: 4px; color: #666;' }, _('Country')),
E('select', {
'id': 'filter-country',
'class': 'cs-input',
'style': 'min-width: 140px;',
'change': function(ev) {
self.handleFilterChange('filterCountry', ev.target.value);
}
}, [
E('option', { 'value': 'all' }, 'All Countries')
].concat(countries.map(function(c) {
return E('option', { 'value': c }, self.csApi.getCountryFlag(c) + ' ' + c);
})))
]),
E('button', {
'class': 'cs-btn',
'style': 'margin-left: auto;',
'click': ui.createHandlerFn(this, 'clearFilters')
}, 'Clear Filters')
])
]),
E('div', { 'class': 'cs-card-body no-padding', 'id': 'decisions-table-container' },
this.renderTable()
)
]),
this.renderBanModal()
]);
// Setup polling
poll.add(function() {
return self.csApi.getDecisions().then(function(newData) {
// Flatten alerts->decisions structure
self.decisions = [];
if (Array.isArray(newData)) {
newData.forEach(function(alert) {
if (alert.decisions && Array.isArray(alert.decisions)) {
self.decisions = self.decisions.concat(alert.decisions);
}
});
}
self.filterDecisions();
self.updateTable();
});
}, 30);
return view;
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});