/**
* Sentinela ? Intelligent Linux Security Framework
* Dashboard JavaScript
*
* Bootstrap 5 + Chart.js + AJAX puro
*/
// =========================================================================
// State
// =========================================================================
let state = {
attacks: [],
banned: [],
attackTypesChart: null,
topCountriesChart: null,
updateInterval: null
};
// =========================================================================
// API Calls
// =========================================================================
const API = {
get: async (action, params = {}) => {
const url = new URL('api/index.php', window.location.href);
url.searchParams.set('action', action);
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
try {
const res = await fetch(url.toString());
return await res.json();
} catch (e) {
console.error('API Error:', e);
return null;
}
},
post: async (action, params = {}) => {
const url = new URL('api/index.php', window.location.href);
url.searchParams.set('action', action);
try {
const res = await fetch(url.toString(), {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(params)
});
return await res.json();
} catch (e) {
console.error('API Error:', e);
return null;
}
}
};
// =========================================================================
// Dashboard Updates
// =========================================================================
function formatTimestamp(ts) {
if (!ts) return '--';
// [2026-07-01 15:30:00]
const match = ts.match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
if (match) {
const d = new Date(match[0].replace(' ', 'T') + 'Z');
return d.toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
return ts;
}
function getAttackBadge(type) {
const badges = {
sqli: 'danger',
xss: 'warning',
lfi: 'info',
rfi: 'primary',
command_injection: 'danger',
path_traversal: 'warning',
scanner: 'secondary',
tool: 'dark',
ddos: 'danger',
ssh_bruteforce: 'warning',
log4shell: 'danger',
xxe: 'info',
honey: 'success',
reputation: 'primary'
};
return badges[type] || 'secondary';
}
function getAttackIcon(type) {
const icons = {
sqli: 'bi-database',
xss: 'bi-bug',
lfi: 'bi-folder2-open',
rfi: 'bi-globe2',
command_injection: 'bi-terminal',
path_traversal: 'bi-folder-symlink',
scanner: 'bi-search',
tool: 'bi-tools',
ddos: 'bi-water',
ssh_bruteforce: 'bi-key',
log4shell: 'bi-explosion',
xxe: 'bi-filetype-xml',
honey: 'bi-flower1',
reputation: 'bi-shield'
};
return icons[type] || 'bi-shield-exclamation';
}
async function updateServerStats() {
const data = await API.get('status');
if (!data || !data.success) return;
const sys = data.system;
// CPU
const cpuVal = parseFloat(sys.cpu) || 0;
document.getElementById('cpuValue').textContent = cpuVal.toFixed(1) + '%';
document.getElementById('cpuBar').style.width = cpuVal + '%';
// RAM
const memVal = parseFloat(sys.mem_percent) || 0;
document.getElementById('memValue').textContent = memVal.toFixed(1) + '%';
document.getElementById('memBar').style.width = memVal + '%';
// Disk
document.getElementById('diskValue').textContent = sys.disk || '0%';
const diskNum = parseInt(sys.disk) || 0;
document.getElementById('diskBar').style.width = diskNum + '%';
// Load
document.getElementById('loadValue').textContent = parseFloat(sys.load_1 || 0).toFixed(2);
document.getElementById('uptimeValue').textContent = sys.uptime_days + ' días en línea';
document.getElementById('serverHostname').textContent = '? ' + sys.hostname;
// Quick stats
document.getElementById('totalAttacks').textContent = data.attack_types ?
Object.values(data.attack_types).reduce((a, b) => a + b, 0) : 0;
document.getElementById('totalBanned').textContent = data.banned_count || 0;
document.getElementById('totalConnections').textContent = sys.connections || 0;
document.getElementById('uptimeDays').textContent = sys.uptime_days || 0;
// Firewall status
const statusBadge = document.getElementById('firewallStatus');
if (data.firewall && data.firewall.enabled) {
statusBadge.className = 'badge bg-success';
statusBadge.textContent = 'ACTIVO';
} else {
statusBadge.className = 'badge bg-danger';
statusBadge.textContent = 'INACTIVO';
}
// Recent attacks
updateRecentAttacks(data.recent_attacks || []);
}
async function updateStats() {
const data = await API.get('stats');
if (!data || !data.success) return;
state.attacks = data.attack_types || {};
// Attack Types Chart
updateAttackTypesChart(data.attack_types || {});
// Top Countries Chart
updateTopCountriesChart(data.top_countries || {});
// Top IPs Table
updateTopIPsTable(data.top_ips || {});
}
async function updateBanned() {
const data = await API.get('banned');
if (!data || !data.success) return;
state.banned = data.banned || [];
updateBannedTable(state.banned);
}
function updateRecentAttacks(attacks) {
const tbody = document.getElementById('recentAttacksBody');
tbody.innerHTML = attacks.slice(0, 20).map(a => `
<tr>
<td class="text-muted">${formatTimestamp(a.timestamp)}</td>
<td>
<span class="text-info">${a.ip || '0.0.0.0'}</span>
</td>
<td>
<span class="badge bg-${getAttackBadge(a.type)}">
<i class="bi ${getAttackIcon(a.type)}"></i>
${a.type || 'unknown'}
</span>
</td>
</tr>
`).join('');
}
function updateAttackTypesChart(types) {
const labels = Object.keys(types);
const values = Object.values(types);
const colors = {
sqli: '#ff5252',
xss: '#ffd740',
lfi: '#40c4ff',
rfi: '#7c4dff',
command_injection: '#ff1744',
path_traversal: '#ffab40',
scanner: '#78909c',
tool: '#424242',
ddos: '#d50000',
ssh_bruteforce: '#ff9100',
log4shell: '#ff3d00',
xxe: '#00bcd4',
honey: '#00e676',
reputation: '#2979ff'
};
const ctx = document.getElementById('attackTypesChart').getContext('2d');
if (state.attackTypesChart) {
state.attackTypesChart.destroy();
}
state.attackTypesChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: labels.map(l => l.toUpperCase()),
datasets: [{
data: values,
backgroundColor: labels.map(l => colors[l] || '#607d8b'),
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
position: 'right',
labels: {
color: '#e0e0e0',
font: { size: 10, family: 'monospace' },
padding: 8,
boxWidth: 12,
boxHeight: 12
}
}
}
}
});
}
function updateTopCountriesChart(countries) {
const ctx = document.getElementById('topCountriesChart').getContext('2d');
const entries = Object.entries(countries).slice(0, 10);
const labels = entries.map(([k]) => k);
const values = entries.map(([, v]) => v);
if (state.topCountriesChart) {
state.topCountriesChart.destroy();
}
state.topCountriesChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Ataques',
data: values,
backgroundColor: '#40c4ff',
borderColor: '#0288d1',
borderWidth: 1,
borderRadius: 4
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: { display: false }
},
scales: {
x: {
ticks: { color: '#6c8a9e', font: { size: 10 } },
grid: { color: 'rgba(255,255,255,0.05)' }
},
y: {
ticks: { color: '#e0e0e0', font: { size: 11, family: 'monospace' } },
grid: { display: false }
}
}
}
});
}
function updateTopIPsTable(ips) {
const tbody = document.getElementById('topIPsBody');
tbody.innerHTML = Object.entries(ips).slice(0, 20).map(([ip, count]) => `
<tr>
<td><span class="text-info">${ip}</span></td>
<td class="text-end fw-bold">${count}</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-danger px-2 py-0" onclick="banIP('${ip}')">
<i class="bi bi-lock"></i>
</button>
</td>
</tr>
`).join('');
}
function updateBannedTable(banned) {
const tbody = document.getElementById('bannedIPsBody');
if (banned.length === 0) {
tbody.innerHTML = '<tr><td colspan="3" class="text-center text-muted py-3">No hay IPs bloqueadas</td></tr>';
return;
}
tbody.innerHTML = banned.map(b => {
const remaining = b.timeout > 0 ? formatTime(b.timeout) : 'Permanente';
return `
<tr>
<td><span class="text-warning">${b.ip}</span></td>
<td class="text-muted">${remaining}</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-success px-2 py-0" onclick="unbanIP('${b.ip}')">
<i class="bi bi-unlock"></i>
</button>
</td>
</tr>
`;
}).join('');
}
function formatTime(seconds) {
if (seconds <= 0) return 'Permanente';
const m = Math.floor(seconds / 60);
const s = seconds % 60;
if (m > 60) {
const h = Math.floor(m / 60);
return `${h}h ${m % 60}m`;
}
return `${m}m ${s}s`;
}
// =========================================================================
// Actions
// =========================================================================
async function unbanIP(ip) {
const modal = new bootstrap.Modal(document.getElementById('confirmModal'));
document.getElementById('confirmModalBody').textContent = `¿Desbloquear IP ${ip}?`;
document.getElementById('confirmModalBtn').onclick = async () => {
await API.post('unban', { ip });
modal.hide();
refreshAll();
};
modal.show();
}
async function banIP(ip) {
const modal = new bootstrap.Modal(document.getElementById('confirmModal'));
document.getElementById('confirmModalBody').textContent = `¿Bloquear IP ${ip}?`;
document.getElementById('confirmModalBtn').onclick = async () => {
await API.post('ban', { ip });
modal.hide();
refreshAll();
};
modal.show();
}
async function flushBanned() {
const modal = new bootstrap.Modal(document.getElementById('confirmModal'));
document.getElementById('confirmModalBody').textContent = '¿Liberar TODAS las IPs bloqueadas?';
document.getElementById('confirmModalBtn').onclick = async () => {
await fetch('api/index.php?action=flush');
modal.hide();
refreshAll();
};
modal.show();
}
// =========================================================================
// Refresh
// =========================================================================
async function refreshAll() {
document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString('es-MX');
await Promise.all([
updateServerStats(),
updateStats(),
updateBanned()
]);
}
// =========================================================================
// Init
// =========================================================================
document.addEventListener('DOMContentLoaded', () => {
refreshAll();
// Auto-refresh cada 10 segundos
state.updateInterval = setInterval(refreshAll, 10000);
});
|