feat(local-build): Add sync command and clean local-feed
- Add sync command to synchronize packages from package/secubox to local-feed - Add local-feed deletion to clean-all command - Add missing packages to package/secubox: - luci-app-secubox-crowdsec - secubox-crowdsec-setup Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b5567ff747
commit
e6835828af
70
package/secubox/luci-app-secubox-crowdsec/Makefile
Normal file
70
package/secubox/luci-app-secubox-crowdsec/Makefile
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
#
|
||||||
|
# LuCI SecuBox CrowdSec Dashboard
|
||||||
|
# Copyright (C) 2025 CyberMind.fr - Gandalf <gandalf@gk2.net>
|
||||||
|
#
|
||||||
|
|
||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NAME:=luci-app-secubox-crowdsec
|
||||||
|
PKG_VERSION:=1.0.0
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
|
LUCI_TITLE:=LuCI SecuBox CrowdSec Dashboard
|
||||||
|
LUCI_DEPENDS:=+luci-base +crowdsec +crowdsec-firewall-bouncer
|
||||||
|
LUCI_PKGARCH:=all
|
||||||
|
|
||||||
|
PKG_MAINTAINER:=Gerald Kerma <gandalf@gk2.net>
|
||||||
|
PKG_LICENSE:=MIT
|
||||||
|
|
||||||
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
|
define Package/luci-app-secubox-crowdsec/conffiles
|
||||||
|
/etc/config/crowdsec
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/luci-app-secubox-crowdsec/install
|
||||||
|
# RPCD backend
|
||||||
|
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||||
|
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.secubox-crowdsec $(1)/usr/libexec/rpcd/
|
||||||
|
|
||||||
|
# ACL permissions
|
||||||
|
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||||
|
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-secubox-crowdsec.json $(1)/usr/share/rpcd/acl.d/
|
||||||
|
|
||||||
|
# LuCI menu
|
||||||
|
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||||
|
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-secubox-crowdsec.json $(1)/usr/share/luci/menu.d/
|
||||||
|
|
||||||
|
# JavaScript API module
|
||||||
|
$(INSTALL_DIR) $(1)/www/luci-static/resources/secubox-crowdsec
|
||||||
|
$(INSTALL_DATA) ./htdocs/luci-static/resources/secubox-crowdsec/api.js $(1)/www/luci-static/resources/secubox-crowdsec/
|
||||||
|
|
||||||
|
# JavaScript views
|
||||||
|
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/secubox-crowdsec
|
||||||
|
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/secubox-crowdsec/dashboard.js $(1)/www/luci-static/resources/view/secubox-crowdsec/
|
||||||
|
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/secubox-crowdsec/decisions.js $(1)/www/luci-static/resources/view/secubox-crowdsec/
|
||||||
|
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/secubox-crowdsec/alerts.js $(1)/www/luci-static/resources/view/secubox-crowdsec/
|
||||||
|
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/secubox-crowdsec/collections.js $(1)/www/luci-static/resources/view/secubox-crowdsec/
|
||||||
|
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/secubox-crowdsec/settings.js $(1)/www/luci-static/resources/view/secubox-crowdsec/
|
||||||
|
|
||||||
|
# UCI default config
|
||||||
|
$(INSTALL_DIR) $(1)/etc/config
|
||||||
|
$(INSTALL_CONF) ./root/etc/config/crowdsec $(1)/etc/config/crowdsec
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/luci-app-secubox-crowdsec/postinst
|
||||||
|
#!/bin/sh
|
||||||
|
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||||
|
# Restart rpcd to load the new backend
|
||||||
|
/etc/init.d/rpcd restart
|
||||||
|
# Clear LuCI cache
|
||||||
|
rm -rf /tmp/luci-modulecache /tmp/luci-indexcache 2>/dev/null
|
||||||
|
echo "SecuBox CrowdSec Dashboard installed."
|
||||||
|
echo "Access via Services -> CrowdSec in LuCI."
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
|
endef
|
||||||
|
|
||||||
|
# call BuildPackage - OpenWrt buildroot
|
||||||
|
$(eval $(call BuildPackage,luci-app-secubox-crowdsec))
|
||||||
@ -0,0 +1,191 @@
|
|||||||
|
'use strict';
|
||||||
|
'require rpc';
|
||||||
|
|
||||||
|
var callStatus = rpc.declare({
|
||||||
|
object: 'luci.secubox-crowdsec',
|
||||||
|
method: 'status',
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callDecisions = rpc.declare({
|
||||||
|
object: 'luci.secubox-crowdsec',
|
||||||
|
method: 'decisions',
|
||||||
|
expect: { decisions: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callAddDecision = rpc.declare({
|
||||||
|
object: 'luci.secubox-crowdsec',
|
||||||
|
method: 'add_decision',
|
||||||
|
params: ['ip', 'duration', 'reason', 'type'],
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callDeleteDecision = rpc.declare({
|
||||||
|
object: 'luci.secubox-crowdsec',
|
||||||
|
method: 'delete_decision',
|
||||||
|
params: ['ip', 'decision_id'],
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callAlerts = rpc.declare({
|
||||||
|
object: 'luci.secubox-crowdsec',
|
||||||
|
method: 'alerts',
|
||||||
|
params: ['limit', 'since'],
|
||||||
|
expect: { alerts: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callMetrics = rpc.declare({
|
||||||
|
object: 'luci.secubox-crowdsec',
|
||||||
|
method: 'metrics',
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callStats = rpc.declare({
|
||||||
|
object: 'luci.secubox-crowdsec',
|
||||||
|
method: 'stats',
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callCollections = rpc.declare({
|
||||||
|
object: 'luci.secubox-crowdsec',
|
||||||
|
method: 'collections',
|
||||||
|
expect: { collections: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callInstallCollection = rpc.declare({
|
||||||
|
object: 'luci.secubox-crowdsec',
|
||||||
|
method: 'install_collection',
|
||||||
|
params: ['collection'],
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callRemoveCollection = rpc.declare({
|
||||||
|
object: 'luci.secubox-crowdsec',
|
||||||
|
method: 'remove_collection',
|
||||||
|
params: ['collection'],
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callUpdateHub = rpc.declare({
|
||||||
|
object: 'luci.secubox-crowdsec',
|
||||||
|
method: 'update_hub',
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callUpgradeHub = rpc.declare({
|
||||||
|
object: 'luci.secubox-crowdsec',
|
||||||
|
method: 'upgrade_hub',
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callBouncers = rpc.declare({
|
||||||
|
object: 'luci.secubox-crowdsec',
|
||||||
|
method: 'bouncers',
|
||||||
|
expect: { bouncers: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callControlService = rpc.declare({
|
||||||
|
object: 'luci.secubox-crowdsec',
|
||||||
|
method: 'control_service',
|
||||||
|
params: ['service', 'action'],
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callNftablesStats = rpc.declare({
|
||||||
|
object: 'luci.secubox-crowdsec',
|
||||||
|
method: 'nftables_stats',
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callBlockedIps = rpc.declare({
|
||||||
|
object: 'luci.secubox-crowdsec',
|
||||||
|
method: 'blocked_ips',
|
||||||
|
expect: { ipv4: [], ipv6: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callConfig = rpc.declare({
|
||||||
|
object: 'luci.secubox-crowdsec',
|
||||||
|
method: 'config',
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callSaveConfig = rpc.declare({
|
||||||
|
object: 'luci.secubox-crowdsec',
|
||||||
|
method: 'save_config',
|
||||||
|
params: ['key', 'value'],
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
|
return L.Class.extend({
|
||||||
|
getStatus: function() {
|
||||||
|
return callStatus();
|
||||||
|
},
|
||||||
|
|
||||||
|
getDecisions: function() {
|
||||||
|
return callDecisions();
|
||||||
|
},
|
||||||
|
|
||||||
|
addDecision: function(ip, duration, reason, type) {
|
||||||
|
return callAddDecision(ip, duration || '24h', reason || 'Manual ban via LuCI', type || 'ban');
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteDecision: function(ip, decisionId) {
|
||||||
|
return callDeleteDecision(ip, decisionId);
|
||||||
|
},
|
||||||
|
|
||||||
|
getAlerts: function(limit, since) {
|
||||||
|
return callAlerts(limit || 50, since || '24h');
|
||||||
|
},
|
||||||
|
|
||||||
|
getMetrics: function() {
|
||||||
|
return callMetrics();
|
||||||
|
},
|
||||||
|
|
||||||
|
getStats: function() {
|
||||||
|
return callStats();
|
||||||
|
},
|
||||||
|
|
||||||
|
getCollections: function() {
|
||||||
|
return callCollections();
|
||||||
|
},
|
||||||
|
|
||||||
|
installCollection: function(collection) {
|
||||||
|
return callInstallCollection(collection);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeCollection: function(collection) {
|
||||||
|
return callRemoveCollection(collection);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateHub: function() {
|
||||||
|
return callUpdateHub();
|
||||||
|
},
|
||||||
|
|
||||||
|
upgradeHub: function() {
|
||||||
|
return callUpgradeHub();
|
||||||
|
},
|
||||||
|
|
||||||
|
getBouncers: function() {
|
||||||
|
return callBouncers();
|
||||||
|
},
|
||||||
|
|
||||||
|
controlService: function(service, action) {
|
||||||
|
return callControlService(service, action);
|
||||||
|
},
|
||||||
|
|
||||||
|
getNftablesStats: function() {
|
||||||
|
return callNftablesStats();
|
||||||
|
},
|
||||||
|
|
||||||
|
getBlockedIps: function() {
|
||||||
|
return callBlockedIps();
|
||||||
|
},
|
||||||
|
|
||||||
|
getConfig: function() {
|
||||||
|
return callConfig();
|
||||||
|
},
|
||||||
|
|
||||||
|
saveConfig: function(key, value) {
|
||||||
|
return callSaveConfig(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,117 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require dom';
|
||||||
|
'require ui';
|
||||||
|
'require secubox-crowdsec/api as api';
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
api: null,
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
this.api = new api();
|
||||||
|
return this.api.getAlerts(100, '7d');
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDate: function(timestamp) {
|
||||||
|
if (!timestamp) return '-';
|
||||||
|
try {
|
||||||
|
var date = new Date(timestamp);
|
||||||
|
return date.toLocaleString();
|
||||||
|
} catch(e) {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderAlertsTable: function(alerts) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (!alerts || alerts.length === 0) {
|
||||||
|
return E('p', { 'class': 'alert-message' }, 'No alerts in the selected period');
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows = alerts.map(function(a) {
|
||||||
|
var sourceIp = '-';
|
||||||
|
if (a.source && a.source.ip) {
|
||||||
|
sourceIp = a.source.ip;
|
||||||
|
} else if (a.source_ip) {
|
||||||
|
sourceIp = a.source_ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
return E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, self.formatDate(a.created_at || a.timestamp)),
|
||||||
|
E('td', { 'class': 'td' }, sourceIp),
|
||||||
|
E('td', { 'class': 'td' }, a.scenario || '-'),
|
||||||
|
E('td', { 'class': 'td' }, String(a.events_count || a.events || 0)),
|
||||||
|
E('td', { 'class': 'td' }, a.message || '-')
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return E('table', { 'class': 'table cbi-section-table' }, [
|
||||||
|
E('tr', { 'class': 'tr table-titles' }, [
|
||||||
|
E('th', { 'class': 'th' }, 'Time'),
|
||||||
|
E('th', { 'class': 'th' }, 'Source IP'),
|
||||||
|
E('th', { 'class': 'th' }, 'Scenario'),
|
||||||
|
E('th', { 'class': 'th' }, 'Events'),
|
||||||
|
E('th', { 'class': 'th' }, 'Message')
|
||||||
|
])
|
||||||
|
].concat(rows));
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(data) {
|
||||||
|
var alerts = data.alerts || [];
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var view = E('div', { 'class': 'cbi-map' }, [
|
||||||
|
E('h2', { 'class': 'cbi-map-title' }, 'CrowdSec Alerts'),
|
||||||
|
E('div', { 'class': 'cbi-map-descr' }, 'Security alerts detected by CrowdSec'),
|
||||||
|
|
||||||
|
// Filter Controls
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', { 'class': 'cbi-section-title' }, 'Filter'),
|
||||||
|
E('div', { 'style': 'margin-bottom: 15px;' }, [
|
||||||
|
E('label', {}, 'Time Period: '),
|
||||||
|
E('select', { 'id': 'alert-period', 'class': 'cbi-input-select', 'style': 'margin-right: 15px;' }, [
|
||||||
|
E('option', { 'value': '1h' }, 'Last hour'),
|
||||||
|
E('option', { 'value': '24h' }, 'Last 24 hours'),
|
||||||
|
E('option', { 'value': '7d', 'selected': 'selected' }, 'Last 7 days'),
|
||||||
|
E('option', { 'value': '30d' }, 'Last 30 days')
|
||||||
|
]),
|
||||||
|
E('label', {}, 'Limit: '),
|
||||||
|
E('select', { 'id': 'alert-limit', 'class': 'cbi-input-select', 'style': 'margin-right: 15px;' }, [
|
||||||
|
E('option', { 'value': '25' }, '25'),
|
||||||
|
E('option', { 'value': '50' }, '50'),
|
||||||
|
E('option', { 'value': '100', 'selected': 'selected' }, '100'),
|
||||||
|
E('option', { 'value': '200' }, '200')
|
||||||
|
]),
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-action',
|
||||||
|
'click': ui.createHandlerFn(this, function() {
|
||||||
|
var period = document.getElementById('alert-period').value;
|
||||||
|
var limit = parseInt(document.getElementById('alert-limit').value);
|
||||||
|
|
||||||
|
return this.api.getAlerts(limit, period).then(function(res) {
|
||||||
|
var table = self.renderAlertsTable(res.alerts || []);
|
||||||
|
var container = document.getElementById('alerts-table');
|
||||||
|
dom.content(container, table);
|
||||||
|
var title = document.getElementById('alerts-count');
|
||||||
|
if (title) title.textContent = 'Alerts (' + (res.alerts ? res.alerts.length : 0) + ')';
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}, 'Refresh')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Alerts Table
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', { 'class': 'cbi-section-title', 'id': 'alerts-count' }, 'Alerts (' + alerts.length + ')'),
|
||||||
|
E('div', { 'id': 'alerts-table' }, this.renderAlertsTable(alerts))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
|
});
|
||||||
@ -0,0 +1,209 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require dom';
|
||||||
|
'require ui';
|
||||||
|
'require secubox-crowdsec/api as api';
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
api: null,
|
||||||
|
|
||||||
|
availableCollections: [
|
||||||
|
{ name: 'crowdsecurity/linux', desc: 'Linux system security' },
|
||||||
|
{ name: 'crowdsecurity/sshd', desc: 'SSH brute-force protection' },
|
||||||
|
{ name: 'crowdsecurity/http-cve', desc: 'HTTP CVE exploits' },
|
||||||
|
{ name: 'crowdsecurity/iptables', desc: 'IPTables/NFTables logs' },
|
||||||
|
{ name: 'crowdsecurity/nginx', desc: 'Nginx web server' },
|
||||||
|
{ name: 'crowdsecurity/apache2', desc: 'Apache2 web server' },
|
||||||
|
{ name: 'crowdsecurity/postfix', desc: 'Postfix mail server' },
|
||||||
|
{ name: 'crowdsecurity/dovecot', desc: 'Dovecot mail server' },
|
||||||
|
{ name: 'crowdsecurity/smb', desc: 'SMB/Samba' },
|
||||||
|
{ name: 'crowdsecurity/wordpress', desc: 'WordPress security' },
|
||||||
|
{ name: 'crowdsecurity/nextcloud', desc: 'Nextcloud security' }
|
||||||
|
],
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
this.api = new api();
|
||||||
|
return Promise.all([
|
||||||
|
this.api.getCollections(),
|
||||||
|
this.api.getBouncers()
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
isInstalled: function(collections, name) {
|
||||||
|
if (!collections || !Array.isArray(collections)) return false;
|
||||||
|
return collections.some(function(c) {
|
||||||
|
return c.name === name && c.status === 'enabled';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderCollectionsTable: function(collections) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (!collections || collections.length === 0) {
|
||||||
|
return E('p', { 'class': 'alert-message' }, 'No collections installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows = collections.map(function(c) {
|
||||||
|
var statusBadge = E('span', {
|
||||||
|
'style': 'background-color: ' + (c.status === 'enabled' ? 'green' : 'gray') + '; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;'
|
||||||
|
}, c.status || 'unknown');
|
||||||
|
|
||||||
|
return E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, c.name || '-'),
|
||||||
|
E('td', { 'class': 'td' }, c.local_version || c.version || '-'),
|
||||||
|
E('td', { 'class': 'td' }, statusBadge),
|
||||||
|
E('td', { 'class': 'td' }, c.description || '-'),
|
||||||
|
E('td', { 'class': 'td' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-remove',
|
||||||
|
'click': ui.createHandlerFn(self, function(ev) {
|
||||||
|
if (!confirm('Remove collection ' + c.name + '?')) return;
|
||||||
|
|
||||||
|
return self.api.removeCollection(c.name).then(function(res) {
|
||||||
|
if (res.success) {
|
||||||
|
ui.addNotification(null, E('p', res.message), 'info');
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', res.error), 'warning');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}, 'Remove')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return E('table', { 'class': 'table cbi-section-table' }, [
|
||||||
|
E('tr', { 'class': 'tr table-titles' }, [
|
||||||
|
E('th', { 'class': 'th' }, 'Name'),
|
||||||
|
E('th', { 'class': 'th' }, 'Version'),
|
||||||
|
E('th', { 'class': 'th' }, 'Status'),
|
||||||
|
E('th', { 'class': 'th' }, 'Description'),
|
||||||
|
E('th', { 'class': 'th' }, 'Actions')
|
||||||
|
])
|
||||||
|
].concat(rows));
|
||||||
|
},
|
||||||
|
|
||||||
|
renderBouncersTable: function(bouncers) {
|
||||||
|
if (!bouncers || bouncers.length === 0) {
|
||||||
|
return E('p', { 'class': 'alert-message' }, 'No bouncers registered');
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows = bouncers.map(function(b) {
|
||||||
|
var validBadge = E('span', {
|
||||||
|
'style': 'background-color: ' + (b.is_valid ? 'green' : 'red') + '; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;'
|
||||||
|
}, b.is_valid ? 'Valid' : 'Invalid');
|
||||||
|
|
||||||
|
return E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, b.name || '-'),
|
||||||
|
E('td', { 'class': 'td' }, b.ip_address || b.ip || '-'),
|
||||||
|
E('td', { 'class': 'td' }, b.type || '-'),
|
||||||
|
E('td', { 'class': 'td' }, b.last_pull || '-'),
|
||||||
|
E('td', { 'class': 'td' }, validBadge)
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return E('table', { 'class': 'table cbi-section-table' }, [
|
||||||
|
E('tr', { 'class': 'tr table-titles' }, [
|
||||||
|
E('th', { 'class': 'th' }, 'Name'),
|
||||||
|
E('th', { 'class': 'th' }, 'IP'),
|
||||||
|
E('th', { 'class': 'th' }, 'Type'),
|
||||||
|
E('th', { 'class': 'th' }, 'Last Pull'),
|
||||||
|
E('th', { 'class': 'th' }, 'Status')
|
||||||
|
])
|
||||||
|
].concat(rows));
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(data) {
|
||||||
|
var collections = (data[0] && data[0].collections) ? data[0].collections : [];
|
||||||
|
var bouncers = (data[1] && data[1].bouncers) ? data[1].bouncers : [];
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// Build available collections dropdown (filter out installed ones)
|
||||||
|
var availableOptions = this.availableCollections
|
||||||
|
.filter(function(c) { return !self.isInstalled(collections, c.name); })
|
||||||
|
.map(function(c) {
|
||||||
|
return E('option', { 'value': c.name }, c.name + ' - ' + c.desc);
|
||||||
|
});
|
||||||
|
|
||||||
|
var view = E('div', { 'class': 'cbi-map' }, [
|
||||||
|
E('h2', { 'class': 'cbi-map-title' }, 'CrowdSec Collections'),
|
||||||
|
E('div', { 'class': 'cbi-map-descr' }, 'Manage security collections and bouncers'),
|
||||||
|
|
||||||
|
// Hub Actions
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', { 'class': 'cbi-section-title' }, 'Hub Management'),
|
||||||
|
E('div', { 'style': 'margin-bottom: 15px;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-action',
|
||||||
|
'click': ui.createHandlerFn(this, function() {
|
||||||
|
return this.api.updateHub().then(function(res) {
|
||||||
|
if (res.success) {
|
||||||
|
ui.addNotification(null, E('p', res.message), 'info');
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', res.error), 'warning');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}, 'Update Hub'),
|
||||||
|
' ',
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-apply',
|
||||||
|
'click': ui.createHandlerFn(this, function() {
|
||||||
|
return this.api.upgradeHub().then(function(res) {
|
||||||
|
if (res.success) {
|
||||||
|
ui.addNotification(null, E('p', res.message), 'info');
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', res.error), 'warning');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}, 'Upgrade All')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Install Collection
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', { 'class': 'cbi-section-title' }, 'Install Collection'),
|
||||||
|
E('div', { 'style': 'margin-bottom: 15px;' }, [
|
||||||
|
availableOptions.length > 0 ? E('select', { 'id': 'new-collection', 'class': 'cbi-input-select', 'style': 'margin-right: 10px; min-width: 300px;' }, availableOptions) : E('span', {}, 'All recommended collections installed'),
|
||||||
|
availableOptions.length > 0 ? E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-add',
|
||||||
|
'click': ui.createHandlerFn(this, function() {
|
||||||
|
var collection = document.getElementById('new-collection').value;
|
||||||
|
if (!collection) return;
|
||||||
|
|
||||||
|
return this.api.installCollection(collection).then(function(res) {
|
||||||
|
if (res.success) {
|
||||||
|
ui.addNotification(null, E('p', res.message), 'info');
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', res.error), 'warning');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}, 'Install') : null
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Installed Collections
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', { 'class': 'cbi-section-title' }, 'Installed Collections (' + collections.length + ')'),
|
||||||
|
E('div', { 'id': 'collections-table' }, this.renderCollectionsTable(collections))
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Bouncers
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', { 'class': 'cbi-section-title' }, 'Registered Bouncers (' + bouncers.length + ')'),
|
||||||
|
E('div', { 'id': 'bouncers-table' }, this.renderBouncersTable(bouncers))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
|
});
|
||||||
@ -0,0 +1,162 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require dom';
|
||||||
|
'require poll';
|
||||||
|
'require ui';
|
||||||
|
'require secubox-crowdsec/api as api';
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
api: null,
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
this.api = new api();
|
||||||
|
return Promise.all([
|
||||||
|
this.api.getStatus(),
|
||||||
|
this.api.getStats(),
|
||||||
|
this.api.getNftablesStats()
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderStatusBadge: function(status, running, stopped) {
|
||||||
|
var color = (status === running) ? 'green' : 'red';
|
||||||
|
var text = (status === running) ? 'Running' : 'Stopped';
|
||||||
|
return E('span', {
|
||||||
|
'class': 'badge',
|
||||||
|
'style': 'background-color: ' + color + '; color: white; padding: 2px 8px; border-radius: 3px;'
|
||||||
|
}, text);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderServiceCard: function(name, status) {
|
||||||
|
return E('div', { 'class': 'cbi-section', 'style': 'display: inline-block; width: 200px; margin: 10px; text-align: center; padding: 15px; border: 1px solid #ddd; border-radius: 5px;' }, [
|
||||||
|
E('h4', { 'style': 'margin: 0 0 10px 0;' }, name),
|
||||||
|
this.renderStatusBadge(status, 'running', 'stopped')
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderStatCard: function(label, value, icon) {
|
||||||
|
return E('div', { 'class': 'cbi-section', 'style': 'display: inline-block; width: 150px; margin: 10px; text-align: center; padding: 15px; border: 1px solid #ddd; border-radius: 5px;' }, [
|
||||||
|
E('div', { 'style': 'font-size: 24px; font-weight: bold; color: #0066cc;' }, String(value || 0)),
|
||||||
|
E('div', { 'style': 'color: #666; font-size: 12px;' }, label)
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(data) {
|
||||||
|
var status = data[0] || {};
|
||||||
|
var stats = data[1] || {};
|
||||||
|
var nftStats = data[2] || {};
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var view = E('div', { 'class': 'cbi-map' }, [
|
||||||
|
E('h2', { 'class': 'cbi-map-title' }, 'CrowdSec Dashboard'),
|
||||||
|
E('div', { 'class': 'cbi-map-descr' }, 'Security engine status and statistics'),
|
||||||
|
|
||||||
|
// Services Status
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', { 'class': 'cbi-section-title' }, 'Services Status'),
|
||||||
|
E('div', { 'id': 'service-status', 'style': 'text-align: center;' }, [
|
||||||
|
this.renderServiceCard('CrowdSec', status.crowdsec),
|
||||||
|
this.renderServiceCard('Bouncer', status.bouncer),
|
||||||
|
this.renderServiceCard('Syslog-ng', status.syslog)
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// API Status
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', { 'class': 'cbi-section-title' }, 'API Status'),
|
||||||
|
E('table', { 'class': 'table cbi-section-table' }, [
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, 'CrowdSec Version'),
|
||||||
|
E('td', { 'class': 'td' }, status.version || 'Unknown')
|
||||||
|
]),
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, 'Local API (LAPI)'),
|
||||||
|
E('td', { 'class': 'td' }, this.renderStatusBadge(status.lapi_status, 'available', 'unavailable'))
|
||||||
|
]),
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, 'Central API (CAPI)'),
|
||||||
|
E('td', { 'class': 'td' }, this.renderStatusBadge(status.capi_status, 'connected', 'disconnected'))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', { 'class': 'cbi-section-title' }, 'Statistics'),
|
||||||
|
E('div', { 'id': 'stats-cards', 'style': 'text-align: center;' }, [
|
||||||
|
this.renderStatCard('Active Decisions', stats.total_decisions),
|
||||||
|
this.renderStatCard('Alerts (24h)', stats.alerts_24h),
|
||||||
|
this.renderStatCard('Bouncers', stats.bouncers),
|
||||||
|
this.renderStatCard('Parsers', stats.parsers),
|
||||||
|
this.renderStatCard('Scenarios', stats.scenarios)
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// NFTables Status
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', { 'class': 'cbi-section-title' }, 'Firewall (nftables)'),
|
||||||
|
E('table', { 'class': 'table cbi-section-table' }, [
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, 'IPv4 Table'),
|
||||||
|
E('td', { 'class': 'td' }, nftStats.ipv4_table ? 'Active' : 'Inactive')
|
||||||
|
]),
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, 'IPv6 Table'),
|
||||||
|
E('td', { 'class': 'td' }, nftStats.ipv6_table ? 'Active' : 'Inactive')
|
||||||
|
]),
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, 'Blocked IPv4'),
|
||||||
|
E('td', { 'class': 'td' }, String(nftStats.blocked_ipv4 || 0))
|
||||||
|
]),
|
||||||
|
E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, 'Blocked IPv6'),
|
||||||
|
E('td', { 'class': 'td' }, String(nftStats.blocked_ipv6 || 0))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Service Controls
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', { 'class': 'cbi-section-title' }, 'Service Controls'),
|
||||||
|
E('div', { 'style': 'text-align: center;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-apply',
|
||||||
|
'click': ui.createHandlerFn(this, function() {
|
||||||
|
return this.api.controlService('crowdsec', 'restart').then(function() {
|
||||||
|
ui.addNotification(null, E('p', 'CrowdSec restarted'), 'info');
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}, 'Restart CrowdSec'),
|
||||||
|
' ',
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-apply',
|
||||||
|
'click': ui.createHandlerFn(this, function() {
|
||||||
|
return this.api.controlService('crowdsec-firewall-bouncer', 'restart').then(function() {
|
||||||
|
ui.addNotification(null, E('p', 'Bouncer restarted'), 'info');
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}, 'Restart Bouncer'),
|
||||||
|
' ',
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-action',
|
||||||
|
'click': ui.createHandlerFn(this, function() {
|
||||||
|
return this.api.updateHub().then(function(res) {
|
||||||
|
if (res.success)
|
||||||
|
ui.addNotification(null, E('p', res.message), 'info');
|
||||||
|
else
|
||||||
|
ui.addNotification(null, E('p', res.error), 'warning');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}, 'Update Hub')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
|
});
|
||||||
@ -0,0 +1,139 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require dom';
|
||||||
|
'require ui';
|
||||||
|
'require secubox-crowdsec/api as api';
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
api: null,
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
this.api = new api();
|
||||||
|
return this.api.getDecisions();
|
||||||
|
},
|
||||||
|
|
||||||
|
renderDecisionsTable: function(decisions) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (!decisions || decisions.length === 0) {
|
||||||
|
return E('p', { 'class': 'alert-message' }, 'No active decisions');
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows = decisions.map(function(d) {
|
||||||
|
return E('tr', { 'class': 'tr' }, [
|
||||||
|
E('td', { 'class': 'td' }, d.value || d.ip || '-'),
|
||||||
|
E('td', { 'class': 'td' }, d.type || 'ban'),
|
||||||
|
E('td', { 'class': 'td' }, d.scope || 'ip'),
|
||||||
|
E('td', { 'class': 'td' }, d.duration || '-'),
|
||||||
|
E('td', { 'class': 'td' }, d.origin || '-'),
|
||||||
|
E('td', { 'class': 'td' }, d.scenario || d.reason || '-'),
|
||||||
|
E('td', { 'class': 'td' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-remove',
|
||||||
|
'click': ui.createHandlerFn(self, function(ev) {
|
||||||
|
return self.api.deleteDecision(d.value || d.ip, d.id).then(function(res) {
|
||||||
|
if (res.success) {
|
||||||
|
ui.addNotification(null, E('p', res.message), 'info');
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', res.error), 'warning');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}, 'Delete')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return E('table', { 'class': 'table cbi-section-table' }, [
|
||||||
|
E('tr', { 'class': 'tr table-titles' }, [
|
||||||
|
E('th', { 'class': 'th' }, 'IP/Range'),
|
||||||
|
E('th', { 'class': 'th' }, 'Type'),
|
||||||
|
E('th', { 'class': 'th' }, 'Scope'),
|
||||||
|
E('th', { 'class': 'th' }, 'Duration'),
|
||||||
|
E('th', { 'class': 'th' }, 'Origin'),
|
||||||
|
E('th', { 'class': 'th' }, 'Reason'),
|
||||||
|
E('th', { 'class': 'th' }, 'Actions')
|
||||||
|
])
|
||||||
|
].concat(rows));
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(data) {
|
||||||
|
var decisions = data.decisions || [];
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var view = E('div', { 'class': 'cbi-map' }, [
|
||||||
|
E('h2', { 'class': 'cbi-map-title' }, 'CrowdSec Decisions'),
|
||||||
|
E('div', { 'class': 'cbi-map-descr' }, 'Active bans and security decisions'),
|
||||||
|
|
||||||
|
// Add Decision Form
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', { 'class': 'cbi-section-title' }, 'Add Manual Ban'),
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, 'IP Address'),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('input', { 'type': 'text', 'id': 'ban-ip', 'class': 'cbi-input-text', 'placeholder': '192.168.1.100' })
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, 'Duration'),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('select', { 'id': 'ban-duration', 'class': 'cbi-input-select' }, [
|
||||||
|
E('option', { 'value': '1h' }, '1 hour'),
|
||||||
|
E('option', { 'value': '4h' }, '4 hours'),
|
||||||
|
E('option', { 'value': '24h', 'selected': 'selected' }, '24 hours'),
|
||||||
|
E('option', { 'value': '7d' }, '7 days'),
|
||||||
|
E('option', { 'value': '30d' }, '30 days'),
|
||||||
|
E('option', { 'value': '365d' }, '1 year')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, 'Reason'),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('input', { 'type': 'text', 'id': 'ban-reason', 'class': 'cbi-input-text', 'placeholder': 'Manual ban via LuCI', 'value': 'Manual ban via LuCI' })
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, ' '),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button cbi-button-apply',
|
||||||
|
'click': ui.createHandlerFn(this, function() {
|
||||||
|
var ip = document.getElementById('ban-ip').value;
|
||||||
|
var duration = document.getElementById('ban-duration').value;
|
||||||
|
var reason = document.getElementById('ban-reason').value;
|
||||||
|
|
||||||
|
if (!ip) {
|
||||||
|
ui.addNotification(null, E('p', 'Please enter an IP address'), 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.api.addDecision(ip, duration, reason, 'ban').then(function(res) {
|
||||||
|
if (res.success) {
|
||||||
|
ui.addNotification(null, E('p', res.message), 'info');
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', res.error), 'warning');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}, 'Add Ban')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Active Decisions
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', { 'class': 'cbi-section-title' }, 'Active Decisions (' + decisions.length + ')'),
|
||||||
|
E('div', { 'id': 'decisions-table' }, this.renderDecisionsTable(decisions))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
|
});
|
||||||
@ -0,0 +1,116 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require dom';
|
||||||
|
'require ui';
|
||||||
|
'require uci';
|
||||||
|
'require form';
|
||||||
|
'require secubox-crowdsec/api as api';
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
api: null,
|
||||||
|
|
||||||
|
load: function() {
|
||||||
|
this.api = new api();
|
||||||
|
return Promise.all([
|
||||||
|
uci.load('crowdsec'),
|
||||||
|
this.api.getConfig()
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(data) {
|
||||||
|
var config = data[1] || {};
|
||||||
|
var m, s, o;
|
||||||
|
|
||||||
|
m = new form.Map('crowdsec', 'CrowdSec Settings',
|
||||||
|
'Configure CrowdSec security engine and firewall bouncer settings.');
|
||||||
|
|
||||||
|
// Main CrowdSec settings
|
||||||
|
s = m.section(form.TypedSection, 'crowdsec', 'CrowdSec Engine');
|
||||||
|
s.anonymous = true;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'enabled', 'Enable CrowdSec',
|
||||||
|
'Enable or disable the CrowdSec security engine');
|
||||||
|
o.default = '1';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
// Bouncer settings
|
||||||
|
s = m.section(form.TypedSection, 'bouncer', 'Firewall Bouncer');
|
||||||
|
s.anonymous = true;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'enabled', 'Enable Bouncer',
|
||||||
|
'Enable the firewall bouncer to block malicious IPs');
|
||||||
|
o.default = '1';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'ipv4', 'IPv4 Blocking',
|
||||||
|
'Enable IPv4 address blocking');
|
||||||
|
o.default = '1';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'ipv6', 'IPv6 Blocking',
|
||||||
|
'Enable IPv6 address blocking');
|
||||||
|
o.default = '1';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.ListValue, 'deny_action', 'Deny Action',
|
||||||
|
'Action to take when blocking an IP');
|
||||||
|
o.value('drop', 'Drop (silent)');
|
||||||
|
o.value('reject', 'Reject (with response)');
|
||||||
|
o.default = 'drop';
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'deny_log', 'Log Blocked IPs',
|
||||||
|
'Log blocked connections to system log');
|
||||||
|
o.default = '1';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.ListValue, 'update_frequency', 'Update Frequency',
|
||||||
|
'How often to fetch new decisions from LAPI');
|
||||||
|
o.value('5s', '5 seconds');
|
||||||
|
o.value('10s', '10 seconds');
|
||||||
|
o.value('30s', '30 seconds');
|
||||||
|
o.value('1m', '1 minute');
|
||||||
|
o.default = '10s';
|
||||||
|
|
||||||
|
// Acquisition settings
|
||||||
|
s = m.section(form.TypedSection, 'acquisition', 'Log Acquisition');
|
||||||
|
s.anonymous = true;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'syslog_enabled', 'Syslog',
|
||||||
|
'Monitor system logs via syslog-ng');
|
||||||
|
o.default = '1';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'firewall_enabled', 'Firewall Logs',
|
||||||
|
'Monitor nftables/iptables firewall logs');
|
||||||
|
o.default = '1';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'ssh_enabled', 'SSH Logs',
|
||||||
|
'Monitor SSH authentication attempts');
|
||||||
|
o.default = '1';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
o = s.option(form.Flag, 'http_enabled', 'HTTP Logs',
|
||||||
|
'Monitor HTTP server logs (if applicable)');
|
||||||
|
o.default = '0';
|
||||||
|
o.rmempty = false;
|
||||||
|
|
||||||
|
// Hub settings
|
||||||
|
s = m.section(form.TypedSection, 'hub', 'Hub Settings');
|
||||||
|
s.anonymous = true;
|
||||||
|
|
||||||
|
o = s.option(form.Value, 'collections', 'Default Collections',
|
||||||
|
'Space-separated list of collections to install');
|
||||||
|
o.default = 'crowdsecurity/linux crowdsecurity/sshd crowdsecurity/iptables';
|
||||||
|
|
||||||
|
o = s.option(form.ListValue, 'update_interval', 'Hub Update Interval',
|
||||||
|
'How often to check for hub updates (days)');
|
||||||
|
o.value('1', 'Daily');
|
||||||
|
o.value('7', 'Weekly');
|
||||||
|
o.value('30', 'Monthly');
|
||||||
|
o.value('0', 'Never');
|
||||||
|
o.default = '7';
|
||||||
|
|
||||||
|
return m.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
config crowdsec 'crowdsec'
|
||||||
|
option enabled '1'
|
||||||
|
option data_dir '/srv/crowdsec/data'
|
||||||
|
option db_path '/srv/crowdsec/data/crowdsec.db'
|
||||||
|
|
||||||
|
config acquisition 'acquisition'
|
||||||
|
option syslog_enabled '1'
|
||||||
|
option firewall_enabled '1'
|
||||||
|
option ssh_enabled '1'
|
||||||
|
option http_enabled '0'
|
||||||
|
|
||||||
|
config hub 'hub'
|
||||||
|
option auto_install '1'
|
||||||
|
option collections 'crowdsecurity/linux crowdsecurity/sshd crowdsecurity/iptables'
|
||||||
|
option update_interval '7'
|
||||||
|
|
||||||
|
config bouncer 'bouncer'
|
||||||
|
option enabled '1'
|
||||||
|
option ipv4 '1'
|
||||||
|
option ipv6 '1'
|
||||||
|
option deny_action 'drop'
|
||||||
|
option deny_log '1'
|
||||||
|
option update_frequency '10s'
|
||||||
|
option filter_input '1'
|
||||||
|
option filter_forward '1'
|
||||||
@ -0,0 +1,691 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# SecuBox CrowdSec RPCD Backend
|
||||||
|
# Copyright (C) 2025 CyberMind.fr - Gandalf
|
||||||
|
|
||||||
|
. /lib/functions.sh
|
||||||
|
. /usr/share/libubox/jshn.sh
|
||||||
|
|
||||||
|
CSCLI="/usr/bin/cscli"
|
||||||
|
CSCLI_TIMEOUT=10
|
||||||
|
|
||||||
|
# Execution cscli avec timeout pour eviter les blocages
|
||||||
|
run_cscli() {
|
||||||
|
timeout "$CSCLI_TIMEOUT" "$CSCLI" "$@" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# FONCTIONS STATUS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Statut global des services
|
||||||
|
get_status() {
|
||||||
|
json_init
|
||||||
|
|
||||||
|
# Service CrowdSec
|
||||||
|
if pgrep -x crowdsec >/dev/null 2>&1; then
|
||||||
|
json_add_string "crowdsec" "running"
|
||||||
|
else
|
||||||
|
json_add_string "crowdsec" "stopped"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Service Bouncer
|
||||||
|
if pgrep -f "crowdsec-firewall-bouncer" >/dev/null 2>&1; then
|
||||||
|
json_add_string "bouncer" "running"
|
||||||
|
else
|
||||||
|
json_add_string "bouncer" "stopped"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Service syslog-ng
|
||||||
|
if pgrep -f "syslog-ng" >/dev/null 2>&1; then
|
||||||
|
json_add_string "syslog" "running"
|
||||||
|
else
|
||||||
|
json_add_string "syslog" "stopped"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Version CrowdSec
|
||||||
|
local version
|
||||||
|
version=$(run_cscli version 2>/dev/null | grep "version:" | awk '{print $2}')
|
||||||
|
json_add_string "version" "${version:-unknown}"
|
||||||
|
|
||||||
|
# LAPI status
|
||||||
|
local lapi_status="unavailable"
|
||||||
|
if [ -x "$CSCLI" ]; then
|
||||||
|
if run_cscli lapi status >/dev/null 2>&1; then
|
||||||
|
lapi_status="available"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
json_add_string "lapi_status" "$lapi_status"
|
||||||
|
|
||||||
|
# CAPI status
|
||||||
|
local capi_status="unknown"
|
||||||
|
local capi_output
|
||||||
|
capi_output=$(run_cscli capi status 2>/dev/null)
|
||||||
|
if echo "$capi_output" | grep -qi "online\|connected"; then
|
||||||
|
capi_status="connected"
|
||||||
|
elif echo "$capi_output" | grep -qi "offline\|disconnected"; then
|
||||||
|
capi_status="disconnected"
|
||||||
|
fi
|
||||||
|
json_add_string "capi_status" "$capi_status"
|
||||||
|
|
||||||
|
# Tables nftables
|
||||||
|
local nft_ipv4=0
|
||||||
|
local nft_ipv6=0
|
||||||
|
if command -v nft >/dev/null 2>&1; then
|
||||||
|
nft list tables 2>/dev/null | grep -q "ip crowdsec" && nft_ipv4=1
|
||||||
|
nft list tables 2>/dev/null | grep -q "ip6 crowdsec6" && nft_ipv6=1
|
||||||
|
fi
|
||||||
|
json_add_boolean "nftables_ipv4" "$nft_ipv4"
|
||||||
|
json_add_boolean "nftables_ipv6" "$nft_ipv6"
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# FONCTIONS DECISIONS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Liste des decisions actives
|
||||||
|
get_decisions() {
|
||||||
|
if [ ! -x "$CSCLI" ]; then
|
||||||
|
echo '{"decisions":[],"error":"cscli not found"}'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! pgrep -x crowdsec >/dev/null 2>&1; then
|
||||||
|
echo '{"decisions":[],"error":"crowdsec not running"}'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local output
|
||||||
|
output=$(run_cscli decisions list -o json)
|
||||||
|
if [ -z "$output" ] || [ "$output" = "null" ]; then
|
||||||
|
echo '{"decisions":[]}'
|
||||||
|
else
|
||||||
|
echo "{\"decisions\":$output}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ajouter une decision (ban)
|
||||||
|
add_decision() {
|
||||||
|
local ip="$1"
|
||||||
|
local duration="${2:-24h}"
|
||||||
|
local reason="${3:-Ban manuel via LuCI}"
|
||||||
|
local decision_type="${4:-ban}"
|
||||||
|
|
||||||
|
json_init
|
||||||
|
|
||||||
|
if [ -z "$ip" ]; then
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "IP requise"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local result
|
||||||
|
result=$(run_cscli decisions add --ip "$ip" --duration "$duration" --reason "$reason" --type "$decision_type" 2>&1)
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "IP $ip bannie pour $duration"
|
||||||
|
logger -t "secubox-crowdsec" "Ban manuel: $ip pour $duration - $reason"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$result"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Supprimer une decision (unban)
|
||||||
|
delete_decision() {
|
||||||
|
local ip="$1"
|
||||||
|
local decision_id="$2"
|
||||||
|
|
||||||
|
json_init
|
||||||
|
|
||||||
|
local result
|
||||||
|
if [ -n "$decision_id" ]; then
|
||||||
|
result=$(run_cscli decisions delete --id "$decision_id" 2>&1)
|
||||||
|
elif [ -n "$ip" ]; then
|
||||||
|
result=$(run_cscli decisions delete --ip "$ip" 2>&1)
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "IP ou ID decision requis"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Decision supprimee"
|
||||||
|
logger -t "secubox-crowdsec" "Unban: ${ip:-ID:$decision_id}"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$result"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# FONCTIONS ALERTES
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Liste des alertes
|
||||||
|
get_alerts() {
|
||||||
|
local limit="${1:-50}"
|
||||||
|
local since="${2:-24h}"
|
||||||
|
|
||||||
|
if [ ! -x "$CSCLI" ]; then
|
||||||
|
echo '{"alerts":[],"error":"cscli not found"}'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local output
|
||||||
|
output=$(run_cscli alerts list -o json --limit "$limit" --since "$since")
|
||||||
|
if [ -z "$output" ] || [ "$output" = "null" ]; then
|
||||||
|
echo '{"alerts":[]}'
|
||||||
|
else
|
||||||
|
echo "{\"alerts\":$output}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# FONCTIONS METRIQUES
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Metriques globales
|
||||||
|
get_metrics() {
|
||||||
|
if [ ! -x "$CSCLI" ]; then
|
||||||
|
echo '{"error":"cscli not found"}'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local output
|
||||||
|
output=$(run_cscli metrics -o json)
|
||||||
|
if [ -z "$output" ]; then
|
||||||
|
echo '{}'
|
||||||
|
else
|
||||||
|
echo "$output"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Statistiques resumees
|
||||||
|
get_stats() {
|
||||||
|
json_init
|
||||||
|
|
||||||
|
if [ ! -x "$CSCLI" ] || ! pgrep -x crowdsec >/dev/null 2>&1; then
|
||||||
|
json_add_int "total_decisions" 0
|
||||||
|
json_add_int "alerts_24h" 0
|
||||||
|
json_add_int "bouncers" 0
|
||||||
|
json_add_int "parsers" 0
|
||||||
|
json_add_int "scenarios" 0
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Compter les decisions actives
|
||||||
|
local decisions_count
|
||||||
|
decisions_count=$(run_cscli decisions list -o json 2>/dev/null | grep -c '"id"' || echo "0")
|
||||||
|
json_add_int "total_decisions" "${decisions_count:-0}"
|
||||||
|
|
||||||
|
# Compter les alertes 24h
|
||||||
|
local alerts_count
|
||||||
|
alerts_count=$(run_cscli alerts list -o json --since 24h 2>/dev/null | grep -c '"id"' || echo "0")
|
||||||
|
json_add_int "alerts_24h" "${alerts_count:-0}"
|
||||||
|
|
||||||
|
# Compter les bouncers
|
||||||
|
local bouncers_count
|
||||||
|
bouncers_count=$(run_cscli bouncers list -o json 2>/dev/null | grep -c '"name"' || echo "0")
|
||||||
|
json_add_int "bouncers" "${bouncers_count:-0}"
|
||||||
|
|
||||||
|
# Compter les parsers installes
|
||||||
|
local parsers_count
|
||||||
|
parsers_count=$(run_cscli parsers list 2>/dev/null | grep -c "INSTALLED" || echo "0")
|
||||||
|
json_add_int "parsers" "${parsers_count:-0}"
|
||||||
|
|
||||||
|
# Compter les scenarios installes
|
||||||
|
local scenarios_count
|
||||||
|
scenarios_count=$(run_cscli scenarios list 2>/dev/null | grep -c "INSTALLED" || echo "0")
|
||||||
|
json_add_int "scenarios" "${scenarios_count:-0}"
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# FONCTIONS COLLECTIONS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Liste des collections
|
||||||
|
get_collections() {
|
||||||
|
if [ ! -x "$CSCLI" ]; then
|
||||||
|
echo '{"collections":[]}'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local output
|
||||||
|
output=$(run_cscli collections list -o json)
|
||||||
|
if [ -z "$output" ] || [ "$output" = "null" ]; then
|
||||||
|
echo '{"collections":[]}'
|
||||||
|
else
|
||||||
|
echo "{\"collections\":$output}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Installer une collection
|
||||||
|
install_collection() {
|
||||||
|
local collection="$1"
|
||||||
|
|
||||||
|
json_init
|
||||||
|
|
||||||
|
if [ -z "$collection" ]; then
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Nom de collection requis"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local result
|
||||||
|
result=$(run_cscli collections install "$collection" 2>&1)
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Collection $collection installee"
|
||||||
|
logger -t "secubox-crowdsec" "Collection installee: $collection"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$result"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Desinstaller une collection
|
||||||
|
remove_collection() {
|
||||||
|
local collection="$1"
|
||||||
|
|
||||||
|
json_init
|
||||||
|
|
||||||
|
if [ -z "$collection" ]; then
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Nom de collection requis"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local result
|
||||||
|
result=$(run_cscli collections remove "$collection" 2>&1)
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Collection $collection supprimee"
|
||||||
|
logger -t "secubox-crowdsec" "Collection supprimee: $collection"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$result"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# FONCTIONS HUB
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Mise a jour du hub
|
||||||
|
update_hub() {
|
||||||
|
json_init
|
||||||
|
|
||||||
|
local result
|
||||||
|
result=$(run_cscli hub update 2>&1)
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Hub mis a jour"
|
||||||
|
logger -t "secubox-crowdsec" "Hub mis a jour"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$result"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Upgrade du hub
|
||||||
|
upgrade_hub() {
|
||||||
|
json_init
|
||||||
|
|
||||||
|
run_cscli hub update >/dev/null 2>&1
|
||||||
|
local result
|
||||||
|
result=$(run_cscli hub upgrade 2>&1)
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Hub upgrade effectue"
|
||||||
|
logger -t "secubox-crowdsec" "Hub upgrade effectue"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "$result"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# FONCTIONS BOUNCERS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Liste des bouncers
|
||||||
|
get_bouncers() {
|
||||||
|
if [ ! -x "$CSCLI" ]; then
|
||||||
|
echo '{"bouncers":[]}'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local output
|
||||||
|
output=$(run_cscli bouncers list -o json)
|
||||||
|
if [ -z "$output" ] || [ "$output" = "null" ]; then
|
||||||
|
echo '{"bouncers":[]}'
|
||||||
|
else
|
||||||
|
echo "{\"bouncers\":$output}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# FONCTIONS SERVICES
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Controle des services
|
||||||
|
control_service() {
|
||||||
|
local service="$1"
|
||||||
|
local action="$2"
|
||||||
|
|
||||||
|
json_init
|
||||||
|
|
||||||
|
case "$service" in
|
||||||
|
crowdsec|syslog-ng|crowdsec-firewall-bouncer)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Service invalide: $service"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$action" in
|
||||||
|
start|stop|restart|enable|disable)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Action invalide: $action"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if /etc/init.d/"$service" "$action" >/dev/null 2>&1; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Service $service: $action OK"
|
||||||
|
logger -t "secubox-crowdsec" "Service $service: $action"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Echec $action sur $service"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# FONCTIONS NFTABLES
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Statistiques nftables
|
||||||
|
get_nftables_stats() {
|
||||||
|
json_init
|
||||||
|
|
||||||
|
if ! command -v nft >/dev/null 2>&1; then
|
||||||
|
json_add_boolean "available" 0
|
||||||
|
json_add_string "error" "nftables non disponible"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_add_boolean "available" 1
|
||||||
|
|
||||||
|
# Table IPv4
|
||||||
|
local ipv4_exists=0
|
||||||
|
if nft list table ip crowdsec >/dev/null 2>&1; then
|
||||||
|
ipv4_exists=1
|
||||||
|
fi
|
||||||
|
json_add_boolean "ipv4_table" "$ipv4_exists"
|
||||||
|
|
||||||
|
# Table IPv6
|
||||||
|
local ipv6_exists=0
|
||||||
|
if nft list table ip6 crowdsec6 >/dev/null 2>&1; then
|
||||||
|
ipv6_exists=1
|
||||||
|
fi
|
||||||
|
json_add_boolean "ipv6_table" "$ipv6_exists"
|
||||||
|
|
||||||
|
# Compter les IPs bloquees
|
||||||
|
local ipv4_count=0
|
||||||
|
local ipv6_count=0
|
||||||
|
|
||||||
|
if [ "$ipv4_exists" = "1" ]; then
|
||||||
|
ipv4_count=$(nft list set ip crowdsec crowdsec-blacklists 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | wc -l || echo "0")
|
||||||
|
fi
|
||||||
|
if [ "$ipv6_exists" = "1" ]; then
|
||||||
|
ipv6_count=$(nft list set ip6 crowdsec6 crowdsec6-blacklists 2>/dev/null | grep -c ':' || echo "0")
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_add_int "blocked_ipv4" "$ipv4_count"
|
||||||
|
json_add_int "blocked_ipv6" "$ipv6_count"
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Liste des IPs bloquees
|
||||||
|
get_blocked_ips() {
|
||||||
|
json_init
|
||||||
|
json_add_array "ipv4"
|
||||||
|
|
||||||
|
if nft list set ip crowdsec crowdsec-blacklists 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(/[0-9]+)?' > /tmp/blocked_ips.tmp; then
|
||||||
|
while read -r ip; do
|
||||||
|
json_add_string "" "$ip"
|
||||||
|
done < /tmp/blocked_ips.tmp
|
||||||
|
rm -f /tmp/blocked_ips.tmp
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
|
||||||
|
json_add_array "ipv6"
|
||||||
|
# IPv6 plus complexe, on simplifie
|
||||||
|
json_close_array
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# FONCTIONS CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Lire la configuration UCI
|
||||||
|
get_config() {
|
||||||
|
json_init
|
||||||
|
|
||||||
|
# Section crowdsec
|
||||||
|
local enabled=$(uci -q get crowdsec.crowdsec.enabled || echo "1")
|
||||||
|
json_add_string "enabled" "$enabled"
|
||||||
|
|
||||||
|
# Section bouncer
|
||||||
|
local bouncer_enabled=$(uci -q get crowdsec.bouncer.enabled || echo "1")
|
||||||
|
local ipv4=$(uci -q get crowdsec.bouncer.ipv4 || echo "1")
|
||||||
|
local ipv6=$(uci -q get crowdsec.bouncer.ipv6 || echo "1")
|
||||||
|
local deny_action=$(uci -q get crowdsec.bouncer.deny_action || echo "drop")
|
||||||
|
local deny_log=$(uci -q get crowdsec.bouncer.deny_log || echo "1")
|
||||||
|
local update_freq=$(uci -q get crowdsec.bouncer.update_frequency || echo "10s")
|
||||||
|
|
||||||
|
json_add_object "bouncer"
|
||||||
|
json_add_string "enabled" "$bouncer_enabled"
|
||||||
|
json_add_string "ipv4" "$ipv4"
|
||||||
|
json_add_string "ipv6" "$ipv6"
|
||||||
|
json_add_string "deny_action" "$deny_action"
|
||||||
|
json_add_string "deny_log" "$deny_log"
|
||||||
|
json_add_string "update_frequency" "$update_freq"
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
# Section acquisition
|
||||||
|
local syslog_enabled=$(uci -q get crowdsec.acquisition.syslog_enabled || echo "1")
|
||||||
|
local firewall_enabled=$(uci -q get crowdsec.acquisition.firewall_enabled || echo "1")
|
||||||
|
local ssh_enabled=$(uci -q get crowdsec.acquisition.ssh_enabled || echo "1")
|
||||||
|
local http_enabled=$(uci -q get crowdsec.acquisition.http_enabled || echo "0")
|
||||||
|
|
||||||
|
json_add_object "acquisition"
|
||||||
|
json_add_string "syslog_enabled" "$syslog_enabled"
|
||||||
|
json_add_string "firewall_enabled" "$firewall_enabled"
|
||||||
|
json_add_string "ssh_enabled" "$ssh_enabled"
|
||||||
|
json_add_string "http_enabled" "$http_enabled"
|
||||||
|
json_close_object
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sauvegarder la configuration UCI
|
||||||
|
save_config() {
|
||||||
|
local key="$1"
|
||||||
|
local value="$2"
|
||||||
|
|
||||||
|
json_init
|
||||||
|
|
||||||
|
if [ -z "$key" ]; then
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Cle requise"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if uci set "crowdsec.$key=$value" && uci commit crowdsec; then
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Configuration sauvegardee"
|
||||||
|
logger -t "secubox-crowdsec" "Config: $key=$value"
|
||||||
|
else
|
||||||
|
json_add_boolean "success" 0
|
||||||
|
json_add_string "error" "Echec sauvegarde configuration"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# DISPATCHER PRINCIPAL
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
list)
|
||||||
|
cat << 'EOF'
|
||||||
|
{
|
||||||
|
"status":{},
|
||||||
|
"decisions":{},
|
||||||
|
"add_decision":{"ip":"string","duration":"string","reason":"string","type":"string"},
|
||||||
|
"delete_decision":{"ip":"string","decision_id":"string"},
|
||||||
|
"alerts":{"limit":"number","since":"string"},
|
||||||
|
"metrics":{},
|
||||||
|
"stats":{},
|
||||||
|
"collections":{},
|
||||||
|
"install_collection":{"collection":"string"},
|
||||||
|
"remove_collection":{"collection":"string"},
|
||||||
|
"update_hub":{},
|
||||||
|
"upgrade_hub":{},
|
||||||
|
"bouncers":{},
|
||||||
|
"control_service":{"service":"string","action":"string"},
|
||||||
|
"nftables_stats":{},
|
||||||
|
"blocked_ips":{},
|
||||||
|
"config":{},
|
||||||
|
"save_config":{"key":"string","value":"string"}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
;;
|
||||||
|
call)
|
||||||
|
case "$2" in
|
||||||
|
status)
|
||||||
|
get_status
|
||||||
|
;;
|
||||||
|
decisions)
|
||||||
|
get_decisions
|
||||||
|
;;
|
||||||
|
add_decision)
|
||||||
|
read -r input
|
||||||
|
ip=$(echo "$input" | jsonfilter -e '@.ip' 2>/dev/null)
|
||||||
|
duration=$(echo "$input" | jsonfilter -e '@.duration' 2>/dev/null)
|
||||||
|
reason=$(echo "$input" | jsonfilter -e '@.reason' 2>/dev/null)
|
||||||
|
dtype=$(echo "$input" | jsonfilter -e '@.type' 2>/dev/null)
|
||||||
|
add_decision "$ip" "$duration" "$reason" "$dtype"
|
||||||
|
;;
|
||||||
|
delete_decision)
|
||||||
|
read -r input
|
||||||
|
ip=$(echo "$input" | jsonfilter -e '@.ip' 2>/dev/null)
|
||||||
|
decision_id=$(echo "$input" | jsonfilter -e '@.decision_id' 2>/dev/null)
|
||||||
|
delete_decision "$ip" "$decision_id"
|
||||||
|
;;
|
||||||
|
alerts)
|
||||||
|
read -r input
|
||||||
|
limit=$(echo "$input" | jsonfilter -e '@.limit' 2>/dev/null)
|
||||||
|
since=$(echo "$input" | jsonfilter -e '@.since' 2>/dev/null)
|
||||||
|
get_alerts "${limit:-50}" "${since:-24h}"
|
||||||
|
;;
|
||||||
|
metrics)
|
||||||
|
get_metrics
|
||||||
|
;;
|
||||||
|
stats)
|
||||||
|
get_stats
|
||||||
|
;;
|
||||||
|
collections)
|
||||||
|
get_collections
|
||||||
|
;;
|
||||||
|
install_collection)
|
||||||
|
read -r input
|
||||||
|
collection=$(echo "$input" | jsonfilter -e '@.collection' 2>/dev/null)
|
||||||
|
install_collection "$collection"
|
||||||
|
;;
|
||||||
|
remove_collection)
|
||||||
|
read -r input
|
||||||
|
collection=$(echo "$input" | jsonfilter -e '@.collection' 2>/dev/null)
|
||||||
|
remove_collection "$collection"
|
||||||
|
;;
|
||||||
|
update_hub)
|
||||||
|
update_hub
|
||||||
|
;;
|
||||||
|
upgrade_hub)
|
||||||
|
upgrade_hub
|
||||||
|
;;
|
||||||
|
bouncers)
|
||||||
|
get_bouncers
|
||||||
|
;;
|
||||||
|
control_service)
|
||||||
|
read -r input
|
||||||
|
service=$(echo "$input" | jsonfilter -e '@.service' 2>/dev/null)
|
||||||
|
action=$(echo "$input" | jsonfilter -e '@.action' 2>/dev/null)
|
||||||
|
control_service "$service" "$action"
|
||||||
|
;;
|
||||||
|
nftables_stats)
|
||||||
|
get_nftables_stats
|
||||||
|
;;
|
||||||
|
blocked_ips)
|
||||||
|
get_blocked_ips
|
||||||
|
;;
|
||||||
|
config)
|
||||||
|
get_config
|
||||||
|
;;
|
||||||
|
save_config)
|
||||||
|
read -r input
|
||||||
|
key=$(echo "$input" | jsonfilter -e '@.key' 2>/dev/null)
|
||||||
|
value=$(echo "$input" | jsonfilter -e '@.value' 2>/dev/null)
|
||||||
|
save_config "$key" "$value"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo '{"error":"Unknown method"}'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"admin/services/secubox-crowdsec": {
|
||||||
|
"title": "CrowdSec",
|
||||||
|
"order": 80,
|
||||||
|
"action": {
|
||||||
|
"type": "firstchild"
|
||||||
|
},
|
||||||
|
"depends": {
|
||||||
|
"acl": ["luci-app-secubox-crowdsec"],
|
||||||
|
"uci": { "crowdsec": true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/services/secubox-crowdsec/dashboard": {
|
||||||
|
"title": "Dashboard",
|
||||||
|
"order": 10,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "secubox-crowdsec/dashboard"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/services/secubox-crowdsec/decisions": {
|
||||||
|
"title": "Decisions",
|
||||||
|
"order": 20,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "secubox-crowdsec/decisions"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/services/secubox-crowdsec/alerts": {
|
||||||
|
"title": "Alerts",
|
||||||
|
"order": 30,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "secubox-crowdsec/alerts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/services/secubox-crowdsec/collections": {
|
||||||
|
"title": "Collections",
|
||||||
|
"order": 40,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "secubox-crowdsec/collections"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/services/secubox-crowdsec/settings": {
|
||||||
|
"title": "Settings",
|
||||||
|
"order": 50,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "secubox-crowdsec/settings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"luci-app-secubox-crowdsec": {
|
||||||
|
"description": "Grant access to SecuBox CrowdSec Dashboard",
|
||||||
|
"read": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.secubox-crowdsec": [
|
||||||
|
"status",
|
||||||
|
"decisions",
|
||||||
|
"alerts",
|
||||||
|
"metrics",
|
||||||
|
"stats",
|
||||||
|
"collections",
|
||||||
|
"bouncers",
|
||||||
|
"nftables_stats",
|
||||||
|
"blocked_ips",
|
||||||
|
"config"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uci": [
|
||||||
|
"crowdsec"
|
||||||
|
],
|
||||||
|
"file": {
|
||||||
|
"/etc/crowdsec/*": ["read"],
|
||||||
|
"/etc/crowdsec/acquis.d/*": ["read"],
|
||||||
|
"/var/log/crowdsec*.log": ["read"],
|
||||||
|
"/tmp/log/*.log": ["read"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"write": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.secubox-crowdsec": [
|
||||||
|
"add_decision",
|
||||||
|
"delete_decision",
|
||||||
|
"install_collection",
|
||||||
|
"remove_collection",
|
||||||
|
"update_hub",
|
||||||
|
"upgrade_hub",
|
||||||
|
"control_service",
|
||||||
|
"save_config"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uci": [
|
||||||
|
"crowdsec"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
package/secubox/secubox-crowdsec-setup/Makefile
Normal file
53
package/secubox/secubox-crowdsec-setup/Makefile
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
#
|
||||||
|
# SecuBox CrowdSec Setup Package
|
||||||
|
# Copyright (C) 2025 CyberMind.fr - Gandalf <gandalf@gk2.net>
|
||||||
|
#
|
||||||
|
|
||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NAME:=secubox-crowdsec-setup
|
||||||
|
PKG_VERSION:=1.0.0
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
|
PKG_MAINTAINER:=Gerald Kerma <gandalf@gk2.net>
|
||||||
|
PKG_LICENSE:=MIT
|
||||||
|
|
||||||
|
include $(INCLUDE_DIR)/package.mk
|
||||||
|
|
||||||
|
define Package/secubox-crowdsec-setup
|
||||||
|
SECTION:=secubox
|
||||||
|
CATEGORY:=SecuBox
|
||||||
|
SUBMENU:=Security
|
||||||
|
TITLE:=SecuBox CrowdSec Setup Utility
|
||||||
|
DEPENDS:=+crowdsec +crowdsec-firewall-bouncer +syslog-ng
|
||||||
|
PKGARCH:=all
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-crowdsec-setup/description
|
||||||
|
Script d'installation automatisee de CrowdSec pour SecuBox.
|
||||||
|
Configure syslog-ng pour le forwarding des logs vers CrowdSec,
|
||||||
|
installe les collections de securite, et configure le bouncer
|
||||||
|
nftables pour fw4.
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Build/Compile
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-crowdsec-setup/install
|
||||||
|
$(INSTALL_DIR) $(1)/usr/sbin
|
||||||
|
$(INSTALL_BIN) ./files/usr/sbin/secubox-crowdsec-setup $(1)/usr/sbin/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/etc/secubox/backups/crowdsec
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-crowdsec-setup/postinst
|
||||||
|
#!/bin/sh
|
||||||
|
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||||
|
echo "SecuBox CrowdSec Setup installe."
|
||||||
|
echo "Executez 'secubox-crowdsec-setup' pour configurer CrowdSec."
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call BuildPackage,secubox-crowdsec-setup))
|
||||||
File diff suppressed because it is too large
Load Diff
@ -562,6 +562,69 @@ LUCI_MK
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Synchronize packages from package/secubox to local-feed
|
||||||
|
# This ensures local-feed always matches the canonical source
|
||||||
|
sync_packages_to_local_feed() {
|
||||||
|
local feed_dir="./local-feed"
|
||||||
|
local pkg_src="$REPO_ROOT/package/secubox"
|
||||||
|
|
||||||
|
print_info "Syncing from $pkg_src to $feed_dir"
|
||||||
|
|
||||||
|
# Create local-feed if it doesn't exist
|
||||||
|
mkdir -p "$feed_dir"
|
||||||
|
|
||||||
|
# Sync packages from package/secubox/ to local-feed
|
||||||
|
if [[ -d "$pkg_src" ]]; then
|
||||||
|
print_info "Syncing secubox core packages..."
|
||||||
|
for pkg in "$pkg_src"/*/; do
|
||||||
|
if [[ -d "$pkg" && -f "${pkg}Makefile" ]]; then
|
||||||
|
local pkg_name=$(basename "$pkg")
|
||||||
|
echo " 📦 $pkg_name"
|
||||||
|
rm -rf "$feed_dir/$pkg_name"
|
||||||
|
cp -r "$pkg" "$feed_dir/"
|
||||||
|
|
||||||
|
# Fix Makefile include paths for feed structure
|
||||||
|
if grep -q "../../lang/golang/golang-package.mk" "$feed_dir/$pkg_name/Makefile" 2>/dev/null; then
|
||||||
|
sed -i 's|include.*../../lang/golang/golang-package.mk|include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk|' "$feed_dir/$pkg_name/Makefile"
|
||||||
|
echo " ✓ Fixed golang package include path"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sync luci-app-* packages from repo root
|
||||||
|
print_info "Syncing LuCI app packages..."
|
||||||
|
for pkg in "$REPO_ROOT"/luci-app-*/; do
|
||||||
|
if [[ -d "$pkg" && -f "${pkg}Makefile" ]]; then
|
||||||
|
local pkg_name=$(basename "$pkg")
|
||||||
|
echo " 📁 $pkg_name"
|
||||||
|
rm -rf "$feed_dir/$pkg_name"
|
||||||
|
cp -r "$pkg" "$feed_dir/"
|
||||||
|
|
||||||
|
# Fix Makefile include path for feed structure
|
||||||
|
sed -i 's|include.*luci\.mk|include $(TOPDIR)/feeds/luci/luci.mk|' "$feed_dir/$pkg_name/Makefile"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Sync luci-theme-* packages from repo root
|
||||||
|
print_info "Syncing LuCI theme packages..."
|
||||||
|
for pkg in "$REPO_ROOT"/luci-theme-*/; do
|
||||||
|
if [[ -d "$pkg" && -f "${pkg}Makefile" ]]; then
|
||||||
|
local pkg_name=$(basename "$pkg")
|
||||||
|
echo " 🎨 $pkg_name"
|
||||||
|
rm -rf "$feed_dir/$pkg_name"
|
||||||
|
cp -r "$pkg" "$feed_dir/"
|
||||||
|
|
||||||
|
# Fix Makefile include path for feed structure
|
||||||
|
sed -i 's|include.*luci\.mk|include $(TOPDIR)/feeds/luci/luci.mk|' "$feed_dir/$pkg_name/Makefile"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Count packages
|
||||||
|
local pkg_count=$(ls -d "$feed_dir"/*/ 2>/dev/null | wc -l)
|
||||||
|
print_success "Synchronized $pkg_count packages to local-feed"
|
||||||
|
}
|
||||||
|
|
||||||
# Copy packages to SDK feed
|
# Copy packages to SDK feed
|
||||||
copy_packages() {
|
copy_packages() {
|
||||||
local single_package="$1"
|
local single_package="$1"
|
||||||
@ -2015,7 +2078,8 @@ COMMANDS:
|
|||||||
debug-firmware <device> Debug firmware build (check config without building)
|
debug-firmware <device> Debug firmware build (check config without building)
|
||||||
full Run validation then build
|
full Run validation then build
|
||||||
clean Clean build directories
|
clean Clean build directories
|
||||||
clean-all Clean all build directories including OpenWrt source
|
clean-all Clean all build directories including OpenWrt source and local-feed
|
||||||
|
sync Sync packages from package/secubox to local-feed
|
||||||
help Show this help message
|
help Show this help message
|
||||||
|
|
||||||
PACKAGES:
|
PACKAGES:
|
||||||
@ -2180,8 +2244,14 @@ main() {
|
|||||||
|
|
||||||
clean-all)
|
clean-all)
|
||||||
print_header "Cleaning All Build Directories"
|
print_header "Cleaning All Build Directories"
|
||||||
rm -rf "$SDK_DIR" "$BUILD_DIR" "$OPENWRT_DIR" "$CACHE_DIR"
|
rm -rf "$SDK_DIR" "$BUILD_DIR" "$OPENWRT_DIR" "$CACHE_DIR" "./local-feed"
|
||||||
print_success "All build directories cleaned (SDK, build, OpenWrt source, cache)"
|
print_success "All build directories cleaned (SDK, build, OpenWrt source, cache, local-feed)"
|
||||||
|
;;
|
||||||
|
|
||||||
|
sync)
|
||||||
|
print_header "Synchronizing packages to local-feed"
|
||||||
|
sync_packages_to_local_feed
|
||||||
|
print_success "Packages synchronized to local-feed"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
help|--help|-h)
|
help|--help|-h)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user