228 lines
46 KiB
PHP
Executable File
228 lines
46 KiB
PHP
Executable File
<?php
|
|
session_start();
|
|
$base_url = 'http://89.167.40.150:5821';
|
|
$current_ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? 'Unknown';
|
|
if (strpos($current_ip, ',') !== false) $current_ip = trim(explode(',', $current_ip)[0]);
|
|
if (!isset($_SESSION['ignored_ips'])) $_SESSION['ignored_ips'] = [];
|
|
$whitelist_file = '/opt/wevads/config/ip-whitelist.json';
|
|
$saved_whitelist = []; if (file_exists($whitelist_file)) { $json = json_decode(file_get_contents($whitelist_file), true); if (isset($json['whitelisted_ips'])) { foreach ($json['whitelisted_ips'] as $entry) { $saved_whitelist[] = $entry['ip']; } } }
|
|
$allowed_db_ips = array_unique(array_merge(['127.0.0.1', '::1', 'localhost', '89.167.40.150'], $saved_whitelist));
|
|
$allowed_db_users = ['admin', 'postgres', 'wevads'];
|
|
$sensitive_tables = ['users', 'passwords', 'credentials', 'tokens', 'api_keys', 'sessions', 'accounts', 'clients', 'payments', 'credit_cards'];
|
|
|
|
if (isset($_POST['action'])) {
|
|
header('Content-Type: application/json');
|
|
$result = ['success' => false, 'message' => ''];
|
|
switch ($_POST['action']) {
|
|
case 'block_ip': $ip = escapeshellarg($_POST['ip'] ?? ''); if ($ip) { exec("fail2ban-client set sshd banip " . trim($_POST['ip'], "'") . " 2>&1", $output, $code); $result = ['success' => $code === 0, 'message' => $code === 0 ? "IP bloquée" : "Échec"]; } break;
|
|
case 'kill_query': case 'kill_connection': $pid = intval($_POST['pid'] ?? 0); if ($pid > 0) { try { $pdo = new PDO("pgsql:host=127.0.0.1;port=5432;dbname=adx_system", 'admin', 'admin123'); $stmt = $pdo->prepare("SELECT pg_terminate_backend(?)"); $stmt->execute([$pid]); $result = ['success' => true, 'message' => "Connexion #{$pid} terminée"]; } catch (Exception $e) { $result = ['success' => false, 'message' => $e->getMessage()]; } } break;
|
|
case 'block_db_ip': $ip = escapeshellarg($_POST['ip'] ?? ''); if ($ip) { exec("iptables -A INPUT -s {$ip} -p tcp --dport 5432 -j DROP 2>&1", $output, $code); $result = ['success' => $code === 0, 'message' => $code === 0 ? "IP bloquée de la DB" : "Échec"]; } break;
|
|
case 'vacuum_table': $table = preg_replace('/[^a-zA-Z0-9_]/', '', $_POST['table'] ?? ''); if ($table) { try { $pdo = new PDO("pgsql:host=127.0.0.1;port=5432;dbname=adx_system", 'admin', 'admin123'); $pdo->exec("VACUUM ANALYZE {$table}"); $result = ['success' => true, 'message' => "VACUUM {$table} OK"]; } catch (Exception $e) { $result = ['success' => false, 'message' => $e->getMessage()]; } } break;
|
|
case 'vacuum_full': $table = preg_replace('/[^a-zA-Z0-9_]/', '', $_POST['table'] ?? ''); if ($table) { exec("sudo -u postgres psql -d adx_system -c \"VACUUM FULL {$table}\" 2>&1", $output, $code); $result = ['success' => $code === 0, 'message' => $code === 0 ? "VACUUM FULL {$table} OK" : implode("\n", $output)]; } break;
|
|
case 'analyze_table': $table = preg_replace('/[^a-zA-Z0-9_]/', '', $_POST['table'] ?? ''); if ($table) { try { $pdo = new PDO("pgsql:host=127.0.0.1;port=5432;dbname=adx_system", 'admin', 'admin123'); $pdo->exec("ANALYZE {$table}"); $result = ['success' => true, 'message' => "ANALYZE {$table} OK"]; } catch (Exception $e) { $result = ['success' => false, 'message' => $e->getMessage()]; } } break;
|
|
case 'reindex_table': $table = preg_replace('/[^a-zA-Z0-9_]/', '', $_POST['table'] ?? ''); if ($table) { exec("sudo -u postgres psql -d adx_system -c \"REINDEX TABLE {$table}\" 2>&1", $output, $code); $result = ['success' => $code === 0, 'message' => $code === 0 ? "REINDEX {$table} OK" : implode("\n", $output)]; } break;
|
|
case 'cancel_query': $pid = intval($_POST['pid'] ?? 0); if ($pid > 0) { try { $pdo = new PDO("pgsql:host=127.0.0.1;port=5432;dbname=adx_system", 'admin', 'admin123'); $stmt = $pdo->prepare("SELECT pg_cancel_backend(?)"); $stmt->execute([$pid]); $result = ['success' => true, 'message' => "Requête #{$pid} annulée"]; } catch (Exception $e) { $result = ['success' => false, 'message' => $e->getMessage()]; } } break;
|
|
case 'clear_idle_connections': try { $pdo = new PDO("pgsql:host=127.0.0.1;port=5432;dbname=adx_system", 'admin', 'admin123'); $stmt = $pdo->query("SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = 'idle' AND pid <> pg_backend_pid() AND query_start < NOW() - INTERVAL '10 minutes'"); $count = $stmt->rowCount(); $result = ['success' => true, 'message' => "{$count} connexion(s) fermée(s)"]; } catch (Exception $e) { $result = ['success' => false, 'message' => $e->getMessage()]; } break;
|
|
case 'reset_stats': try { $pdo = new PDO("pgsql:host=127.0.0.1;port=5432;dbname=adx_system", 'admin', 'admin123'); $pdo->exec("SELECT pg_stat_reset()"); $result = ['success' => true, 'message' => "Stats remises à zéro"]; } catch (Exception $e) { $result = ['success' => false, 'message' => $e->getMessage()]; } break;
|
|
case 'ignore_ip': $ip = $_POST['ip'] ?? ''; if ($ip && !in_array($ip, $_SESSION['ignored_ips'])) { $_SESSION['ignored_ips'][] = $ip; $result = ['success' => true, 'message' => "IP {$ip} ignorée"]; } break;
|
|
case 'whitelist_ip': $ip = $_POST['ip'] ?? ''; if ($ip && filter_var($ip, FILTER_VALIDATE_IP)) { @mkdir('/opt/wevads/config', 0755, true); $current = file_exists($whitelist_file) ? file_get_contents($whitelist_file) : ''; if (strpos($current, $ip) === false) { file_put_contents($whitelist_file, $ip . "\n", FILE_APPEND); $result = ['success' => true, 'message' => "IP {$ip} autorisée"]; } else { $result = ['success' => true, 'message' => "IP déjà autorisée"]; } } break;
|
|
case 'clear_ignored': $_SESSION['ignored_ips'] = []; $result = ['success' => true, 'message' => 'IPs ignorées effacées']; break;
|
|
}
|
|
echo json_encode($result); exit;
|
|
}
|
|
|
|
$effective_allowed_ips = $allowed_db_ips;
|
|
|
|
function getDatabaseStats() {
|
|
global $effective_allowed_ips, $allowed_db_users, $sensitive_tables;
|
|
$ignored_ips = $_SESSION['ignored_ips'] ?? [];
|
|
$stats = ['connected' => false, 'connections' => ['active' => 0, 'idle' => 0, 'idle_in_transaction' => 0, 'max' => 100, 'total' => 0], 'cache_hit_ratio' => 0, 'databases' => [], 'tables' => [], 'slow_queries' => [], 'active_queries' => [], 'locks' => 0, 'waiting_locks' => [], 'uptime' => 'N/A', 'transactions' => 0, 'intrusion_alerts' => [], 'recent_connections' => [], 'long_running' => [], 'idle_connections' => 0];
|
|
try {
|
|
$pdo = new PDO("pgsql:host=127.0.0.1;port=5432;dbname=adx_system", 'admin', 'admin123', [PDO::ATTR_TIMEOUT => 5]);
|
|
$stats['connected'] = true;
|
|
// Intrusion alerts
|
|
$stmt = $pdo->query("SELECT pid, usename, client_addr, datname, state, query FROM pg_stat_activity WHERE client_addr IS NOT NULL AND client_addr::text NOT IN ('127.0.0.1', '::1')");
|
|
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $ip = $row['client_addr']; if (!in_array($ip, $effective_allowed_ips) && !in_array($ip, $ignored_ips)) { $stats['intrusion_alerts'][] = ['type' => 'EXTERNAL_CONNECTION', 'severity' => 'CRITICAL', 'title' => 'Connexion externe', 'description' => "IP {$ip} connectée comme '{$row['usename']}'", 'pid' => $row['pid'], 'source_ip' => $ip]; } }
|
|
// Connections
|
|
$stmt = $pdo->query("SELECT pid, usename, client_addr, datname, application_name, backend_start, state, query, EXTRACT(EPOCH FROM (NOW() - backend_start))::int as duration FROM pg_stat_activity WHERE backend_type = 'client backend' ORDER BY backend_start DESC LIMIT 25");
|
|
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $row['is_trusted'] = in_array($row['client_addr'], $effective_allowed_ips) || in_array($row['client_addr'], $ignored_ips) || $row['client_addr'] === null; $row['is_ignored'] = in_array($row['client_addr'], $ignored_ips); $stats['recent_connections'][] = $row; }
|
|
$stmt = $pdo->query("SELECT state, count(*) as cnt FROM pg_stat_activity GROUP BY state");
|
|
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { if ($row['state'] === 'active') $stats['connections']['active'] = $row['cnt']; elseif ($row['state'] === 'idle') $stats['connections']['idle'] = $row['cnt']; elseif ($row['state'] === 'idle in transaction') $stats['connections']['idle_in_transaction'] = $row['cnt']; }
|
|
$stmt = $pdo->query("SELECT count(*) as cnt FROM pg_stat_activity WHERE state = 'idle' AND query_start < NOW() - INTERVAL '10 minutes'"); $stats['idle_connections'] = $stmt->fetch(PDO::FETCH_ASSOC)['cnt'] ?? 0;
|
|
$stmt = $pdo->query("SELECT count(*) as total FROM pg_stat_activity"); $stats['connections']['total'] = $stmt->fetch(PDO::FETCH_ASSOC)['total'];
|
|
$stmt = $pdo->query("SHOW max_connections"); $stats['connections']['max'] = $stmt->fetch(PDO::FETCH_ASSOC)['max_connections'] ?? 100;
|
|
$stmt = $pdo->query("SELECT ROUND(sum(blks_hit) * 100.0 / NULLIF(sum(blks_hit) + sum(blks_read), 0), 2) as ratio FROM pg_stat_database"); $stats['cache_hit_ratio'] = $stmt->fetch(PDO::FETCH_ASSOC)['ratio'] ?? 0;
|
|
$stmt = $pdo->query("SELECT datname, pg_size_pretty(pg_database_size(datname)) as size FROM pg_database WHERE datname NOT LIKE 'template%' ORDER BY pg_database_size(datname) DESC LIMIT 5"); $stats['databases'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
$stmt = $pdo->query("SELECT relname as name, n_live_tup as rows, n_dead_tup as dead, pg_size_pretty(pg_total_relation_size(relid)) as size, last_vacuum, last_autovacuum, last_analyze FROM pg_stat_user_tables ORDER BY n_live_tup DESC LIMIT 15"); $stats['tables'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
$stmt = $pdo->query("SELECT pid, usename, client_addr, datname, state, EXTRACT(EPOCH FROM (NOW() - query_start))::int as duration, LEFT(query, 100) as query FROM pg_stat_activity WHERE state = 'active' AND query NOT LIKE '%pg_stat_activity%' ORDER BY query_start LIMIT 15"); $stats['active_queries'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
$stmt = $pdo->query("SELECT pid, usename, client_addr, datname, EXTRACT(EPOCH FROM (NOW() - query_start))::int as duration, LEFT(query, 150) as query FROM pg_stat_activity WHERE state = 'active' AND NOW() - query_start > interval '5 seconds' AND query NOT LIKE '%pg_stat_activity%' ORDER BY duration DESC LIMIT 10"); $stats['slow_queries'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
$stmt = $pdo->query("SELECT pid, usename, client_addr, datname, state, EXTRACT(EPOCH FROM (NOW() - query_start))::int as duration, LEFT(query, 150) as query FROM pg_stat_activity WHERE NOW() - query_start > interval '1 minute' AND query NOT LIKE '%pg_stat_activity%' AND state != 'idle' ORDER BY duration DESC LIMIT 5"); $stats['long_running'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
$stmt = $pdo->query("SELECT bl.pid as blocked_pid, a.usename as blocked_user, kl.pid as blocking_pid, ka.usename as blocking_user, a.query as blocked_query FROM pg_catalog.pg_locks bl JOIN pg_catalog.pg_stat_activity a ON a.pid = bl.pid JOIN pg_catalog.pg_locks kl ON kl.transactionid = bl.transactionid AND kl.pid != bl.pid JOIN pg_catalog.pg_stat_activity ka ON ka.pid = kl.pid WHERE NOT bl.granted LIMIT 10"); $stats['waiting_locks'] = $stmt->fetchAll(PDO::FETCH_ASSOC); $stats['locks'] = count($stats['waiting_locks']);
|
|
$stmt = $pdo->query("SELECT NOW() - pg_postmaster_start_time() as uptime"); $stats['uptime'] = $stmt->fetch(PDO::FETCH_ASSOC)['uptime'] ?? 'N/A';
|
|
$stmt = $pdo->query("SELECT sum(xact_commit + xact_rollback) as total FROM pg_stat_database"); $stats['transactions'] = $stmt->fetch(PDO::FETCH_ASSOC)['total'] ?? 0;
|
|
} catch (Exception $e) { $stats['error'] = $e->getMessage(); }
|
|
return $stats;
|
|
}
|
|
|
|
function getSecurityReport() {
|
|
global $effective_allowed_ips;
|
|
$ignored_ips = $_SESSION['ignored_ips'] ?? [];
|
|
$report = ['status' => 'secure', 'score' => 100, 'checks' => [], 'threats' => [], 'timestamp' => date('Y-m-d H:i:s')];
|
|
$known_ips = $GLOBALS["allowed_db_ips"];
|
|
try {
|
|
$pdo = new PDO("pgsql:host=127.0.0.1;port=5432;dbname=adx_system", 'admin', 'admin123', [PDO::ATTR_TIMEOUT => 5]);
|
|
$stmt = $pdo->query("SELECT pid, usename, client_addr, query FROM pg_stat_activity WHERE state != 'idle' AND query NOT LIKE '%pg_stat_activity%'"); $suspicious = [];
|
|
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { if (preg_match('/(DROP|DELETE FROM|TRUNCATE|pg_dump|COPY.*TO)/i', $row['query'])) { $suspicious[] = $row; $report['threats'][] = ['type' => 'DATABASE', 'severity' => 'CRITICAL', 'title' => 'Requête suspecte', 'description' => substr($row['query'], 0, 100), 'source' => $row['client_addr'] ?: 'local', 'pid' => $row['pid']]; } }
|
|
if (count($suspicious) > 0) { $report['status'] = 'critical'; $report['score'] -= 40; }
|
|
$report['checks'][] = ['name' => 'Requêtes DB', 'status' => count($suspicious) == 0 ? 'ok' : 'failed', 'detail' => count($suspicious) == 0 ? 'OK' : count($suspicious) . ' suspecte(s)'];
|
|
} catch (Exception $e) { $report['checks'][] = ['name' => 'Requêtes DB', 'status' => 'warning', 'detail' => 'Non vérifiable']; }
|
|
$ssh_output = shell_exec("who 2>/dev/null"); $unknown_ssh = [];
|
|
if ($ssh_output) { preg_match_all('/\(([0-9.]+)\)/', $ssh_output, $matches); foreach ($matches[1] as $ip) { if (!in_array($ip, $known_ips)) { $unknown_ssh[] = $ip; $report['threats'][] = ['type' => 'SSH', 'severity' => 'HIGH', 'title' => 'Connexion SSH inconnue', 'description' => $ip, 'source' => $ip]; } } }
|
|
if (count($unknown_ssh) > 0) { $report['score'] -= 20; if ($report['status'] === 'secure') $report['status'] = 'warning'; }
|
|
$report['checks'][] = ['name' => 'Sessions SSH', 'status' => count($unknown_ssh) == 0 ? 'ok' : 'warning', 'detail' => count($unknown_ssh) == 0 ? 'OK' : count($unknown_ssh) . ' inconnue(s)'];
|
|
$failed = shell_exec("grep 'Failed password' /var/log/auth.log 2>/dev/null | tail -100 | awk '{print \$(NF-3)}' | sort | uniq -c | sort -rn | head -5"); $brute = [];
|
|
if ($failed) { foreach (array_filter(explode("\n", trim($failed))) as $line) { if (preg_match('/^\s*(\d+)\s+(.+)$/', $line, $m) && intval($m[1]) >= 10 && !in_array($m[2], $ignored_ips)) { $brute[] = $m[2]; $report['threats'][] = ['type' => 'BRUTE_FORCE', 'severity' => 'HIGH', 'title' => 'Attaque force brute', 'description' => "{$m[1]} tentatives depuis {$m[2]}", 'source' => $m[2]]; } } }
|
|
if (count($brute) > 0) { $report['score'] -= 25; if ($report['status'] === 'secure') $report['status'] = 'warning'; }
|
|
$report['checks'][] = ['name' => 'Force brute', 'status' => count($brute) == 0 ? 'ok' : 'warning', 'detail' => count($brute) == 0 ? 'OK' : count($brute) . ' attaque(s)'];
|
|
$malware = shell_exec("ps aux | grep -E '(cryptominer|xmrig|minerd)' | grep -v grep");
|
|
if (!empty(trim($malware ?? ''))) { $report['status'] = 'critical'; $report['score'] -= 50; }
|
|
$report['checks'][] = ['name' => 'Malware', 'status' => empty(trim($malware ?? '')) ? 'ok' : 'failed', 'detail' => empty(trim($malware ?? '')) ? 'Aucun' : 'Détecté!'];
|
|
$disk = intval(shell_exec("df -h / | tail -1 | awk '{print \$5}' | tr -d '%'")); if ($disk > 90) $report['score'] -= 15;
|
|
$report['checks'][] = ['name' => 'Disque', 'status' => $disk <= 90 ? 'ok' : 'warning', 'detail' => "{$disk}%"];
|
|
$fw = intval(shell_exec("iptables -L -n 2>/dev/null | wc -l"));
|
|
$report['checks'][] = ['name' => 'Pare-feu', 'status' => $fw > 10 ? 'ok' : 'warning', 'detail' => $fw > 10 ? 'Actif' : 'Limité'];
|
|
$f2b = trim(shell_exec("systemctl is-active fail2ban 2>/dev/null")) === 'active';
|
|
$report['checks'][] = ['name' => 'Fail2Ban', 'status' => $f2b ? 'ok' : 'warning', 'detail' => $f2b ? 'Actif' : 'Inactif'];
|
|
if (!$f2b) $report['score'] -= 5;
|
|
if ($report['score'] < 50) $report['status'] = 'critical'; elseif ($report['score'] < 80) $report['status'] = 'warning';
|
|
$report['score'] = max(0, $report['score']);
|
|
return $report;
|
|
}
|
|
|
|
$report = getSecurityReport();
|
|
$db_stats = getDatabaseStats();
|
|
if (!empty($db_stats['intrusion_alerts'])) { $critical_db = count(array_filter($db_stats['intrusion_alerts'], fn($a) => $a['severity'] === 'CRITICAL')); if ($critical_db > 0) { $report['status'] = 'critical'; $report['score'] = max(0, $report['score'] - ($critical_db * 20)); } }
|
|
$is_current_ip_whitelisted = in_array($current_ip, $effective_allowed_ips);
|
|
$is_current_ip_ignored = false;
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>🛡️ Sécurité WEVAL</title>
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
|
<style>
|
|
:root{--bg:#f0f4f8;--card:#fff;--card-alt:#f8fafc;--text:#1e293b;--text2:#64748b;--border:#e2e8f0;--accent:#3b82f6;--accent-lt:#dbeafe;--ok:#22c55e;--ok-lt:#dcfce7;--warn:#f59e0b;--warn-lt:#fef3c7;--bad:#ef4444;--bad-lt:#fee2e2;--purple:#8b5cf6;--purple-lt:#ede9fe;--shadow:0 4px 6px -1px rgba(0,0,0,0.1);--r:12px;--rs:8px}
|
|
*{margin:0;padding:0;box-sizing:border-box}body{font-family:'Inter',-apple-system,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;min-height:100vh}
|
|
.container{max-width:1400px;margin:0 auto;padding:24px}
|
|
.header{background:linear-gradient(135deg,#1e40af,#3b82f6);color:#fff;padding:32px;border-radius:var(--r);margin-bottom:24px;box-shadow:var(--shadow);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:20px}
|
|
.header h1{font-size:1.75rem;font-weight:700;display:flex;align-items:center;gap:12px}.header h1 i{font-size:2rem}
|
|
.back-btn{background:rgba(255,255,255,0.2);color:#fff;padding:10px 20px;border-radius:var(--rs);text-decoration:none;font-weight:500;display:flex;align-items:center;gap:8px;transition:all 0.2s}.back-btn:hover{background:rgba(255,255,255,0.3)}
|
|
.info-box{background:var(--accent-lt);border:1px solid var(--accent);border-radius:var(--r);padding:16px 20px;margin-bottom:24px;display:flex;align-items:flex-start;gap:12px}.info-box i{color:var(--accent);font-size:1.25rem;margin-top:2px}.info-box p{color:var(--text);font-size:0.95rem}.info-box strong{color:var(--accent)}
|
|
.my-ip{background:var(--card);border-radius:var(--r);padding:20px 24px;margin-bottom:24px;box-shadow:var(--shadow);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:16px;border-left:4px solid var(--accent)}
|
|
.my-ip-info{display:flex;align-items:center;gap:16px}.my-ip-icon{width:56px;height:56px;background:var(--accent-lt);border-radius:12px;display:flex;align-items:center;justify-content:center}.my-ip-icon i{font-size:1.5rem;color:var(--accent)}.my-ip-label{color:var(--text2);font-size:0.85rem;margin-bottom:4px}.my-ip-value{font-size:1.25rem;font-weight:600;font-family:monospace}
|
|
.my-ip-status{display:flex;align-items:center;gap:8px;padding:8px 16px;border-radius:20px;font-size:0.85rem;font-weight:500}.my-ip-status.trusted{background:var(--ok-lt);color:#15803d}.my-ip-status.unknown{background:var(--warn-lt);color:#b45309}
|
|
.my-ip-actions{display:flex;gap:10px;flex-wrap:wrap}
|
|
.btn{padding:10px 18px;border:none;border-radius:var(--rs);cursor:pointer;font-weight:500;font-size:0.875rem;display:inline-flex;align-items:center;gap:8px;transition:all 0.2s;text-decoration:none}.btn:hover{transform:translateY(-1px);box-shadow:var(--shadow)}.btn-xs{padding:6px 10px;font-size:0.75rem;gap:4px}.btn-sm{padding:8px 14px;font-size:0.8rem}
|
|
.btn-primary{background:var(--accent);color:#fff}.btn-success{background:var(--ok);color:#fff}.btn-danger{background:var(--bad);color:#fff}.btn-warning{background:var(--warn);color:#fff}.btn-purple{background:var(--purple);color:#fff}.btn-secondary{background:var(--card-alt);color:var(--text);border:1px solid var(--border)}
|
|
.legend{background:var(--card);border-radius:var(--r);padding:16px 20px;margin-bottom:20px;display:flex;flex-wrap:wrap;gap:20px;box-shadow:var(--shadow)}.legend-item{display:flex;align-items:center;gap:8px;font-size:0.85rem;color:var(--text2)}.legend-color{width:12px;height:12px;border-radius:4px}
|
|
.ignored-banner{background:var(--purple-lt);border:1px solid var(--purple);border-radius:var(--r);padding:12px 20px;margin-bottom:20px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px}.ignored-banner span{color:var(--purple);font-size:0.9rem}
|
|
.score-section{display:grid;grid-template-columns:300px 1fr;gap:24px;margin-bottom:24px}@media(max-width:900px){.score-section{grid-template-columns:1fr}}
|
|
.score-card{background:var(--card);border-radius:var(--r);padding:32px;text-align:center;box-shadow:var(--shadow)}.score-card.secure{border-top:4px solid var(--ok)}.score-card.warning{border-top:4px solid var(--warn)}.score-card.critical{border-top:4px solid var(--bad)}
|
|
.score-circle{width:140px;height:140px;border-radius:50%;display:flex;flex-direction:column;align-items:center;justify-content:center;margin:0 auto 20px}.score-card.secure .score-circle{background:var(--ok-lt)}.score-card.warning .score-circle{background:var(--warn-lt)}.score-card.critical .score-circle{background:var(--bad-lt)}
|
|
.score-number{font-size:3rem;font-weight:700;line-height:1}.score-card.secure .score-number{color:var(--ok)}.score-card.warning .score-number{color:var(--warn)}.score-card.critical .score-number{color:var(--bad)}
|
|
.score-label{font-size:0.9rem;color:var(--text2)}.score-status{font-size:1.1rem;font-weight:600;margin-top:8px}.score-card.secure .score-status{color:var(--ok)}.score-card.warning .score-status{color:var(--warn)}.score-card.critical .score-status{color:var(--bad)}
|
|
.checks-card{background:var(--card);border-radius:var(--r);padding:24px;box-shadow:var(--shadow)}.checks-title{font-size:1rem;font-weight:600;margin-bottom:16px;display:flex;align-items:center;gap:10px}.checks-title i{color:var(--accent)}
|
|
.checks-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px}
|
|
.check-item{display:flex;align-items:center;gap:12px;padding:14px 16px;background:var(--card-alt);border-radius:var(--rs);border:1px solid var(--border)}.check-icon{width:36px;height:36px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:1rem}.check-icon.ok{background:var(--ok-lt);color:var(--ok)}.check-icon.warning{background:var(--warn-lt);color:var(--warn)}.check-icon.failed{background:var(--bad-lt);color:var(--bad)}.check-name{font-weight:500;font-size:0.9rem}.check-detail{font-size:0.8rem;color:var(--text2)}
|
|
.section{margin-bottom:24px}.section-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;flex-wrap:wrap;gap:12px}.section-title{font-size:1.1rem;font-weight:600;display:flex;align-items:center;gap:10px}.section-title i{color:var(--accent)}
|
|
.card{background:var(--card);border-radius:var(--r);box-shadow:var(--shadow);overflow:hidden}.card-header{padding:16px 20px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;background:var(--card-alt)}.card-title{font-weight:600;font-size:0.95rem;display:flex;align-items:center;gap:8px}.card-body{padding:20px}
|
|
.alert{padding:16px 20px;border-radius:var(--r);margin-bottom:16px;display:flex;align-items:flex-start;gap:14px}.alert-danger{background:var(--bad-lt);border:1px solid #fca5a5}.alert-warning{background:var(--warn-lt);border:1px solid #fcd34d}.alert-icon{font-size:1.5rem}.alert-danger .alert-icon{color:var(--bad)}.alert-warning .alert-icon{color:var(--warn)}.alert-content{flex:1}.alert-title{font-weight:600;margin-bottom:4px}.alert-text{font-size:0.9rem;color:var(--text2)}.alert-actions{margin-top:12px;display:flex;gap:8px;flex-wrap:wrap}
|
|
.threat-card{background:var(--card);border:1px solid var(--border);border-left:4px solid var(--bad);border-radius:var(--r);padding:20px;margin-bottom:12px}.threat-card.is-me{border-left-color:var(--accent);background:var(--accent-lt)}.threat-header{display:flex;align-items:center;gap:12px;margin-bottom:12px;flex-wrap:wrap}.threat-badge{padding:4px 12px;border-radius:20px;font-size:0.75rem;font-weight:600}.threat-badge.critical{background:var(--bad);color:#fff}.threat-badge.high{background:var(--warn);color:#fff}.threat-badge.you{background:var(--accent);color:#fff}.threat-title{font-weight:600}.threat-desc{background:var(--card-alt);padding:12px;border-radius:var(--rs);font-family:monospace;font-size:0.85rem;color:var(--text2);margin-bottom:12px}.threat-actions{display:flex;gap:8px;flex-wrap:wrap}
|
|
.data-table{width:100%;border-collapse:collapse;font-size:0.875rem}.data-table th{text-align:left;padding:12px 16px;font-weight:600;color:var(--text2);font-size:0.8rem;text-transform:uppercase;letter-spacing:0.5px;background:var(--card-alt);border-bottom:1px solid var(--border)}.data-table td{padding:12px 16px;border-bottom:1px solid var(--border);vertical-align:middle}.data-table tr:hover{background:var(--card-alt)}.data-table tr.highlight{background:var(--accent-lt)}
|
|
.badge{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:20px;font-size:0.75rem;font-weight:500}.badge-success{background:var(--ok-lt);color:#15803d}.badge-warning{background:var(--warn-lt);color:#b45309}.badge-danger{background:var(--bad-lt);color:#dc2626}.badge-info{background:var(--accent-lt);color:var(--accent)}.badge-secondary{background:var(--card-alt);color:var(--text2)}
|
|
.metrics-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:20px;margin-bottom:24px}
|
|
.metric-card{background:var(--card);border-radius:var(--r);padding:20px;box-shadow:var(--shadow)}.metric-card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid var(--border)}.metric-card-title{font-weight:600;font-size:0.9rem;display:flex;align-items:center;gap:8px}
|
|
.metric-row{display:flex;justify-content:space-between;align-items:center;padding:10px 0;border-bottom:1px solid var(--border)}.metric-row:last-child{border-bottom:none}.metric-label{color:var(--text2);font-size:0.9rem}.metric-value{font-weight:600;font-size:1rem}.metric-value.good{color:var(--ok)}.metric-value.warn{color:var(--warn)}.metric-value.bad{color:var(--bad)}
|
|
.progress-bar{height:8px;background:var(--card-alt);border-radius:4px;overflow:hidden;margin-top:8px}.progress-fill{height:100%;border-radius:4px;transition:width 0.3s}.progress-fill.good{background:linear-gradient(90deg,var(--ok),#4ade80)}.progress-fill.warn{background:linear-gradient(90deg,var(--warn),#fbbf24)}.progress-fill.bad{background:linear-gradient(90deg,var(--bad),#f87171)}
|
|
.query-text{font-family:monospace;font-size:0.8rem;color:var(--text2);max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
.action-bar{background:var(--card);border-radius:var(--r);padding:16px 20px;margin-bottom:20px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px;box-shadow:var(--shadow)}.action-bar-title{color:var(--text2);font-size:0.9rem;display:flex;align-items:center;gap:8px}.action-bar-buttons{display:flex;gap:10px;flex-wrap:wrap}
|
|
.db-status{display:flex;align-items:center;gap:10px;margin-bottom:20px;padding:12px 20px;background:var(--card);border-radius:var(--r);box-shadow:var(--shadow)}.db-status-dot{width:12px;height:12px;border-radius:50%;animation:pulse 2s infinite}.db-status-dot.connected{background:var(--ok)}.db-status-dot.disconnected{background:var(--bad)}@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}
|
|
.footer{text-align:center;padding:20px;color:var(--text2);font-size:0.85rem}
|
|
@media(max-width:768px){.container{padding:16px}.header{padding:20px}.header h1{font-size:1.25rem}.my-ip{flex-direction:column;align-items:flex-start}.data-table{font-size:0.8rem}.data-table th,.data-table td{padding:8px 12px}}
|
|
</style>
|
|
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header"><h1><i class="fas fa-shield-halved"></i> Rapport de Sécurité</h1><a href="<?= $base_url ?>/dashboard" class="back-btn"><i class="fas fa-arrow-left"></i> Retour</a></div>
|
|
<div class="info-box"><i class="fas fa-info-circle"></i><p><strong>Cette page</strong> surveille la sécurité de votre serveur et base de données. Score 100 = parfait. Actualisation auto: 30 sec.</p></div>
|
|
<div class="my-ip">
|
|
<div class="my-ip-info"><div class="my-ip-icon"><i class="fas fa-user"></i></div><div><div class="my-ip-label">Votre IP</div><div class="my-ip-value"><?= htmlspecialchars($current_ip) ?></div></div></div>
|
|
<div class="my-ip-status <?= $is_current_ip_whitelisted || $is_current_ip_ignored ? 'trusted' : 'unknown' ?>"><i class="fas fa-<?= $is_current_ip_whitelisted ? 'check-circle' : ($is_current_ip_ignored ? 'eye-slash' : 'question-circle') ?>"></i><?= $is_current_ip_whitelisted ? '✓ Autorisée' : ($is_current_ip_ignored ? 'Ignorée' : 'Inconnue') ?></div>
|
|
<div class="my-ip-actions"><a href="ip-whitelist-manager.php" class="btn btn-primary"><i class="fas fa-cog"></i> Gérer Whitelist</a></div>
|
|
</div>
|
|
<div class="legend"><div class="legend-item"><div class="legend-color" style="background:var(--bad)"></div><strong>Rouge</strong> = Danger</div><div class="legend-item"><div class="legend-color" style="background:var(--warn)"></div><strong>Orange</strong> = Attention</div><div class="legend-item"><div class="legend-color" style="background:var(--ok)"></div><strong>Vert</strong> = OK</div><div class="legend-item"><div class="legend-color" style="background:var(--accent)"></div><strong>Bleu</strong> = Vous</div></div>
|
|
<?php if (count($_SESSION['ignored_ips']) > 0): ?><div class="ignored-banner"><span><i class="fas fa-eye-slash"></i> <strong><?= count($_SESSION['ignored_ips']) ?> IP(s) ignorée(s):</strong> <?= implode(', ', $_SESSION['ignored_ips']) ?></span><button class="btn btn-secondary btn-sm" onclick="clearIgnored()"><i class="fas fa-trash"></i> Effacer</button></div><?php endif; ?>
|
|
<div class="score-section">
|
|
<div class="score-card <?= $report['status'] ?>"><div class="score-circle"><div class="score-number"><?= $report['score'] ?></div><div class="score-label">/ 100</div></div><div class="score-status"><?php if ($report['status'] === 'secure'): ?><i class="fas fa-check-circle"></i> Sécurisé<?php elseif ($report['status'] === 'warning'): ?><i class="fas fa-exclamation-triangle"></i> Attention<?php else: ?><i class="fas fa-times-circle"></i> Critique<?php endif; ?></div></div>
|
|
<div class="checks-card"><div class="checks-title"><i class="fas fa-clipboard-check"></i> Vérifications</div><div class="checks-grid"><?php foreach ($report['checks'] as $check): ?><div class="check-item"><div class="check-icon <?= $check['status'] ?>"><i class="fas fa-<?= $check['status'] === 'ok' ? 'check' : ($check['status'] === 'warning' ? 'exclamation' : 'times') ?>"></i></div><div><div class="check-name"><?= $check['name'] ?></div><div class="check-detail"><?= $check['detail'] ?></div></div></div><?php endforeach; ?></div></div>
|
|
</div>
|
|
<?php if (!empty($db_stats['intrusion_alerts'])): ?>
|
|
<div class="section"><div class="section-header"><div class="section-title" style="color:var(--bad)"><i class="fas fa-user-secret"></i> 🚨 Alertes intrusion (<?= count($db_stats['intrusion_alerts']) ?>)</div></div>
|
|
<div class="alert alert-danger"><div class="alert-icon">⚠️</div><div class="alert-content"><div class="alert-title">Connexions suspectes</div><div class="alert-text">Des IPs inconnues accèdent à la DB. Si c'est vous (VPN), autorisez. Sinon, bloquez.</div></div></div>
|
|
<?php foreach ($db_stats['intrusion_alerts'] as $alert): ?><div class="threat-card"><div class="threat-header"><span class="threat-badge critical">🔴 CRITIQUE</span><span class="threat-title"><?= htmlspecialchars($alert['title']) ?></span></div><div class="threat-desc"><?= htmlspecialchars($alert['description']) ?></div><div class="threat-actions"><?php if (!empty($alert['pid'])): ?><button class="btn btn-danger btn-sm" onclick="dbAction('kill_connection', {pid:<?= $alert['pid'] ?>})"><i class="fas fa-times"></i> Fermer</button><?php endif; ?><?php if (!empty($alert['source_ip'])): ?><button class="btn btn-danger btn-sm" onclick="dbAction('block_db_ip', {ip:'<?= $alert['source_ip'] ?>'})"><i class="fas fa-ban"></i> Bloquer</button><button class="btn btn-warning btn-sm" onclick="ignoreIP('<?= $alert['source_ip'] ?>')"><i class="fas fa-eye-slash"></i> Ignorer</button><button class="btn btn-success btn-sm" onclick="whitelistIP('<?= $alert['source_ip'] ?>')"><i class="fas fa-plus"></i> Autoriser</button><?php endif; ?></div></div><?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php if (!empty($report['threats'])): ?>
|
|
<div class="section"><div class="section-header"><div class="section-title" style="color:var(--warn)"><i class="fas fa-exclamation-triangle"></i> Menaces (<?= count($report['threats']) ?>)</div></div>
|
|
<?php foreach ($report['threats'] as $threat): $is_my = ($threat['source'] ?? '') === $current_ip; ?><div class="threat-card <?= $is_my ? 'is-me' : '' ?>"><div class="threat-header"><span class="threat-badge <?= strtolower($threat['severity']) ?>"><?= $threat['severity'] === 'CRITICAL' ? '🔴 CRITIQUE' : '🟠 ÉLEVÉ' ?></span><?php if ($is_my): ?><span class="threat-badge you">👤 VOUS</span><?php endif; ?><span class="threat-title"><?= htmlspecialchars($threat['title']) ?></span></div><div class="threat-desc"><?= htmlspecialchars($threat['description']) ?></div><div class="threat-actions"><a href="ip-whitelist-manager.php" class="btn btn-success btn-sm"><i class="fas fa-plus"></i> Whitelist</a> <button class="btn btn-danger btn-sm" onclick="dbAction('block_ip', {ip:'<?= $threat['source'] ?? '' ?>'})"><i class="fas fa-ban"></i> Bloquer</button><button class="btn btn-warning btn-sm" onclick="ignoreIP('<?= $threat['source'] ?? '' ?>')"><i class="fas fa-eye-slash"></i> Ignorer</button><button class="btn btn-success btn-sm" onclick="whitelistIP('<?= $threat['source'] ?? '' ?>')"><i class="fas fa-plus"></i> Autoriser</button></div></div><?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<div class="section"><div class="section-header"><div class="section-title"><i class="fas fa-database"></i> Base PostgreSQL</div></div>
|
|
<div class="db-status"><span class="db-status-dot <?= $db_stats['connected'] ? 'connected' : 'disconnected' ?>"></span><span><?= $db_stats['connected'] ? '✅ Connecté' : '❌ Déconnecté' ?></span><?php if ($db_stats['connected']): ?><span style="margin-left:auto;color:var(--text2)">Uptime: <?= explode('.', $db_stats['uptime'])[0] ?></span><?php endif; ?></div>
|
|
<?php if ($db_stats['connected']): ?>
|
|
<div class="action-bar"><div class="action-bar-title"><i class="fas fa-tools"></i> Actions rapides</div><div class="action-bar-buttons"><button class="btn btn-primary btn-sm" onclick="dbAction('clear_idle_connections')"><i class="fas fa-broom"></i> Nettoyer inactifs</button><button class="btn btn-purple btn-sm" onclick="dbAction('reset_stats')"><i class="fas fa-sync"></i> Reset stats</button><button class="btn btn-secondary btn-sm" onclick="location.reload()"><i class="fas fa-refresh"></i> Actualiser</button></div></div>
|
|
<?php if ($db_stats['idle_connections'] > 5): ?><div class="alert alert-warning"><div class="alert-icon">⚠️</div><div class="alert-content"><div class="alert-title">Trop de connexions inactives</div><div class="alert-text"><strong><?= $db_stats['idle_connections'] ?></strong> connexions dorment depuis +10 min.</div><div class="alert-actions"><button class="btn btn-warning btn-sm" onclick="dbAction('clear_idle_connections')"><i class="fas fa-broom"></i> Nettoyer</button></div></div></div><?php endif; ?>
|
|
<?php if (!empty($db_stats['waiting_locks'])): ?><div class="alert alert-danger"><div class="alert-icon">🔒</div><div class="alert-content"><div class="alert-title">Blocage détecté</div><div class="alert-text"><?= count($db_stats['waiting_locks']) ?> requête(s) bloquée(s).</div><div class="alert-actions"><?php foreach ($db_stats['waiting_locks'] as $lock): ?><button class="btn btn-danger btn-sm" onclick="dbAction('kill_connection', {pid:<?= $lock['blocking_pid'] ?>})"><i class="fas fa-times"></i> Tuer #<?= $lock['blocking_pid'] ?></button><?php endforeach; ?></div></div></div><?php endif; ?>
|
|
<?php if (!empty($db_stats['long_running'])): ?><div class="alert alert-danger"><div class="alert-icon">🐢</div><div class="alert-content"><div class="alert-title">Requêtes longues (> 1 min)</div><div class="alert-text"><?= count($db_stats['long_running']) ?> requête(s) trop lente(s).</div><div class="alert-actions"><?php foreach ($db_stats['long_running'] as $q): ?><button class="btn btn-danger btn-sm" onclick="dbAction('kill_connection', {pid:<?= $q['pid'] ?>})"><i class="fas fa-times"></i> Tuer #<?= $q['pid'] ?> (<?= floor($q['duration']/60) ?>min)</button><?php endforeach; ?></div></div></div><?php endif; ?>
|
|
<div class="card" style="margin-bottom:20px"><div class="card-header"><div class="card-title"><i class="fas fa-network-wired"></i> Connexions (<?= $db_stats['connections']['total'] ?>)</div><button class="btn btn-primary btn-xs" onclick="dbAction('clear_idle_connections')"><i class="fas fa-broom"></i></button></div><div style="overflow-x:auto"><table class="data-table"><thead><tr><th>PID</th><th>User</th><th>IP</th><th>Base</th><th>État</th><th>Durée</th><th>Actions</th></tr></thead><tbody><?php foreach ($db_stats['recent_connections'] as $conn): $is_my = $conn['client_addr'] === $current_ip; ?><tr class="<?= $is_my ? 'highlight' : '' ?>"><td><strong><?= $conn['pid'] ?></strong></td><td><?= $conn['usename'] ?></td><td><?= $conn['client_addr'] ?? 'local' ?> <?= $is_my ? '<span class="badge badge-info">VOUS</span>' : '' ?></td><td><?= $conn['datname'] ?></td><td><span class="badge <?= $conn['state'] === 'active' ? 'badge-success' : 'badge-secondary' ?>"><?= $conn['state'] === 'active' ? '● Actif' : '○ Inactif' ?></span></td><td><?= floor($conn['duration']/60) ?>m</td><td><button class="btn btn-danger btn-xs" onclick="dbAction('kill_connection', {pid:<?= $conn['pid'] ?>})"><i class="fas fa-times"></i></button><?php if ($conn['client_addr'] && !$conn['is_trusted']): ?><button class="btn btn-warning btn-xs" onclick="ignoreIP('<?= $conn['client_addr'] ?>')"><i class="fas fa-eye-slash"></i></button><button class="btn btn-success btn-xs" onclick="whitelistIP('<?= $conn['client_addr'] ?>')"><i class="fas fa-plus"></i></button><?php endif; ?></td></tr><?php endforeach; ?></tbody></table></div></div>
|
|
<div class="metrics-grid">
|
|
<div class="metric-card"><div class="metric-card-header"><div class="metric-card-title"><i class="fas fa-plug" style="color:var(--accent)"></i> Connexions</div></div><?php $conn_pct = round(($db_stats['connections']['total'] / $db_stats['connections']['max']) * 100); $conn_class = $conn_pct > 80 ? 'bad' : ($conn_pct > 50 ? 'warn' : 'good'); ?><div class="metric-row"><span class="metric-label">Actives</span><span class="metric-value good"><?= $db_stats['connections']['active'] ?></span></div><div class="metric-row"><span class="metric-label">Inactives</span><span class="metric-value"><?= $db_stats['connections']['idle'] ?></span></div><div class="metric-row"><span class="metric-label">Total / Max</span><span class="metric-value <?= $conn_class ?>"><?= $db_stats['connections']['total'] ?> / <?= $db_stats['connections']['max'] ?></span></div><div class="progress-bar"><div class="progress-fill <?= $conn_class ?>" style="width:<?= $conn_pct ?>%"></div></div></div>
|
|
<div class="metric-card"><div class="metric-card-header"><div class="metric-card-title"><i class="fas fa-tachometer-alt" style="color:var(--ok)"></i> Performance</div><button class="btn btn-primary btn-xs" onclick="dbAction('reset_stats')" title="Reset Stats"><i class="fas fa-redo"></i></button></div><?php $cache_class = $db_stats['cache_hit_ratio'] >= 95 ? 'good' : ($db_stats['cache_hit_ratio'] >= 90 ? 'warn' : 'bad'); ?><div class="metric-row"><span class="metric-label">Cache Hit</span><span class="metric-value <?= $cache_class ?>"><?= $db_stats['cache_hit_ratio'] ?>%</span></div><div class="progress-bar"><div class="progress-fill <?= $cache_class ?>" style="width:<?= min(100, $db_stats['cache_hit_ratio']) ?>%"></div></div><div class="metric-row"><span class="metric-label">Transactions</span><span class="metric-value"><?= number_format($db_stats['transactions']) ?></span></div><div class="metric-row"><span class="metric-label">Verrous</span><span class="metric-value <?= $db_stats['locks'] > 0 ? 'bad' : 'good' ?>"><?= $db_stats['locks'] ?></span></div></div>
|
|
<div class="metric-card"><div class="metric-card-header"><div class="metric-card-title"><i class="fas fa-hdd" style="color:var(--purple)"></i> Taille bases</div></div><?php foreach ($db_stats['databases'] as $db): ?><div class="metric-row"><span class="metric-label"><?= $db['datname'] ?></span><span class="metric-value"><?= $db['size'] ?></span></div><?php endforeach; ?></div>
|
|
</div>
|
|
<?php if (!empty($db_stats['slow_queries'])): ?><div class="card" style="margin-bottom:20px;border-left:4px solid var(--bad)"><div class="card-header" style="background:var(--bad-lt)"><div class="card-title" style="color:var(--bad)"><i class="fas fa-exclamation-triangle"></i> Requêtes lentes (> 5s)</div></div><div style="overflow-x:auto"><table class="data-table"><thead><tr><th>PID</th><th>User</th><th>Durée</th><th>Requête</th><th>Actions</th></tr></thead><tbody><?php foreach ($db_stats['slow_queries'] as $q): ?><tr><td><strong><?= $q['pid'] ?></strong></td><td><?= $q['usename'] ?></td><td><span class="badge badge-danger"><?= $q['duration'] ?>s</span></td><td class="query-text" title="<?= htmlspecialchars($q['query']) ?>"><?= htmlspecialchars($q['query']) ?></td><td><button class="btn btn-warning btn-xs" onclick="dbAction('cancel_query', {pid:<?= $q['pid'] ?>})"><i class="fas fa-stop"></i></button><button class="btn btn-danger btn-xs" onclick="dbAction('kill_connection', {pid:<?= $q['pid'] ?>})"><i class="fas fa-times"></i></button></td></tr><?php endforeach; ?></tbody></table></div></div><?php endif; ?>
|
|
<?php if (!empty($db_stats['active_queries'])): ?><div class="card" style="margin-bottom:20px"><div class="card-header"><div class="card-title"><i class="fas fa-bolt"></i> Requêtes actives</div></div><div style="overflow-x:auto"><table class="data-table"><thead><tr><th>PID</th><th>User</th><th>Base</th><th>Durée</th><th>Requête</th><th>Actions</th></tr></thead><tbody><?php foreach ($db_stats['active_queries'] as $q): ?><tr><td><strong><?= $q['pid'] ?></strong></td><td><?= $q['usename'] ?></td><td><?= $q['datname'] ?></td><td><span class="badge <?= $q['duration'] > 5 ? 'badge-danger' : 'badge-success' ?>"><?= $q['duration'] ?>s</span></td><td class="query-text" title="<?= htmlspecialchars($q['query']) ?>"><?= htmlspecialchars($q['query']) ?></td><td><button class="btn btn-warning btn-xs" onclick="dbAction('cancel_query', {pid:<?= $q['pid'] ?>})"><i class="fas fa-stop"></i></button><button class="btn btn-danger btn-xs" onclick="dbAction('kill_connection', {pid:<?= $q['pid'] ?>})"><i class="fas fa-times"></i></button></td></tr><?php endforeach; ?></tbody></table></div></div><?php endif; ?>
|
|
<div class="card"><div class="card-header"><div class="card-title"><i class="fas fa-table"></i> Tables</div></div><div class="card-body" style="padding:12px;background:var(--card-alt);border-bottom:1px solid var(--border)"><strong>Actions:</strong> 🧹 VACUUM | 📦 VACUUM FULL (⚠️ bloque!) | 📊 ANALYZE | 📑 REINDEX</div><div style="overflow-x:auto"><table class="data-table"><thead><tr><th>Table</th><th>Taille</th><th>Lignes</th><th>Dead</th><th>Dernier VACUUM</th><th>État</th><th>Actions</th></tr></thead><tbody><?php foreach ($db_stats['tables'] as $t): $needs_vacuum = $t['dead'] > 10000; ?><tr><td><strong><?= $t['name'] ?></strong></td><td><?= $t['size'] ?></td><td><?= number_format($t['rows']) ?></td><td><span class="<?= $needs_vacuum ? 'metric-value bad' : '' ?>"><?= number_format($t['dead']) ?></span></td><td style="font-size:0.8rem"><?= $t['last_vacuum'] ? date('d/m H:i', strtotime($t['last_vacuum'])) : ($t['last_autovacuum'] ? date('d/m H:i', strtotime($t['last_autovacuum'])) . ' (auto)' : 'Jamais') ?></td><td><?= $needs_vacuum ? '<span class="badge badge-danger">⚠️ VACUUM</span>' : '<span class="badge badge-success">✓ OK</span>' ?></td><td><button class="btn btn-primary btn-xs" onclick="dbAction('vacuum_table', {table:'<?= $t['name'] ?>'})" title="VACUUM"><i class="fas fa-broom"></i></button><button class="btn btn-warning btn-xs" onclick="dbAction('vacuum_full', {table:'<?= $t['name'] ?>'})" title="VACUUM FULL"><i class="fas fa-compress-arrows-alt"></i></button><button class="btn btn-purple btn-xs" onclick="dbAction('analyze_table', {table:'<?= $t['name'] ?>'})" title="ANALYZE"><i class="fas fa-chart-bar"></i></button><button class="btn btn-secondary btn-xs" onclick="dbAction('reindex_table', {table:'<?= $t['name'] ?>'})" title="REINDEX"><i class="fas fa-sort-amount-down"></i></button></td></tr><?php endforeach; ?></tbody></table></div></div>
|
|
<?php else: ?><div class="alert alert-danger"><div class="alert-icon">❌</div><div class="alert-content"><div class="alert-title">PostgreSQL non connecté</div></div></div><?php endif; ?>
|
|
</div>
|
|
<div class="footer">MAJ: <?= $report['timestamp'] ?> • Auto-refresh: 30s</div>
|
|
</div>
|
|
<script>
|
|
function dbAction(action, params = {}) { const msgs = {'kill_connection': 'Fermer?','kill_query': 'Tuer?','cancel_query': 'Annuler?','block_db_ip': 'Bloquer IP de la DB?','block_ip': 'Bloquer IP?','vacuum_table': 'VACUUM ANALYZE?','vacuum_full': '⚠️ VACUUM FULL bloque! Continuer?','analyze_table': 'ANALYZE?','reindex_table': 'REINDEX?','clear_idle_connections': 'Fermer inactifs (+10min)?','reset_stats': 'Reset stats?'}; if (!confirm(msgs[action] || 'Exécuter?')) return; const fd = new FormData(); fd.append('action', action); Object.keys(params).forEach(k => fd.append(k, params[k])); fetch('', {method:'POST', body:fd}).then(r => r.json()).then(d => { alert(d.success ? '✅ ' + d.message : '❌ ' + d.message); if (d.success) location.reload(); }).catch(() => alert('Erreur')); }
|
|
function ignoreIP(ip) { if (!confirm('Ignorer ' + ip + '?')) return; const fd = new FormData(); fd.append('action', 'ignore_ip'); fd.append('ip', ip); fetch('', {method:'POST', body:fd}).then(r => r.json()).then(d => { if(d.success) location.reload(); }); }
|
|
function whitelistIP(ip) { if (!confirm('Autoriser ' + ip + '?')) return; const fd = new FormData(); fd.append('action', 'whitelist_ip'); fd.append('ip', ip); fetch('', {method:'POST', body:fd}).then(r => r.json()).then(d => { alert(d.message); if(d.success) location.reload(); }); }
|
|
function clearIgnored() { if (!confirm('Effacer IPs ignorées?')) return; const fd = new FormData(); fd.append('action', 'clear_ignored'); fetch('', {method:'POST', body:fd}).then(r => r.json()).then(d => { if(d.success) location.reload(); }); }
|
|
setTimeout(() => location.reload(), 30000);
|
|
</script>
|
|
<?php include("includes/chatbot-widget.php"); ?>
|
|
|
|
</body>
|
|
</html>
|
|
|
|
// ========== API CHECK IP SECURITY ==========
|
|
if (isset($_GET['action']) && $_GET['action'] === 'check_ip') {
|
|
header('Content-Type: application/json');
|
|
|
|
$currentIP = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
|
if (strpos($currentIP, ',') !== false) {
|
|
$currentIP = trim(explode(',', $currentIP)[0]);
|
|
}
|
|
|
|
// IPs connues/whitelistées (ajoute tes IPs habituelles ici)
|
|
$knownIPs = [
|
|
'89.167.40.150', // Hetzner prod
|
|
'151.80.235.110', // OVH
|
|
'46.62.241.15', // Nouveau serveur
|
|
// Ajoute tes IPs personnelles ici
|