- Rename crowdsec-firewall-bouncer to secubox-app-cs-firewall-bouncer - Rename secubox-auth-logger to secubox-app-auth-logger - Delete secubox-crowdsec-setup (merged into other packages) - Fix circular dependencies in luci-app-secubox-crowdsec - Fix dependency chain in secubox-app-crowdsec-bouncer - Add consolidated get_overview API to crowdsec-dashboard - Improve crowdsec-dashboard overview performance Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1088 lines
40 KiB
JavaScript
1088 lines
40 KiB
JavaScript
'use strict';
|
||
'require view';
|
||
'require secubox-theme/theme as Theme';
|
||
'require dom';
|
||
'require poll';
|
||
'require ui';
|
||
'require fs';
|
||
'require crowdsec-dashboard/api as api';
|
||
'require crowdsec-dashboard/nav as CsNav';
|
||
'require secubox-portal/header as SbHeader';
|
||
|
||
/**
|
||
* CrowdSec Dashboard - Overview View
|
||
* Main dashboard with stats, charts, and recent activity
|
||
* Copyright (C) 2024 CyberMind.fr - Gandalf
|
||
*/
|
||
|
||
return view.extend({
|
||
title: _('CrowdSec Dashboard'),
|
||
|
||
css: null,
|
||
data: null,
|
||
csApi: null,
|
||
|
||
load: function() {
|
||
// Load CSS
|
||
var cssLink = document.createElement('link');
|
||
cssLink.rel = 'stylesheet';
|
||
cssLink.href = L.resource('crowdsec-dashboard/dashboard.css');
|
||
document.head.appendChild(cssLink);
|
||
|
||
// Load API
|
||
this.csApi = api;
|
||
|
||
// Use consolidated API call + secondary calls for extended data
|
||
return Promise.all([
|
||
this.csApi.getOverview().catch(function() { return {}; }),
|
||
this.csApi.getCapiMetrics().catch(function() { return {}; }),
|
||
this.csApi.getCollections().catch(function() { return { collections: [] }; }),
|
||
this.csApi.getNftablesStats().catch(function() { return {}; }),
|
||
this.csApi.getHub().catch(function() { return {}; })
|
||
]).then(this.transformOverviewData.bind(this));
|
||
},
|
||
|
||
// Transform getOverview response to expected data structure
|
||
transformOverviewData: function(results) {
|
||
var overview = results[0] || {};
|
||
var capiMetrics = results[1] || {};
|
||
var collectionsData = results[2] || {};
|
||
var nftablesStats = results[3] || {};
|
||
var hubData = results[4] || {};
|
||
|
||
// Parse raw JSON strings for scenarios and countries
|
||
var topScenarios = [];
|
||
var topCountries = [];
|
||
try {
|
||
if (overview.top_scenarios_raw) {
|
||
topScenarios = JSON.parse(overview.top_scenarios_raw);
|
||
}
|
||
} catch(e) { topScenarios = []; }
|
||
try {
|
||
if (overview.top_countries_raw) {
|
||
topCountries = JSON.parse(overview.top_countries_raw);
|
||
}
|
||
} catch(e) { topCountries = []; }
|
||
|
||
// Build compatible data structure
|
||
var dashboardData = {
|
||
status: {
|
||
crowdsec: overview.crowdsec || 'unknown',
|
||
bouncer: overview.bouncer || 'unknown',
|
||
version: overview.version || 'unknown'
|
||
},
|
||
stats: {
|
||
total_decisions: overview.total_decisions || 0,
|
||
alerts_today: overview.alerts_24h || 0,
|
||
alerts_week: overview.alerts_24h || 0,
|
||
scenarios_triggered: topScenarios.length,
|
||
top_countries: topCountries.map(function(c) {
|
||
return { country: c.country, count: c.count };
|
||
})
|
||
},
|
||
decisions: overview.decisions || [],
|
||
alerts: overview.alerts || [],
|
||
error: null
|
||
};
|
||
|
||
var logsData = {
|
||
entries: overview.logs || []
|
||
};
|
||
|
||
var healthCheck = {
|
||
crowdsec_running: overview.crowdsec === 'running',
|
||
lapi_status: overview.lapi_status || 'unavailable',
|
||
capi_status: overview.capi_enrolled ? 'connected' : 'disconnected',
|
||
capi_enrolled: overview.capi_enrolled || false,
|
||
capi_subscription: 'COMMUNITY',
|
||
sharing_signals: overview.capi_enrolled || false,
|
||
pulling_blocklist: overview.capi_enrolled || false,
|
||
version: overview.version || 'N/A',
|
||
decisions_count: overview.total_decisions || 0
|
||
};
|
||
|
||
// Return array matching expected payload structure
|
||
return [
|
||
dashboardData,
|
||
logsData,
|
||
healthCheck,
|
||
capiMetrics,
|
||
collectionsData,
|
||
nftablesStats,
|
||
hubData
|
||
];
|
||
},
|
||
|
||
renderHeader: function(status) {
|
||
var header = E('div', { 'class': 'cs-header' }, [
|
||
E('div', { 'class': 'cs-logo' }, [
|
||
E('div', { 'class': 'cs-logo-icon' }, '🛡️'),
|
||
E('div', { 'class': 'cs-logo-text' }, [
|
||
'Crowd',
|
||
E('span', {}, 'Sec'),
|
||
' Dashboard'
|
||
])
|
||
]),
|
||
E('div', { 'class': 'cs-status-badges' }, [
|
||
E('div', { 'class': 'cs-badge' }, [
|
||
E('span', {
|
||
'class': 'cs-badge-dot ' + (status.crowdsec === 'running' ? 'running' : 'stopped')
|
||
}),
|
||
'Engine: ' + (status.crowdsec || 'unknown')
|
||
]),
|
||
E('div', { 'class': 'cs-badge' }, [
|
||
E('span', {
|
||
'class': 'cs-badge-dot ' + (status.bouncer === 'running' ? 'running' : 'stopped')
|
||
}),
|
||
'Bouncer: ' + (status.bouncer || 'unknown')
|
||
]),
|
||
E('div', { 'class': 'cs-badge' }, [
|
||
'v' + (status.version || 'N/A')
|
||
])
|
||
])
|
||
]);
|
||
|
||
return header;
|
||
},
|
||
|
||
renderStatsGrid: function(stats, decisions) {
|
||
var self = this;
|
||
|
||
// Count by action type
|
||
var banCount = 0;
|
||
var captchaCount = 0;
|
||
|
||
if (Array.isArray(decisions)) {
|
||
decisions.forEach(function(d) {
|
||
if (d.type === 'ban') banCount++;
|
||
else if (d.type === 'captcha') captchaCount++;
|
||
});
|
||
}
|
||
|
||
var grid = E('div', { 'class': 'cs-stats-grid' }, [
|
||
E('div', { 'class': 'cs-stat-card' }, [
|
||
E('div', { 'class': 'cs-stat-label' }, 'Active Bans'),
|
||
E('div', { 'class': 'cs-stat-value danger' }, String(stats.total_decisions || 0)),
|
||
E('div', { 'class': 'cs-stat-trend' }, 'Currently blocked IPs'),
|
||
E('div', { 'class': 'cs-stat-icon' }, '🚫')
|
||
]),
|
||
E('div', { 'class': 'cs-stat-card' }, [
|
||
E('div', { 'class': 'cs-stat-label' }, 'Alerts (24h)'),
|
||
E('div', { 'class': 'cs-stat-value warning' }, String(stats.alerts_24h || 0)),
|
||
E('div', { 'class': 'cs-stat-trend' }, 'Detected threats'),
|
||
E('div', { 'class': 'cs-stat-icon' }, '⚠️')
|
||
]),
|
||
E('div', { 'class': 'cs-stat-card' }, [
|
||
E('div', { 'class': 'cs-stat-label' }, 'Bouncers'),
|
||
E('div', { 'class': 'cs-stat-value success' }, String(stats.bouncers || 0)),
|
||
E('div', { 'class': 'cs-stat-trend' }, 'Active remediation'),
|
||
E('div', { 'class': 'cs-stat-icon' }, '🔒')
|
||
]),
|
||
E('div', { 'class': 'cs-stat-card' }, [
|
||
E('div', { 'class': 'cs-stat-label' }, 'Ban Rate'),
|
||
E('div', { 'class': 'cs-stat-value' }, banCount > 0 ? '100%' : '0%'),
|
||
E('div', { 'class': 'cs-stat-trend' }, banCount + ' bans / ' + captchaCount + ' captchas'),
|
||
E('div', { 'class': 'cs-stat-icon' }, '📊')
|
||
])
|
||
]);
|
||
|
||
return grid;
|
||
},
|
||
|
||
renderDecisionsTable: function(decisions) {
|
||
var self = this;
|
||
|
||
if (!Array.isArray(decisions) || decisions.length === 0) {
|
||
return E('div', { 'class': 'cs-empty' }, [
|
||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
||
E('div', { 'class': 'cs-empty-icon' }, '✅'),
|
||
E('p', {}, 'No active decisions - All clear!')
|
||
]);
|
||
}
|
||
|
||
var rows = decisions.slice(0, 10).map(function(d) {
|
||
return E('tr', {}, [
|
||
E('td', {}, E('span', { 'class': 'cs-ip' }, d.value || 'N/A')),
|
||
E('td', {}, E('span', { 'class': 'cs-scenario' }, self.csApi.parseScenario(d.scenario))),
|
||
E('td', {}, E('span', { 'class': 'cs-country' }, [
|
||
E('span', { 'class': 'cs-country-flag' }, self.csApi.getCountryFlag(d.country)),
|
||
d.country || 'N/A'
|
||
])),
|
||
E('td', {}, E('span', { 'class': 'cs-action ' + (d.type || 'ban') }, d.type || 'ban')),
|
||
E('td', {}, E('span', { 'class': 'cs-time' }, self.csApi.formatDuration(d.duration))),
|
||
E('td', {}, E('button', {
|
||
'class': 'cs-btn cs-btn-danger cs-btn-sm',
|
||
'data-ip': d.value,
|
||
'click': ui.createHandlerFn(self, 'handleUnban', d.value)
|
||
}, 'Unban'))
|
||
]);
|
||
});
|
||
|
||
return E('table', { 'class': 'cs-table' }, [
|
||
E('thead', {}, E('tr', {}, [
|
||
E('th', {}, 'IP Address'),
|
||
E('th', {}, 'Scenario'),
|
||
E('th', {}, 'Country'),
|
||
E('th', {}, 'Action'),
|
||
E('th', {}, 'Expires'),
|
||
E('th', {}, 'Actions')
|
||
])),
|
||
E('tbody', {}, rows)
|
||
]);
|
||
},
|
||
|
||
renderAlertsTimeline: function(alerts) {
|
||
var self = this;
|
||
|
||
if (!Array.isArray(alerts) || alerts.length === 0) {
|
||
return E('div', { 'class': 'cs-empty' }, [
|
||
E('div', { 'class': 'cs-empty-icon' }, '📭'),
|
||
E('p', {}, 'No recent alerts')
|
||
]);
|
||
}
|
||
|
||
var items = alerts.slice(0, 8).map(function(a) {
|
||
return E('div', { 'class': 'cs-timeline-item alert' }, [
|
||
E('div', { 'class': 'cs-timeline-time' }, self.csApi.formatRelativeTime(a.created_at)),
|
||
E('div', { 'class': 'cs-timeline-content' }, [
|
||
E('strong', {}, self.csApi.parseScenario(a.scenario)),
|
||
E('br', {}),
|
||
E('span', { 'class': 'cs-ip' }, a.source?.ip || 'N/A'),
|
||
' → ',
|
||
E('span', {}, (a.events_count || 0) + ' events')
|
||
])
|
||
]);
|
||
});
|
||
|
||
return E('div', { 'class': 'cs-timeline' }, items);
|
||
},
|
||
|
||
renderTopScenarios: function(stats) {
|
||
var scenarios = [];
|
||
|
||
try {
|
||
if (stats.top_scenarios_raw) {
|
||
scenarios = JSON.parse(stats.top_scenarios_raw);
|
||
}
|
||
} catch(e) {
|
||
scenarios = [];
|
||
}
|
||
|
||
if (scenarios.length === 0) {
|
||
return E('div', { 'class': 'cs-empty' }, [
|
||
E('p', {}, 'No scenario data available')
|
||
]);
|
||
}
|
||
|
||
var maxCount = Math.max.apply(null, scenarios.map(function(s) { return s.count; }));
|
||
|
||
var bars = scenarios.map(function(s) {
|
||
var pct = maxCount > 0 ? (s.count / maxCount * 100) : 0;
|
||
return E('div', { 'class': 'cs-bar-item' }, [
|
||
E('div', { 'class': 'cs-bar-label', 'title': s.scenario }, s.scenario.split('/').pop()),
|
||
E('div', { 'class': 'cs-bar-track' }, [
|
||
E('div', { 'class': 'cs-bar-fill', 'style': 'width: ' + pct + '%' })
|
||
]),
|
||
E('div', { 'class': 'cs-bar-value' }, String(s.count))
|
||
]);
|
||
});
|
||
|
||
return E('div', { 'class': 'cs-bar-chart' }, bars);
|
||
},
|
||
|
||
renderTopCountries: function(stats) {
|
||
var self = this;
|
||
var countries = [];
|
||
|
||
try {
|
||
if (stats.top_countries_raw) {
|
||
countries = JSON.parse(stats.top_countries_raw);
|
||
}
|
||
} catch(e) {
|
||
countries = [];
|
||
}
|
||
|
||
if (countries.length === 0) {
|
||
return E('div', { 'class': 'cs-empty' }, [
|
||
E('p', {}, 'No country data available')
|
||
]);
|
||
}
|
||
|
||
var maxCount = Math.max.apply(null, countries.map(function(c) { return c.count; }));
|
||
|
||
var bars = countries.map(function(c) {
|
||
var pct = maxCount > 0 ? (c.count / maxCount * 100) : 0;
|
||
return E('div', { 'class': 'cs-bar-item' }, [
|
||
E('div', { 'class': 'cs-bar-label' }, [
|
||
self.csApi.getCountryFlag(c.country),
|
||
' ',
|
||
c.country || 'N/A'
|
||
]),
|
||
E('div', { 'class': 'cs-bar-track' }, [
|
||
E('div', { 'class': 'cs-bar-fill', 'style': 'width: ' + pct + '%' })
|
||
]),
|
||
E('div', { 'class': 'cs-bar-value' }, String(c.count))
|
||
]);
|
||
});
|
||
|
||
return E('div', { 'class': 'cs-bar-chart' }, bars);
|
||
},
|
||
|
||
renderBanModal: function() {
|
||
return E('div', { 'class': 'cs-modal-overlay', 'id': 'ban-modal', 'style': 'display: none' }, [
|
||
E('div', { 'class': 'cs-modal' }, [
|
||
E('div', { 'class': 'cs-modal-header' }, [
|
||
E('div', { 'class': 'cs-modal-title' }, 'Add IP Ban'),
|
||
E('button', {
|
||
'class': 'cs-modal-close',
|
||
'click': ui.createHandlerFn(this, 'closeBanModal')
|
||
}, '×')
|
||
]),
|
||
E('div', { 'class': 'cs-modal-body' }, [
|
||
E('div', { 'class': 'cs-form-group' }, [
|
||
E('label', { 'class': 'cs-form-label' }, 'IP Address'),
|
||
E('input', {
|
||
'class': 'cs-input',
|
||
'id': 'ban-ip',
|
||
'type': 'text',
|
||
'placeholder': '192.168.1.100 or 10.0.0.0/24'
|
||
})
|
||
]),
|
||
E('div', { 'class': 'cs-form-group' }, [
|
||
E('label', { 'class': 'cs-form-label' }, 'Duration'),
|
||
E('input', {
|
||
'class': 'cs-input',
|
||
'id': 'ban-duration',
|
||
'type': 'text',
|
||
'placeholder': '4h',
|
||
'value': '4h'
|
||
})
|
||
]),
|
||
E('div', { 'class': 'cs-form-group' }, [
|
||
E('label', { 'class': 'cs-form-label' }, 'Reason'),
|
||
E('input', {
|
||
'class': 'cs-input',
|
||
'id': 'ban-reason',
|
||
'type': 'text',
|
||
'placeholder': 'Manual ban from dashboard'
|
||
})
|
||
])
|
||
]),
|
||
E('div', { 'class': 'cs-modal-footer' }, [
|
||
E('button', {
|
||
'class': 'cs-btn',
|
||
'click': ui.createHandlerFn(this, 'closeBanModal')
|
||
}, 'Cancel'),
|
||
E('button', {
|
||
'class': 'cs-btn cs-btn-primary',
|
||
'click': ui.createHandlerFn(this, 'submitBan')
|
||
}, 'Add Ban')
|
||
])
|
||
])
|
||
]);
|
||
},
|
||
|
||
handleUnban: function(ip, ev) {
|
||
var self = this;
|
||
|
||
if (!confirm('Remove ban for ' + ip + '?')) {
|
||
return;
|
||
}
|
||
|
||
this.csApi.unbanIP(ip).then(function(result) {
|
||
if (result.success) {
|
||
self.showToast('IP ' + ip + ' unbanned successfully', 'success');
|
||
// Refresh data
|
||
return self.refreshDashboard();
|
||
} else {
|
||
self.showToast('Failed to unban: ' + (result.error || 'Unknown error'), 'error');
|
||
}
|
||
}).then(function(data) {
|
||
if (data) {
|
||
self.data = data;
|
||
self.updateView();
|
||
}
|
||
}).catch(function(err) {
|
||
self.showToast('Error: ' + err.message, 'error');
|
||
});
|
||
},
|
||
|
||
openBanModal: function(ev) {
|
||
document.getElementById('ban-modal').style.display = 'flex';
|
||
},
|
||
|
||
closeBanModal: function(ev) {
|
||
document.getElementById('ban-modal').style.display = 'none';
|
||
document.getElementById('ban-ip').value = '';
|
||
document.getElementById('ban-duration').value = '4h';
|
||
document.getElementById('ban-reason').value = '';
|
||
},
|
||
|
||
submitBan: function(ev) {
|
||
var self = this;
|
||
var ip = document.getElementById('ban-ip').value.trim();
|
||
var duration = document.getElementById('ban-duration').value.trim() || '4h';
|
||
var reason = document.getElementById('ban-reason').value.trim() || 'Manual ban from dashboard';
|
||
|
||
if (!ip) {
|
||
self.showToast('Please enter an IP address', 'error');
|
||
return;
|
||
}
|
||
|
||
if (!self.csApi.isValidIP(ip)) {
|
||
self.showToast('Invalid IP address format', 'error');
|
||
return;
|
||
}
|
||
|
||
self.csApi.banIP(ip, duration, reason).then(function(result) {
|
||
if (result.success) {
|
||
self.showToast('IP ' + ip + ' banned for ' + duration, 'success');
|
||
self.closeBanModal();
|
||
return self.refreshDashboard();
|
||
} else {
|
||
self.showToast('Failed to ban: ' + (result.error || 'Unknown error'), 'error');
|
||
}
|
||
}).then(function(data) {
|
||
if (data) {
|
||
self.data = data;
|
||
self.updateView();
|
||
}
|
||
}).catch(function(err) {
|
||
self.showToast('Error: ' + err.message, 'error');
|
||
});
|
||
},
|
||
|
||
showToast: function(message, type) {
|
||
var existing = document.querySelector('.cs-toast');
|
||
if (existing) existing.remove();
|
||
|
||
var toast = E('div', { 'class': 'cs-toast ' + (type || '') }, message);
|
||
document.body.appendChild(toast);
|
||
|
||
setTimeout(function() {
|
||
toast.remove();
|
||
}, 4000);
|
||
},
|
||
|
||
updateView: function() {
|
||
var container = document.getElementById('cs-dashboard-content');
|
||
if (!container || !this.data) return;
|
||
|
||
dom.content(container, this.renderContent(this.data));
|
||
},
|
||
|
||
renderContent: function(data) {
|
||
var status = data.status || {};
|
||
var stats = data.stats || {};
|
||
var decisions = data.decisions || [];
|
||
var alerts = data.alerts || [];
|
||
var logs = this.logs || [];
|
||
|
||
// Check if service is not running
|
||
var serviceWarning = null;
|
||
if (data.error && status.crowdsec !== 'running') {
|
||
serviceWarning = E('div', { 'class': 'cs-warning-banner' }, [
|
||
E('div', { 'class': 'cs-warning-icon' }, '⚠️'),
|
||
E('div', { 'class': 'cs-warning-content' }, [
|
||
E('div', { 'class': 'cs-warning-title' }, 'CrowdSec Service Not Running'),
|
||
E('div', { 'class': 'cs-warning-message' }, [
|
||
'The CrowdSec engine is currently stopped. ',
|
||
E('a', {
|
||
'href': '#',
|
||
'click': ui.createHandlerFn(this, 'startCrowdSec')
|
||
}, 'Click here to start the service'),
|
||
' or use the command: ',
|
||
E('code', {}, '/etc/init.d/crowdsec start')
|
||
])
|
||
])
|
||
]);
|
||
}
|
||
|
||
return E('div', {}, [
|
||
this.renderHeader(status),
|
||
serviceWarning || E([]),
|
||
this.renderHealthCheck(),
|
||
this.renderStatsGrid(stats, decisions),
|
||
|
||
E('div', { 'class': 'cs-charts-row' }, [
|
||
E('div', { 'class': 'cs-card' }, [
|
||
E('div', { 'class': 'cs-card-header' }, [
|
||
E('div', { 'class': 'cs-card-title' }, 'Top Scenarios'),
|
||
]),
|
||
E('div', { 'class': 'cs-card-body' }, this.renderTopScenarios(stats))
|
||
]),
|
||
E('div', { 'class': 'cs-card' }, [
|
||
E('div', { 'class': 'cs-card-header' }, [
|
||
E('div', { 'class': 'cs-card-title' }, 'Top Countries'),
|
||
]),
|
||
E('div', { 'class': 'cs-card-body' }, this.renderTopCountries(stats))
|
||
])
|
||
]),
|
||
|
||
E('div', { 'class': 'cs-charts-row' }, [
|
||
this.renderCapiBlocklist(),
|
||
this.renderCollectionsCard(),
|
||
this.renderHubStatsCard()
|
||
]),
|
||
|
||
E('div', { 'class': 'cs-charts-row' }, [
|
||
this.renderFirewallHealth(),
|
||
this.renderFirewallBlocks()
|
||
]),
|
||
|
||
E('div', { 'class': 'cs-charts-row' }, [
|
||
E('div', { 'class': 'cs-card', 'style': 'flex: 2' }, [
|
||
E('div', { 'class': 'cs-card-header' }, [
|
||
E('div', { 'class': 'cs-card-title' }, 'Active Decisions'),
|
||
E('button', {
|
||
'class': 'cs-btn cs-btn-primary cs-btn-sm',
|
||
'click': ui.createHandlerFn(this, 'openBanModal')
|
||
}, '+ Add Ban')
|
||
]),
|
||
E('div', { 'class': 'cs-card-body no-padding' }, this.renderDecisionsTable(decisions))
|
||
]),
|
||
E('div', { 'class': 'cs-card', 'style': 'flex: 1' }, [
|
||
E('div', { 'class': 'cs-card-header' }, [
|
||
E('div', { 'class': 'cs-card-title' }, 'Recent Alerts'),
|
||
]),
|
||
E('div', { 'class': 'cs-card-body' }, this.renderAlertsTimeline(alerts))
|
||
]),
|
||
this.renderLogCard(logs)
|
||
]),
|
||
|
||
this.renderBanModal()
|
||
]);
|
||
},
|
||
|
||
startCrowdSec: function(ev) {
|
||
var self = this;
|
||
ev.preventDefault();
|
||
|
||
ui.showModal(_('Start CrowdSec'), [
|
||
E('p', {}, _('Do you want to start the CrowdSec service?')),
|
||
E('div', { 'class': 'right' }, [
|
||
E('button', {
|
||
'class': 'btn',
|
||
'click': ui.hideModal
|
||
}, _('Cancel')),
|
||
' ',
|
||
E('button', {
|
||
'class': 'btn cbi-button-positive',
|
||
'click': function() {
|
||
return fs.exec('/etc/init.d/crowdsec', ['start']).then(function() {
|
||
ui.hideModal();
|
||
self.showToast('CrowdSec service started', 'success');
|
||
setTimeout(function() {
|
||
return self.refreshDashboard();
|
||
}, 2000);
|
||
}).catch(function(err) {
|
||
ui.hideModal();
|
||
self.showToast('Failed to start service: ' + err, 'error');
|
||
});
|
||
}
|
||
}, _('Start Service'))
|
||
])
|
||
]);
|
||
},
|
||
|
||
render: function(payload) {
|
||
var self = this;
|
||
this.data = payload[0] || {};
|
||
this.logs = (payload[1] && payload[1].entries) || [];
|
||
this.healthCheck = payload[2] || {};
|
||
this.capiMetrics = payload[3] || {};
|
||
this.collections = (payload[4] && payload[4].collections) || [];
|
||
this.nftablesStats = payload[5] || {};
|
||
this.hubData = payload[6] || {};
|
||
|
||
// Main wrapper with SecuBox header
|
||
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
|
||
wrapper.appendChild(SbHeader.render());
|
||
|
||
var view = E('div', { 'class': 'crowdsec-dashboard' }, [
|
||
CsNav.renderTabs('overview'),
|
||
E('div', { 'id': 'cs-dashboard-content' }, this.renderContent(this.data))
|
||
]);
|
||
|
||
wrapper.appendChild(view);
|
||
|
||
// Setup polling for auto-refresh (every 60 seconds)
|
||
poll.add(function() {
|
||
return self.refreshDashboard();
|
||
}, 60);
|
||
|
||
return wrapper;
|
||
},
|
||
|
||
refreshDashboard: function() {
|
||
var self = this;
|
||
// Use consolidated API call + secondary calls for extended data
|
||
return Promise.all([
|
||
self.csApi.getOverview().catch(function() { return {}; }),
|
||
self.csApi.getCapiMetrics().catch(function() { return {}; }),
|
||
self.csApi.getCollections().catch(function() { return { collections: [] }; }),
|
||
self.csApi.getNftablesStats().catch(function() { return {}; }),
|
||
self.csApi.getHub().catch(function() { return {}; })
|
||
]).then(function(results) {
|
||
var transformed = self.transformOverviewData(results);
|
||
self.data = transformed[0];
|
||
self.logs = (transformed[1] && transformed[1].entries) || [];
|
||
self.healthCheck = transformed[2] || {};
|
||
self.capiMetrics = transformed[3] || {};
|
||
self.collections = (transformed[4] && transformed[4].collections) || [];
|
||
self.nftablesStats = transformed[5] || {};
|
||
self.hubData = transformed[6] || {};
|
||
self.updateView();
|
||
});
|
||
},
|
||
|
||
// Health Check Section - Shows LAPI/CAPI/Console status
|
||
renderHealthCheck: function() {
|
||
var health = this.healthCheck || {};
|
||
var csRunning = health.crowdsec_running;
|
||
var lapiStatus = health.lapi_status || 'unavailable';
|
||
var capiStatus = health.capi_status || 'disconnected';
|
||
var capiEnrolled = health.capi_enrolled;
|
||
var capiSubscription = health.capi_subscription || '-';
|
||
var sharingSignals = health.sharing_signals;
|
||
var pullingBlocklist = health.pulling_blocklist;
|
||
var version = health.version || 'N/A';
|
||
var decisionsCount = health.decisions_count || 0;
|
||
|
||
return E('div', { 'class': 'cs-health-check', 'style': 'margin-bottom: 1.5em;' }, [
|
||
E('div', { 'class': 'cs-card' }, [
|
||
E('div', { 'class': 'cs-card-header' }, [
|
||
E('div', { 'class': 'cs-card-title' }, _('System Health'))
|
||
]),
|
||
E('div', { 'class': 'cs-card-body' }, [
|
||
E('div', { 'class': 'cs-health-grid', 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1em;' }, [
|
||
// CrowdSec Status
|
||
E('div', { 'class': 'cs-health-item', 'style': 'text-align: center; padding: 1em; background: rgba(0,0,0,0.1); border-radius: 8px;' }, [
|
||
E('div', { 'style': 'font-size: 2em; margin-bottom: 0.25em;' }, csRunning ? '✅' : '❌'),
|
||
E('div', { 'style': 'font-weight: 600; margin-bottom: 0.25em;' }, 'CrowdSec'),
|
||
E('div', { 'style': 'font-size: 0.85em; color: ' + (csRunning ? '#00d4aa' : '#ff4757') + ';' }, csRunning ? 'Running' : 'Stopped'),
|
||
E('div', { 'style': 'font-size: 0.75em; color: #888;' }, version ? (version.charAt(0) === 'v' ? version : 'v' + version) : '')
|
||
]),
|
||
// LAPI Status
|
||
E('div', { 'class': 'cs-health-item', 'style': 'text-align: center; padding: 1em; background: rgba(0,0,0,0.1); border-radius: 8px;' }, [
|
||
E('div', { 'style': 'font-size: 2em; margin-bottom: 0.25em;' }, lapiStatus === 'available' ? '✅' : '❌'),
|
||
E('div', { 'style': 'font-weight: 600; margin-bottom: 0.25em;' }, 'LAPI'),
|
||
E('div', { 'style': 'font-size: 0.85em; color: ' + (lapiStatus === 'available' ? '#00d4aa' : '#ff4757') + ';' }, lapiStatus === 'available' ? 'Available' : 'Unavailable'),
|
||
E('div', { 'style': 'font-size: 0.75em; color: #888;' }, ':8080')
|
||
]),
|
||
// CAPI Status
|
||
E('div', { 'class': 'cs-health-item', 'style': 'text-align: center; padding: 1em; background: rgba(0,0,0,0.1); border-radius: 8px;' }, [
|
||
E('div', { 'style': 'font-size: 2em; margin-bottom: 0.25em;' }, capiStatus === 'connected' ? '✅' : '⚠️'),
|
||
E('div', { 'style': 'font-weight: 600; margin-bottom: 0.25em;' }, 'CAPI'),
|
||
E('div', { 'style': 'font-size: 0.85em; color: ' + (capiStatus === 'connected' ? '#00d4aa' : '#ffa500') + ';' }, capiStatus === 'connected' ? 'Connected' : 'Disconnected'),
|
||
E('div', { 'style': 'font-size: 0.75em; color: #888;' }, capiSubscription)
|
||
]),
|
||
// Console Status
|
||
E('div', { 'class': 'cs-health-item', 'style': 'text-align: center; padding: 1em; background: rgba(0,0,0,0.1); border-radius: 8px;' }, [
|
||
E('div', { 'style': 'font-size: 2em; margin-bottom: 0.25em;' }, capiEnrolled ? '✅' : '⚪'),
|
||
E('div', { 'style': 'font-weight: 600; margin-bottom: 0.25em;' }, 'Console'),
|
||
E('div', { 'style': 'font-size: 0.85em; color: ' + (capiEnrolled ? '#00d4aa' : '#888') + ';' }, capiEnrolled ? 'Enrolled' : 'Not Enrolled'),
|
||
E('div', { 'style': 'font-size: 0.75em; color: #888;' }, sharingSignals ? 'Sharing: ON' : 'Sharing: OFF')
|
||
]),
|
||
// Blocklist Status
|
||
E('div', { 'class': 'cs-health-item', 'style': 'text-align: center; padding: 1em; background: rgba(0,0,0,0.1); border-radius: 8px;' }, [
|
||
E('div', { 'style': 'font-size: 2em; margin-bottom: 0.25em;' }, pullingBlocklist ? '🛡️' : '⚪'),
|
||
E('div', { 'style': 'font-weight: 600; margin-bottom: 0.25em;' }, 'Blocklist'),
|
||
E('div', { 'style': 'font-size: 0.85em; color: ' + (pullingBlocklist ? '#00d4aa' : '#888') + ';' }, pullingBlocklist ? 'Active' : 'Inactive'),
|
||
E('div', { 'style': 'font-size: 0.75em; color: #667eea; font-weight: 600;' }, decisionsCount.toLocaleString() + ' IPs')
|
||
])
|
||
])
|
||
])
|
||
])
|
||
]);
|
||
},
|
||
|
||
// CAPI Blocklist Metrics - Shows blocked IPs by category
|
||
renderCapiBlocklist: function() {
|
||
var metrics = this.capiMetrics || {};
|
||
var totalCapi = metrics.total_capi || 0;
|
||
var totalLocal = metrics.total_local || 0;
|
||
var breakdown = metrics.breakdown || [];
|
||
|
||
if (totalCapi === 0 && totalLocal === 0) {
|
||
return E('span'); // Empty if no data
|
||
}
|
||
|
||
// Build breakdown bars
|
||
var maxCount = Math.max.apply(null, breakdown.map(function(b) { return b.count || 0; }).concat([1]));
|
||
var breakdownBars = breakdown.slice(0, 5).map(function(item) {
|
||
var scenario = item.scenario || 'unknown';
|
||
var count = item.count || 0;
|
||
var pct = Math.round((count / maxCount) * 100);
|
||
var displayName = scenario.split('/').pop().replace(/-/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase(); });
|
||
|
||
return E('div', { 'style': 'margin-bottom: 0.75em;' }, [
|
||
E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 0.25em;' }, [
|
||
E('span', { 'style': 'font-size: 0.85em;' }, displayName),
|
||
E('span', { 'style': 'font-size: 0.85em; font-weight: 600; color: #667eea;' }, count.toLocaleString())
|
||
]),
|
||
E('div', { 'style': 'height: 8px; background: rgba(102,126,234,0.2); border-radius: 4px; overflow: hidden;' }, [
|
||
E('div', { 'style': 'height: 100%; width: ' + pct + '%; background: linear-gradient(90deg, #667eea, #764ba2); border-radius: 4px;' })
|
||
])
|
||
]);
|
||
});
|
||
|
||
return E('div', { 'class': 'cs-capi-blocklist', 'style': 'margin-bottom: 1.5em;' }, [
|
||
E('div', { 'class': 'cs-card' }, [
|
||
E('div', { 'class': 'cs-card-header' }, [
|
||
E('div', { 'class': 'cs-card-title' }, _('Community Blocklist (CAPI)'))
|
||
]),
|
||
E('div', { 'class': 'cs-card-body' }, [
|
||
E('div', { 'style': 'display: flex; gap: 2em; margin-bottom: 1em;' }, [
|
||
E('div', { 'style': 'text-align: center;' }, [
|
||
E('div', { 'style': 'font-size: 1.5em; font-weight: 700; color: #667eea;' }, totalCapi.toLocaleString()),
|
||
E('div', { 'style': 'font-size: 0.8em; color: #888;' }, 'CAPI Blocked')
|
||
]),
|
||
E('div', { 'style': 'text-align: center;' }, [
|
||
E('div', { 'style': 'font-size: 1.5em; font-weight: 700; color: #00d4aa;' }, totalLocal.toLocaleString()),
|
||
E('div', { 'style': 'font-size: 0.8em; color: #888;' }, 'Local Blocked')
|
||
])
|
||
]),
|
||
breakdownBars.length > 0 ? E('div', { 'style': 'margin-top: 1em;' }, [
|
||
E('div', { 'style': 'font-size: 0.85em; font-weight: 600; margin-bottom: 0.75em; color: #888;' }, _('Top Blocked Categories')),
|
||
E('div', {}, breakdownBars)
|
||
]) : E('span')
|
||
])
|
||
])
|
||
]);
|
||
},
|
||
|
||
// Collections Card - Shows installed collections with quick actions
|
||
renderCollectionsCard: function() {
|
||
var self = this;
|
||
var collections = this.collections || [];
|
||
|
||
if (!collections.length) {
|
||
return E('span'); // Empty if no collections
|
||
}
|
||
|
||
var collectionItems = collections.slice(0, 6).map(function(col) {
|
||
var name = col.name || col.Name || 'unknown';
|
||
var status = col.status || col.Status || '';
|
||
var version = col.version || col.Version || '';
|
||
var isInstalled = status.toLowerCase().indexOf('enabled') >= 0 || status.toLowerCase().indexOf('installed') >= 0;
|
||
var hasUpdate = status.toLowerCase().indexOf('update') >= 0;
|
||
|
||
return E('div', { 'style': 'display: flex; align-items: center; justify-content: space-between; padding: 0.5em 0; border-bottom: 1px solid rgba(255,255,255,0.1);' }, [
|
||
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
|
||
E('span', { 'style': 'font-size: 1.2em;' }, isInstalled ? '✅' : '⬜'),
|
||
E('span', { 'style': 'font-size: 0.9em;' }, name)
|
||
]),
|
||
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
|
||
E('span', { 'style': 'font-size: 0.75em; color: #888;' }, 'v' + version),
|
||
hasUpdate ? E('span', { 'style': 'font-size: 0.7em; padding: 0.15em 0.4em; background: #ffa500; color: #000; border-radius: 3px;' }, 'UPDATE') : E('span')
|
||
])
|
||
]);
|
||
});
|
||
|
||
return E('div', { 'class': 'cs-collections-card' }, [
|
||
E('div', { 'class': 'cs-card' }, [
|
||
E('div', { 'class': 'cs-card-header' }, [
|
||
E('div', { 'class': 'cs-card-title' }, _('Installed Collections')),
|
||
E('button', {
|
||
'class': 'cs-btn cs-btn-secondary cs-btn-sm',
|
||
'click': ui.createHandlerFn(this, 'handleUpdateHub')
|
||
}, _('Update Hub'))
|
||
]),
|
||
E('div', { 'class': 'cs-card-body' }, collectionItems)
|
||
])
|
||
]);
|
||
},
|
||
|
||
handleUpdateHub: function() {
|
||
var self = this;
|
||
ui.showModal(_('Updating Hub'), [
|
||
E('p', {}, _('Downloading latest hub index...')),
|
||
E('div', { 'class': 'spinning' })
|
||
]);
|
||
|
||
this.csApi.updateHub().then(function(result) {
|
||
ui.hideModal();
|
||
if (result && result.success) {
|
||
self.showToast(_('Hub updated successfully'), 'success');
|
||
self.refreshDashboard();
|
||
} else {
|
||
self.showToast((result && result.error) || _('Hub update failed'), 'error');
|
||
}
|
||
}).catch(function(err) {
|
||
ui.hideModal();
|
||
self.showToast(err.message || _('Hub update failed'), 'error');
|
||
});
|
||
},
|
||
|
||
// Hub Stats Card - Shows installed parsers, scenarios, collections counts
|
||
renderHubStatsCard: function() {
|
||
var hub = this.hubData || {};
|
||
|
||
// Count installed items by type
|
||
var parsers = hub.parsers || [];
|
||
var scenarios = hub.scenarios || [];
|
||
var collections = hub.collections || [];
|
||
var postoverflows = hub.postoverflows || [];
|
||
|
||
var installedParsers = parsers.filter(function(p) {
|
||
return p.installed || (p.status && p.status.toLowerCase().indexOf('enabled') >= 0);
|
||
});
|
||
var installedScenarios = scenarios.filter(function(s) {
|
||
return s.installed || (s.status && s.status.toLowerCase().indexOf('enabled') >= 0);
|
||
});
|
||
var installedCollections = collections.filter(function(c) {
|
||
return c.installed || (c.status && c.status.toLowerCase().indexOf('enabled') >= 0);
|
||
});
|
||
|
||
// Get parser details for display
|
||
var parserList = installedParsers.slice(0, 8).map(function(p) {
|
||
var name = p.name || p.Name || 'unknown';
|
||
var shortName = name.split('/').pop();
|
||
return E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em; padding: 0.35em 0; border-bottom: 1px solid rgba(255,255,255,0.05);' }, [
|
||
E('span', { 'style': 'color: #22c55e;' }, '✓'),
|
||
E('span', { 'style': 'font-size: 0.85em;' }, shortName)
|
||
]);
|
||
});
|
||
|
||
return E('div', { 'class': 'cs-card' }, [
|
||
E('div', { 'class': 'cs-card-header' }, [
|
||
E('div', { 'class': 'cs-card-title' }, _('Hub Components'))
|
||
]),
|
||
E('div', { 'class': 'cs-card-body' }, [
|
||
// Mini stats row
|
||
E('div', { 'style': 'display: flex; gap: 1em; margin-bottom: 1em;' }, [
|
||
E('div', { 'style': 'flex: 1; text-align: center; padding: 0.75em; background: rgba(34,197,94,0.1); border-radius: 8px;' }, [
|
||
E('div', { 'style': 'font-size: 1.5em; font-weight: 700; color: #22c55e;' }, String(installedParsers.length)),
|
||
E('div', { 'style': 'font-size: 0.75em; color: #888;' }, _('Parsers'))
|
||
]),
|
||
E('div', { 'style': 'flex: 1; text-align: center; padding: 0.75em; background: rgba(59,130,246,0.1); border-radius: 8px;' }, [
|
||
E('div', { 'style': 'font-size: 1.5em; font-weight: 700; color: #3b82f6;' }, String(installedScenarios.length)),
|
||
E('div', { 'style': 'font-size: 0.75em; color: #888;' }, _('Scenarios'))
|
||
]),
|
||
E('div', { 'style': 'flex: 1; text-align: center; padding: 0.75em; background: rgba(168,85,247,0.1); border-radius: 8px;' }, [
|
||
E('div', { 'style': 'font-size: 1.5em; font-weight: 700; color: #a855f7;' }, String(installedCollections.length)),
|
||
E('div', { 'style': 'font-size: 0.75em; color: #888;' }, _('Collections'))
|
||
])
|
||
]),
|
||
// Parser list
|
||
E('div', { 'style': 'font-size: 0.8em; font-weight: 600; color: #94a3b8; margin-bottom: 0.5em;' }, _('Installed Parsers')),
|
||
parserList.length > 0 ? E('div', {}, parserList) : E('div', { 'style': 'color: #666; font-size: 0.85em;' }, _('No parsers installed'))
|
||
])
|
||
]);
|
||
},
|
||
|
||
// Firewall Health Status Card
|
||
renderFirewallHealth: function() {
|
||
var stats = this.nftablesStats || {};
|
||
var health = stats.firewall_health || {};
|
||
|
||
var status = health.status || 'unknown';
|
||
var issues = health.issues || '';
|
||
var bouncerRunning = health.bouncer_running;
|
||
var uciEnabled = health.uci_enabled;
|
||
var apiKeyConfigured = health.api_key_configured;
|
||
var inputHooked = health.input_chain_hooked;
|
||
var forwardHooked = health.forward_chain_hooked;
|
||
var setsHaveTimeout = health.sets_have_timeout;
|
||
var decisionsSynced = health.decisions_synced;
|
||
var cscliDecisions = health.cscli_decisions_count || 0;
|
||
var nftElements = health.nft_elements_count || 0;
|
||
|
||
var statusColor = status === 'ok' ? '#00d4aa' : (status === 'warning' ? '#ffa500' : '#ff4757');
|
||
var statusIcon = status === 'ok' ? '✅' : (status === 'warning' ? '⚠️' : '❌');
|
||
var statusText = status === 'ok' ? 'Healthy' : (status === 'warning' ? 'Warning' : 'Error');
|
||
|
||
var checkItems = [
|
||
{ label: 'Bouncer Process', ok: bouncerRunning, detail: bouncerRunning ? 'Running' : 'Not running' },
|
||
{ label: 'UCI Enabled', ok: uciEnabled, detail: uciEnabled ? 'Enabled' : 'Disabled' },
|
||
{ label: 'API Key', ok: apiKeyConfigured, detail: apiKeyConfigured ? 'Configured' : 'Missing or default' },
|
||
{ label: 'Input Chain', ok: inputHooked, detail: inputHooked ? 'Hooked' : 'Not hooked' },
|
||
{ label: 'Forward Chain', ok: forwardHooked, detail: forwardHooked ? 'Hooked' : 'Not hooked' },
|
||
{ label: 'Set Timeout', ok: setsHaveTimeout, detail: setsHaveTimeout ? 'Enabled' : 'Disabled' },
|
||
{ label: 'Decisions Sync', ok: decisionsSynced, detail: decisionsSynced ? (nftElements + ' synced') : 'Out of sync' }
|
||
];
|
||
|
||
var checkRows = checkItems.map(function(item) {
|
||
return E('div', { 'style': 'display: flex; align-items: center; justify-content: space-between; padding: 0.4em 0; border-bottom: 1px solid rgba(255,255,255,0.05);' }, [
|
||
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
|
||
E('span', { 'style': 'font-size: 1em;' }, item.ok ? '✅' : '❌'),
|
||
E('span', { 'style': 'font-size: 0.85em;' }, item.label)
|
||
]),
|
||
E('span', { 'style': 'font-size: 0.75em; color: ' + (item.ok ? '#00d4aa' : '#ff4757') + ';' }, item.detail)
|
||
]);
|
||
});
|
||
|
||
return E('div', { 'class': 'cs-card', 'style': 'flex: 1;' }, [
|
||
E('div', { 'class': 'cs-card-header' }, [
|
||
E('div', { 'class': 'cs-card-title' }, [
|
||
_('Firewall Health'),
|
||
E('span', {
|
||
'style': 'margin-left: 0.75em; font-size: 0.8em; padding: 0.2em 0.6em; background: ' + statusColor + '; border-radius: 12px;'
|
||
}, statusIcon + ' ' + statusText)
|
||
])
|
||
]),
|
||
E('div', { 'class': 'cs-card-body' }, [
|
||
// Status summary
|
||
issues ? E('div', { 'style': 'background: rgba(255,71,87,0.1); border: 1px solid rgba(255,71,87,0.3); border-radius: 8px; padding: 0.75em; margin-bottom: 1em;' }, [
|
||
E('div', { 'style': 'font-size: 0.85em; color: #ff4757;' }, issues)
|
||
]) : E('span'),
|
||
// Sync stats
|
||
E('div', { 'style': 'display: flex; gap: 1em; margin-bottom: 1em;' }, [
|
||
E('div', { 'style': 'flex: 1; text-align: center; padding: 0.5em; background: rgba(102,126,234,0.1); border-radius: 8px;' }, [
|
||
E('div', { 'style': 'font-size: 1.25em; font-weight: 700; color: #667eea;' }, String(cscliDecisions)),
|
||
E('div', { 'style': 'font-size: 0.7em; color: #888;' }, 'Decisions')
|
||
]),
|
||
E('div', { 'style': 'flex: 1; text-align: center; padding: 0.5em; background: rgba(0,212,170,0.1); border-radius: 8px;' }, [
|
||
E('div', { 'style': 'font-size: 1.25em; font-weight: 700; color: #00d4aa;' }, String(nftElements)),
|
||
E('div', { 'style': 'font-size: 0.7em; color: #888;' }, 'In Firewall')
|
||
])
|
||
]),
|
||
// Check items
|
||
E('div', {}, checkRows)
|
||
])
|
||
]);
|
||
},
|
||
|
||
// Firewall Blocks - Shows IPs blocked in nftables
|
||
renderFirewallBlocks: function() {
|
||
var self = this;
|
||
var stats = this.nftablesStats || {};
|
||
|
||
// Check if nftables available
|
||
if (stats.error) {
|
||
return E('div', { 'class': 'cs-card' }, [
|
||
E('div', { 'class': 'cs-card-header' }, [
|
||
E('div', { 'class': 'cs-card-title' }, _('Firewall Blocks'))
|
||
]),
|
||
E('div', { 'class': 'cs-card-body' }, [
|
||
E('div', { 'class': 'cs-empty' }, [
|
||
E('div', { 'class': 'cs-empty-icon' }, '⚠️'),
|
||
E('p', {}, stats.error)
|
||
])
|
||
])
|
||
]);
|
||
}
|
||
|
||
var ipv4Active = stats.ipv4_table_exists;
|
||
var ipv6Active = stats.ipv6_table_exists;
|
||
var ipv4List = stats.ipv4_blocked_ips || [];
|
||
var ipv6List = stats.ipv6_blocked_ips || [];
|
||
var ipv4Rules = stats.ipv4_rules_count || 0;
|
||
var ipv6Rules = stats.ipv6_rules_count || 0;
|
||
// Use total counts from API (includes all IPs, not just sample)
|
||
var ipv4Total = stats.ipv4_total_count || ipv4List.length;
|
||
var ipv6Total = stats.ipv6_total_count || ipv6List.length;
|
||
var ipv4Capi = stats.ipv4_capi_count || 0;
|
||
var ipv4Cscli = stats.ipv4_cscli_count || 0;
|
||
var ipv6Capi = stats.ipv6_capi_count || 0;
|
||
var ipv6Cscli = stats.ipv6_cscli_count || 0;
|
||
var totalBlocked = ipv4Total + ipv6Total;
|
||
|
||
// Build IP list (combine IPv4 and IPv6, limit to 20)
|
||
var allIps = [];
|
||
ipv4List.forEach(function(ip) { allIps.push({ ip: ip, type: 'IPv4' }); });
|
||
ipv6List.forEach(function(ip) { allIps.push({ ip: ip, type: 'IPv6' }); });
|
||
var displayIps = allIps.slice(0, 20);
|
||
|
||
var ipRows = displayIps.map(function(item) {
|
||
return E('div', {
|
||
'style': 'display: flex; align-items: center; justify-content: space-between; padding: 0.5em 0; border-bottom: 1px solid rgba(255,255,255,0.1);'
|
||
}, [
|
||
E('div', { 'style': 'display: flex; align-items: center; gap: 0.75em;' }, [
|
||
E('span', { 'style': 'font-size: 1.1em;' }, '🚫'),
|
||
E('code', { 'style': 'font-size: 0.85em; background: rgba(0,0,0,0.2); padding: 0.2em 0.5em; border-radius: 4px;' }, item.ip),
|
||
E('span', {
|
||
'style': 'font-size: 0.7em; padding: 0.15em 0.4em; background: ' + (item.type === 'IPv4' ? '#667eea' : '#764ba2') + '; border-radius: 3px;'
|
||
}, item.type)
|
||
]),
|
||
E('button', {
|
||
'class': 'cs-btn cs-btn-danger cs-btn-sm',
|
||
'style': 'font-size: 0.75em; padding: 0.25em 0.5em;',
|
||
'click': ui.createHandlerFn(self, 'handleUnban', item.ip)
|
||
}, _('Unban'))
|
||
]);
|
||
});
|
||
|
||
// Status indicators with breakdown by origin
|
||
var statusRow = E('div', { 'style': 'display: flex; gap: 1.5em; margin-bottom: 1em; flex-wrap: wrap;' }, [
|
||
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
|
||
E('span', { 'style': 'font-size: 1.2em;' }, ipv4Active ? '✅' : '❌'),
|
||
E('span', { 'style': 'font-size: 0.85em;' }, 'IPv4'),
|
||
E('span', { 'style': 'font-size: 0.75em; color: #888;' }, ipv4Total.toLocaleString() + ' IPs')
|
||
]),
|
||
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
|
||
E('span', { 'style': 'font-size: 1.2em;' }, ipv6Active ? '✅' : '❌'),
|
||
E('span', { 'style': 'font-size: 0.85em;' }, 'IPv6'),
|
||
E('span', { 'style': 'font-size: 0.75em; color: #888;' }, ipv6Total.toLocaleString() + ' IPs')
|
||
]),
|
||
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em; padding-left: 1em; border-left: 1px solid rgba(255,255,255,0.2);' }, [
|
||
E('span', { 'style': 'font-size: 0.8em; padding: 0.2em 0.5em; background: #667eea; border-radius: 4px;' }, 'CAPI'),
|
||
E('span', { 'style': 'font-size: 0.85em; color: #667eea;' }, (ipv4Capi + ipv6Capi).toLocaleString())
|
||
]),
|
||
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
|
||
E('span', { 'style': 'font-size: 0.8em; padding: 0.2em 0.5em; background: #00d4aa; border-radius: 4px;' }, 'Local'),
|
||
E('span', { 'style': 'font-size: 0.85em; color: #00d4aa;' }, (ipv4Cscli + ipv6Cscli).toLocaleString())
|
||
])
|
||
]);
|
||
|
||
return E('div', { 'class': 'cs-card', 'style': 'flex: 2;' }, [
|
||
E('div', { 'class': 'cs-card-header' }, [
|
||
E('div', { 'class': 'cs-card-title' }, [
|
||
_('Firewall Blocks'),
|
||
E('span', {
|
||
'style': 'margin-left: 0.75em; font-size: 0.8em; padding: 0.2em 0.6em; background: linear-gradient(90deg, #ff4757, #ff6b81); border-radius: 12px;'
|
||
}, totalBlocked + ' blocked')
|
||
])
|
||
]),
|
||
E('div', { 'class': 'cs-card-body' }, [
|
||
statusRow,
|
||
ipRows.length > 0 ?
|
||
E('div', { 'style': 'max-height: 300px; overflow-y: auto;' }, ipRows) :
|
||
E('div', { 'class': 'cs-empty', 'style': 'padding: 1em;' }, [
|
||
E('div', { 'class': 'cs-empty-icon' }, '✅'),
|
||
E('p', {}, _('No IPs currently blocked in firewall'))
|
||
]),
|
||
allIps.length > 20 ? E('div', { 'style': 'text-align: center; padding: 0.5em; font-size: 0.8em; color: #888;' },
|
||
_('Showing 20 of ') + allIps.length + _(' blocked IPs')
|
||
) : E('span')
|
||
])
|
||
]);
|
||
},
|
||
|
||
renderLogCard: function(entries) {
|
||
return E('div', { 'class': 'cs-card cs-log-card' }, [
|
||
E('div', { 'class': 'cs-card-header' }, [
|
||
E('div', { 'class': 'cs-card-title' }, _('CrowdSec Logs'))
|
||
]),
|
||
entries && entries.length ?
|
||
E('pre', { 'class': 'cs-log-output' }, entries.slice(-30).join('\n')) :
|
||
E('p', { 'class': 'cs-empty' }, _('No log entries'))
|
||
]);
|
||
},
|
||
|
||
handleSnapshot: function() {
|
||
var self = this;
|
||
ui.showModal(_('Collecting snapshot'), [
|
||
E('p', {}, _('Aggregating dmesg/logread into SecuBox log…')),
|
||
E('div', { 'class': 'spinning' })
|
||
]);
|
||
this.csApi.collectDebugSnapshot().then(function(result) {
|
||
ui.hideModal();
|
||
if (result && result.success) {
|
||
self.refreshDashboard();
|
||
self.showToast(_('Snapshot appended to /var/log/seccubox.log'), 'success');
|
||
} else {
|
||
self.showToast((result && result.error) || _('Snapshot failed'), 'error');
|
||
}
|
||
}).catch(function(err) {
|
||
ui.hideModal();
|
||
self.showToast(err.message || _('Snapshot failed'), 'error');
|
||
});
|
||
},
|
||
|
||
handleSaveApply: null,
|
||
handleSave: null,
|
||
handleReset: null
|
||
});
|