<?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 & 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'); ?> | <?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): ?>
<span class="cl-muted"><?php echo htmlspecialchars($access_log_path); ?></span>
<?php elseif ($log_status_reason): ?>
<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>
— 알려진 스캐너/봇 도구 감지 <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>
— 수상한 경로 탐색 <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>
<?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>
— 자동화 요청 패턴 감지
<span class="badge badge-purple">평균 <?php echo $info['mean']; ?>초 간격</span>
<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> <?php endif; ?>
<?php if ($is_suspicious): ?><span class="badge badge-orange">PROBE</span> <?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); ?> (<?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); ?> (<?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>