<?php
/**
* Sentinela ? Intelligent Linux Security Framework
* API REST para el dashboard
*
* Endpoints:
* GET /api/?action=status - Estado del servidor y firewall
* GET /api/?action=stats - Estadísticas de ataques
* GET /api/?action=banned - Lista de IPs bloqueadas
* GET /api/?action=attacks - Últimos ataques detectados
* GET /api/?action=top_ips - Top IPs atacantes
* GET /api/?action=top_countries - Top países atacantes
* GET /api/?action=geoip&ip=X - GeoIP de una IP
* POST /api/?action=unban&ip=X - Desbloquear IP
* POST /api/?action=ban&ip=X - Bloquear IP
*/
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
// Configuración de rutas
$logDir = '/var/log/sentinela';
$installDir = '/usr/local/sentinela';
$configFile = '/etc/sentinela/config.conf';
if (!is_dir($logDir)) {
$logDir = '/root/sentinela/logs';
}
if (!is_dir($installDir)) {
$installDir = '/root/sentinela';
}
// Cargar configuración
function getConfig() {
global $configFile;
$config = [];
if (file_exists($configFile)) {
$lines = file($configFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$line = trim($line);
if (strpos($line, '#') === 0 || strpos($line, '=') === false) continue;
list($key, $value) = explode('=', $line, 2);
$key = trim($key);
$value = trim($value, "\"' \t\r\n");
$config[$key] = $value;
}
}
return $config;
}
// Leer archivos de log
function readLogFile($path, $lines = 100) {
if (!file_exists($path)) return [];
$content = file_get_contents($path);
if (empty($content)) return [];
$allLines = explode("\n", trim($content));
return array_slice($allLines, -$lines);
}
// Parsear línea de log de ataque
function parseAttackLine($line) {
// Formato: [FECHA] [ATTACK] TIPO|IP|REASON|URI|UA
$parts = explode('] [ATTACK] ', $line);
if (count($parts) < 2) return null;
$timestamp = trim($parts[0], '[]');
$data = $parts[1];
$fields = explode('|', $data);
return [
'timestamp' => $timestamp,
'type' => $fields[0] ?? 'unknown',
'ip' => $fields[1] ?? '0.0.0.0',
'reason' => $fields[2] ?? '',
'uri' => $fields[3] ?? '',
'ua' => $fields[4] ?? '',
'service' => $fields[5] ?? 'web'
];
}
// Parsear línea de log de banned
function parseBannedLine($line) {
// Formato: [FECHA] [BAN] IP blocked (razón, timeout: Xs)
$parts = explode('] [BAN] ', $line);
if (count($parts) < 2) return null;
$timestamp = trim($parts[0], '[]');
$data = $parts[1];
preg_match('/^(\S+) blocked \((.*), timeout: (\d+)s\)/', $data, $m);
if (count($m) >= 4) {
return [
'timestamp' => $timestamp,
'ip' => $m[1],
'reason' => $m[2],
'timeout' => (int)$m[3]
];
}
preg_match('/^(\S+) blocked \((.*)\)/', $data, $m);
if (count($m) >= 3) {
return [
'timestamp' => $timestamp,
'ip' => $m[1],
'reason' => $m[2],
'timeout' => 0
];
}
return null;
}
// Obtener IPs bloqueadas desde ipset
function getBannedIPs() {
$output = shell_exec('sudo ipset list SENTINELA_BLACKLIST 2>/dev/null');
if (empty($output)) return [];
$ips = [];
$lines = explode("\n", $output);
$inList = false;
foreach ($lines as $line) {
if (preg_match('/^\d/', $line)) {
$parts = preg_split('/\s+/', trim($line));
if (count($parts) >= 2) {
$ips[] = [
'ip' => $parts[0],
'timeout' => (int)$parts[1]
];
}
}
}
return $ips;
}
// Obtener stats del sistema
function getSystemStats() {
// CPU
$cpu = shell_exec("top -bn1 | grep 'Cpu(s)' | awk '{print $2}'");
$cpu = trim($cpu ?? '0');
// RAM
$memInfo = file_get_contents('/proc/meminfo');
preg_match('/MemTotal:\s+(\d+)/', $memInfo, $mt);
preg_match('/MemAvailable:\s+(\d+)/', $memInfo, $ma);
$memTotal = (int)($mt[1] ?? 0);
$memAvail = (int)($ma[1] ?? 0);
$memUsed = $memTotal - $memAvail;
$memPercent = $memTotal > 0 ? round(($memUsed / $memTotal) * 100, 1) : 0;
// Disk
$disk = shell_exec("df -h / | tail -1 | awk '{print $5}'");
$disk = trim($disk ?? '0%');
// Load
$load = sys_getloadavg();
// Uptime
$uptime = file_get_contents('/proc/uptime');
$uptimeSeconds = (int)($uptime ? explode(' ', $uptime)[0] : 0);
$uptimeDays = floor($uptimeSeconds / 86400);
// Conexiones
$connections = shell_exec("ss -tn state established 2>/dev/null | tail -n +2 | wc -l");
$connections = trim($connections ?? '0');
// Puertos escucha
$listening = shell_exec("ss -tln 2>/dev/null | tail -n +2 | wc -l");
$listening = trim($listening ?? '0');
return [
'cpu' => $cpu,
'mem_total' => $memTotal,
'mem_used' => $memUsed,
'mem_percent' => $memPercent,
'disk' => $disk,
'load_1' => $load[0] ?? 0,
'load_5' => $load[1] ?? 0,
'load_15' => $load[2] ?? 0,
'uptime_days' => $uptimeDays,
'connections' => (int)$connections,
'listening' => (int)$listening,
'hostname' => gethostname(),
'kernel' => php_uname('r')
];
}
// Obtener estado del firewall
function getFirewallStatus() {
$chains = [];
$chainInput = shell_exec('sudo iptables -L SENTINELA_INPUT -n 2>/dev/null | tail -n +3 | wc -l');
$chainNew = shell_exec('sudo iptables -L SENTINELA_NEW -n 2>/dev/null | tail -n +3 | wc -l');
$chains['SENTINELA_INPUT'] = (int)trim($chainInput ?? '0');
$chains['SENTINELA_NEW'] = (int)trim($chainNew ?? '0');
$enabled = ($chains['SENTINELA_INPUT'] > 0);
// Ipsets
$ipsets = [];
$setNames = ['SENTINELA_TRUSTED', 'SENTINELA_BLACKLIST', 'SENTINELA_DDOS', 'SENTINELA_REPUTATION', 'SENTINELA_HONEY'];
foreach ($setNames as $name) {
$output = shell_exec("sudo ipset list {$name} 2>/dev/null | grep -cE '^[0-9]'");
$ipsets[$name] = (int)trim($output ?? '0');
}
return [
'enabled' => $enabled,
'chains' => $chains,
'ipsets' => $ipsets
];
}
// Obtener ataques recientes
function getRecentAttacks($limit = 50) {
global $logDir;
$lines = readLogFile("{$logDir}/attacks.log", $limit);
$attacks = [];
foreach ($lines as $line) {
$parsed = parseAttackLine($line);
if ($parsed) $attacks[] = $parsed;
}
return array_reverse($attacks);
}
// Obtener IPs bloqueadas (desde log + ipset)
function getBannedLog($limit = 50) {
global $logDir;
$lines = readLogFile("{$logDir}/banned.log", $limit);
$banned = [];
foreach ($lines as $line) {
$parsed = parseBannedLine($line);
if ($parsed) $banned[] = $parsed;
}
return array_reverse($banned);
}
// Contar tipos de ataque
function countAttackTypes($attacks) {
$counts = [];
foreach ($attacks as $a) {
$type = $a['type'] ?? 'unknown';
if (!isset($counts[$type])) $counts[$type] = 0;
$counts[$type]++;
}
arsort($counts);
return $counts;
}
// Contar IPs
function countTopIPs($attacks, $limit = 10) {
$counts = [];
foreach ($attacks as $a) {
$ip = $a['ip'] ?? '0.0.0.0';
if (!isset($counts[$ip])) $counts[$ip] = 0;
$counts[$ip]++;
}
arsort($counts);
return array_slice($counts, 0, $limit);
}
// Contar países (simulado desde archivo cache o API)
function countTopCountries($attacks, $limit = 10) {
$counts = [];
foreach ($attacks as $a) {
$ip = $a['ip'] ?? '0.0.0.0';
$country = getCountryFromCache($ip);
if (!isset($counts[$country])) $counts[$country] = 0;
$counts[$country]++;
}
arsort($counts);
return array_slice($counts, 0, $limit);
}
// Cache de GeoIP
function getCountryFromCache($ip) {
$cacheFile = '/tmp/sentinela_geoip_cache.json';
$cache = [];
if (file_exists($cacheFile)) {
$cache = json_decode(file_get_contents($cacheFile), true) ?? [];
}
if (isset($cache[$ip])) return $cache[$ip];
// Intentar con geoiplookup
$country = '??';
$output = shell_exec("geoiplookup {$ip} 2>/dev/null | grep -oP 'Country:\s+\K\w+'");
if ($output) {
$country = trim($output);
} else {
// API externa
$json = @file_get_contents("http://ip-api.com/json/{$ip}");
if ($json) {
$data = json_decode($json, true);
$country = $data['countryCode'] ?? '??';
usleep(100000); // Rate limit: 45 req/min
}
}
$cache[$ip] = $country;
if (count($cache) > 1000) $cache = array_slice($cache, -500, 500, true);
file_put_contents($cacheFile, json_encode($cache));
return $country;
}
// =========================================================================
// Manejo de acciones
// =========================================================================
$action = $_GET['action'] ?? $_POST['action'] ?? 'status';
switch ($action) {
case 'status':
$system = getSystemStats();
$firewall = getFirewallStatus();
$banned = getBannedIPs();
$attacks = getRecentAttacks(10);
$attackTypes = countAttackTypes(getRecentAttacks(500));
echo json_encode([
'success' => true,
'system' => $system,
'firewall' => $firewall,
'banned_count' => count($banned),
'recent_attacks' => $attacks,
'attack_types' => $attackTypes
]);
break;
case 'stats':
$attacks = getRecentAttacks(1000);
$banned = getBannedIPs();
$attackTypes = countAttackTypes($attacks);
$topIPs = countTopIPs($attacks, 15);
$topCountries = countTopCountries($attacks, 15);
echo json_encode([
'success' => true,
'total_attacks' => count($attacks),
'total_banned' => count($banned),
'attack_types' => $attackTypes,
'top_ips' => $topIPs,
'top_countries' => $topCountries
]);
break;
case 'banned':
$banned = getBannedIPs();
$bannedLog = getBannedLog(100);
echo json_encode([
'success' => true,
'banned' => $banned,
'banned_log' => $bannedLog,
'total' => count($banned)
]);
break;
case 'attacks':
$attacks = getRecentAttacks(200);
echo json_encode([
'success' => true,
'attacks' => $attacks,
'total' => count($attacks)
]);
break;
case 'top_ips':
$attacks = getRecentAttacks(1000);
$topIPs = countTopIPs($attacks, 20);
echo json_encode([
'success' => true,
'top_ips' => $topIPs
]);
break;
case 'top_countries':
$attacks = getRecentAttacks(1000);
$topCountries = countTopCountries($attacks, 20);
echo json_encode([
'success' => true,
'top_countries' => $topCountries
]);
break;
case 'geoip':
$ip = $_GET['ip'] ?? '';
if (empty($ip) || !filter_var($ip, FILTER_VALIDATE_IP)) {
echo json_encode(['success' => false, 'error' => 'IP inválida']);
exit;
}
$country = getCountryFromCache($ip);
$asn = shell_exec("mmdblookup --file /usr/share/GeoIP/GeoLite2-ASN.mmdb --ip {$ip} autonomous_system_number 2>/dev/null | grep -oP '\d+' | head -1");
$org = shell_exec("mmdblookup --file /usr/share/GeoIP/GeoLite2-ASN.mmdb --ip {$ip} autonomous_system_organization 2>/dev/null | grep -oP '\"[^\"]+\"' | head -1 | tr -d '\"'");
echo json_encode([
'success' => true,
'ip' => $ip,
'country' => $country ?: '??',
'asn' => $asn ? 'AS' . trim($asn) : 'N/A',
'org' => $org ?: 'N/A'
]);
break;
case 'unban':
$ip = $_POST['ip'] ?? $_GET['ip'] ?? '';
if (empty($ip) || !filter_var($ip, FILTER_VALIDATE_IP)) {
echo json_encode(['success' => false, 'error' => 'IP inválida']);
exit;
}
$output = shell_exec("sudo ipset del SENTINELA_BLACKLIST {$ip} 2>&1");
echo json_encode([
'success' => true,
'message' => "IP {$ip} desbloqueada",
'output' => $output
]);
break;
case 'ban':
$ip = $_POST['ip'] ?? $_GET['ip'] ?? '';
if (empty($ip) || !filter_var($ip, FILTER_VALIDATE_IP)) {
echo json_encode(['success' => false, 'error' => 'IP inválida']);
exit;
}
$output = shell_exec("sudo ipset add SENTINELA_BLACKLIST {$ip} timeout 600 2>&1");
echo json_encode([
'success' => true,
'message' => "IP {$ip} bloqueada",
'output' => $output
]);
break;
default:
echo json_encode([
'success' => false,
'error' => 'Acción no válida',
'available_actions' => [
'status', 'stats', 'banned', 'attacks',
'top_ips', 'top_countries', 'geoip',
'unban', 'ban'
]
]);
}
|