GNU/_PAGE/structure/server/Eulji_Mundeok.php
<?php
/**
 * Eulji_Mundeok.php - 서버 로그 분석 및 보안 모니터링
 * 목적: 찝쩝거리는 것들 잡아내기
 */

ini_set('display_errors', 0);
error_reporting(E_ALL);

// -------------------------
// 로그 경로 설정
// -------------------------
$log_paths = [
    '/var/log/httpd/access_log',
];
$error_log_path = '/var/log/httpd/error_log';

$access_log_path = '';
$log_status = 'FAILED';
$log_status_reason = '';

foreach ($log_paths as $path) {
    if (@file_exists($path)) {
        if (@is_readable($path)) {
            $access_log_path = $path;
            $log_status = 'ACTIVE';
            break;
        } else {
            $log_status_reason = 'PERMISSION DENIED: ' . $path;
        }
    }
}

// -------------------------
// 알려진 스캐너/봇 패턴
// -------------------------
$scanner_patterns = [
    'masscan', 'zgrab', 'nmap', 'nikto', 'sqlmap', 'dirbuster', 'gobuster',
    'wfuzz', 'hydra', 'curl/', 'python-requests', 'go-http-client',
    'nuclei', 'shodan', 'censys', 'burpsuite', 'havij', 'acunetix',
    'nessus', 'openvas', 'metasploit', 'libwww-perl', 'lwp-trivial',
    'wget/', 'zgrab', 'scanner', 'crawler', 'spider', 'bot/'
];

$suspicious_paths = [
    'wp-admin', 'wp-login', 'xmlrpc', '.env', '.git', 'config.php',
    'phpinfo', 'shell.php', 'c99', 'r57', 'adminer', 'phpmyadmin',
    'manager/html', 'solr/', 'jenkins', '/.well-known', '/etc/passwd',
    'eval(', 'base64_decode', '../', 'union+select', 'SELECT+FROM',
    '/cgi-bin/', 'cmd.exe', 'powershell', 'passwd', 'shadow'
];

// -------------------------
// 유틸리티 함수
// -------------------------
function getSystemLoad() {
    if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') return ['1m' => 'N/A', '5m' => 'N/A', '15m' => 'N/A'];
    $load = @sys_getloadavg();
    return $load ? [
        '1m'  => round($load[0], 2),
        '5m'  => round($load[1], 2),
        '15m' => round($load[2], 2)
    ] : ['1m' => '0.00', '5m' => '0.00', '15m' => '0.00'];
}

function getMemoryUsage() {
    if (!@is_readable('/proc/meminfo')) return ['pct' => 0, 'used' => 0, 'total' => 0];
    $mem = file_get_contents('/proc/meminfo');
    preg_match('/MemTotal:\s+(\d+)/', $mem, $mt);
    preg_match('/MemAvailable:\s+(\d+)/', $mem, $ma);
    if (!isset($mt[1], $ma[1]) || $mt[1] == 0) return ['pct' => 0, 'used' => 0, 'total' => 0];
    $used = $mt[1] - $ma[1];
    return [
        'pct'   => round(($used / $mt[1]) * 100, 1),
        'used'  => round($used / 1024 / 1024, 1),
        'total' => round($mt[1] / 1024 / 1024, 1)
    ];
}

function getDiskUsage() {
    $total = @disk_total_space('/');
    $free  = @disk_free_space('/');
    if (!$total) return ['pct' => 0, 'used' => 0, 'total' => 0];
    $used = $total - $free;
    return [
        'pct'   => round(($used / $total) * 100, 1),
        'used'  => round($used / 1073741824, 1),
        'total' => round($total / 1073741824, 1)
    ];
}

function isScanner($ua, $patterns) {
    $ua_lower = strtolower($ua);
    foreach ($patterns as $p) {
        if (strpos($ua_lower, strtolower($p)) !== false) return true;
    }
    return false;
}

function isSuspiciousPath($uri, $patterns) {
    $uri_lower = strtolower($uri);
    foreach ($patterns as $p) {
        if (strpos($uri_lower, strtolower($p)) !== false) return true;
    }
    return false;
}

function getStatusClass($status) {
    if ($status >= 500) return 'cl-red';
    if ($status >= 400) return 'cl-orange';
    if ($status >= 300) return 'cl-blue';
    return 'cl-green';
}

// -------------------------
// 로그 파싱
// -------------------------
$requests_5m   = [];
$all_requests  = [];
$ip_count_5m   = [];
$ip_count_all  = [];
$uri_count     = [];
$ua_count      = [];
$method_count  = [];
$status_count  = [];
$scanner_hits  = [];
$suspicious_hits = [];
$ip_ua_map     = [];
$ip_last_seen  = [];
$ip_intervals  = [];
$error_logs    = [];

$total_5m      = 0;
$total_all     = 0;
$time_5m       = time() - 300;
$time_1h       = time() - 3600;

if ($access_log_path !== '') {
    $fp = @fopen($access_log_path, 'r');
    if ($fp) {
        @fseek($fp, 0, SEEK_END);
        $pos   = @ftell($fp);
        $chunk = 512000; // 500KB
        @fseek($fp, max(0, $pos - $chunk));
        $buffer = @fread($fp, $chunk);
        @fclose($fp);

        $lines = explode("\n", $buffer);
        foreach (array_reverse($lines) as $line) {
            if (empty(trim($line))) continue;

            // other_vhosts_access.log 포맷: vhost ip - - [time] "method uri proto" status size "ref" "ua"
            // access.log 포맷:             ip - - [time] "method uri proto" status size "ref" "ua"
            if (preg_match('/^(\S+) (\S+) \S+ \S+ \[([^\]]+)\] "(\S+) (\S+)[^"]*" (\d+) \S+(?:\s+"([^"]*)")?(?:\s+"([^"]*)")?/', $line, $m)) {
                // other_vhosts 포맷
                $ip     = $m[2];
                $dt     = DateTime::createFromFormat('d/M/Y:H:i:s O', $m[3]);
                $ts     = $dt ? $dt->getTimestamp() : 0;
                $method = $m[4];
                $uri    = $m[5];
                $status = (int)$m[6];
                $ref    = isset($m[7]) ? $m[7] : '-';
                $ua     = isset($m[8]) ? $m[8] : '-';
            } elseif (preg_match('/^(\S+) \S+ \S+ \[([^\]]+)\] "(\S+) (\S+)[^"]*" (\d+) \S+(?:\s+"([^"]*)")?(?:\s+"([^"]*)")?/', $line, $m)) {
                // standard access.log
                $ip     = $m[1];
                $dt     = DateTime::createFromFormat('d/M/Y:H:i:s O', $m[2]);
                $ts     = $dt ? $dt->getTimestamp() : 0;
                $method = $m[3];
                $uri    = $m[4];
                $status = (int)$m[5];
                $ref    = isset($m[6]) ? $m[6] : '-';
                $ua     = isset($m[7]) ? $m[7] : '-';
            } else {
                continue;
            }

            if ($ts === 0) continue;

            $entry = [
                'time'   => $ts,
                'ip'     => $ip,
                'method' => $method,
                'uri'    => $uri,
                'status' => $status,
                'ref'    => $ref,
                'ua'     => $ua,
            ];

            // 전체 통계
            $ip_count_all[$ip]     = ($ip_count_all[$ip] ?? 0) + 1;
            $uri_count[$uri]       = ($uri_count[$uri] ?? 0) + 1;
            $ua_count[$ua]         = ($ua_count[$ua] ?? 0) + 1;
            $method_count[$method] = ($method_count[$method] ?? 0) + 1;
            $status_count[$status] = ($status_count[$status] ?? 0) + 1;
            $ip_ua_map[$ip][$ua]   = true;
            $total_all++;

            // 반복 간격 추적
            if (isset($ip_last_seen[$ip])) {
                $interval = abs($ip_last_seen[$ip] - $ts);
                if ($interval > 0) $ip_intervals[$ip][] = $interval;
            }
            $ip_last_seen[$ip] = $ts;

            // 5분 내
            if ($ts >= $time_5m) {
                $requests_5m[]         = $entry;
                $ip_count_5m[$ip]      = ($ip_count_5m[$ip] ?? 0) + 1;
                $total_5m++;
            }

            // 스캐너 감지
            if (isScanner($ua, $scanner_patterns)) {
                $scanner_hits[$ip] = ($scanner_hits[$ip] ?? 0) + 1;
            }

            // 수상한 경로 감지
            if (isSuspiciousPath($uri, $suspicious_paths)) {
                if (!isset($suspicious_hits[$ip])) $suspicious_hits[$ip] = [];
                $suspicious_hits[$ip][] = $uri;
            }

            if ($total_all > 5000) break; // 메모리 보호
        }
    }
}

// 에러 로그
if (@is_readable($error_log_path)) {
    $err_fp = @fopen($error_log_path, 'r');
    if ($err_fp) {
        @fseek($err_fp, -20000, SEEK_END);
        $err_buf = @fread($err_fp, 20000);
        @fclose($err_fp);
        foreach (array_reverse(explode("\n", $err_buf)) as $el) {
            if (strpos($el, 'PHP Warning') !== false || strpos($el, 'PHP Fatal') !== false || strpos($el, 'PHP Error') !== false) {
                $error_logs[] = mb_strimwidth($el, 0, 180, '...');
                if (count($error_logs) >= 8) break;
            }
        }
    }
}

// 정렬
arsort($ip_count_5m);
arsort($ip_count_all);
arsort($uri_count);
arsort($ua_count);
arsort($scanner_hits);
arsort($suspicious_hits);

// 자동화 판단 (간격 표준편차 낮으면 봇)
$bot_candidates = [];
foreach ($ip_intervals as $ip => $intervals) {
    if (count($intervals) < 5) continue;
    $mean = array_sum($intervals) / count($intervals);
    $variance = array_sum(array_map(fn($x) => pow($x - $mean, 2), $intervals)) / count($intervals);
    $stddev = sqrt($variance);
    if ($mean < 5 && $stddev < 2) {
        $bot_candidates[$ip] = ['mean' => round($mean, 2), 'stddev' => round($stddev, 2), 'count' => count($intervals)];
    }
}

$load = getSystemLoad();
$mem  = getMemoryUsage();
$disk = getDiskUsage();

require_once '/home/www/GNU/_PAGE/head.php';
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&family=Rajdhani:wght@400;500;600;700&display=swap');

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

:root {
    --bg:       #020617;
    --panel:    #0c1224;
    --panel2:   #1a2436;
    --panel3:   #232e42;
    --border:   #1e2d45;
    --border2:  #2a3d5a;
    --text:     #94a3b8;
    --text2:    #cbd5e1;
    --text3:    #e2e8f0;
    --blue:     #38bdf8;
    --blue2:    #0ea5e9;
    --green:    #22d3a5;
    --red:      #f43f5e;
    --orange:   #fb923c;
    --yellow:   #facc15;
    --purple:   #a78bfa;
    --mono:     'JetBrains Mono', monospace;
    --sans:     'Rajdhani', sans-serif;
}

html, body { background: var(--bg); color: var(--text2); font-family: var(--mono); line-height: 1.5; }

/* ── LAYOUT ── */
#em-root { padding: 0 40px 60px; min-height: 100vh; }

.em-header {
    padding: 32px 0 24px;
    border-bottom: 1px solid var(--border);
    margin-bottom: 24px;
    display: flex;
    align-items: center;
    justify-content: space-between;
}
.em-header-left h1 {
    font-family: var(--sans);
    font-size: 28px;
    font-weight: 700;
    color: var(--blue);
    letter-spacing: 6px;
    text-transform: uppercase;
}
.em-header-left p {
    font-size: 10px;
    color: var(--text);
    letter-spacing: 3px;
    margin-top: 4px;
}
.em-header-right {
    text-align: right;
    font-size: 11px;
    color: var(--text);
}
.em-header-right .time {
    font-size: 20px;
    font-weight: 700;
    color: var(--text3);
    letter-spacing: 2px;
}

/* ── STATUS BAR ── */
.em-statusbar {
    display: flex;
    gap: 8px;
    margin-bottom: 20px;
    flex-wrap: wrap;
}
.em-stat {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 4px;
    padding: 10px 16px;
    flex: 1;
    min-width: 120px;
}
.em-stat label {
    display: block;
    font-size: 9px;
    letter-spacing: 2px;
    color: var(--text);
    text-transform: uppercase;
    margin-bottom: 4px;
}
.em-stat .val {
    font-size: 20px;
    font-weight: 700;
    color: var(--text3);
}
.em-stat .sub {
    font-size: 9px;
    color: var(--text);
    margin-top: 2px;
}

/* ── GRID ── */
.em-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px; }
.em-grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; margin-bottom: 16px; }

/* ── PANEL ── */
.em-panel {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 6px;
    overflow: hidden;
    margin-bottom: 16px;
}
.em-panel-header {
    background: var(--panel2);
    border-bottom: 1px solid var(--border);
    padding: 8px 14px;
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 2px;
    color: var(--blue);
    text-transform: uppercase;
}
.em-panel-header .dot {
    width: 6px; height: 6px;
    border-radius: 50%;
    background: var(--blue);
    box-shadow: 0 0 6px var(--blue);
    flex-shrink: 0;
}
.em-panel-header .dot.red    { background: var(--red);    box-shadow: 0 0 6px var(--red); }
.em-panel-header .dot.orange { background: var(--orange); box-shadow: 0 0 6px var(--orange); }
.em-panel-header .dot.green  { background: var(--green);  box-shadow: 0 0 6px var(--green); }
.em-panel-header .dot.purple { background: var(--purple); box-shadow: 0 0 6px var(--purple); }
.em-panel-body { padding: 12px 14px; }

/* ── TABLE ── */
.em-table { width: 100%; border-collapse: collapse; }
.em-table th {
    font-size: 9px;
    letter-spacing: 2px;
    color: var(--text);
    text-transform: uppercase;
    padding: 6px 8px;
    text-align: left;
    border-bottom: 1px solid var(--border);
}
.em-table td {
    padding: 7px 8px;
    border-bottom: 1px solid var(--border);
    vertical-align: middle;
}
.em-table tr:last-child td { border-bottom: none; }
.em-table tr:hover td { background: var(--panel2); }

/* ── BADGE ── */
.badge {
    display: inline-block;
    padding: 1px 6px;
    border-radius: 3px;
    font-size: 9px;
    font-weight: 700;
    letter-spacing: 1px;
}
.badge-red    { background: rgba(244,63,94,0.15);  color: var(--red);    border: 1px solid rgba(244,63,94,0.3); }
.badge-orange { background: rgba(251,146,60,0.15); color: var(--orange); border: 1px solid rgba(251,146,60,0.3); }
.badge-green  { background: rgba(34,211,165,0.1);  color: var(--green);  border: 1px solid rgba(34,211,165,0.25); }
.badge-blue   { background: rgba(56,189,248,0.1);  color: var(--blue);   border: 1px solid rgba(56,189,248,0.25); }
.badge-purple { background: rgba(167,139,250,0.1); color: var(--purple); border: 1px solid rgba(167,139,250,0.25); }

/* ── CODE ── */
code {
    font-family: var(--mono);
    font-size: 11px;
    color: #7dd3fc;
    background: rgba(14,165,233,0.08);
    padding: 1px 5px;
    border-radius: 3px;
    word-break: break-all;
}

/* ── COLORS ── */
.cl-red    { color: var(--red); }
.cl-orange { color: var(--orange); }
.cl-green  { color: var(--green); }
.cl-blue   { color: var(--blue); }
.cl-yellow { color: var(--yellow); }
.cl-purple { color: var(--purple); }
.cl-muted  { color: var(--text); }

/* ── PROGRESS BAR ── */
.pbar-wrap { background: var(--panel3); border-radius: 2px; height: 4px; margin-top: 4px; overflow: hidden; }
.pbar { height: 4px; border-radius: 2px; transition: width .3s; }

/* ── LOG STREAM ── */
.log-stream {
    background: #000;
    border-radius: 4px;
    padding: 10px;
    font-size: 11px;
    max-height: 320px;
    overflow-y: auto;
}
.log-row { display: flex; gap: 10px; padding: 3px 0; border-bottom: 1px solid #0d1117; align-items: flex-start; }
.log-row:last-child { border-bottom: none; }
.log-time  { color: #475569; flex-shrink: 0; width: 56px; }
.log-ip    { color: #7dd3fc; flex-shrink: 0; width: 120px; font-weight: 500; }
.log-method { flex-shrink: 0; width: 42px; font-weight: 700; }
.log-uri   { color: #94a3b8; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.log-status { flex-shrink: 0; width: 36px; font-weight: 700; text-align: right; }
.log-ua    { color: #475569; font-size: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 180px; flex-shrink: 0; }
.log-flag  { flex-shrink: 0; }

/* ── ALERT BOX ── */
.em-alert {
    border-radius: 4px;
    padding: 10px 14px;
    margin-bottom: 10px;
    font-size: 11px;
    border-left: 3px solid;
    display: flex;
    gap: 10px;
    align-items: flex-start;
}
.em-alert.red    { background: rgba(244,63,94,0.08);  border-color: var(--red); }
.em-alert.orange { background: rgba(251,146,60,0.08); border-color: var(--orange); }
.em-alert-icon { flex-shrink: 0; font-size: 14px; margin-top: -1px; }

/* ── IP BLOCK CMD ── */
.block-cmd {
    background: #000;
    border: 1px solid var(--border);
    border-radius: 4px;
    padding: 6px 10px;
    font-size: 10px;
    color: #22d3a5;
    font-family: var(--mono);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-top: 4px;
}
.block-cmd .copy-btn {
    background: var(--panel3);
    border: 1px solid var(--border2);
    color: var(--text);
    padding: 2px 8px;
    border-radius: 3px;
    cursor: pointer;
    font-size: 9px;
    font-family: var(--mono);
    flex-shrink: 0;
}
.block-cmd .copy-btn:hover { background: var(--panel2); color: var(--text3); }

/* ── REFRESH BTN ── */
.em-refresh {
    position: fixed;
    bottom: 30px;
    right: 40px;
    background: var(--panel2);
    border: 1px solid var(--border2);
    color: var(--blue);
    padding: 10px 20px;
    border-radius: 4px;
    cursor: pointer;
    font-family: var(--mono);
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 2px;
    z-index: 999;
}
.em-refresh:hover { background: var(--panel3); color: var(--text3); }

/* ── EMPTY STATE ── */
.em-empty { padding: 20px; text-align: center; color: var(--text); font-size: 11px; }

/* ── SECTION DIVIDER ── */
.em-divider {
    font-size: 9px;
    letter-spacing: 3px;
    color: var(--text);
    text-transform: uppercase;
    padding: 6px 0;
    margin: 4px 0 12px;
    border-bottom: 1px solid var(--border);
}

/* 페이지 스크롤바 스타일 */
::-webkit-scrollbar {
    width: 7px;
}
    
::-webkit-scrollbar-track {
    background: #020617;
}
    
::-webkit-scrollbar-thumb {
    background: rgba(99, 102, 241, 0.5);
    border-radius: 6px;
}
    
::-webkit-scrollbar-thumb:hover {
    background: rgba(99, 102, 241, 0.7);
}
</style>
</head>
<body>
<div id="em-root">

    <!-- HEADER -->
    <header class="em-header">
        <div class="em-header-left">
            <h1>EULJI MUNDEOK</h1>
            <p>VHOST SECURITY ANALYSIS &amp; INTRUSION MONITOR</p>
        </div>
        <div class="em-header-right">
            <div class="time" id="em-clock">--:--:--</div>
            <div style="margin-top:4px;"><?php echo date('Y-m-d'); ?> &nbsp;|&nbsp; <?php echo php_uname('n'); ?></div>
            <div style="margin-top:3px; font-size:10px;">
                LOG: 
                <span class="<?php echo $log_status === 'ACTIVE' ? 'cl-green' : 'cl-red'; ?>">
                    <?php echo $log_status; ?>
                </span>
                <?php if ($access_log_path): ?>
                    &nbsp;<span class="cl-muted"><?php echo htmlspecialchars($access_log_path); ?></span>
                <?php elseif ($log_status_reason): ?>
                    &nbsp;<span class="cl-orange"><?php echo htmlspecialchars($log_status_reason); ?></span>
                <?php endif; ?>
            </div>
        </div>
    </header>

    <!-- STATUS BAR -->
    <div class="em-statusbar">
        <div class="em-stat">
            <label>CPU Load 1m</label>
            <div class="val <?php echo (is_numeric($load['1m']) && $load['1m'] > 2) ? 'cl-red' : 'cl-green'; ?>">
                <?php echo $load['1m']; ?>
            </div>
            <div class="sub"><?php echo $load['5m']; ?> / <?php echo $load['15m']; ?> (5m/15m)</div>
        </div>
        <div class="em-stat">
            <label>RAM Usage</label>
            <div class="val <?php echo $mem['pct'] > 85 ? 'cl-red' : ($mem['pct'] > 70 ? 'cl-orange' : 'cl-green'); ?>">
                <?php echo $mem['pct']; ?>%
            </div>
            <div class="sub"><?php echo $mem['used']; ?>GB / <?php echo $mem['total']; ?>GB</div>
            <div class="pbar-wrap"><div class="pbar" style="width:<?php echo $mem['pct']; ?>%; background:<?php echo $mem['pct']>85?'var(--red)':($mem['pct']>70?'var(--orange)':'var(--green)'); ?>;"></div></div>
        </div>
        <div class="em-stat">
            <label>Disk Usage</label>
            <div class="val <?php echo $disk['pct'] > 90 ? 'cl-red' : ($disk['pct'] > 75 ? 'cl-orange' : 'cl-green'); ?>">
                <?php echo $disk['pct']; ?>%
            </div>
            <div class="sub"><?php echo $disk['used']; ?>GB / <?php echo $disk['total']; ?>GB</div>
            <div class="pbar-wrap"><div class="pbar" style="width:<?php echo $disk['pct']; ?>%; background:<?php echo $disk['pct']>90?'var(--red)':($disk['pct']>75?'var(--orange)':'var(--green)'); ?>;"></div></div>
        </div>
        <div class="em-stat">
            <label>HTTP Reqs (5m)</label>
            <div class="val cl-blue"><?php echo number_format($total_5m); ?></div>
            <div class="sub">전체 분석: <?php echo number_format($total_all); ?>건</div>
        </div>
        <div class="em-stat">
            <label>Scanner Hits</label>
            <div class="val <?php echo count($scanner_hits) > 0 ? 'cl-red' : 'cl-green'; ?>">
                <?php echo count($scanner_hits); ?>
            </div>
            <div class="sub">알려진 스캐너 IP</div>
        </div>
        <div class="em-stat">
            <label>Suspicious Path</label>
            <div class="val <?php echo count($suspicious_hits) > 0 ? 'cl-orange' : 'cl-green'; ?>">
                <?php echo count($suspicious_hits); ?>
            </div>
            <div class="sub">수상한 경로 접근 IP</div>
        </div>
        <div class="em-stat">
            <label>Bot Candidates</label>
            <div class="val <?php echo count($bot_candidates) > 0 ? 'cl-purple' : 'cl-green'; ?>">
                <?php echo count($bot_candidates); ?>
            </div>
            <div class="sub">자동화 패턴 감지</div>
        </div>
    </div>

    <!-- ALERTS -->
    <?php if (!empty($scanner_hits) || !empty($suspicious_hits) || !empty($bot_candidates)): ?>
    <div class="em-panel" style="border-color: rgba(244,63,94,0.4);">
        <div class="em-panel-header"><span class="dot red"></span>THREAT SUMMARY</div>
        <div class="em-panel-body">
            <?php foreach(array_slice($scanner_hits, 0, 3) as $ip => $cnt): ?>
            <div class="em-alert red">
                <div class="em-alert-icon">⚡</div>
                <div>
                    <strong class="cl-red"><?php echo htmlspecialchars($ip); ?></strong>
                    &nbsp;— 알려진 스캐너/봇 도구 감지 &nbsp;<span class="badge badge-red"><?php echo $cnt; ?>회</span>
                    <div class="block-cmd">
                        <span>iptables -I INPUT -s <?php echo htmlspecialchars($ip); ?> -j DROP</span>
                        <button class="copy-btn" onclick="copyCmd(this)">COPY</button>
                    </div>
                </div>
            </div>
            <?php endforeach; ?>
            <?php foreach(array_slice($suspicious_hits, 0, 3) as $ip => $uris): ?>
            <div class="em-alert orange">
                <div class="em-alert-icon">⚠️</div>
                <div>
                    <strong class="cl-orange"><?php echo htmlspecialchars($ip); ?></strong>
                    &nbsp;— 수상한 경로 탐색 &nbsp;<span class="badge badge-orange"><?php echo count($uris); ?>건</span>
                    <div style="font-size:10px; color:var(--text); margin-top:4px;">
                        <?php foreach(array_slice(array_unique($uris), 0, 3) as $u): ?>
                            <code><?php echo htmlspecialchars(substr($u, 0, 80)); ?></code>&nbsp;
                        <?php endforeach; ?>
                    </div>
                    <div class="block-cmd">
                        <span>iptables -I INPUT -s <?php echo htmlspecialchars($ip); ?> -j DROP</span>
                        <button class="copy-btn" onclick="copyCmd(this)">COPY</button>
                    </div>
                </div>
            </div>
            <?php endforeach; ?>
            <?php foreach(array_slice($bot_candidates, 0, 3, true) as $ip => $info): ?>
            <div class="em-alert" style="background:rgba(167,139,250,0.08); border-color:var(--purple);">
                <div class="em-alert-icon">🤖</div>
                <div>
                    <strong class="cl-purple"><?php echo htmlspecialchars($ip); ?></strong>
                    &nbsp;— 자동화 요청 패턴 감지
                    &nbsp;<span class="badge badge-purple">평균 <?php echo $info['mean']; ?>초 간격</span>
                    &nbsp;<span class="badge badge-purple">stddev <?php echo $info['stddev']; ?></span>
                    <div class="block-cmd">
                        <span>iptables -I INPUT -s <?php echo htmlspecialchars($ip); ?> -j DROP</span>
                        <button class="copy-btn" onclick="copyCmd(this)">COPY</button>
                    </div>
                </div>
            </div>
            <?php endforeach; ?>
        </div>
    </div>
    <?php endif; ?>

    <!-- ROW 1: IP 현황 + 수상한 경로 -->
    <div class="em-grid-2">
        <div class="em-panel">
            <div class="em-panel-header"><span class="dot red"></span>IP ATTACK WATCH (5분)</div>
            <div class="em-panel-body" style="padding:0;">
                <?php if (empty($ip_count_5m)): ?>
                    <div class="em-empty">데이터 없음</div>
                <?php else: ?>
                <table class="em-table">
                    <thead><tr><th>IP</th><th>Reqs</th><th>UA 수</th><th>판정</th></tr></thead>
                    <tbody>
                    <?php foreach(array_slice($ip_count_5m, 0, 12, true) as $ip => $cnt):
                        $ua_variety = isset($ip_ua_map[$ip]) ? count($ip_ua_map[$ip]) : 0;
                        $is_scanner = isset($scanner_hits[$ip]);
                        $is_suspicious = isset($suspicious_hits[$ip]);
                        $is_heavy = $cnt > 40;
                    ?>
                    <tr>
                        <td><strong style="color:var(--text3);"><?php echo htmlspecialchars($ip); ?></strong></td>
                        <td class="<?php echo $cnt > 40 ? 'cl-red' : ($cnt > 20 ? 'cl-orange' : 'cl-green'); ?>">
                            <strong><?php echo $cnt; ?></strong>
                        </td>
                        <td class="cl-muted"><?php echo $ua_variety; ?></td>
                        <td>
                            <?php if ($is_scanner): ?><span class="badge badge-red">SCANNER</span>&nbsp;<?php endif; ?>
                            <?php if ($is_suspicious): ?><span class="badge badge-orange">PROBE</span>&nbsp;<?php endif; ?>
                            <?php if ($is_heavy && !$is_scanner && !$is_suspicious): ?><span class="badge badge-orange">HEAVY</span><?php endif; ?>
                            <?php if (!$is_scanner && !$is_suspicious && !$is_heavy): ?><span class="badge badge-green">OK</span><?php endif; ?>
                        </td>
                    </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>
                <?php endif; ?>
            </div>
        </div>

        <div class="em-panel">
            <div class="em-panel-header"><span class="dot orange"></span>SUSPICIOUS PATH PROBE</div>
            <div class="em-panel-body" style="padding:0;">
                <?php if (empty($suspicious_hits)): ?>
                    <div class="em-empty">수상한 경로 접근 없음</div>
                <?php else: ?>
                <table class="em-table">
                    <thead><tr><th>IP</th><th>탐색 경로</th><th>건수</th></tr></thead>
                    <tbody>
                    <?php foreach(array_slice($suspicious_hits, 0, 10, true) as $ip => $uris):
                        $unique_uris = array_unique($uris);
                    ?>
                    <tr>
                        <td><strong class="cl-orange"><?php echo htmlspecialchars($ip); ?></strong></td>
                        <td style="max-width:200px;">
                            <?php foreach(array_slice($unique_uris, 0, 2) as $u): ?>
                                <code><?php echo htmlspecialchars(substr($u, 0, 50)); ?></code><br>
                            <?php endforeach; ?>
                            <?php if (count($unique_uris) > 2): ?>
                                <span class="cl-muted">+<?php echo count($unique_uris)-2; ?>개</span>
                            <?php endif; ?>
                        </td>
                        <td><span class="badge badge-orange"><?php echo count($uris); ?></span></td>
                    </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>
                <?php endif; ?>
            </div>
        </div>
    </div>

    <!-- ROW 2: Top URI + User Agent -->
    <div class="em-grid-2">
        <div class="em-panel">
            <div class="em-panel-header"><span class="dot blue"></span>TOP URI (전체)</div>
            <div class="em-panel-body" style="padding:0;">
                <?php if (empty($uri_count)): ?>
                    <div class="em-empty">데이터 없음</div>
                <?php else: ?>
                <table class="em-table">
                    <thead><tr><th>Path</th><th>Hits</th><th></th></tr></thead>
                    <tbody>
                    <?php $max_uri = max(array_slice($uri_count, 0, 1)) ?: 1;
                    foreach(array_slice($uri_count, 0, 12, true) as $uri => $cnt):
                        $is_sus = isSuspiciousPath($uri, $suspicious_paths);
                    ?>
                    <tr>
                        <td style="max-width:260px;"><code><?php echo htmlspecialchars(substr($uri, 0, 70)); ?></code></td>
                        <td class="cl-blue"><strong><?php echo $cnt; ?></strong></td>
                        <td><?php if($is_sus): ?><span class="badge badge-orange">PROBE</span><?php endif; ?></td>
                    </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>
                <?php endif; ?>
            </div>
        </div>

        <div class="em-panel">
            <div class="em-panel-header"><span class="dot purple"></span>USER AGENT 분석</div>
            <div class="em-panel-body" style="padding:0;">
                <?php if (empty($ua_count)): ?>
                    <div class="em-empty">데이터 없음</div>
                <?php else: ?>
                <table class="em-table">
                    <thead><tr><th>User Agent</th><th>Hits</th><th></th></tr></thead>
                    <tbody>
                    <?php foreach(array_slice($ua_count, 0, 12, true) as $ua => $cnt):
                        $is_scan = isScanner($ua, $scanner_patterns);
                    ?>
                    <tr>
                        <td style="max-width:260px; font-size:10px; color:var(--text);" title="<?php echo htmlspecialchars($ua); ?>">
                            <?php echo htmlspecialchars(substr($ua, 0, 65)); ?>
                        </td>
                        <td class="<?php echo $is_scan ? 'cl-red' : 'cl-muted'; ?>"><strong><?php echo $cnt; ?></strong></td>
                        <td><?php if($is_scan): ?><span class="badge badge-red">SCANNER</span><?php endif; ?></td>
                    </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>
                <?php endif; ?>
            </div>
        </div>
    </div>

    <!-- ROW 3: Method 분포 + Status 분포 -->
    <div class="em-grid-2">
        <div class="em-panel">
            <div class="em-panel-header"><span class="dot green"></span>REQUEST METHOD 분포</div>
            <div class="em-panel-body">
                <?php if (empty($method_count)): ?>
                    <div class="em-empty">데이터 없음</div>
                <?php else:
                    $total_methods = array_sum($method_count);
                    $method_colors = ['GET'=>'var(--blue)','POST'=>'var(--green)','HEAD'=>'var(--yellow)','PUT'=>'var(--orange)','DELETE'=>'var(--red)','OPTIONS'=>'var(--purple)'];
                    foreach ($method_count as $m => $c):
                        $pct = $total_methods > 0 ? round($c/$total_methods*100, 1) : 0;
                        $color = $method_colors[$m] ?? 'var(--text)';
                ?>
                <div style="margin-bottom:10px;">
                    <div style="display:flex; justify-content:space-between; margin-bottom:3px;">
                        <span style="color:<?php echo $color; ?>; font-weight:700;"><?php echo $m; ?></span>
                        <span class="cl-muted"><?php echo number_format($c); ?> &nbsp;(<?php echo $pct; ?>%)</span>
                    </div>
                    <div class="pbar-wrap"><div class="pbar" style="width:<?php echo $pct; ?>%; background:<?php echo $color; ?>;"></div></div>
                </div>
                <?php endforeach; endif; ?>
            </div>
        </div>

        <div class="em-panel">
            <div class="em-panel-header"><span class="dot orange"></span>HTTP STATUS 분포</div>
            <div class="em-panel-body">
                <?php if (empty($status_count)): ?>
                    <div class="em-empty">데이터 없음</div>
                <?php else:
                    arsort($status_count);
                    $total_status = array_sum($status_count);
                    foreach ($status_count as $st => $c):
                        $pct = $total_status > 0 ? round($c/$total_status*100, 1) : 0;
                        $cls = $st >= 500 ? 'var(--red)' : ($st >= 400 ? 'var(--orange)' : ($st >= 300 ? 'var(--yellow)' : 'var(--green)'));
                ?>
                <div style="margin-bottom:8px;">
                    <div style="display:flex; justify-content:space-between; margin-bottom:3px;">
                        <span style="color:<?php echo $cls; ?>; font-weight:700;"><?php echo $st; ?></span>
                        <span class="cl-muted"><?php echo number_format($c); ?> &nbsp;(<?php echo $pct; ?>%)</span>
                    </div>
                    <div class="pbar-wrap"><div class="pbar" style="width:<?php echo $pct; ?>%; background:<?php echo $cls; ?>;"></div></div>
                </div>
                <?php endforeach; endif; ?>
            </div>
        </div>
    </div>

    <!-- LIVE LOG -->
    <div class="em-panel">
        <div class="em-panel-header"><span class="dot green"></span>LIVE TRAFFIC LOG (5분 최근 30건)</div>
        <div class="em-panel-body" style="padding:8px;">
            <?php if (empty($requests_5m)): ?>
                <div class="em-empty">5분 내 트래픽 없음</div>
            <?php else: ?>
            <div class="log-stream">
                <?php foreach(array_slice($requests_5m, 0, 30) as $r):
                    $is_scan = isScanner($r['ua'], $scanner_patterns);
                    $is_sus  = isSuspiciousPath($r['uri'], $suspicious_paths);
                    $m_color = ['GET'=>'cl-blue','POST'=>'cl-green','HEAD'=>'cl-yellow','DELETE'=>'cl-red','PUT'=>'cl-orange'][$r['method']] ?? 'cl-muted';
                ?>
                <div class="log-row" style="<?php echo $is_scan ? 'background:rgba(244,63,94,0.05);' : ($is_sus ? 'background:rgba(251,146,60,0.05);' : ''); ?>">
                    <span class="log-time"><?php echo date('H:i:s', $r['time']); ?></span>
                    <span class="log-ip"><?php echo htmlspecialchars($r['ip']); ?></span>
                    <span class="log-method <?php echo $m_color; ?>"><?php echo $r['method']; ?></span>
                    <span class="log-uri" title="<?php echo htmlspecialchars($r['uri']); ?>"><?php echo htmlspecialchars(substr($r['uri'], 0, 100)); ?></span>
                    <span class="log-status <?php echo getStatusClass($r['status']); ?>"><?php echo $r['status']; ?></span>
                    <span class="log-ua" title="<?php echo htmlspecialchars($r['ua']); ?>"><?php echo htmlspecialchars(substr($r['ua'], 0, 60)); ?></span>
                    <span class="log-flag">
                        <?php if($is_scan): ?><span class="badge badge-red">S</span><?php endif; ?>
                        <?php if($is_sus):  ?><span class="badge badge-orange">P</span><?php endif; ?>
                    </span>
                </div>
                <?php endforeach; ?>
            </div>
            <?php endif; ?>
        </div>
    </div>

    <!-- PHP ERROR LOG -->
    <?php if (!empty($error_logs)): ?>
    <div class="em-panel" style="border-color: rgba(251,146,60,0.4);">
        <div class="em-panel-header"><span class="dot orange"></span>PHP ENGINE WARNINGS</div>
        <div class="em-panel-body">
            <div style="background:#000; border-radius:4px; padding:10px; font-size:10px; overflow-x:auto;">
                <?php foreach($error_logs as $el): ?>
                    <div style="margin-bottom:5px; color:#fb923c; white-space:nowrap;">> <?php echo htmlspecialchars($el); ?></div>
                <?php endforeach; ?>
            </div>
        </div>
    </div>
    <?php endif; ?>

    <!-- IP BLOCK TOOL -->
    <div class="em-panel">
        <div class="em-panel-header"><span class="dot red"></span>BLOCK COMMAND GENERATOR</div>
        <div class="em-panel-body">
            <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
                <input type="text" id="block-ip-input" placeholder="차단할 IP 입력 (예: 1.2.3.4)"
                    style="background:var(--panel3); border:1px solid var(--border2); color:var(--text3); padding:7px 12px; border-radius:4px; font-family:var(--mono); font-size:12px; flex:1; min-width:200px;">
                <button onclick="generateBlock()" style="background:var(--panel3); border:1px solid var(--border2); color:var(--blue); padding:7px 14px; border-radius:4px; cursor:pointer; font-family:var(--mono); font-size:11px; font-weight:700;">생성</button>
            </div>
            <div id="block-output" style="margin-top:10px;"></div>
        </div>
    </div>

</div>

<button class="em-refresh" onclick="location.reload()">↺ REFRESH</button>

<script>
// 시계
function updateClock() {
    const now = new Date();
    document.getElementById('em-clock').textContent =
        String(now.getHours()).padStart(2,'0') + ':' +
        String(now.getMinutes()).padStart(2,'0') + ':' +
        String(now.getSeconds()).padStart(2,'0');
}
updateClock();
setInterval(updateClock, 1000);

// 복사
function copyCmd(btn) {
    const cmd = btn.previousElementSibling.textContent.trim();
    navigator.clipboard.writeText(cmd).then(() => {
        const orig = btn.textContent;
        btn.textContent = 'COPIED';
        btn.style.color = 'var(--green)';
        setTimeout(() => { btn.textContent = orig; btn.style.color = ''; }, 1500);
    });
}

// 차단 명령 생성
function generateBlock() {
    const ip = document.getElementById('block-ip-input').value.trim();
    if (!ip) return;
    const out = document.getElementById('block-output');
    const cmds = [
        'iptables -I INPUT -s ' + ip + ' -j DROP',
        'iptables -I OUTPUT -d ' + ip + ' -j DROP',
        'echo "deny from ' + ip + '" >> /etc/apache2/.htaccess',
    ];
    out.innerHTML = cmds.map(c => `
        <div class="block-cmd" style="margin-bottom:6px;">
            <span>${c}</span>
            <button class="copy-btn" onclick="copyCmd(this)">COPY</button>
        </div>
    `).join('');
}
</script>

<?php require_once '/home/www/GNU/_PAGE/tail.php'; ?>
</body>
</html>