From e6835828af15a8ba59224b2eb97a202321661df6 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Tue, 13 Jan 2026 16:50:53 +0100 Subject: [PATCH] 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 --- .../luci-app-secubox-crowdsec/Makefile | 70 ++ .../resources/secubox-crowdsec/api.js | 191 +++ .../resources/view/secubox-crowdsec/alerts.js | 117 ++ .../view/secubox-crowdsec/collections.js | 209 ++++ .../view/secubox-crowdsec/dashboard.js | 162 +++ .../view/secubox-crowdsec/decisions.js | 139 +++ .../view/secubox-crowdsec/settings.js | 116 ++ .../root/etc/config/crowdsec | 25 + .../usr/libexec/rpcd/luci.secubox-crowdsec | 691 +++++++++++ .../menu.d/luci-app-secubox-crowdsec.json | 53 + .../rpcd/acl.d/luci-app-secubox-crowdsec.json | 47 + .../secubox/secubox-crowdsec-setup/Makefile | 53 + .../files/usr/sbin/secubox-crowdsec-setup | 1052 +++++++++++++++++ secubox-tools/local-build.sh | 76 +- 14 files changed, 2998 insertions(+), 3 deletions(-) create mode 100644 package/secubox/luci-app-secubox-crowdsec/Makefile create mode 100644 package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/secubox-crowdsec/api.js create mode 100644 package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/view/secubox-crowdsec/alerts.js create mode 100644 package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/view/secubox-crowdsec/collections.js create mode 100644 package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/view/secubox-crowdsec/dashboard.js create mode 100644 package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/view/secubox-crowdsec/decisions.js create mode 100644 package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/view/secubox-crowdsec/settings.js create mode 100644 package/secubox/luci-app-secubox-crowdsec/root/etc/config/crowdsec create mode 100755 package/secubox/luci-app-secubox-crowdsec/root/usr/libexec/rpcd/luci.secubox-crowdsec create mode 100644 package/secubox/luci-app-secubox-crowdsec/root/usr/share/luci/menu.d/luci-app-secubox-crowdsec.json create mode 100644 package/secubox/luci-app-secubox-crowdsec/root/usr/share/rpcd/acl.d/luci-app-secubox-crowdsec.json create mode 100644 package/secubox/secubox-crowdsec-setup/Makefile create mode 100644 package/secubox/secubox-crowdsec-setup/files/usr/sbin/secubox-crowdsec-setup diff --git a/package/secubox/luci-app-secubox-crowdsec/Makefile b/package/secubox/luci-app-secubox-crowdsec/Makefile new file mode 100644 index 00000000..96a61ea4 --- /dev/null +++ b/package/secubox/luci-app-secubox-crowdsec/Makefile @@ -0,0 +1,70 @@ +# SPDX-License-Identifier: MIT +# +# LuCI SecuBox CrowdSec Dashboard +# Copyright (C) 2025 CyberMind.fr - Gandalf +# + +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 +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)) diff --git a/package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/secubox-crowdsec/api.js b/package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/secubox-crowdsec/api.js new file mode 100644 index 00000000..18de65e7 --- /dev/null +++ b/package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/secubox-crowdsec/api.js @@ -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); + } +}); diff --git a/package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/view/secubox-crowdsec/alerts.js b/package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/view/secubox-crowdsec/alerts.js new file mode 100644 index 00000000..49867012 --- /dev/null +++ b/package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/view/secubox-crowdsec/alerts.js @@ -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 +}); diff --git a/package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/view/secubox-crowdsec/collections.js b/package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/view/secubox-crowdsec/collections.js new file mode 100644 index 00000000..1986e307 --- /dev/null +++ b/package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/view/secubox-crowdsec/collections.js @@ -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 +}); diff --git a/package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/view/secubox-crowdsec/dashboard.js b/package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/view/secubox-crowdsec/dashboard.js new file mode 100644 index 00000000..e4f3eaca --- /dev/null +++ b/package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/view/secubox-crowdsec/dashboard.js @@ -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 +}); diff --git a/package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/view/secubox-crowdsec/decisions.js b/package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/view/secubox-crowdsec/decisions.js new file mode 100644 index 00000000..371e7c39 --- /dev/null +++ b/package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/view/secubox-crowdsec/decisions.js @@ -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 +}); diff --git a/package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/view/secubox-crowdsec/settings.js b/package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/view/secubox-crowdsec/settings.js new file mode 100644 index 00000000..e4cc05ac --- /dev/null +++ b/package/secubox/luci-app-secubox-crowdsec/htdocs/luci-static/resources/view/secubox-crowdsec/settings.js @@ -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(); + } +}); diff --git a/package/secubox/luci-app-secubox-crowdsec/root/etc/config/crowdsec b/package/secubox/luci-app-secubox-crowdsec/root/etc/config/crowdsec new file mode 100644 index 00000000..95313df8 --- /dev/null +++ b/package/secubox/luci-app-secubox-crowdsec/root/etc/config/crowdsec @@ -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' diff --git a/package/secubox/luci-app-secubox-crowdsec/root/usr/libexec/rpcd/luci.secubox-crowdsec b/package/secubox/luci-app-secubox-crowdsec/root/usr/libexec/rpcd/luci.secubox-crowdsec new file mode 100755 index 00000000..f79de565 --- /dev/null +++ b/package/secubox/luci-app-secubox-crowdsec/root/usr/libexec/rpcd/luci.secubox-crowdsec @@ -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 diff --git a/package/secubox/luci-app-secubox-crowdsec/root/usr/share/luci/menu.d/luci-app-secubox-crowdsec.json b/package/secubox/luci-app-secubox-crowdsec/root/usr/share/luci/menu.d/luci-app-secubox-crowdsec.json new file mode 100644 index 00000000..308a849b --- /dev/null +++ b/package/secubox/luci-app-secubox-crowdsec/root/usr/share/luci/menu.d/luci-app-secubox-crowdsec.json @@ -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" + } + } +} diff --git a/package/secubox/luci-app-secubox-crowdsec/root/usr/share/rpcd/acl.d/luci-app-secubox-crowdsec.json b/package/secubox/luci-app-secubox-crowdsec/root/usr/share/rpcd/acl.d/luci-app-secubox-crowdsec.json new file mode 100644 index 00000000..b022d4a3 --- /dev/null +++ b/package/secubox/luci-app-secubox-crowdsec/root/usr/share/rpcd/acl.d/luci-app-secubox-crowdsec.json @@ -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" + ] + } + } +} diff --git a/package/secubox/secubox-crowdsec-setup/Makefile b/package/secubox/secubox-crowdsec-setup/Makefile new file mode 100644 index 00000000..d9596748 --- /dev/null +++ b/package/secubox/secubox-crowdsec-setup/Makefile @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: MIT +# +# SecuBox CrowdSec Setup Package +# Copyright (C) 2025 CyberMind.fr - Gandalf +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=secubox-crowdsec-setup +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=Gerald Kerma +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)) diff --git a/package/secubox/secubox-crowdsec-setup/files/usr/sbin/secubox-crowdsec-setup b/package/secubox/secubox-crowdsec-setup/files/usr/sbin/secubox-crowdsec-setup new file mode 100644 index 00000000..0b5728ad --- /dev/null +++ b/package/secubox/secubox-crowdsec-setup/files/usr/sbin/secubox-crowdsec-setup @@ -0,0 +1,1052 @@ +#!/bin/sh +# SPDX-License-Identifier: MIT +# SecuBox CrowdSec Setup Script pour OpenWrt 24.10+ +# Copyright (C) 2025 CyberMind.fr - Gandalf +# +# Script d'installation automatisee de CrowdSec avec : +# - syslog-ng4 pour le forwarding des logs +# - crowdsec-firewall-bouncer-nftables pour fw4 +# - Integration complete avec SecuBox +# +# Usage: secubox-crowdsec-setup [--uninstall|--status|--repair] + +set -e + +# ============================================================================= +# CONFIGURATION +# ============================================================================= + +VERSION="1.0.0" +SCRIPT_NAME="secubox-crowdsec-setup" +LOG_TAG="secubox-crowdsec" + +# Repertoires +CROWDSEC_CONFIG_DIR="/etc/crowdsec" +CROWDSEC_DATA_DIR="/srv/crowdsec/data" +CROWDSEC_HUB_DIR="/etc/crowdsec/hub" +SYSLOG_NG_DIR="/etc/syslog-ng" +BACKUP_DIR="/etc/secubox/backups/crowdsec" + +# Ports et adresses +SYSLOG_LISTEN_ADDR="127.0.0.1" +SYSLOG_LISTEN_PORT="5140" +LAPI_LISTEN_ADDR="127.0.0.1" +LAPI_LISTEN_PORT="8080" + +# Prérequis +MIN_FLASH_MB=50 +MIN_RAM_MB=256 +MIN_OPENWRT_MAJOR=24 + +# Collections a installer +COLLECTIONS="crowdsecurity/linux crowdsecurity/sshd crowdsecurity/http-cve crowdsecurity/iptables" + +# Couleurs (si terminal supporte) +if [ -t 1 ]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[1;33m' + BLUE='\033[0;34m' + NC='\033[0m' +else + RED='' + GREEN='' + YELLOW='' + BLUE='' + NC='' +fi + +# ============================================================================= +# FONCTIONS UTILITAIRES +# ============================================================================= + +# Journalisation +log_info() { + printf "${GREEN}[INFO]${NC} %s\n" "$1" + logger -t "$LOG_TAG" -p info "$1" +} + +log_warn() { + printf "${YELLOW}[WARN]${NC} %s\n" "$1" >&2 + logger -t "$LOG_TAG" -p warning "$1" +} + +log_error() { + printf "${RED}[ERREUR]${NC} %s\n" "$1" >&2 + logger -t "$LOG_TAG" -p err "$1" +} + +log_step() { + printf "\n${BLUE}==> %s${NC}\n" "$1" +} + +# Verification de commande disponible +check_cmd() { + command -v "$1" >/dev/null 2>&1 +} + +# Lecture securisee d'une valeur UCI +uci_get() { + uci -q get "$1" 2>/dev/null || echo "" +} + +# Ecriture UCI avec creation de section si necessaire +uci_set() { + uci -q set "$1" 2>/dev/null +} + +# Verification que le script tourne en root +check_root() { + if [ "$(id -u)" != "0" ]; then + log_error "Ce script doit etre execute en tant que root" + exit 1 + fi +} + +# Detection de l'architecture +detect_arch() { + ARCH=$(uname -m) + case "$ARCH" in + aarch64) + ARCH_NAME="arm64" + ;; + armv7l|armv7) + ARCH_NAME="armv7" + ;; + x86_64) + ARCH_NAME="x86_64" + ;; + mips|mipsel) + ARCH_NAME="mips" + ;; + *) + ARCH_NAME="$ARCH" + ;; + esac + log_info "Architecture detectee: $ARCH_NAME ($ARCH)" +} + +# Detection de la version OpenWrt +detect_openwrt_version() { + if [ -f /etc/openwrt_release ]; then + . /etc/openwrt_release + OPENWRT_VERSION="${DISTRIB_RELEASE:-unknown}" + OPENWRT_CODENAME="${DISTRIB_CODENAME:-unknown}" + + # Extraire le numero de version majeur + OPENWRT_MAJOR=$(echo "$OPENWRT_VERSION" | cut -d'.' -f1) + + log_info "Version OpenWrt: $OPENWRT_VERSION ($OPENWRT_CODENAME)" + else + log_error "Fichier /etc/openwrt_release non trouve. Est-ce bien OpenWrt?" + exit 1 + fi +} + +# Verification des prerequis materiels +check_prerequisites() { + log_step "Verification des prerequis" + + local errors=0 + + # Verification version OpenWrt + if [ "$OPENWRT_MAJOR" -lt "$MIN_OPENWRT_MAJOR" ]; then + log_error "OpenWrt $MIN_OPENWRT_MAJOR+ requis (detecte: $OPENWRT_VERSION)" + errors=$((errors + 1)) + else + log_info "Version OpenWrt OK: $OPENWRT_VERSION" + fi + + # Verification espace flash (en Mo) + # Utiliser df sur /overlay qui represente l'espace disponible + if [ -d /overlay ]; then + FLASH_AVAIL=$(df -m /overlay 2>/dev/null | awk 'NR==2 {print $4}') + else + # Fallback sur / + FLASH_AVAIL=$(df -m / 2>/dev/null | awk 'NR==2 {print $4}') + fi + + if [ -n "$FLASH_AVAIL" ] && [ "$FLASH_AVAIL" -lt "$MIN_FLASH_MB" ]; then + log_error "Espace flash insuffisant: ${FLASH_AVAIL}Mo disponible, ${MIN_FLASH_MB}Mo requis" + errors=$((errors + 1)) + else + log_info "Espace flash OK: ${FLASH_AVAIL:-?}Mo disponible" + fi + + # Verification RAM + RAM_TOTAL=$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo) + if [ "$RAM_TOTAL" -lt "$MIN_RAM_MB" ]; then + log_error "RAM insuffisante: ${RAM_TOTAL}Mo detecte, ${MIN_RAM_MB}Mo requis" + errors=$((errors + 1)) + else + log_info "RAM OK: ${RAM_TOTAL}Mo" + fi + + # Verification nftables/fw4 + if ! check_cmd nft; then + log_error "nftables non disponible. fw4 requis pour OpenWrt 24.10+" + errors=$((errors + 1)) + else + log_info "nftables OK" + fi + + # Verification connectivite Internet + if ! ping -c 1 -W 3 downloads.openwrt.org >/dev/null 2>&1; then + log_warn "Pas de connectivite Internet detectee" + # Pas fatal, l'utilisateur peut avoir des paquets locaux + else + log_info "Connectivite Internet OK" + fi + + if [ "$errors" -gt 0 ]; then + log_error "$errors erreur(s) de prerequis. Installation annulee." + exit 1 + fi + + log_info "Tous les prerequis sont satisfaits" +} + +# ============================================================================= +# SAUVEGARDE ET RESTAURATION +# ============================================================================= + +backup_configs() { + log_step "Sauvegarde des configurations existantes" + + BACKUP_TIMESTAMP=$(date +%Y%m%d_%H%M%S) + BACKUP_PATH="${BACKUP_DIR}/${BACKUP_TIMESTAMP}" + + mkdir -p "$BACKUP_PATH" + + # Sauvegarder les configs existantes + [ -d "$CROWDSEC_CONFIG_DIR" ] && cp -a "$CROWDSEC_CONFIG_DIR" "$BACKUP_PATH/" 2>/dev/null || true + [ -d "$SYSLOG_NG_DIR" ] && cp -a "$SYSLOG_NG_DIR" "$BACKUP_PATH/" 2>/dev/null || true + [ -f /etc/config/system ] && cp /etc/config/system "$BACKUP_PATH/" 2>/dev/null || true + + # Sauvegarder la liste des paquets installes + opkg list-installed > "$BACKUP_PATH/packages.list" 2>/dev/null || true + + log_info "Sauvegarde creee dans $BACKUP_PATH" + + # Garder les 5 dernieres sauvegardes + ls -dt "${BACKUP_DIR}"/*/ 2>/dev/null | tail -n +6 | xargs rm -rf 2>/dev/null || true +} + +rollback() { + log_error "Erreur detectee, tentative de rollback..." + + # Trouver la derniere sauvegarde + LAST_BACKUP=$(ls -dt "${BACKUP_DIR}"/*/ 2>/dev/null | head -1) + + if [ -z "$LAST_BACKUP" ]; then + log_error "Aucune sauvegarde disponible pour rollback" + return 1 + fi + + log_info "Restauration depuis $LAST_BACKUP" + + # Arreter les services + /etc/init.d/crowdsec stop 2>/dev/null || true + /etc/init.d/crowdsec-firewall-bouncer stop 2>/dev/null || true + /etc/init.d/syslog-ng stop 2>/dev/null || true + + # Restaurer les configs + [ -d "$LAST_BACKUP/crowdsec" ] && cp -a "$LAST_BACKUP/crowdsec"/* "$CROWDSEC_CONFIG_DIR/" 2>/dev/null || true + [ -d "$LAST_BACKUP/syslog-ng" ] && cp -a "$LAST_BACKUP/syslog-ng"/* "$SYSLOG_NG_DIR/" 2>/dev/null || true + [ -f "$LAST_BACKUP/system" ] && cp "$LAST_BACKUP/system" /etc/config/system 2>/dev/null || true + + # Reactiver logd si necessaire + /etc/init.d/log enable 2>/dev/null || true + /etc/init.d/log start 2>/dev/null || true + + log_warn "Rollback effectue. Verifiez l'etat du systeme." +} + +# ============================================================================= +# INSTALLATION DES PAQUETS +# ============================================================================= + +install_packages() { + log_step "Installation des paquets" + + # Mise a jour de la liste des paquets + log_info "Mise a jour de la liste des paquets..." + opkg update || { + log_error "Echec de la mise a jour opkg" + return 1 + } + + # Liste des paquets a installer + # Note: syslog-ng4 pour OpenWrt 24.10+ (pas syslog-ng3) + PACKAGES="crowdsec crowdsec-firewall-bouncer-nftables syslog-ng4" + + # Installation des paquets + for pkg in $PACKAGES; do + log_info "Installation de $pkg..." + if opkg install "$pkg"; then + log_info "$pkg installe avec succes" + else + # Verifier si deja installe + if opkg list-installed | grep -q "^$pkg "; then + log_warn "$pkg deja installe" + else + log_error "Echec installation de $pkg" + return 1 + fi + fi + done + + # Verifier que cscli est disponible + if ! check_cmd cscli; then + log_error "cscli non disponible apres installation" + return 1 + fi + + log_info "Tous les paquets installes avec succes" +} + +# ============================================================================= +# CONFIGURATION SYSLOG-NG4 +# ============================================================================= + +configure_syslog_ng() { + log_step "Configuration de syslog-ng4" + + mkdir -p "$SYSLOG_NG_DIR" + mkdir -p /tmp/log + + # Configuration principale syslog-ng + cat > "${SYSLOG_NG_DIR}/syslog-ng.conf" << 'SYSLOGCONF' +# Configuration syslog-ng4 pour SecuBox/CrowdSec +# Genere automatiquement par secubox-crowdsec-setup +# Ne pas modifier manuellement - utiliser UCI + +@version: 4.2 +@include "scl.conf" + +# Options globales +options { + chain_hostnames(off); + flush_lines(0); + use_dns(no); + use_fqdn(no); + dns_cache(no); + owner("root"); + group("root"); + perm(0640); + stats_freq(0); + bad_hostname("^gconfd$"); + keep_hostname(yes); + create_dirs(yes); +}; + +# Sources +# Source systeme principale (socket Unix) +source s_sys { + unix-dgram("/dev/log"); + internal(); +}; + +# Source kernel +source s_kernel { + file("/proc/kmsg" program_override("kernel")); +}; + +# Destinations +# Destination CrowdSec (UDP local) +destination d_crowdsec { + syslog("127.0.0.1" + transport("udp") + port(5140) + ); +}; + +# Destination fichier debug (optionnel, dans tmpfs) +destination d_messages { + file("/tmp/log/messages" + template("${DATE} ${HOST} ${MSGHDR}${MSG}\n") + create_dirs(yes) + ); +}; + +# Destination kernel log +destination d_kern { + file("/tmp/log/kern.log" + template("${DATE} ${HOST} ${MSGHDR}${MSG}\n") + ); +}; + +# Destination auth log (SSH, etc.) +destination d_auth { + file("/tmp/log/auth.log" + template("${DATE} ${HOST} ${MSGHDR}${MSG}\n") + ); +}; + +# Filtres +filter f_kern { facility(kern); }; +filter f_auth { facility(auth) or facility(authpriv); }; +filter f_messages { level(info..emerg) and not facility(auth, authpriv, kern); }; + +# Log paths +# Tout vers CrowdSec +log { source(s_sys); source(s_kernel); destination(d_crowdsec); }; + +# Fichiers locaux pour debug +log { source(s_sys); filter(f_messages); destination(d_messages); }; +log { source(s_kernel); filter(f_kern); destination(d_kern); }; +log { source(s_sys); filter(f_auth); destination(d_auth); }; +SYSLOGCONF + + log_info "Configuration syslog-ng4 creee" + + # Desactiver logd natif OpenWrt + log_info "Desactivation de logd natif..." + /etc/init.d/log stop 2>/dev/null || true + /etc/init.d/log disable 2>/dev/null || true + + # Configurer UCI pour desactiver le log buffer interne + uci_set system.@system[0].log_size='0' + uci_set system.@system[0].log_buffer_size='0' + uci commit system 2>/dev/null || true + + # Activer et demarrer syslog-ng + /etc/init.d/syslog-ng enable + /etc/init.d/syslog-ng start || { + log_error "Echec demarrage syslog-ng" + return 1 + } + + # Verifier que syslog-ng fonctionne + sleep 2 + if pgrep -f "syslog-ng" >/dev/null; then + log_info "syslog-ng4 demarre avec succes" + else + log_error "syslog-ng4 ne semble pas fonctionner" + return 1 + fi +} + +# ============================================================================= +# CONFIGURATION CROWDSEC +# ============================================================================= + +configure_crowdsec() { + log_step "Configuration de CrowdSec" + + mkdir -p "$CROWDSEC_CONFIG_DIR" + mkdir -p "$CROWDSEC_DATA_DIR" + mkdir -p "$CROWDSEC_HUB_DIR" + mkdir -p "${CROWDSEC_CONFIG_DIR}/acquis.d" + + # Configuration acquisition syslog + cat > "${CROWDSEC_CONFIG_DIR}/acquis.d/syslog.yaml" << ACQUISCONF +# Acquisition syslog pour OpenWrt/SecuBox +# Source: syslog-ng4 via UDP + +source: syslog +listen_addr: ${SYSLOG_LISTEN_ADDR} +listen_port: ${SYSLOG_LISTEN_PORT} +labels: + type: syslog +ACQUISCONF + + log_info "Configuration acquisition syslog creee" + + # Verifier/corriger config.yaml + if [ -f "${CROWDSEC_CONFIG_DIR}/config.yaml" ]; then + # S'assurer que les chemins sont corrects + sed -i "s|data_dir:.*|data_dir: ${CROWDSEC_DATA_DIR}/|" "${CROWDSEC_CONFIG_DIR}/config.yaml" + sed -i "s|db_path:.*|db_path: ${CROWDSEC_DATA_DIR}/crowdsec.db|" "${CROWDSEC_CONFIG_DIR}/config.yaml" + log_info "Configuration CrowdSec mise a jour" + fi + + # Creer la configuration UCI pour CrowdSec + if [ ! -f /etc/config/crowdsec ]; then + cat > /etc/config/crowdsec << 'UCICONF' +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' +UCICONF + log_info "Configuration UCI CrowdSec creee" + fi +} + +# ============================================================================= +# ENREGISTREMENT CAPI ET INSTALLATION COLLECTIONS +# ============================================================================= + +register_and_setup_hub() { + log_step "Enregistrement CAPI et installation des collections" + + # Demarrer CrowdSec temporairement pour les commandes cscli + /etc/init.d/crowdsec start 2>/dev/null || true + sleep 3 + + # Enregistrement CAPI (Central API) + log_info "Enregistrement aupres de la CAPI CrowdSec..." + if cscli capi register 2>/dev/null; then + log_info "Enregistrement CAPI reussi" + else + log_warn "Echec enregistrement CAPI (peut-etre deja enregistre)" + fi + + # Mise a jour du hub + log_info "Mise a jour du hub CrowdSec..." + cscli hub update || log_warn "Echec mise a jour hub" + + # Installation des collections + log_info "Installation des collections..." + for collection in $COLLECTIONS; do + log_info "Installation de $collection..." + if cscli collections install "$collection" 2>/dev/null; then + log_info "Collection $collection installee" + else + log_warn "Echec installation de $collection (peut-etre deja installee)" + fi + done + + # Lister les collections installees + log_info "Collections installees:" + cscli collections list 2>/dev/null || true +} + +# ============================================================================= +# CONFIGURATION FIREWALL BOUNCER +# ============================================================================= + +configure_bouncer() { + log_step "Configuration du firewall bouncer" + + # Generer la cle API pour le bouncer + log_info "Generation de la cle API bouncer..." + + # Supprimer l'ancien bouncer s'il existe + cscli bouncers delete crowdsec-firewall-bouncer 2>/dev/null || true + + # Creer le nouveau bouncer et recuperer la cle + API_KEY=$(cscli bouncers add crowdsec-firewall-bouncer -o raw 2>/dev/null) + + if [ -z "$API_KEY" ] || [ "${#API_KEY}" -lt 10 ]; then + log_error "Echec generation de la cle API bouncer" + return 1 + fi + + log_info "Cle API bouncer generee" + + # Configuration du bouncer nftables + mkdir -p /etc/crowdsec/bouncers + + cat > /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml << BOUNCERCONF +# Configuration Firewall Bouncer pour OpenWrt/SecuBox +# Genere automatiquement par secubox-crowdsec-setup + +mode: nftables +pid_dir: /var/run/ +update_frequency: 10s +daemonize: true +log_mode: file +log_dir: /var/log/ +log_level: info +log_compression: true +log_max_size: 10 +log_max_backups: 3 +log_max_age: 30 + +api_url: http://${LAPI_LISTEN_ADDR}:${LAPI_LISTEN_PORT}/ +api_key: ${API_KEY} + +disable_ipv6: false + +# Configuration nftables optimisee pour fw4 +nftables: + ipv4: + enabled: true + set-only: true + table: crowdsec + chain: crowdsec-chain + priority: -10 + ipv6: + enabled: true + set-only: true + table: crowdsec6 + chain: crowdsec6-chain + priority: -10 + +deny_action: drop +deny_log: true +deny_log_prefix: "crowdsec: " + +# Timers +blacklists_ipv4: crowdsec-blacklists +blacklists_ipv6: crowdsec6-blacklists +BOUNCERCONF + + log_info "Configuration bouncer creee" + + # Sauvegarder la cle API dans UCI + uci_set crowdsec.bouncer.api_key="$API_KEY" + uci commit crowdsec 2>/dev/null || true +} + +# ============================================================================= +# CONFIGURATION WHITELIST +# ============================================================================= + +configure_whitelist() { + log_step "Configuration des whitelists" + + # Creer un fichier de whitelist pour les reseaux locaux + cat > "${CROWDSEC_CONFIG_DIR}/parsers/s02-enrich/secubox-whitelist.yaml" << 'WHITELIST' +# Whitelist SecuBox - Reseaux locaux +# Ne pas bannir les adresses RFC1918 et link-local + +name: secubox/whitelists +description: "Whitelist des reseaux locaux SecuBox" +whitelist: + reason: "Reseau local SecuBox" + ip: + - "127.0.0.0/8" + - "10.0.0.0/8" + - "172.16.0.0/12" + - "192.168.0.0/16" + - "169.254.0.0/16" + - "::1/128" + - "fe80::/10" + - "fc00::/7" +WHITELIST + + log_info "Whitelist reseaux locaux creee" +} + +# ============================================================================= +# DEMARRAGE DES SERVICES +# ============================================================================= + +start_services() { + log_step "Demarrage des services" + + # Ordre de demarrage: syslog-ng -> crowdsec -> bouncer + + # Redemarrer syslog-ng pour s'assurer qu'il fonctionne + log_info "Redemarrage syslog-ng..." + /etc/init.d/syslog-ng restart || log_warn "Probleme redemarrage syslog-ng" + sleep 2 + + # Demarrer CrowdSec + log_info "Demarrage CrowdSec..." + /etc/init.d/crowdsec enable + /etc/init.d/crowdsec restart || { + log_error "Echec demarrage CrowdSec" + return 1 + } + sleep 3 + + # Verifier que LAPI est disponible + local retries=10 + while [ $retries -gt 0 ]; do + if cscli lapi status >/dev/null 2>&1; then + log_info "LAPI CrowdSec disponible" + break + fi + retries=$((retries - 1)) + sleep 1 + done + + if [ $retries -eq 0 ]; then + log_error "LAPI CrowdSec non disponible apres 10 secondes" + return 1 + fi + + # Demarrer le bouncer + log_info "Demarrage du firewall bouncer..." + /etc/init.d/crowdsec-firewall-bouncer enable + /etc/init.d/crowdsec-firewall-bouncer restart || { + log_error "Echec demarrage bouncer" + return 1 + } + sleep 2 + + # Verifier que les services tournent + log_info "Verification des services..." + + local all_ok=1 + + if pgrep -f "syslog-ng" >/dev/null; then + log_info " syslog-ng: OK" + else + log_error " syslog-ng: ECHEC" + all_ok=0 + fi + + if pgrep -x crowdsec >/dev/null; then + log_info " crowdsec: OK" + else + log_error " crowdsec: ECHEC" + all_ok=0 + fi + + if pgrep -f "crowdsec-firewall-bouncer" >/dev/null; then + log_info " bouncer: OK" + else + log_error " bouncer: ECHEC" + all_ok=0 + fi + + # Verifier les tables nftables + if nft list tables 2>/dev/null | grep -q "crowdsec"; then + log_info " nftables crowdsec: OK" + else + log_warn " nftables crowdsec: Tables non creees (normal au premier demarrage)" + fi + + if [ "$all_ok" -eq 0 ]; then + return 1 + fi +} + +# ============================================================================= +# VERIFICATION ET RAPPORT +# ============================================================================= + +verify_installation() { + log_step "Verification de l'installation" + + local errors=0 + + # Test connectivite CAPI + log_info "Test connectivite CAPI..." + if cscli capi status 2>/dev/null | grep -qi "online\|connected"; then + log_info " CAPI: Connecte" + else + log_warn " CAPI: Deconnecte (verifier la connectivite Internet)" + fi + + # Afficher les metriques + log_info "Metriques CrowdSec:" + cscli metrics 2>/dev/null | head -30 || log_warn " Impossible de recuperer les metriques" + + # Lister les bouncers + log_info "Bouncers enregistres:" + cscli bouncers list 2>/dev/null || log_warn " Impossible de lister les bouncers" + + # Verifier l'acquisition + log_info "Sources d'acquisition:" + cscli metrics show acquisition 2>/dev/null | head -10 || true +} + +generate_report() { + log_step "Rapport d'installation" + + REPORT_FILE="/tmp/secubox-crowdsec-install-report.txt" + + cat > "$REPORT_FILE" << REPORT +================================================================================ + RAPPORT D'INSTALLATION SECUBOX CROWDSEC +================================================================================ +Date: $(date) +Version script: $VERSION +Systeme: OpenWrt $OPENWRT_VERSION ($OPENWRT_CODENAME) +Architecture: $ARCH_NAME + +SERVICES INSTALLES: +------------------- +$(opkg list-installed | grep -E "crowdsec|syslog-ng" || echo "Erreur listing paquets") + +STATUT SERVICES: +---------------- +syslog-ng: $(pgrep -f syslog-ng >/dev/null && echo "ACTIF" || echo "INACTIF") +crowdsec: $(pgrep -x crowdsec >/dev/null && echo "ACTIF" || echo "INACTIF") +bouncer: $(pgrep -f crowdsec-firewall-bouncer >/dev/null && echo "ACTIF" || echo "INACTIF") + +COLLECTIONS INSTALLEES: +----------------------- +$(cscli collections list 2>/dev/null || echo "Erreur listing collections") + +BOUNCERS: +--------- +$(cscli bouncers list 2>/dev/null || echo "Erreur listing bouncers") + +TABLES NFTABLES: +---------------- +$(nft list tables 2>/dev/null | grep crowdsec || echo "Aucune table crowdsec") + +CONFIGURATION: +-------------- +Syslog listen: ${SYSLOG_LISTEN_ADDR}:${SYSLOG_LISTEN_PORT} +LAPI listen: ${LAPI_LISTEN_ADDR}:${LAPI_LISTEN_PORT} +Data dir: ${CROWDSEC_DATA_DIR} +Config dir: ${CROWDSEC_CONFIG_DIR} + +COMMANDES UTILES: +----------------- +# Voir les decisions actives +cscli decisions list + +# Voir les alertes +cscli alerts list + +# Bannir une IP manuellement +cscli decisions add --ip --duration 24h --reason "Ban manuel" + +# Debannir une IP +cscli decisions delete --ip + +# Mettre a jour le hub +cscli hub update && cscli hub upgrade + +# Voir les metriques +cscli metrics + +# Logs CrowdSec +logread | grep crowdsec +cat /var/log/crowdsec.log + +================================================================================ +REPORT + + log_info "Rapport genere: $REPORT_FILE" + cat "$REPORT_FILE" +} + +# ============================================================================= +# DESINSTALLATION +# ============================================================================= + +uninstall() { + log_step "Desinstallation de CrowdSec SecuBox" + + # Confirmation + printf "ATTENTION: Ceci va supprimer CrowdSec et ses configurations.\n" + printf "Continuer? [y/N] " + read -r confirm + case "$confirm" in + [yY][eE][sS]|[yY]) + ;; + *) + log_info "Desinstallation annulee" + exit 0 + ;; + esac + + # Arreter les services + log_info "Arret des services..." + /etc/init.d/crowdsec-firewall-bouncer stop 2>/dev/null || true + /etc/init.d/crowdsec-firewall-bouncer disable 2>/dev/null || true + /etc/init.d/crowdsec stop 2>/dev/null || true + /etc/init.d/crowdsec disable 2>/dev/null || true + /etc/init.d/syslog-ng stop 2>/dev/null || true + /etc/init.d/syslog-ng disable 2>/dev/null || true + + # Supprimer les tables nftables + log_info "Suppression des tables nftables..." + nft delete table ip crowdsec 2>/dev/null || true + nft delete table ip6 crowdsec6 2>/dev/null || true + + # Desinstaller les paquets + log_info "Desinstallation des paquets..." + opkg remove crowdsec-firewall-bouncer-nftables 2>/dev/null || true + opkg remove crowdsec 2>/dev/null || true + opkg remove syslog-ng4 2>/dev/null || true + + # Reactiver logd + log_info "Reactivation de logd..." + /etc/init.d/log enable 2>/dev/null || true + /etc/init.d/log start 2>/dev/null || true + + # Optionnel: supprimer les configurations + printf "Supprimer les fichiers de configuration? [y/N] " + read -r confirm_conf + case "$confirm_conf" in + [yY][eE][sS]|[yY]) + rm -rf "$CROWDSEC_CONFIG_DIR" 2>/dev/null || true + rm -rf "$CROWDSEC_DATA_DIR" 2>/dev/null || true + rm -rf "$SYSLOG_NG_DIR" 2>/dev/null || true + rm -f /etc/config/crowdsec 2>/dev/null || true + log_info "Configurations supprimees" + ;; + *) + log_info "Configurations conservees" + ;; + esac + + log_info "Desinstallation terminee" +} + +# ============================================================================= +# STATUT +# ============================================================================= + +show_status() { + log_step "Statut CrowdSec SecuBox" + + printf "\n${BLUE}Services:${NC}\n" + printf " syslog-ng: " + if pgrep -f "syslog-ng" >/dev/null; then + printf "${GREEN}ACTIF${NC}\n" + else + printf "${RED}INACTIF${NC}\n" + fi + + printf " crowdsec: " + if pgrep -x crowdsec >/dev/null; then + printf "${GREEN}ACTIF${NC}\n" + else + printf "${RED}INACTIF${NC}\n" + fi + + printf " bouncer: " + if pgrep -f "crowdsec-firewall-bouncer" >/dev/null; then + printf "${GREEN}ACTIF${NC}\n" + else + printf "${RED}INACTIF${NC}\n" + fi + + printf "\n${BLUE}LAPI Status:${NC}\n" + cscli lapi status 2>/dev/null || printf " ${RED}Non disponible${NC}\n" + + printf "\n${BLUE}Decisions actives:${NC}\n" + cscli decisions list 2>/dev/null | head -10 || printf " Aucune ou erreur\n" + + printf "\n${BLUE}Tables nftables:${NC}\n" + nft list tables 2>/dev/null | grep crowdsec || printf " ${YELLOW}Aucune table crowdsec${NC}\n" +} + +# ============================================================================= +# REPARATION +# ============================================================================= + +repair() { + log_step "Reparation de l'installation CrowdSec" + + # Arreter les services + /etc/init.d/crowdsec-firewall-bouncer stop 2>/dev/null || true + /etc/init.d/crowdsec stop 2>/dev/null || true + + # Verifier/recreer les repertoires + mkdir -p "$CROWDSEC_DATA_DIR" + mkdir -p "$CROWDSEC_CONFIG_DIR" + + # Regenerer les credentials machine si necessaire + if [ ! -f "${CROWDSEC_CONFIG_DIR}/local_api_credentials.yaml" ]; then + log_info "Regeneration des credentials machine..." + cscli machines add localhost --auto --force 2>/dev/null || true + fi + + # Redemarrer CrowdSec + /etc/init.d/crowdsec start + sleep 3 + + # Verifier LAPI + if ! cscli lapi status >/dev/null 2>&1; then + log_error "LAPI toujours non disponible apres reparation" + return 1 + fi + + # Re-enregistrer le bouncer si necessaire + if ! cscli bouncers list 2>/dev/null | grep -q "crowdsec-firewall-bouncer"; then + log_info "Re-enregistrement du bouncer..." + configure_bouncer + fi + + # Redemarrer le bouncer + /etc/init.d/crowdsec-firewall-bouncer start + + log_info "Reparation terminee" + show_status +} + +# ============================================================================= +# MAIN +# ============================================================================= + +usage() { + cat << EOF +Usage: $SCRIPT_NAME [OPTIONS] + +Options: + --install Installation complete (defaut) + --uninstall Desinstallation complete + --status Afficher le statut des services + --repair Tenter de reparer une installation cassee + --help Afficher cette aide + +Version: $VERSION + +EOF +} + +main() { + check_root + detect_arch + detect_openwrt_version + + case "${1:-install}" in + --install|install) + log_step "Installation SecuBox CrowdSec v$VERSION" + + check_prerequisites + backup_configs + + # Piege pour rollback en cas d'erreur + trap 'rollback; exit 1' ERR + + install_packages + configure_syslog_ng + configure_crowdsec + register_and_setup_hub + configure_bouncer + configure_whitelist + start_services + verify_installation + generate_report + + # Desactiver le piege + trap - ERR + + log_step "Installation terminee avec succes!" + log_info "Consultez le rapport: /tmp/secubox-crowdsec-install-report.txt" + ;; + --uninstall|uninstall) + uninstall + ;; + --status|status) + show_status + ;; + --repair|repair) + repair + ;; + --help|-h|help) + usage + ;; + *) + log_error "Option inconnue: $1" + usage + exit 1 + ;; + esac +} + +main "$@" diff --git a/secubox-tools/local-build.sh b/secubox-tools/local-build.sh index 2a1ed5e6..c84bf5be 100755 --- a/secubox-tools/local-build.sh +++ b/secubox-tools/local-build.sh @@ -562,6 +562,69 @@ LUCI_MK 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() { local single_package="$1" @@ -2015,7 +2078,8 @@ COMMANDS: debug-firmware Debug firmware build (check config without building) full Run validation then build 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 PACKAGES: @@ -2180,8 +2244,14 @@ main() { clean-all) print_header "Cleaning All Build Directories" - rm -rf "$SDK_DIR" "$BUILD_DIR" "$OPENWRT_DIR" "$CACHE_DIR" - print_success "All build directories cleaned (SDK, build, OpenWrt source, cache)" + rm -rf "$SDK_DIR" "$BUILD_DIR" "$OPENWRT_DIR" "$CACHE_DIR" "./local-feed" + 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)