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:
parent
3e52444a73
commit
d80501b33a
@ -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>
|
||||
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user