SIMON Master Console Rebuild
True command center for SIMON + WEB360
This rebuild gives you a cleaner production spine: interactive tree, live viewer, guarded editor, dynamic graphics, architecture map, and note storage. It is designed to sit beside your existing
console.php
and become the stronger replacement path.
Dashboard
Tree
Viewer
System Map
File Registry
Notes
Files
15,312
Folders
410
Scanned Size
3.85 GB
PHP Files
891
Editable Text Files
12,573
File viewer
guarded to /htdocs
/tools/simon_console.php
<?php declare(strict_types=1); require_once __DIR__ . '/_bootstrap.php'; date_default_timezone_set('America/Chicago'); require_once __DIR__ . '/../guardian/tracker.php'; guardian_track_event('admin_page_view', [ 'page' => '/simon/master_eco.php', 'title' => 'SIMON Master Eco', 'area' => 'admin', 'classification' => 'protected_admin', ]); /** * /tools/simon_console.php — SIMON Security Intelligence + Speed Checker (SINGLE FILE) * * Combines: * - Website Speed Checker (cURL timing + size + redirect chain) * - SIMON Security Intelligence (pattern scanning + scoring + optional AI analysis) * - Live Log Console (NDJSON tail) * - Clear button (clear NDJSON + optional quarantine snapshots) * * SECURITY (DO THIS): * - Protect /tools/ with Basic Auth or IP allowlist. * - Keep /_logs/ blocked from public web access via .htaccess. * * Optional AI (local proxy you already use): * - Configure $OPENAI_PROXY to your existing endpoint (example: /api/openai.php) * - If unavailable, system falls back to deterministic pattern engine. */ header('Content-Type: text/html; charset=utf-8'); // ------------------------- // Config // ------------------------- $TITLE = 'SIMON Console — Security Intelligence'; $OPENAI_PROXY = '/api/openai.php'; // set '' to disable $ENABLE_AI = true; // AI only runs when proxy reachable $AI_MODEL = 'gpt-4.1-mini'; // your proxy decides; safe default label // Log files (NDJSON) $LOG_DIR = rtrim((string)($_SERVER['DOCUMENT_ROOT'] ?? ''), '/') . '/_logs'; $CONSOLE_LOG = $LOG_DIR . '/simon_console.ndjson'; $SCAN_SNAP_DIR = $LOG_DIR . '/scan_snapshots'; // optional snapshots // Tail settings $TAIL_LINES_DEFAULT = 200; $TAIL_MAX_BYTES = 1024 * 512; // 512KB // Speed checker settings $SPEED_TIMEOUT = 20; $SPEED_MAX_REDIRECTS = 6; // Pattern engine tuning $BLOCK_SCORE = 85; $WARN_SCORE = 55; // ------------------------- // Helpers // ------------------------- function h(string $s): string { return htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); } function ensure_dir(string $dir): void { if (!is_dir($dir)) @mkdir($dir, 0755, true); } function now_iso_utc(): string { return gmdate('Y-m-d\TH:i:s\Z'); } function rnd_id(int $len = 12): string { $chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; $out = ''; for ($i=0; $i<$len; $i++) $out .= $chars[random_int(0, strlen($chars)-1)]; return $out; } function write_ndjson(string $file, array $row): void { $line = json_encode($row, JSON_UNESCAPED_SLASHES) . "\n"; @file_put_contents($file, $line, FILE_APPEND | LOCK_EX); } function json_out(array $data, int $code = 200): void { http_response_code($code); header('Content-Type: application/json; charset=utf-8'); echo json_encode($data, JSON_UNESCAPED_SLASHES); exit; } function safe_read_tail(string $file, int $lines, int $maxBytes): array { if (!is_file($file)) return []; $size = filesize($file); if ($size === false || $size <= 0) return []; $fh = @fopen($file, 'rb'); if (!$fh) return []; $readBytes = min($maxBytes, $size); @fseek($fh, -$readBytes, SEEK_END); $chunk = (string)@fread($fh, $readBytes); @fclose($fh); // normalize and split $chunk = str_replace("\r\n", "\n", $chunk); $arr = explode("\n", $chunk); // if file bigger than chunk, first line may be partial; drop it if ($readBytes < $size) array_shift($arr); $arr = array_values(array_filter($arr, static fn($x) => trim($x) !== '')); if ($lines > 0 && count($arr) > $lines) $arr = array_slice($arr, -$lines); $out = []; foreach ($arr as $line) { $j = json_decode($line, true); if (is_array($j)) $out[] = $j; } return $out; } function is_same_origin_or_public_url(string $url): bool { // Allow only http(s) if (!preg_match('~^https?://~i', $url)) return false; // Basic SSRF guard: block localhost / private ranges by hostname if it's an IP literal. $host = parse_url($url, PHP_URL_HOST); if (!is_string($host) || $host === '') return false; // If IP literal, block private/reserved ranges if (filter_var($host, FILTER_VALIDATE_IP)) { $ip = $host; $blocked = [ '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', '127.0.0.0/8', '0.0.0.0/8', '169.254.0.0/16', '::1/128', 'fc00::/7', 'fe80::/10' ]; foreach ($blocked as $cidr) { if (ip_in_cidr($ip, $cidr)) return false; } } return true; } function ip_in_cidr(string $ip, string $cidr): bool { // IPv4 only for simplicity; IPv6 strings in list are blocked earlier unless literal if (strpos($cidr, ':') !== false) { // for IPv6 literal cases, conservatively block return true; } if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return false; [$subnet, $mask] = explode('/', $cidr, 2); $mask = (int)$mask; $ipLong = ip2long($ip); $subLong = ip2long($subnet); if ($ipLong === false || $subLong === false) return false; $maskLong = -1 << (32 - $mask); return (($ipLong & $maskLong) === ($subLong & $maskLong)); } // ------------------------- // Pattern engine // ------------------------- function patterns(): array { // Each entry: [name, regex, weight, category] return [ // LFI / Traversal ['path_traversal', '~(\.\./|\.\.\\\\|%2e%2e%2f|%2e%2e%5c)~i', 28, 'lfi'], ['etc_passwd', '~(/etc/passwd|%2fetc%2fpasswd)~i', 40, 'lfi'], ['proc_self', '~(/proc/self/environ|%2fproc%2fself%2fenviron)~i', 40, 'lfi'], ['windows_ini', '~(win\.ini|system32)~i', 22, 'lfi'], // RFI / SSRF-ish indicators ['php_wrapper', '~(php://|data:|expect://|zip://|phar://)~i', 35, 'rfi'], ['url_include', '~(https?:%2f%2f|https?://)~i', 12, 'rfi'], // SQLi ['sqli_union', '~\bunion\b\s*(all\s*)?\bselect\b~i', 35, 'sqli'], ['sqli_bool', '~(\bor\b|\band\b)\s+\d+\s*=\s*\d+~i', 22, 'sqli'], ['sqli_comment', '~(--\s|#|/\*)~', 10, 'sqli'], ['sqli_sleep', '~\b(sleep|benchmark)\s*\(~i', 30, 'sqli'], ['sqli_info', '~\b(information_schema|pg_catalog|sqlite_master)\b~i', 30, 'sqli'], // XSS ['xss_script', '~<\s*script\b|%3c\s*script\b~i', 30, 'xss'], ['xss_event', '~on\w+\s*=|%6f%6e\w+%3d~i', 18, 'xss'], ['xss_jsproto', '~(javascript:|data:text/html)~i', 22, 'xss'], // Command injection ['cmd_separators', '~(;|\|\||&&|\|)~', 14, 'cmdi'], ['cmd_bins', '~\b(curl|wget|bash|sh|powershell|cmd\.exe|nc|ncat|perl|python)\b~i', 22, 'cmdi'], // Recon / common probes ['dotenv', '~(^|/)\.env(\b|$)~i', 35, 'probe'], ['wp_probe', '~(^|/)(wp-admin|wp-login|xmlrpc\.php)\b~i', 18, 'probe'], ['git_probe', '~(^|/)\.git\b~i', 28, 'probe'], ['well_known_probe','~(^|/)\.well-known/.*\.(php|asp|aspx|jsp)\b~i', 25, 'probe'], // Sensitive file pulls ['config_pull', '~(config\.php|wp-config\.php|composer\.json|composer\.lock|package\.json)\b~i', 18, 'probe'], ]; } function scan_text(string $text): array { $hits = []; $score = 0; $cats = []; foreach (patterns() as $p) { [$name, $rx, $weight, $cat] = $p; if (preg_match($rx, $text)) { $hits[] = ['name' => $name, 'category' => $cat, 'weight' => $weight, 'regex' => $rx]; $score += $weight; $cats[$cat] = ($cats[$cat] ?? 0) + 1; } } // Normalize & cap if ($score > 100) $score = 100; // Severity $sev = 'low'; if ($score >= 85) $sev = 'high'; elseif ($score >= 55) $sev = 'medium'; return [ 'score' => $score, 'severity' => $sev, 'categories' => $cats, 'hits' => $hits, ]; } function ai_analyze(string $proxy, string $model, array $payload): array { // Conservative proxy contract: // POST JSON: { task:"security_scan", model, input:{...} } // Response: { ok:true, result:{summary, risk, suggestions, tags[]} } $ch = @curl_init($proxy); if (!$ch) return ['ok'=>false, 'error'=>'curl_init_failed']; $body = json_encode([ 'task' => 'security_scan', 'model' => $model, 'input' => $payload, ], JSON_UNESCAPED_SLASHES); @curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_POSTFIELDS => $body, CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 3, CURLOPT_TIMEOUT => 8, ]); $resp = (string)@curl_exec($ch); $code = (int)@curl_getinfo($ch, CURLINFO_RESPONSE_CODE); @curl_close($ch); if ($code < 200 || $code >= 300) return ['ok'=>false, 'error'=>'proxy_http_'.$code]; $j = json_decode($resp, true); if (!is_array($j)) return ['ok'=>false, 'error'=>'bad_json']; return $j; } // ------------------------- // Actions (AJAX) // ------------------------- ensure_dir($LOG_DIR); ensure_dir($SCAN_SNAP_DIR); $action = (string)($_GET['action'] ?? ''); if ($action === 'logs') { $n = (int)($_GET['n'] ?? $TAIL_LINES_DEFAULT); if ($n < 1) $n = $TAIL_LINES_DEFAULT; if ($n > 1000) $n = 1000; $rows = safe_read_tail($CONSOLE_LOG, $n, $TAIL_MAX_BYTES); json_out(['ok'=>true, 'rows'=>$rows]); } if ($action === 'clear') { // Clear console log + snapshots (optional) $which = (string)($_POST['which'] ?? 'console'); // console | snapshots | all $cleared = []; if ($which === 'console' || $which === 'all') { if (is_file($CONSOLE_LOG)) @file_put_contents($CONSOLE_LOG, ''); $cleared[] = 'console'; } if ($which === 'snapshots' || $which === 'all') { if (is_dir($SCAN_SNAP_DIR)) { $files = glob($SCAN_SNAP_DIR . '/*.json'); if (is_array($files)) { foreach ($files as $f) @unlink($f); } } $cleared[] = 'snapshots'; } write_ndjson($CONSOLE_LOG, [ 'ts' => now_iso_utc(), 'type' => 'console_clear', 'rid' => rnd_id(), 'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown', 'which' => $which, 'cleared' => $cleared, ]); json_out(['ok'=>true, 'cleared'=>$cleared]); } if ($action === 'speed') { $url = trim((string)($_POST['url'] ?? '')); if ($url === '' || !is_same_origin_or_public_url($url)) { json_out(['ok'=>false, 'error'=>'invalid_url'], 400); } $rid = rnd_id(); $ch = @curl_init($url); if (!$ch) json_out(['ok'=>false, 'error'=>'curl_init_failed'], 500); @curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => $SPEED_MAX_REDIRECTS, CURLOPT_CONNECTTIMEOUT => 6, CURLOPT_TIMEOUT => $SPEED_TIMEOUT, CURLOPT_USERAGENT => 'SIMON-Speed/1.0', CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, ]); $raw = (string)@curl_exec($ch); $errno = (int)@curl_errno($ch); $err = (string)@curl_error($ch); $info = (array)@curl_getinfo($ch); @curl_close($ch); if ($errno !== 0) { write_ndjson($CONSOLE_LOG, [ 'ts' => now_iso_utc(), 'type' => 'speed_check', 'rid' => $rid, 'url' => $url, 'ok' => false, 'error' => $err, 'errno' => $errno, ]); json_out(['ok'=>false, 'error'=>$err, 'errno'=>$errno], 502); } $headerSize = (int)($info['header_size'] ?? 0); $status = (int)($info['http_code'] ?? 0); $totalTime = (float)($info['total_time'] ?? 0); $namelookup = (float)($info['namelookup_time'] ?? 0); $connect = (float)($info['connect_time'] ?? 0); $appconnect = (float)($info['appconnect_time'] ?? 0); $pretransfer = (float)($info['pretransfer_time'] ?? 0); $starttransfer = (float)($info['starttransfer_time'] ?? 0); $redirectTime = (float)($info['redirect_time'] ?? 0); $sizeDownload = (float)($info['size_download'] ?? 0); // Rough grading $ttfb = $starttransfer; $grade = 'good'; if ($ttfb > 1.2 || $totalTime > 3.5) $grade = 'slow'; elseif ($ttfb > 0.7 || $totalTime > 2.0) $grade = 'medium'; $result = [ 'rid' => $rid, 'url' => $url, 'status' => $status, 'grade' => $grade, 'timings' => [ 'dns' => $namelookup, 'connect' => $connect, 'tls' => $appconnect, 'pretransfer' => $pretransfer, 'ttfb' => $ttfb, 'redirect' => $redirectTime, 'total' => $totalTime, ], 'bytes' => [ 'header_size' => $headerSize, 'download' => (int)$sizeDownload, ], 'final_url' => (string)($info['url'] ?? $url), ]; write_ndjson($CONSOLE_LOG, [ 'ts' => now_iso_utc(), 'type' => 'speed_check', 'rid' => $rid, 'ok' => true, 'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown', 'result' => $result, ]); json_out(['ok'=>true, 'result'=>$result]); } if ($action === 'scan') { $rid = rnd_id(); $mode = (string)($_POST['mode'] ?? 'request'); // request | custom $custom = (string)($_POST['text'] ?? ''); $source = [ 'host' => (string)($_SERVER['HTTP_HOST'] ?? ''), 'ip' => (string)($_SERVER['REMOTE_ADDR'] ?? 'unknown'), 'ua' => (string)($_SERVER['HTTP_USER_AGENT'] ?? ''), 'uri' => (string)($_SERVER['REQUEST_URI'] ?? ''), 'method' => (string)($_SERVER['REQUEST_METHOD'] ?? ''), ]; $text = ''; if ($mode === 'custom') { $text = $custom; } else { $qs = (string)($_SERVER['QUERY_STRING'] ?? ''); $headers = ''; foreach (['HTTP_REFERER','HTTP_ACCEPT','HTTP_ACCEPT_LANGUAGE','HTTP_ACCEPT_ENCODING'] as $k) { if (!empty($_SERVER[$k])) $headers .= $k . ': ' . (string)$_SERVER[$k] . "\n"; } $text = "URI: {$source['uri']}\nQS: {$qs}\nUA: {$source['ua']}\n{$headers}"; } $rule = scan_text($text); $decision = 'allow'; if ($rule['score'] >= $BLOCK_SCORE) $decision = 'block'; elseif ($rule['score'] >= $WARN_SCORE) $decision = 'warn'; // Optional snapshot $snapFile = $SCAN_SNAP_DIR . '/scan_' . now_iso_utc() . '_' . $rid . '.json'; $snap = [ 'ts' => now_iso_utc(), 'rid' => $rid, 'source' => $source, 'mode' => $mode, 'text' => mb_substr($text, 0, 8000), 'rule' => $rule, 'decision' => $decision, ]; @file_put_contents($snapFile, json_encode($snap, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); // AI assist if enabled $ai = ['ok'=>false, 'used'=>false]; if ($ENABLE_AI && $OPENAI_PROXY !== '') { $aiResp = ai_analyze($OPENAI_PROXY, $AI_MODEL, [ 'rid' => $rid, 'source' => $source, 'rule' => [ 'score' => $rule['score'], 'severity' => $rule['severity'], 'categories' => $rule['categories'], 'hits' => array_map(static fn($x) => ['name'=>$x['name'],'category'=>$x['category'],'weight'=>$x['weight']], $rule['hits']), ], 'sample' => mb_substr($text, 0, 2500), 'goal' => 'Return a short security assessment and recommended actions. No secrets.', ]); if (!empty($aiResp['ok'])) { $ai = $aiResp; $ai['used'] = true; } } $event = [ 'ts' => now_iso_utc(), 'type' => 'security_scan', 'rid' => $rid, 'decision' => $decision, 'source' => $source, 'rule' => $rule, 'ai_used' => (bool)($ai['used'] ?? false), ]; if (!empty($ai['ok'])) $event['ai'] = $ai['result'] ?? $ai; write_ndjson($CONSOLE_LOG, $event); json_out([ 'ok' => true, 'rid' => $rid, 'decision' => $decision, 'rule' => $rule, 'ai' => $ai, 'snapshot' => basename($snapFile), ]); } // ------------------------- // UI // ------------------------- ?> <!doctype html> <html lang="en"> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"/> <title><?=h($TITLE)?></title> <meta name="theme-color" content="#070b16"/> <style> :root{ --bg0:#050816; --bg1:#070b16; --bg2:#0c1430; --card:rgba(255,255,255,.06); --edge:rgba(255,255,255,.14); --txt:rgba(255,255,255,.92); --mut:rgba(255,255,255,.68); --bad:#ff4d6d; --warn:#ffb703; --good:#2dd4bf; --glow: 0 0 24px rgba(120,140,255,.22); --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono","Courier New", monospace; --sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; } *{box-sizing:border-box} body{ margin:0; color:var(--txt); font-family:var(--sans); background: radial-gradient(900px 600px at 20% 10%, rgba(130,160,255,.14), transparent 60%), radial-gradient(900px 600px at 80% 20%, rgba(45,212,191,.10), transparent 60%), linear-gradient(180deg, var(--bg0), var(--bg1) 40%, #02040c); min-height:100vh; } header{ padding:18px 16px 12px; border-bottom:1px solid rgba(255,255,255,.08); position:sticky; top:0; backdrop-filter: blur(10px); background: linear-gradient(180deg, rgba(5,8,22,.85), rgba(5,8,22,.55)); z-index:10; } .wrap{max-width:1120px; margin:0 auto;} .top{ display:flex; gap:12px; align-items:center; justify-content:space-between; } .brand{ display:flex; flex-direction:column; gap:3px; } .brand b{letter-spacing:.4px} .brand small{color:var(--mut)} .tabs{display:flex; gap:8px; flex-wrap:wrap} .tab{ padding:8px 10px; border:1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.04); color:var(--txt); border-radius:12px; cursor:pointer; user-select:none; } .tab.active{border-color: rgba(140,160,255,.45); box-shadow: var(--glow);} main{padding:16px} .grid{display:grid; grid-template-columns: 1fr; gap:14px} @media (min-width: 980px){ .grid.two{grid-template-columns: 1.15fr .85fr;} } .card{ border:1px solid rgba(255,255,255,.12); background: var(--card); border-radius:18px; padding:14px; box-shadow: 0 12px 40px rgba(0,0,0,.25); } .card h2{margin:0 0 10px; font-size:16px} .row{display:flex; gap:10px; flex-wrap:wrap; align-items:center} input, textarea, select{ width:100%; border-radius:12px; border:1px solid rgba(255,255,255,.14); background: rgba(0,0,0,.18); color: var(--txt); padding:10px 10px; outline:none; } textarea{min-height:120px; font-family: var(--mono)} .btn{ border-radius:12px; border:1px solid rgba(255,255,255,.14); background: rgba(255,255,255,.06); color:var(--txt); padding:10px 12px; cursor:pointer; } .btn.primary{border-color: rgba(45,212,191,.35); box-shadow: 0 0 0 1px rgba(45,212,191,.12) inset;} .btn.danger{border-color: rgba(255,77,109,.35); } .btn:disabled{opacity:.55; cursor:not-allowed} .pill{ font-family: var(--mono); font-size:12px; padding:6px 8px; border-radius:999px; border:1px solid rgba(255,255,255,.14); background: rgba(0,0,0,.22); } .pill.good{border-color: rgba(45,212,191,.35); color: rgba(180,255,240,.92);} .pill.warn{border-color: rgba(255,183,3,.35); color: rgba(255,225,150,.92);} .pill.bad{border-color: rgba(255,77,109,.35); color: rgba(255,180,195,.92);} .mono{font-family: var(--mono)} .mut{color: var(--mut)} .console{ height: 420px; overflow:auto; border-radius:14px; border:1px solid rgba(255,255,255,.12); background: rgba(0,0,0,.22); padding:10px; font-family: var(--mono); font-size: 12px; line-height: 1.45; white-space: pre-wrap; } .kv{display:grid; grid-template-columns: 160px 1fr; gap:6px 10px; align-items:center} .kv div:nth-child(odd){color:var(--mut)} .split{ display:grid; grid-template-columns: 1fr; gap:12px; } @media (min-width: 980px){ .split{grid-template-columns: 1fr 1fr;} } </style> </head> <body> <header> <div class="wrap top"> <div class="brand"> <b><?=h($TITLE)?></b> <small>Speed checker + pattern scanning + optional AI assist + NDJSON console</small> </div> <div class="tabs"> <div class="tab active" data-tab="speed">Speed</div> <div class="tab" data-tab="security">Security Scan</div> <div class="tab" data-tab="console">Console</div> </div> </div> </header> <main class="wrap"> <section id="tab-speed" class="grid two"> <div class="card"> <h2>Website Speed Checker</h2> <div class="row"> <div style="flex:1 1 520px"> <input id="speedUrl" placeholder="https://www.gaylordsinclair.com/" value="<?=h((!empty($_SERVER['HTTP_HOST']) ? ('https://' . $_SERVER['HTTP_HOST'] . '/') : 'https://example.com/'))?>"> <div class="mut" style="margin-top:8px">Uses server-side cURL timing (DNS, connect, TLS, TTFB, total). Not Lighthouse.</div> </div> <div style="flex:0 0 auto"> <button id="speedRun" class="btn primary">Run</button> </div> </div> <div id="speedOut" style="margin-top:12px" class="split"></div> </div> <div class="card"> <h2>Speed Guidance</h2> <div class="mut" style="line-height:1.55"> Targets (rough): <div class="mono" style="margin-top:8px"> TTFB < 0.7s (good), < 1.2s (ok), > 1.2s (slow)<br/> Total < 2.0s (good), < 3.5s (ok), > 3.5s (slow) </div> <div style="margin-top:10px"> Common wins: full-page caching, image compression/WebP, reduce PHP work per request, CDN, preconnect, fewer redirects. </div> </div> </div> </section> <section id="tab-security" class="grid two" style="display:none"> <div class="card"> <h2>SIMON Security Intelligence</h2> <div class="row"> <div style="flex:1 1 280px"> <select id="scanMode"> <option value="request">Scan current request (URI/QS/UA/headers)</option> <option value="custom">Scan custom text payload</option> </select> </div> <div style="flex:0 0 auto"> <button id="scanRun" class="btn primary">Scan</button> </div> </div> <div id="scanCustomWrap" style="display:none; margin-top:10px"> <textarea id="scanText" placeholder="Paste a log line, URL, payload, UA, etc..."></textarea> </div> <div id="scanOut" style="margin-top:12px"></div> </div> <div class="card"> <h2>Predefined Pattern Set</h2> <div class="mut" style="line-height:1.55"> Detects: traversal/LFI, wrappers (php://, data:), SQLi (union/select, sleep), XSS (script/on*), command injection (&&, ;, bins), and common probes (.env, wp-login, .git, .well-known php). <div class="mono" style="margin-top:10px"> BLOCK ≥ <?=$BLOCK_SCORE?> | WARN ≥ <?=$WARN_SCORE?><br/> AI assist: <?=($ENABLE_AI && $OPENAI_PROXY !== '') ? h($OPENAI_PROXY) : 'disabled'?> </div> </div> </div> </section> <section id="tab-console" class="grid two" style="display:none"> <div class="card"> <h2>Live Console (NDJSON tail)</h2> <div class="row" style="margin-bottom:10px"> <div style="flex:1 1 240px"> <input id="tailLines" value="<?=$TAIL_LINES_DEFAULT?>" /> <div class="mut" style="margin-top:6px">Tail lines (max 1000). Auto-refresh 3s.</div> </div> <div class="row" style="flex:0 0 auto"> <button id="consolePause" class="btn">Pause</button> <button id="consoleClear" class="btn danger">Clear</button> </div> </div> <div id="consoleBox" class="console"></div> </div> <div class="card"> <h2>Clear Options</h2> <div class="mut" style="line-height:1.55"> Clear affects server files in: <div class="mono" style="margin-top:8px"> <?=h($CONSOLE_LOG)?><br/> <?=h($SCAN_SNAP_DIR)?>/*.json </div> <div style="margin-top:10px" class="row"> <select id="clearWhich" style="max-width:260px"> <option value="console">Console only</option> <option value="snapshots">Snapshots only</option> <option value="all">Console + snapshots</option> </select> <button id="consoleClear2" class="btn danger">Clear Selected</button> </div> </div> </div> </section> </main> <script> const $ = (id)=>document.getElementById(id); function pill(sev){ if(sev==='high'||sev==='slow'||sev==='block') return 'pill bad'; if(sev==='medium'||sev==='warn') return 'pill warn'; return 'pill good'; } function fmtSec(x){ return (Math.round(x*1000)/1000).toFixed(3)+'s'; } async function post(action, data){ const res = await fetch(`?action=${encodeURIComponent(action)}`, { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded;charset=UTF-8'}, body: new URLSearchParams(data) }); const j = await res.json().catch(()=>({ok:false,error:'bad_json'})); if(!res.ok) throw new Error(j.error || ('http_'+res.status)); return j; } async function get(action, params={}){ const qs = new URLSearchParams(params); const res = await fetch(`?action=${encodeURIComponent(action)}&${qs.toString()}`); const j = await res.json().catch(()=>({ok:false,error:'bad_json'})); if(!res.ok) throw new Error(j.error || ('http_'+res.status)); return j; } // Tabs document.querySelectorAll('.tab').forEach(t=>{ t.addEventListener('click', ()=>{ document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active')); t.classList.add('active'); const tab = t.getAttribute('data-tab'); ['speed','security','console'].forEach(k=>{ $('tab-'+k).style.display = (k===tab)?'grid':'none'; }); }); }); // Speed $('speedRun').addEventListener('click', async ()=>{ const url = $('speedUrl').value.trim(); $('speedRun').disabled = true; $('speedOut').innerHTML = `<div class="mut">Running…</div>`; try{ const j = await post('speed',{url}); const r = j.result; const grade = r.grade; $('speedOut').innerHTML = ` <div class="card"> <div class="row" style="justify-content:space-between"> <div><b>Status</b> <span class="${pill(grade)}">${grade.toUpperCase()}</span></div> <div class="mono">RID: ${r.rid}</div> </div> <div class="kv" style="margin-top:10px"> <div>Final URL</div><div class="mono">${r.final_url}</div> <div>HTTP</div><div class="mono">${r.status}</div> <div>TTFB</div><div class="mono">${fmtSec(r.timings.ttfb)}</div> <div>Total</div><div class="mono">${fmtSec(r.timings.total)}</div> <div>DNS</div><div class="mono">${fmtSec(r.timings.dns)}</div> <div>Connect</div><div class="mono">${fmtSec(r.timings.connect)}</div> <div>TLS</div><div class="mono">${fmtSec(r.timings.tls)}</div> <div>Redirect</div><div class="mono">${fmtSec(r.timings.redirect)}</div> </div> </div> <div class="card"> <b>Bytes</b> <div class="kv" style="margin-top:10px"> <div>Header</div><div class="mono">${r.bytes.header_size}</div> <div>Download</div><div class="mono">${r.bytes.download}</div> </div> <div class="mut" style="margin-top:10px"> If Total is slow but TTFB is good, focus on payload size (images/scripts). If TTFB is slow, focus on server/PHP/cache. </div> </div> `; }catch(e){ $('speedOut').innerHTML = `<div class="pill bad">ERROR</div><div class="mut" style="margin-top:10px">${String(e.message||e)}</div>`; }finally{ $('speedRun').disabled = false; } }); // Scan mode UI $('scanMode').addEventListener('change', ()=>{ $('scanCustomWrap').style.display = ($('scanMode').value==='custom')?'block':'none'; }); $('scanRun').addEventListener('click', async ()=>{ const mode = $('scanMode').value; const text = $('scanText').value || ''; $('scanRun').disabled = true; $('scanOut').innerHTML = `<div class="mut">Scanning…</div>`; try{ const j = await post('scan',{mode, text}); const rule = j.rule; const dec = j.decision; const sev = rule.severity; const hits = (rule.hits||[]).map(h=>`• ${h.name} [${h.category}] (+${h.weight})`).join('\n'); const cats = Object.entries(rule.categories||{}).map(([k,v])=>`${k}:${v}`).join(' '); let aiBlock = ''; if(j.ai && j.ai.used && j.ai.ok){ const r = j.ai.result || j.ai; aiBlock = ` <div class="card" style="margin-top:12px"> <b>AI Assist</b> <div class="mut" style="margin-top:8px">${(r.summary||'').toString().slice(0,800)}</div> <div class="mono" style="margin-top:10px">${(r.risk||'').toString().slice(0,240)}</div> </div> `; } $('scanOut').innerHTML = ` <div class="row" style="gap:10px; align-items:center"> <span class="${pill(sev)}">${sev.toUpperCase()}</span> <span class="${pill(dec)}">${dec.toUpperCase()}</span> <span class="pill">Score: ${rule.score}</span> <span class="pill mono">RID: ${j.rid}</span> <span class="pill mono">Snapshot: ${j.snapshot}</span> </div> <div class="card" style="margin-top:12px"> <b>Categories</b> <div class="mono" style="margin-top:10px">${cats || '(none)'}</div> </div> <div class="card" style="margin-top:12px"> <b>Hits</b> <pre class="mono" style="margin:10px 0 0; white-space:pre-wrap">${hits || '(none)'}</pre> </div> ${aiBlock} `; }catch(e){ $('scanOut').innerHTML = `<div class="pill bad">ERROR</div><div class="mut" style="margin-top:10px">${String(e.message||e)}</div>`; }finally{ $('scanRun').disabled = false; } }); // Console let consolePaused = false; let consoleTimer = null; function renderRows(rows){ const lines = rows.map(r=>{ const ts = r.ts || ''; const type = r.type || ''; const rid = r.rid || ''; const decision = r.decision ? ` decision=${r.decision}` : ''; const ok = (r.ok===false) ? ' ok=false' : ''; let extra = ''; if(type==='speed_check' && r.result){ extra = ` status=${r.result.status} ttfb=${r.result.timings?.ttfb} total=${r.result.timings?.total}`; } if(type==='security_scan' && r.rule){ extra = ` score=${r.rule.score} sev=${r.rule.severity}`; } return `[${ts}] ${type} rid=${rid}${decision}${ok}${extra}`; }); $('consoleBox').textContent = lines.join("\n") + (lines.length? "\n":""); $('consoleBox').scrollTop = $('consoleBox').scrollHeight; } async function refreshConsole(){ if(consolePaused) return; let n = parseInt($('tailLines').value||'200',10); if(!Number.isFinite(n) || n<1) n=200; if(n>1000) n=1000; try{ const j = await get('logs',{n}); renderRows(j.rows||[]); }catch(e){ $('consoleBox').textContent = `Console error: ${String(e.message||e)}`; } } function startConsole(){ if(consoleTimer) clearInterval(consoleTimer); consoleTimer = setInterval(refreshConsole, 3000); refreshConsole(); } startConsole(); $('consolePause').addEventListener('click', ()=>{ consolePaused = !consolePaused; $('consolePause').textContent = consolePaused ? 'Resume' : 'Pause'; if(!consolePaused) refreshConsole(); }); async function doClear(which){ try{ await post('clear',{which}); await refreshConsole(); }catch(e){ alert('Clear failed: ' + String(e.message||e)); } } $('consoleClear').addEventListener('click', ()=> doClear('console')); $('consoleClear2').addEventListener('click', ()=> doClear($('clearWhich').value)); // Auto-open correct tab if hash if(location.hash){ const tab = location.hash.replace('#',''); const el = document.querySelector(`.tab[data-tab="${tab}"]`); if(el) el.click(); } </script> </body> </html>
Save file
Quick jump
open a path
Open