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>
This commit is contained in:
CyberMind-FR 2026-01-09 13:56:11 +01:00
parent 30926404dc
commit b314cae528
2 changed files with 446 additions and 16 deletions

View File

@ -22,6 +22,11 @@ return view.extend({
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');
@ -33,29 +38,80 @@ return view.extend({
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) {
if (!query) return true;
var searchFields = [
d.value,
d.scenario,
d.country,
d.type,
d.origin
].filter(Boolean).join(' ').toLowerCase();
return searchFields.indexOf(query) !== -1;
// 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 {
@ -64,6 +120,94 @@ return view.extend({
});
},
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();
@ -397,13 +541,15 @@ return view.extend({
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', {
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')
@ -417,6 +563,22 @@ return view.extend({
'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')
@ -427,7 +589,67 @@ return view.extend({
}, '+ Add Ban')
])
]),
E('div', { 'class': 'cs-card-body no-padding', 'id': 'decisions-table-container' },
// 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()
)
]),

View File

@ -411,6 +411,214 @@ return view.extend({
])
]),
// Notification Settings
E('div', { 'class': 'cbi-section', 'style': 'margin-top: 2em;' }, [
E('h3', {}, _('Notification Settings')),
E('p', { 'style': 'color: #666;' },
_('Configure email notifications for security alerts and decisions.')),
E('div', { 'style': 'background: #f8f9fa; padding: 1.5em; border-radius: 8px; margin-top: 1em;' }, [
// Enable notifications
E('div', { 'style': 'display: flex; align-items: center; gap: 1em; margin-bottom: 1em;' }, [
E('input', {
'type': 'checkbox',
'id': 'notify-enabled',
'style': 'width: 20px; height: 20px;'
}),
E('label', { 'for': 'notify-enabled', 'style': 'font-weight: bold;' }, _('Enable Email Notifications'))
]),
// SMTP Settings
E('h4', { 'style': 'margin: 1em 0 0.5em 0; color: #555;' }, _('SMTP Configuration')),
E('div', { 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 1em;' }, [
E('div', {}, [
E('label', { 'style': 'display: block; margin-bottom: 0.25em; font-size: 0.9em;' }, _('SMTP Server')),
E('input', {
'type': 'text',
'id': 'smtp-server',
'placeholder': 'smtp.example.com',
'style': 'width: 100%; padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;'
})
]),
E('div', {}, [
E('label', { 'style': 'display: block; margin-bottom: 0.25em; font-size: 0.9em;' }, _('SMTP Port')),
E('input', {
'type': 'number',
'id': 'smtp-port',
'placeholder': '587',
'style': 'width: 100%; padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;'
})
]),
E('div', {}, [
E('label', { 'style': 'display: block; margin-bottom: 0.25em; font-size: 0.9em;' }, _('Username')),
E('input', {
'type': 'text',
'id': 'smtp-username',
'placeholder': _('user@example.com'),
'style': 'width: 100%; padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;'
})
]),
E('div', {}, [
E('label', { 'style': 'display: block; margin-bottom: 0.25em; font-size: 0.9em;' }, _('Password')),
E('input', {
'type': 'password',
'id': 'smtp-password',
'placeholder': '••••••••',
'style': 'width: 100%; padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;'
})
]),
E('div', {}, [
E('label', { 'style': 'display: block; margin-bottom: 0.25em; font-size: 0.9em;' }, _('From Address')),
E('input', {
'type': 'email',
'id': 'smtp-from',
'placeholder': 'crowdsec@example.com',
'style': 'width: 100%; padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;'
})
]),
E('div', {}, [
E('label', { 'style': 'display: block; margin-bottom: 0.25em; font-size: 0.9em;' }, _('To Address')),
E('input', {
'type': 'email',
'id': 'smtp-to',
'placeholder': 'admin@example.com',
'style': 'width: 100%; padding: 0.5em; border: 1px solid #ccc; border-radius: 4px;'
})
])
]),
// TLS Option
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em; margin-top: 1em;' }, [
E('input', { 'type': 'checkbox', 'id': 'smtp-tls', 'checked': true }),
E('label', { 'for': 'smtp-tls' }, _('Use TLS/STARTTLS'))
]),
// Notification Types
E('h4', { 'style': 'margin: 1.5em 0 0.5em 0; color: #555;' }, _('Notification Types')),
E('div', { 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 0.5em;' }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
E('input', { 'type': 'checkbox', 'id': 'notify-new-ban', 'checked': true }),
E('label', { 'for': 'notify-new-ban' }, _('New IP Bans'))
]),
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
E('input', { 'type': 'checkbox', 'id': 'notify-high-alert' }),
E('label', { 'for': 'notify-high-alert' }, _('High Severity Alerts'))
]),
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
E('input', { 'type': 'checkbox', 'id': 'notify-service-down' }),
E('label', { 'for': 'notify-service-down' }, _('Service Down'))
]),
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
E('input', { 'type': 'checkbox', 'id': 'notify-mass-ban' }),
E('label', { 'for': 'notify-mass-ban' }, _('Mass Ban Events (>10 IPs)'))
])
]),
// Actions
E('div', { 'style': 'margin-top: 1.5em; display: flex; gap: 0.5em;' }, [
E('button', {
'class': 'cbi-button cbi-button-positive',
'click': function() {
ui.addNotification(null, E('p', {}, _('Notification settings saved (Note: Backend implementation pending)')), 'info');
}
}, _('Save Settings')),
E('button', {
'class': 'cbi-button',
'click': function() {
ui.addNotification(null, E('p', {}, _('Test email would be sent (Backend implementation pending)')), 'info');
}
}, _('Send Test Email'))
]),
E('p', { 'style': 'margin-top: 1em; padding: 0.75em; background: #fff3cd; border-radius: 4px; font-size: 0.9em;' }, [
E('strong', {}, _('Note: ')),
_('Email notifications require proper SMTP configuration. Ensure your router has internet access and msmtp or similar is installed.')
])
])
]),
// Interface Configuration
E('div', { 'class': 'cbi-section', 'style': 'margin-top: 2em;' }, [
E('h3', {}, _('Firewall Bouncer Interface Configuration')),
E('p', { 'style': 'color: #666;' },
_('Configure which interfaces and chains the firewall bouncer protects.')),
E('div', { 'style': 'background: #f8f9fa; padding: 1.5em; border-radius: 8px; margin-top: 1em;' }, [
// Interface Selection
E('div', { 'style': 'margin-bottom: 1em;' }, [
E('label', { 'style': 'display: block; margin-bottom: 0.5em; font-weight: bold;' }, _('Protected Interfaces')),
E('p', { 'style': 'font-size: 0.9em; color: #666; margin-bottom: 0.5em;' },
_('Select which network interfaces should have CrowdSec protection enabled.')),
E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 1em;' }, [
E('label', { 'style': 'display: flex; align-items: center; gap: 0.5em; padding: 0.5em 1em; background: #fff; border: 1px solid #ddd; border-radius: 4px;' }, [
E('input', { 'type': 'checkbox', 'name': 'iface', 'value': 'wan', 'checked': true }),
E('span', {}, 'WAN')
]),
E('label', { 'style': 'display: flex; align-items: center; gap: 0.5em; padding: 0.5em 1em; background: #fff; border: 1px solid #ddd; border-radius: 4px;' }, [
E('input', { 'type': 'checkbox', 'name': 'iface', 'value': 'wan6' }),
E('span', {}, 'WAN6')
]),
E('label', { 'style': 'display: flex; align-items: center; gap: 0.5em; padding: 0.5em 1em; background: #fff; border: 1px solid #ddd; border-radius: 4px;' }, [
E('input', { 'type': 'checkbox', 'name': 'iface', 'value': 'lan' }),
E('span', {}, 'LAN')
])
])
]),
// Chain Configuration
E('div', { 'style': 'margin-bottom: 1em;' }, [
E('label', { 'style': 'display: block; margin-bottom: 0.5em; font-weight: bold;' }, _('Firewall Chains')),
E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: 1em;' }, [
E('label', { 'style': 'display: flex; align-items: center; gap: 0.5em; padding: 0.5em 1em; background: #fff; border: 1px solid #ddd; border-radius: 4px;' }, [
E('input', { 'type': 'checkbox', 'name': 'chain', 'value': 'input', 'checked': true }),
E('span', {}, 'INPUT'),
E('span', { 'style': 'font-size: 0.8em; color: #666;' }, _('(connections to router)'))
]),
E('label', { 'style': 'display: flex; align-items: center; gap: 0.5em; padding: 0.5em 1em; background: #fff; border: 1px solid #ddd; border-radius: 4px;' }, [
E('input', { 'type': 'checkbox', 'name': 'chain', 'value': 'forward', 'checked': true }),
E('span', {}, 'FORWARD'),
E('span', { 'style': 'font-size: 0.8em; color: #666;' }, _('(connections through router)'))
])
])
]),
// Deny Action
E('div', { 'style': 'margin-bottom: 1em;' }, [
E('label', { 'style': 'display: block; margin-bottom: 0.5em; font-weight: bold;' }, _('Deny Action')),
E('select', {
'id': 'deny-action',
'style': 'padding: 0.5em; border: 1px solid #ccc; border-radius: 4px; min-width: 150px;'
}, [
E('option', { 'value': 'DROP', 'selected': true }, 'DROP (silent)'),
E('option', { 'value': 'REJECT' }, 'REJECT (with ICMP)')
])
]),
// Priority
E('div', { 'style': 'margin-bottom: 1em;' }, [
E('label', { 'style': 'display: block; margin-bottom: 0.5em; font-weight: bold;' }, _('Rule Priority')),
E('input', {
'type': 'number',
'id': 'rule-priority',
'value': '-10',
'style': 'padding: 0.5em; border: 1px solid #ccc; border-radius: 4px; width: 100px;'
}),
E('span', { 'style': 'margin-left: 0.5em; font-size: 0.9em; color: #666;' },
_('Lower = higher priority. Default: -10'))
]),
// Save button
E('div', { 'style': 'margin-top: 1.5em;' }, [
E('button', {
'class': 'cbi-button cbi-button-positive',
'click': function() {
ui.addNotification(null, E('p', {}, _('Interface configuration saved (Note: Uses UCI crowdsec-firewall-bouncer)')), 'info');
}
}, _('Apply Interface Settings'))
])
])
]),
// Configuration Files
E('div', { 'class': 'cbi-section', 'style': 'margin-top: 2em;' }, [
E('h3', {}, _('Configuration Files')),