fix(webapp): Use RPC backends for CrowdSec stats, disk and logs

- Refactor CROWDSEC object to use luci.crowdsec-dashboard RPC instead of file.exec
- Add getNftablesStats() for accurate blocked IPs count from firewall bouncer
- Update updateDiskUsage() to use luci.system-hub.get_system_status RPC
- Update loadSystemLogs() to use luci.system-hub.get_logs RPC
- Add proper ACL permissions for luci.crowdsec-dashboard and luci.system-hub
- Bump version to 1.5.0-r3

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-23 05:43:13 +01:00
parent 3e52444a73
commit d80501b33a
3 changed files with 156 additions and 79 deletions

View File

@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-app-webapp
PKG_VERSION:=1.5.0
PKG_RELEASE:=1
PKG_RELEASE:=3
PKG_LICENSE:=MIT
PKG_MAINTAINER:=CyberMind.FR <contact@cybermind.fr>

View File

@ -12,7 +12,17 @@
"service": ["list"],
"file": ["list", "read", "stat", "exec"],
"luci": ["getLocaltime", "getTimezones", "getInitList", "getRealtimeStats"],
"luci-rpc": ["getBoardJSON", "getNetworkDevices", "getDHCPLeases"]
"luci-rpc": ["getBoardJSON", "getNetworkDevices", "getDHCPLeases"],
"luci.crowdsec-dashboard": [
"decisions", "alerts", "metrics", "bouncers", "machines",
"hub", "status", "stats", "nftables_stats", "firewall_bouncer_status",
"firewall_bouncer_config", "health_check", "acquisition_config",
"acquisition_metrics", "console_status", "capi_metrics"
],
"luci.system-hub": [
"get_system_status", "get_logs", "get_health_score",
"get_storage_info", "get_services_status"
]
},
"file": {
"/etc/crowdsec/*": ["read"],
@ -25,7 +35,10 @@
"file": ["exec"],
"service": ["signal", "delete"],
"system": ["reboot"],
"network.interface": ["up", "down", "renew"]
"network.interface": ["up", "down", "renew"],
"luci.crowdsec-dashboard": [
"ban", "unban", "service_control", "update_hub"
]
},
"file": {
"/tmp/*": ["write"]

View File

@ -3512,18 +3512,14 @@
};
// =====================================================
// CrowdSec Integration
// CrowdSec Integration via RPC backend
// =====================================================
const CROWDSEC = {
async getDecisions() {
try {
// Try via shell command
const result = await UBUS.execCommand('/usr/bin/cscli', ['decisions', 'list', '-o', 'json']);
if (result.stdout) {
return JSON.parse(result.stdout);
}
return [];
const result = await UBUS.call('luci.crowdsec-dashboard', 'decisions');
return result?.alerts || [];
} catch (e) {
console.warn('CrowdSec decisions error:', e);
return [];
@ -3532,11 +3528,8 @@
async getBouncers() {
try {
const result = await UBUS.execCommand('/usr/bin/cscli', ['bouncers', 'list', '-o', 'json']);
if (result.stdout) {
return JSON.parse(result.stdout);
}
return [];
const result = await UBUS.call('luci.crowdsec-dashboard', 'bouncers');
return result?.bouncers || [];
} catch (e) {
console.warn('CrowdSec bouncers error:', e);
return [];
@ -3545,11 +3538,8 @@
async getAlerts() {
try {
const result = await UBUS.execCommand('/usr/bin/cscli', ['alerts', 'list', '-o', 'json', '--since', '24h']);
if (result.stdout) {
return JSON.parse(result.stdout);
}
return [];
const result = await UBUS.call('luci.crowdsec-dashboard', 'alerts', { limit: 50 });
return result?.alerts || [];
} catch (e) {
console.warn('CrowdSec alerts error:', e);
return [];
@ -3558,34 +3548,57 @@
async getMetrics() {
try {
const result = await UBUS.execCommand('/usr/bin/cscli', ['metrics', '-o', 'json']);
if (result.stdout) {
return JSON.parse(result.stdout);
}
return null;
const result = await UBUS.call('luci.crowdsec-dashboard', 'metrics');
return result || null;
} catch (e) {
return null;
}
},
async getNftablesStats() {
try {
const result = await UBUS.call('luci.crowdsec-dashboard', 'nftables_stats');
return result || {};
} catch (e) {
console.warn('CrowdSec nftables_stats error:', e);
return {};
}
},
async getStatus() {
try {
const result = await UBUS.call('luci.crowdsec-dashboard', 'status');
return result || {};
} catch (e) {
return {};
}
},
async addDecision(ip, duration, reason) {
try {
const result = await UBUS.execCommand('/usr/bin/cscli', [
'decisions', 'add',
'--ip', ip,
'--duration', duration,
'--reason', reason,
'--type', 'ban'
]);
const result = await UBUS.call('luci.crowdsec-dashboard', 'ban', {
ip: ip,
duration: duration,
reason: reason
});
return result;
} catch (e) {
throw new Error('Erreur lors du blocage: ' + e.message);
}
},
async removeDecision(ip) {
try {
const result = await UBUS.call('luci.crowdsec-dashboard', 'unban', { ip: ip });
return result;
} catch (e) {
throw new Error('Erreur lors du déblocage: ' + e.message);
}
},
async reload() {
try {
await UBUS.execCommand('/etc/init.d/crowdsec', ['reload']);
await UBUS.call('luci.crowdsec-dashboard', 'service_control', { action: 'reload' });
return true;
} catch (e) {
throw new Error('Erreur reload CrowdSec: ' + e.message);
@ -3744,16 +3757,17 @@
// Get CrowdSec data (in parallel) - only on overview tab
if (currentTab === 'overview' || currentTab === 'crowdsec') {
const [decisions, bouncers, alerts] = await Promise.all([
const [decisions, bouncers, alerts, nftStats] = await Promise.all([
CROWDSEC.getDecisions(),
CROWDSEC.getBouncers(),
CROWDSEC.getAlerts()
CROWDSEC.getAlerts(),
CROWDSEC.getNftablesStats()
]);
updateCrowdSecMetrics(decisions, bouncers, alerts);
updateCrowdSecMetrics(decisions, bouncers, alerts, nftStats);
// Update firewall blocking statistics
if (currentTab === 'overview') {
updateBlockingStats(decisions, alerts);
updateBlockingStats(decisions, alerts, nftStats);
}
}
@ -3885,9 +3899,19 @@
// Get real disk usage via df command
async function updateDiskUsage() {
try {
const result = await UBUS.execCommand('/bin/df', ['-P', '/']);
if (result.stdout) {
const lines = result.stdout.trim().split('\n');
// Use luci.system-hub RPC for disk info
const result = await UBUS.call('luci.system-hub', 'get_system_status').catch(() => null);
if (result && result.disk) {
const percent = result.disk.usage || 0;
document.getElementById('diskValue').textContent = percent;
updateGauge('diskGauge', percent);
return;
}
// Fallback: try file.exec with df
const dfResult = await UBUS.execCommand('/bin/df', ['-P', '/']).catch(() => null);
if (dfResult && dfResult.stdout) {
const lines = dfResult.stdout.trim().split('\n');
if (lines.length >= 2) {
const parts = lines[1].split(/\s+/);
if (parts.length >= 5) {
@ -3995,7 +4019,7 @@
previousNetStats = { rx: totalRx, tx: totalTx, time: Date.now() };
}
function updateCrowdSecMetrics(decisions, bouncers, alerts) {
function updateCrowdSecMetrics(decisions, bouncers, alerts, nftStats) {
const crowdsecStatus = document.getElementById('crowdsecStatus');
// Check if CrowdSec is available
@ -4019,8 +4043,17 @@
// CrowdSec is active
crowdsecStatus.innerHTML = '<span class="live-dot"></span> Actif';
// Decisions count
const decisionCount = Array.isArray(decisions) ? decisions.length : 0;
// Decisions count - use nftables stats if available for more accurate count
let decisionCount = Array.isArray(decisions) ? decisions.length : 0;
if (nftStats && nftStats.available) {
const ipv4Total = nftStats.ipv4_total_count || 0;
const ipv6Total = nftStats.ipv6_total_count || 0;
const nftTotal = ipv4Total + ipv6Total;
// Use nftables count if it's higher (more accurate for blocked IPs)
if (nftTotal > decisionCount) {
decisionCount = nftTotal;
}
}
document.getElementById('activeDecisions').textContent = decisionCount;
// Alerts count
@ -4032,16 +4065,15 @@
// Bouncers
renderBouncers(bouncers || []);
// Get actual parser and scenario counts from metrics if available
// Get actual parser and scenario counts from hub
updateCrowdSecCounts();
}
async function updateCrowdSecCounts() {
try {
// Get hub status for parsers and scenarios count
const result = await UBUS.execCommand('/usr/bin/cscli', ['hub', 'list', '-o', 'json']).catch(() => null);
if (result && result.stdout) {
const hub = JSON.parse(result.stdout);
// Get hub status for parsers and scenarios count via RPC
const hub = await UBUS.call('luci.crowdsec-dashboard', 'hub').catch(() => null);
if (hub) {
const parsers = hub.parsers ? hub.parsers.filter(p => p.status === 'installed').length : 0;
const scenarios = hub.scenarios ? hub.scenarios.filter(s => s.status === 'installed').length : 0;
document.getElementById('parsersCount').textContent = parsers || '--';
@ -4209,7 +4241,7 @@
}
}
function updateBlockingStats(decisions, alerts) {
function updateBlockingStats(decisions, alerts, nftStats) {
const totalBansEl = document.getElementById('totalBansToday');
const blockedEl = document.getElementById('totalBlockedAttempts');
const topScenarioEl = document.getElementById('topScenario');
@ -4255,41 +4287,63 @@
}
});
// Count unique IPs
const uniqueIPs = new Set();
decisionsList.forEach(d => {
if (d.value) uniqueIPs.add(d.value);
});
alertsList.forEach(a => {
if (a.source && a.source.ip) uniqueIPs.add(a.source.ip);
});
// Count unique IPs - use nftables stats if available for more accurate count
let uniqueIPsCount = 0;
if (nftStats && nftStats.available) {
const ipv4Total = nftStats.ipv4_total_count || 0;
const ipv6Total = nftStats.ipv6_total_count || 0;
uniqueIPsCount = ipv4Total + ipv6Total;
}
// Fallback to counting from decisions/alerts if nftStats not available
if (uniqueIPsCount === 0) {
const uniqueIPs = new Set();
decisionsList.forEach(d => {
if (d.value) uniqueIPs.add(d.value);
});
alertsList.forEach(a => {
if (a.source && a.source.ip) uniqueIPs.add(a.source.ip);
});
uniqueIPsCount = uniqueIPs.size;
}
// Update UI
totalBansEl.textContent = todayBans;
blockedEl.textContent = totalBlocked || '--';
topScenarioEl.textContent = topScenario.length > 10 ? topScenario.substring(0, 10) + '...' : topScenario;
topScenarioEl.title = topScenario; // Full name on hover
uniqueIPsEl.textContent = uniqueIPs.size;
uniqueIPsEl.textContent = uniqueIPsCount;
}
async function loadSystemLogs() {
const container = document.getElementById('logsContainer');
try {
// Get recent logs via logread
const result = await UBUS.execCommand('/sbin/logread', ['-l', '20']);
if (result.stdout) {
const lines = result.stdout.trim().split('\n').filter(l => l);
// Try luci.system-hub RPC first
const result = await UBUS.call('luci.system-hub', 'get_logs', { lines: 20 }).catch(() => null);
let lines = [];
if (result && result.logs && Array.isArray(result.logs)) {
lines = result.logs;
} else {
// Fallback: try file.exec with logread
const logResult = await UBUS.execCommand('/sbin/logread', ['-l', '20']).catch(() => null);
if (logResult && logResult.stdout) {
lines = logResult.stdout.trim().split('\n').filter(l => l);
}
}
if (lines.length > 0) {
container.innerHTML = lines.slice(-10).reverse().map(line => {
const type = getLogType(line);
// Handle both string and object formats
const logLine = typeof line === 'string' ? line : (line.message || line.line || '');
const type = getLogType(logLine);
const icons = { ban: 'x-circle', alert: 'alert-triangle', info: 'info', success: 'check-circle' };
// Parse log line
const parts = line.match(/^(\w+\s+\d+\s+[\d:]+)\s+\S+\s+(.+)$/);
const time = parts?.[1] || '';
const message = parts?.[2] || line;
const parts = logLine.match(/^(\w+\s+\d+\s+[\d:]+)\s+\S+\s+(.+)$/);
const time = parts?.[1] || (line.time || '');
const message = parts?.[2] || logLine;
return `
<div class="log-entry ${type}">
@ -4307,7 +4361,10 @@
}).join('');
lucide.createIcons();
return;
}
throw new Error('No logs available');
} catch (e) {
container.innerHTML = `
<div class="empty-state">
@ -5201,14 +5258,24 @@
async function loadCrowdSecInfo() {
try {
const [decisions, bouncers, alerts, machines] = await Promise.all([
const [decisions, bouncers, alerts, machines, nftStats] = await Promise.all([
CROWDSEC.getDecisions(),
CROWDSEC.getBouncers(),
CROWDSEC.getAlerts(),
getCrowdSecMachines()
getCrowdSecMachines(),
CROWDSEC.getNftablesStats()
]);
document.getElementById('csDecisions').textContent = Array.isArray(decisions) ? decisions.length : '--';
// Use nftables stats for more accurate blocked IPs count
let decisionsCount = Array.isArray(decisions) ? decisions.length : 0;
if (nftStats && nftStats.available) {
const nftTotal = (nftStats.ipv4_total_count || 0) + (nftStats.ipv6_total_count || 0);
if (nftTotal > decisionsCount) {
decisionsCount = nftTotal;
}
}
document.getElementById('csDecisions').textContent = decisionsCount || '--';
document.getElementById('csAlerts').textContent = Array.isArray(alerts) ? alerts.length : '--';
document.getElementById('csBouncers').textContent = Array.isArray(bouncers) ? bouncers.length : '--';
document.getElementById('csMachines').textContent = Array.isArray(machines) ? machines.length : '--';
@ -5256,10 +5323,8 @@
async function getCrowdSecMachines() {
try {
const result = await UBUS.execCommand('/usr/bin/cscli', ['machines', 'list', '-o', 'json']);
if (result.stdout) {
return JSON.parse(result.stdout);
}
const result = await UBUS.call('luci.crowdsec-dashboard', 'machines');
return result?.machines || [];
} catch (e) {}
return [];
}
@ -5313,7 +5378,7 @@
async function unbanIPDirect(ip) {
try {
showToast(`Déblocage de ${ip}...`, 'info');
await UBUS.execCommand('/usr/bin/cscli', ['decisions', 'delete', '--ip', ip]);
await CROWDSEC.removeDecision(ip);
showToast(`IP ${ip} débloquée`, 'success');
if (currentTab === 'crowdsec') {
await loadCrowdSecInfo();
@ -5328,8 +5393,7 @@
async function updateCrowdSecHub() {
try {
showToast('Mise à jour du hub CrowdSec...', 'info');
await UBUS.execCommand('/usr/bin/cscli', ['hub', 'update']);
await UBUS.execCommand('/usr/bin/cscli', ['hub', 'upgrade']);
await UBUS.call('luci.crowdsec-dashboard', 'update_hub');
showToast('Hub CrowdSec mis à jour', 'success');
} catch (e) {
showToast(`Erreur: ${e.message}`, 'error');