secubox-openwrt/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/overview.js

462 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
'require view';
'require dom';
'require poll';
'require ui';
'require crowdsec-dashboard/api as api';
/**
* 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 = new api();
return this.csApi.getDashboardData();
},
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('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(this, '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.csApi.getDashboardData();
} 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.csApi.getDashboardData();
} 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 || [];
return E('div', {}, [
this.renderHeader(status),
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' }, [
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.renderBanModal()
]);
},
render: function(data) {
var self = this;
this.data = data;
var view = E('div', { 'class': 'crowdsec-dashboard' }, [
E('div', { 'id': 'cs-dashboard-content' }, this.renderContent(data))
]);
// Setup polling for auto-refresh (every 30 seconds)
poll.add(function() {
return self.csApi.getDashboardData().then(function(newData) {
self.data = newData;
self.updateView();
});
}, 30);
return view;
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});