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,279
Folders
408
Scanned Size
3.84 GB
PHP Files
891
Editable Text Files
12,540
File viewer
guarded to /htdocs
/simon/links.php
<?php /** * SIMON BUG Nebula File Scanner + Function Mapper * Single-file PHP 8.0+ admin scanner with AI reports and verified SFTP import. * * Drop-in path: /file-scanner.php * Default login: admin / admin * Override with SCANNER_USERNAME and SCANNER_PASSWORD_HASH. */ declare(strict_types=1); session_start(); /* * Optional pure-PHP SFTP support. * Install once in this folder with: * composer require phpseclib/phpseclib:^3.0 */ $autoloadCandidates = [ __DIR__ . '/vendor/autoload.php', dirname(__DIR__) . '/vendor/autoload.php', __DIR__ . '/phpseclib/vendor/autoload.php', ]; foreach ($autoloadCandidates as $autoloadFile) { if (is_file($autoloadFile)) { require_once $autoloadFile; break; } } const APP_NAME = 'SIMON BUG Nebula Scanner'; const APP_VERSION = '7.0.0'; const MAX_UPLOAD_BYTES = 120_000_000; const GA4_ID = 'G-S6MEHB4BEY'; const DEFAULT_EMAIL_TO = 'admin@gaylordsinclair.com'; $privateBase = dirname(__DIR__) . '/_simon_private_scanner'; if (!is_writable(dirname(__DIR__))) { $privateBase = __DIR__ . '/_scanner_private'; } define('DATA_DIR', $privateBase); define('UPLOAD_DIR', DATA_DIR . '/uploads'); define('REPORT_DIR', DATA_DIR . '/reports'); define('HISTORY_FILE', DATA_DIR . '/scan_history.json'); define('EVENT_FILE', DATA_DIR . '/events.ndjson'); define('SETTINGS_FILE', DATA_DIR . '/settings.json'); $defaultUsername = 'admin'; $defaultPasswordHash = '$2y$12$riNhtocD.FGYEaEOo6TI5eQJ6DbaF2j8z4glB7d2MfXQ.dxTIGvG.'; // admin $scannerUsername = getenv('SCANNER_USERNAME') ?: $defaultUsername; $scannerPasswordHash = getenv('SCANNER_PASSWORD_HASH') ?: $defaultPasswordHash; $emailTo = getenv('SCANNER_EMAIL_TO') ?: DEFAULT_EMAIL_TO; $keywordRules = [ 'Secrets' => [ '/\bpassword\b\s*[:=]/i' => 'Password assignment', '/\b(passwd|pwd)\b\s*[:=]/i' => 'Password shorthand', '/\b(api[_-]?key|secret|client[_-]?secret|access[_-]?token|bearer)\b/i' => 'API secret/token wording', '/-----BEGIN (RSA |DSA |EC |OPENSSH |)PRIVATE KEY-----/' => 'Private key block', '/AKIA[0-9A-Z]{16}/' => 'AWS access key pattern', '/ghp_[A-Za-z0-9_]{20,}/' => 'GitHub token pattern', '/sk-[A-Za-z0-9]{20,}/' => 'OpenAI-style key pattern', ], 'Malware functions' => [ '/\beval\s*\(/i' => 'eval execution', '/\b(base64_decode|gzinflate|str_rot13|assert)\s*\(/i' => 'Obfuscation/execution function', '/\b(shell_exec|exec|system|passthru|proc_open|popen)\s*\(/i' => 'Shell execution', '/\b(file_put_contents|fwrite)\s*\(.{0,120}\$_(POST|GET|REQUEST)/is' => 'Possible webshell writer', '/\bchmod\s*\(.{0,80}0777/is' => 'Dangerous chmod', '/\b(curl_exec|fsockopen|stream_socket_client)\s*\(/i' => 'Outbound network execution', ], 'JavaScript threats' => [ '/<script\b[^>]*>/i' => 'Script tag', '/<iframe\b[^>]*>/i' => 'Iframe tag', '/on(error|load|click|mouseover)\s*=/i' => 'Inline event handler', '/document\.cookie/i' => 'Cookie access', '/localStorage|sessionStorage/i' => 'Browser storage access', '/atob\s*\(|Function\s*\(|setTimeout\s*\(\s*["\']/i' => 'JS obfuscation/execution', ], 'SQL/database' => [ '/\bDROP\s+TABLE\b/i' => 'DROP TABLE', '/\bUNION\s+SELECT\b/i' => 'UNION SELECT', '/\bSELECT\s+\*\s+FROM\b/i' => 'SELECT * FROM', '/\b(INSERT|UPDATE|DELETE)\s+(INTO\s+)?(users|admin|accounts)\b/i' => 'Sensitive table mutation', '/\$_(GET|POST|REQUEST)\b.{0,160}\b(mysql_query|mysqli_query|PDO::query)\b/is' => 'Possible unsanitized SQL', ], 'Admin/root access' => [ '/\b(root|administrator|superuser|sudo)\b/i' => 'Admin/root wording', '/\b(admin_login|is_admin|role\s*[:=]\s*["\']admin)/i' => 'Admin role logic', '/\b\.htaccess\b|Require all granted|Options \+Indexes/i' => 'Server access rule', '/\b(chown|chgrp|usermod|iptables)\b/i' => 'System administration command', ], ]; $riskExtensions = [ 'php' => 28, 'phtml' => 40, 'phar' => 45, 'js' => 22, 'mjs' => 22, 'html' => 12, 'htm' => 12, 'svg' => 18, 'sh' => 45, 'bash' => 45, 'py' => 25, 'pl' => 35, 'cgi' => 40, 'exe' => 60, 'dll' => 55, 'so' => 50, 'dylib' => 50, 'zip' => 18, 'rar' => 18, '7z' => 18, 'tar' => 12, 'gz' => 12, 'sql' => 20, 'env' => 50, 'pem' => 55, 'key' => 55, ]; function h(?string $value): string { return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } function now_id(): string { return date('Ymd_His') . '_' . bin2hex(random_bytes(4)); } function human_bytes(int $bytes): string { $units = ['B', 'KB', 'MB', 'GB', 'TB']; $index = 0; $size = (float)$bytes; while ($size >= 1024 && $index < count($units) - 1) { $size /= 1024; $index++; } return ($index ? number_format($size, 2) : (string)$bytes) . ' ' . $units[$index]; } function ensure_dirs(): void { foreach ([DATA_DIR, UPLOAD_DIR, REPORT_DIR] as $dir) { if (!is_dir($dir)) { mkdir($dir, 0700, true); } } $privateHtaccess = __DIR__ . '/_scanner_private/.htaccess'; if (str_starts_with(DATA_DIR, __DIR__) && !file_exists($privateHtaccess)) { @file_put_contents($privateHtaccess, "Require all denied\nOptions -Indexes\n"); } } function read_json_file(string $path, array $fallback = []): array { if (!is_file($path)) { return $fallback; } $decoded = json_decode((string)file_get_contents($path), true); return is_array($decoded) ? $decoded : $fallback; } function write_json_file(string $path, array $data): void { file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), LOCK_EX); } function history(): array { return read_json_file(HISTORY_FILE, []); } function save_history(array $history): void { write_json_file(HISTORY_FILE, array_slice($history, 0, 300)); } function app_settings(): array { $defaults = [ 'ai_endpoint' => 'https://api.openai.com/v1/chat/completions', 'ai_model' => 'gpt-4o-mini', 'ai_enabled' => false, 'idle_seconds' => 180, 'email_to' => DEFAULT_EMAIL_TO, 'sftp_require_fingerprint' => true, 'sftp_host' => 'access-5018739336.webspace-host.com', 'sftp_port' => 22, 'sftp_user' => 'a3106912', 'sftp_fingerprint' => 'SHA256:1gx2w8Rtv3wCgi7Jh8myf/KVd72cRQbow03UP8P095Q', 'accent' => '#32ff73', 'accent_2' => '#54f1ff', 'accent_3' => '#805cff', 'background' => '#040606', 'panel' => '#111514', 'sound_enabled' => true, 'sound_volume' => 0.32, 'sound_theme' => 'bug', 'spatial_enabled' => true, 'ticker_seconds' => 82, ]; return array_replace($defaults, read_json_file(SETTINGS_FILE, [])); } function save_app_settings(array $settings): void { write_json_file(SETTINGS_FILE, $settings); @chmod(SETTINGS_FILE, 0600); } function update_history_record(string $id, callable $updater): ?array { $items = history(); $updated = null; foreach ($items as &$item) { if (($item['id'] ?? '') === $id) { $item = $updater($item); $updated = $item; break; } } unset($item); if ($updated !== null) { save_history($items); } return $updated; } function curl_json_request(string $url, array $headers, array $payload, int $timeout = 45): array { if (!function_exists('curl_init')) { throw new RuntimeException('PHP cURL extension is not enabled.'); } $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_SLASHES), CURLOPT_TIMEOUT => $timeout, CURLOPT_CONNECTTIMEOUT => 12, CURLOPT_FOLLOWLOCATION => false, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, ]); $body = curl_exec($ch); $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $error = curl_error($ch); curl_close($ch); if ($body === false || $error !== '') { throw new RuntimeException('AI connection failed: ' . $error); } $decoded = json_decode((string)$body, true); if (!is_array($decoded)) { throw new RuntimeException('AI returned invalid JSON.'); } if ($status < 200 || $status >= 300) { $message = $decoded['error']['message'] ?? ('HTTP ' . $status); throw new RuntimeException('AI request failed: ' . $message); } return $decoded; } function ai_call(array $settings, string $apiKey, string $prompt): string { $endpoint = trim((string)($settings['ai_endpoint'] ?? '')); $model = trim((string)($settings['ai_model'] ?? '')); if ($endpoint === '' || $model === '') { throw new RuntimeException('AI endpoint and model are required.'); } if ($apiKey === '') { throw new RuntimeException('AI API key is missing. Add it in Settings or set OPENAI_API_KEY.'); } $response = curl_json_request( $endpoint, [ 'Content-Type: application/json', 'Authorization: Bearer ' . $apiKey, ], [ 'model' => $model, 'messages' => [ [ 'role' => 'system', 'content' => 'You are SIMON BUG, a defensive source-code and file-analysis assistant. Explain findings, dependencies, security risks, false positives, and prioritized fixes. Do not provide exploit instructions.', ], ['role' => 'user', 'content' => $prompt], ], 'max_completion_tokens' => 900, ] ); $text = $response['choices'][0]['message']['content'] ?? ''; if (!is_string($text) || trim($text) === '') { throw new RuntimeException('AI returned an empty response.'); } return trim($text); } function local_ai_analysis(array $record, string $question, string $scope): string { $flags = $record['flags'] ?? []; $hits = $record['hits'] ?? []; $map = $record['source_map'] ?? []; $functions = $map['functions'] ?? []; $includes = $map['includes'] ?? []; $apis = $map['api_calls'] ?? []; $links = $map['links'] ?? []; $forms = $map['forms'] ?? []; $edges = $map['edges'] ?? []; $lines = []; $lines[] = 'SIMON Local Analysis'; $lines[] = 'File: ' . ($record['original_name'] ?? 'unknown'); $lines[] = 'Scope: ' . $scope; $lines[] = 'Question: ' . $question; $lines[] = ''; $lines[] = 'What was analyzed'; $lines[] = '- Risk score: ' . (int)($record['score'] ?? 0) . '/100 (' . ($record['severity'] ?? 'unknown') . ')'; $lines[] = '- Entropy: ' . ($record['entropy'] ?? 0); $lines[] = '- Flags: ' . count($flags); $lines[] = '- Rule findings: ' . count($hits); $lines[] = '- Functions: ' . count($functions); $lines[] = '- Includes: ' . count($includes); $lines[] = '- API calls: ' . count($apis); $lines[] = '- Forms: ' . count($forms); $lines[] = '- Links: ' . count($links); $lines[] = '- Dependency edges: ' . count($edges); $lines[] = ''; if ($flags) { $lines[] = 'Confirmed flags'; foreach (array_slice($flags, 0, 12) as $flag) { $lines[] = '- ' . $flag; } $lines[] = ''; } if ($hits) { $lines[] = 'Rule findings'; foreach (array_slice($hits, 0, 15) as $hit) { $lines[] = '- [' . ($hit['category'] ?? 'Finding') . '] ' . ($hit['label'] ?? 'Rule') . ' × ' . (int)($hit['count'] ?? 1); } $lines[] = ''; } if ($scope === 'dependencies' || $scope === 'full') { $lines[] = 'Dependency summary'; $lines[] = '- Functions detected: ' . count($functions); $lines[] = '- Includes detected: ' . count($includes); $lines[] = '- API targets detected: ' . count($apis); foreach (array_slice($includes, 0, 8) as $item) { $lines[] = ' • include: ' . ($item['path'] ?? 'unknown'); } foreach (array_slice($apis, 0, 8) as $item) { $lines[] = ' • API: ' . ($item['target'] ?? 'unknown'); } $lines[] = ''; } $lines[] = 'Prioritized next actions'; if (($record['score'] ?? 0) >= 50) { $lines[] = '1. Do not deploy this file until the critical and high-confidence findings are reviewed.'; } else { $lines[] = '1. Review every flagged pattern in context before treating it as malicious.'; } $lines[] = '2. Verify secrets are loaded from environment variables and are never committed in source.'; $lines[] = '3. Trace includes, API calls, and form inputs to confirm authentication, CSRF, and validation boundaries.'; $lines[] = '4. Re-scan after changes and compare the score, findings, and dependency graph.'; $lines[] = ''; $lines[] = 'Note: This response was generated by SIMON Local from scanner evidence. Connect an external AI provider for deeper natural-language reasoning and code-specific remediation.'; return implode("\n", $lines); } function append_ai_message(string $id, string $role, string $content, string $source = 'user'): ?array { return update_history_record($id, function (array $item) use ($role, $content, $source): array { $item['ai_chat'] = is_array($item['ai_chat'] ?? null) ? $item['ai_chat'] : []; $item['ai_chat'][] = [ 'role' => $role, 'content' => $content, 'source' => $source, 'at' => date('c'), ]; $item['ai_chat'] = array_slice($item['ai_chat'], -40); return $item; }); } function normalize_fingerprint(string $value): string { return strtolower(preg_replace('/[^a-f0-9]/i', '', $value) ?? ''); } function open_verified_sftp( string $host, int $port, string $username, string $password, string $expectedFingerprint ): array { if (!class_exists('\\phpseclib3\\Net\\SFTP')) { throw new RuntimeException( 'The SFTP browser requires phpseclib. Run: composer require phpseclib/phpseclib:^3.0' ); } if ($host === '' || $username === '' || $password === '' || $expectedFingerprint === '') { throw new RuntimeException('Host, username, password, and verified fingerprint are required.'); } $sftp = new \phpseclib3\Net\SFTP($host, $port, 18); $serverKey = $sftp->getServerPublicHostKey(); if (!is_string($serverKey) || $serverKey === '') { throw new RuntimeException('Could not retrieve the SFTP server host key.'); } $actualFingerprint = 'SHA256:' . rtrim(base64_encode(hash('sha256', $serverKey, true)), '='); if (!hash_equals(trim($expectedFingerprint), $actualFingerprint)) { throw new RuntimeException( 'Host fingerprint mismatch. Connection blocked. Presented: ' . $actualFingerprint ); } if (!$sftp->login($username, $password)) { throw new RuntimeException('SFTP authentication failed.'); } return [$sftp, $actualFingerprint]; } function normalize_remote_path(string $path): string { $path = trim(str_replace('\\', '/', $path)); if ($path === '') { return '.'; } $parts = []; foreach (explode('/', $path) as $part) { if ($part === '' || $part === '.') continue; if ($part === '..') { array_pop($parts); continue; } $parts[] = $part; } return '/' . implode('/', $parts); } function remote_parent_path(string $path): string { $path = normalize_remote_path($path); if ($path === '/' || $path === '.') return '/'; $parent = dirname($path); return $parent === '.' ? '/' : $parent; } function browse_sftp_directory(array $connection, string $path): array { [$sftp] = $connection; $path = normalize_remote_path($path); $list = $sftp->rawlist($path); if (!is_array($list)) { throw new RuntimeException('Could not read the remote directory.'); } $rows = []; foreach ($list as $name => $meta) { if ($name === '.' || $name === '..') continue; $isDir = (($meta['type'] ?? 0) === 2); $rows[] = [ 'name' => (string)$name, 'path' => rtrim($path, '/') . '/' . $name, 'is_dir' => $isDir, 'size' => (int)($meta['size'] ?? 0), 'modified' => isset($meta['mtime']) ? date('Y-m-d H:i:s', (int)$meta['mtime']) : '', 'permissions' => isset($meta['permissions']) ? decoct((int)$meta['permissions'] & 0777) : '', ]; } usort($rows, static function(array $a, array $b): int { if ($a['is_dir'] !== $b['is_dir']) { return $a['is_dir'] ? -1 : 1; } return strcasecmp($a['name'], $b['name']); }); return $rows; } function secure_sftp_download( string $host, int $port, string $username, string $password, string $expectedFingerprint, string $remotePath ): array { if ($host === '' || $username === '' || $password === '' || $remotePath === '') { throw new RuntimeException('Host, username, password, and remote file path are required.'); } if ($port < 1 || $port > 65535) { throw new RuntimeException('Invalid SFTP port.'); } $expectedFingerprint = trim($expectedFingerprint); if ($expectedFingerprint === '') { throw new RuntimeException('A verified SHA-256 host fingerprint is required.'); } $name = safe_name(basename($remotePath)); $destination = UPLOAD_DIR . '/' . now_id() . '_sftp_' . $name; /* * Preferred path on shared hosting: phpseclib v3. * It requires no custom PHP extension. */ if (class_exists('\\phpseclib3\\Net\\SFTP')) { $sftp = new \phpseclib3\Net\SFTP($host, $port, 18); $serverKey = $sftp->getServerPublicHostKey(); if (!is_string($serverKey) || $serverKey === '') { throw new RuntimeException('Could not retrieve the SFTP server host key.'); } $actualFingerprint = 'SHA256:' . rtrim(base64_encode(hash('sha256', $serverKey, true)), '='); if (!hash_equals($expectedFingerprint, $actualFingerprint)) { throw new RuntimeException( 'Host fingerprint mismatch. Connection blocked. Presented: ' . $actualFingerprint ); } if (!$sftp->login($username, $password)) { throw new RuntimeException('SFTP authentication failed.'); } $size = $sftp->filesize($remotePath); if (!is_int($size) || $size <= 0) { throw new RuntimeException('Remote file does not exist or is empty.'); } if ($size > MAX_UPLOAD_BYTES) { throw new RuntimeException('Remote file exceeds the scanner transfer limit.'); } if (!$sftp->get($remotePath, $destination)) { @unlink($destination); throw new RuntimeException('SFTP download failed.'); } if (!is_file($destination) || filesize($destination) !== $size) { @unlink($destination); throw new RuntimeException('Remote transfer was incomplete.'); } @chmod($destination, 0600); return [ 'path' => $destination, 'name' => $name, 'size' => $size, 'fingerprint' => $actualFingerprint, 'driver' => 'phpseclib', ]; } /* * Native SSH2 fallback when the extension happens to be available. */ if (function_exists('ssh2_connect')) { $connection = @ssh2_connect($host, $port, [ 'hostkey' => 'ssh-ed25519,ecdsa-sha2-nistp256,rsa-sha2-512,rsa-sha2-256,ssh-rsa', ]); if (!$connection) { throw new RuntimeException('Could not establish SSH connection.'); } $actualHex = (string)ssh2_fingerprint( $connection, SSH2_FINGERPRINT_SHA256 | SSH2_FINGERPRINT_HEX ); /* * Native ssh2 often returns hexadecimal, while the UI stores OpenSSH SHA256 base64. * Accept exact normalized hex only for the native path. */ if ( str_starts_with($expectedFingerprint, 'SHA256:') || !hash_equals(normalize_fingerprint($expectedFingerprint), normalize_fingerprint($actualHex)) ) { throw new RuntimeException( 'Native SSH2 presented a hexadecimal fingerprint. Install phpseclib for OpenSSH SHA256 verification, or enter the matching native hexadecimal fingerprint. Presented: ' . $actualHex ); } if (!@ssh2_auth_password($connection, $username, $password)) { throw new RuntimeException('SFTP authentication failed.'); } $sftp = @ssh2_sftp($connection); if (!$sftp) { throw new RuntimeException('Could not initialize SFTP subsystem.'); } $remoteUrl = 'ssh2.sftp://' . intval($sftp) . '/' . ltrim($remotePath, '/'); $stat = @stat($remoteUrl); if (!$stat || !isset($stat['size'])) { throw new RuntimeException('Remote file does not exist or is not readable.'); } $size = (int)$stat['size']; if ($size <= 0 || $size > MAX_UPLOAD_BYTES) { throw new RuntimeException('Remote file size is invalid or exceeds the transfer limit.'); } $input = @fopen($remoteUrl, 'rb'); $output = @fopen($destination, 'wb'); if (!$input || !$output) { if (is_resource($input)) fclose($input); if (is_resource($output)) fclose($output); @unlink($destination); throw new RuntimeException('Could not stream the remote file.'); } $copied = stream_copy_to_stream($input, $output, MAX_UPLOAD_BYTES + 1); fclose($input); fclose($output); if ($copied === false || $copied !== $size) { @unlink($destination); throw new RuntimeException('Remote transfer was incomplete.'); } @chmod($destination, 0600); return [ 'path' => $destination, 'name' => $name, 'size' => $size, 'fingerprint' => $actualHex, 'driver' => 'ssh2', ]; } throw new RuntimeException( 'SFTP support is not installed. Run "composer require phpseclib/phpseclib:^3.0" in this application folder. No PHP extension is required.' ); } function safe_name(string $name): string { $name = basename($name); $name = preg_replace('/[^A-Za-z0-9._-]+/', '_', $name) ?: 'file'; return substr($name, 0, 150); } function is_logged_in(): bool { return !empty($_SESSION['scanner_auth']); } function require_login(): void { if (!is_logged_in()) { header('Location: ?login=1'); exit; } } function csrf(): string { if (empty($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(16)); } return $_SESSION['csrf']; } function check_csrf(): void { if (($_POST['csrf'] ?? '') !== ($_SESSION['csrf'] ?? '')) { http_response_code(403); exit('Bad CSRF token'); } } function log_event(string $type, array $payload = []): void { $row = [ 'at' => date('c'), 'type' => $type, 'payload' => $payload, 'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown', 'ua' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 200), ]; @file_put_contents(EVENT_FILE, json_encode($row, JSON_UNESCAPED_SLASHES) . "\n", FILE_APPEND | LOCK_EX); } function mime_for(string $path): string { $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime = $finfo ? finfo_file($finfo, $path) : ''; if ($finfo) { finfo_close($finfo); } return $mime ?: 'application/octet-stream'; } function shannon_entropy(string $data): float { $length = strlen($data); if ($length === 0) { return 0.0; } $freq = count_chars($data, 1); $entropy = 0.0; foreach ($freq as $count) { $p = $count / $length; $entropy -= $p * log($p, 2); } return round($entropy, 3); } function can_read_text(string $mime, string $ext): bool { $textExtensions = [ 'php', 'phtml', 'js', 'mjs', 'css', 'html', 'htm', 'json', 'xml', 'svg', 'txt', 'md', 'csv', 'sql', 'env', 'ini', 'log', 'htaccess', 'yml', 'yaml', 'swift', 'java', 'py', 'rb', 'go', 'ts', 'tsx', 'jsx', 'vue', 'svelte', 'c', 'cpp', 'h', 'hpp', 'cs', 'rs', 'sh', 'bash', 'plist', 'config' ]; return str_starts_with($mime, 'text/') || in_array($ext, $textExtensions, true); } function find_record(string $id): ?array { foreach (history() as $record) { if (($record['id'] ?? '') === $id) { return $record; } } return null; } function controlled_file_path(string $id): ?string { $record = find_record($id); if (!$record) { return null; } $storedPath = $record['stored_path'] ?? ''; $real = realpath($storedPath); $base = realpath(UPLOAD_DIR); return ($real && $base && str_starts_with($real, $base)) ? $real : null; } function extract_source_map(string $source, string $ext, string $filename): array { $map = [ 'filename' => $filename, 'extension' => $ext, 'functions' => [], 'classes' => [], 'methods' => [], 'includes' => [], 'routes' => [], 'forms' => [], 'links' => [], 'assets' => [], 'api_calls' => [], 'globals' => [], 'events' => [], 'edges' => [], 'summary' => [], ]; if (!can_read_text('text/plain', $ext)) { $map['summary'] = ['Binary file: source graph unavailable']; return $map; } if (preg_match_all('/\bfunction\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/', $source, $matches, PREG_OFFSET_CAPTURE)) { foreach ($matches[1] as $match) { $line = substr_count(substr($source, 0, $match[1]), "\n") + 1; $map['functions'][] = ['name' => $match[0], 'line' => $line]; $map['edges'][] = ['from' => $filename, 'to' => $match[0], 'type' => 'defines function']; } } if (preg_match_all('/\bclass\s+([A-Za-z_][A-Za-z0-9_]*)/i', $source, $matches, PREG_OFFSET_CAPTURE)) { foreach ($matches[1] as $match) { $line = substr_count(substr($source, 0, $match[1]), "\n") + 1; $map['classes'][] = ['name' => $match[0], 'line' => $line]; $map['edges'][] = ['from' => $filename, 'to' => $match[0], 'type' => 'defines class']; } } if (preg_match_all('/\b(public|private|protected)\s+function\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/i', $source, $matches, PREG_OFFSET_CAPTURE)) { foreach ($matches[2] as $match) { $line = substr_count(substr($source, 0, $match[1]), "\n") + 1; $map['methods'][] = ['name' => $match[0], 'line' => $line]; } } if (preg_match_all('/\b(include|include_once|require|require_once)\s*\(?\s*["\']([^"\']+)["\']/i', $source, $matches, PREG_OFFSET_CAPTURE)) { foreach ($matches[2] as $index => $match) { $line = substr_count(substr($source, 0, $match[1]), "\n") + 1; $map['includes'][] = ['path' => $match[0], 'line' => $line, 'mode' => $matches[1][$index][0]]; $map['edges'][] = ['from' => $filename, 'to' => $match[0], 'type' => 'requires/includes']; } } if (preg_match_all('/\b(action|href|src)\s*=\s*["\']([^"\']+)["\']/i', $source, $matches, PREG_OFFSET_CAPTURE)) { foreach ($matches[2] as $index => $match) { $value = html_entity_decode($match[0], ENT_QUOTES | ENT_HTML5, 'UTF-8'); $line = substr_count(substr($source, 0, $match[1]), "\n") + 1; $kind = strtolower($matches[1][$index][0]); if ($kind === 'href') { $map['links'][] = ['url' => $value, 'line' => $line]; } elseif ($kind === 'src') { $map['assets'][] = ['url' => $value, 'line' => $line]; } else { $map['forms'][] = ['action' => $value, 'line' => $line]; } $map['edges'][] = ['from' => $filename, 'to' => $value, 'type' => $kind]; } } if (preg_match_all('/\bfetch\s*\(\s*["\']([^"\']+)["\']|\$\.ajax\s*\(|axios\.(get|post|put|delete)\s*\(\s*["\']([^"\']+)/i', $source, $matches, PREG_OFFSET_CAPTURE)) { foreach ($matches[0] as $index => $full) { $line = substr_count(substr($source, 0, $full[1]), "\n") + 1; $target = $matches[1][$index][0] ?? $matches[3][$index][0] ?? 'dynamic ajax call'; $map['api_calls'][] = ['target' => $target, 'line' => $line]; $map['edges'][] = ['from' => $filename, 'to' => $target, 'type' => 'api/fetch']; } } if (preg_match_all('/\$_(GET|POST|REQUEST|SESSION|COOKIE|FILES|SERVER)\s*\[\s*["\']([^"\']+)["\']\s*\]/', $source, $matches, PREG_OFFSET_CAPTURE)) { foreach ($matches[2] as $index => $match) { $line = substr_count(substr($source, 0, $match[1]), "\n") + 1; $map['globals'][] = ['scope' => $matches[1][$index][0], 'key' => $match[0], 'line' => $line]; } } if (preg_match_all('/addEventListener\s*\(\s*["\']([^"\']+)["\']|on(click|change|submit|load|error)\s*=/i', $source, $matches, PREG_OFFSET_CAPTURE)) { foreach ($matches[0] as $index => $full) { $line = substr_count(substr($source, 0, $full[1]), "\n") + 1; $eventName = $matches[1][$index][0] ?? $matches[2][$index][0] ?? 'event'; $map['events'][] = ['event' => $eventName, 'line' => $line]; } } $map['summary'] = [ count($map['functions']) . ' functions', count($map['classes']) . ' classes', count($map['includes']) . ' includes', count($map['forms']) . ' forms', count($map['links']) . ' links', count($map['api_calls']) . ' API calls', count($map['globals']) . ' superglobal references', ]; return $map; } function scan_file(string $path, string $original, array $rules, array $riskExtensions): array { $size = filesize($path) ?: 0; $ext = strtolower(pathinfo($original, PATHINFO_EXTENSION)); $mime = mime_for($path); $hash = hash_file('sha256', $path); $sample = (string)file_get_contents($path, false, null, 0, min($size, 2_500_000)); $textReadable = can_read_text($mime, $ext); $entropy = shannon_entropy(substr($sample, 0, 500000)); $categories = []; $hits = []; $score = 0; foreach ($rules as $category => $patterns) { $categories[$category] = 0; foreach ($patterns as $pattern => $label) { $count = @preg_match_all($pattern, $sample, $matchSet); if ($count && $count > 0) { $categories[$category] += $count; $weight = match ($category) { 'Secrets' => 18, 'Malware functions' => 20, 'JavaScript threats' => 10, 'SQL/database' => 12, 'Admin/root access' => 8, default => 5, }; $score += min(35, $count * $weight); $hits[] = [ 'category' => $category, 'label' => $label, 'count' => $count, 'pattern' => $pattern, ]; } } } $extensionRisk = $riskExtensions[$ext] ?? 0; $score += $extensionRisk; $flags = []; if ($extensionRisk >= 40) { $flags[] = 'High-risk executable/server extension'; } elseif ($extensionRisk > 0) { $flags[] = 'Extension deserves review'; } if ($entropy >= 7.2 && $size > 1000) { $score += 18; $flags[] = 'High entropy / possible packing or obfuscation'; } if (preg_match('/eval\s*\(\s*(base64_decode|gzinflate|str_rot13)/i', $sample)) { $score += 30; $flags[] = 'Layered PHP obfuscation pattern'; } if (preg_match('/<iframe[^>]+style\s*=\s*["\'][^"\']*(display\s*:\s*none|visibility\s*:\s*hidden|width\s*:\s*0|height\s*:\s*0)/i', $sample)) { $score += 25; $flags[] = 'Hidden iframe injection pattern'; } $sourceMap = extract_source_map($sample, $ext, $original); $mapDensity = count($sourceMap['functions']) + count($sourceMap['includes']) + count($sourceMap['api_calls']) + count($sourceMap['globals']); $categories['SIMON Bug Map'] = $mapDensity; if (count($sourceMap['includes']) > 8) { $flags[] = 'Many includes/requirements: inspect dependency chain'; $score += 7; } if (count($sourceMap['globals']) > 20) { $flags[] = 'Heavy superglobal usage: inspect request/input handling'; $score += 8; } $score = max(0, min(100, $score)); $severity = $score >= 75 ? 'critical' : ($score >= 50 ? 'high' : ($score >= 25 ? 'medium' : 'low')); return [ 'id' => now_id(), 'original_name' => $original, 'stored_path' => $path, 'size' => $size, 'size_human' => human_bytes($size), 'extension' => $ext ?: 'none', 'mime' => $mime, 'sha256' => $hash, 'entropy' => $entropy, 'score' => $score, 'severity' => $severity, 'categories' => $categories, 'hits' => $hits, 'flags' => $flags, 'source_map' => $sourceMap, 'text_preview' => $textReadable ? substr($sample, 0, 24000) : '', 'created_at' => date('c'), ]; } function make_html_report(array $record): string { $hits = ''; foreach (($record['hits'] ?? []) as $hit) { $hits .= '<tr><td>' . h($hit['category']) . '</td><td>' . h($hit['label']) . '</td><td>' . (int)$hit['count'] . '</td></tr>'; } $flags = ''; foreach (($record['flags'] ?? []) as $flag) { $flags .= '<li>' . h($flag) . '</li>'; } $mapRows = ''; foreach (($record['source_map']['edges'] ?? []) as $edge) { $mapRows .= '<tr><td>' . h($edge['from']) . '</td><td>' . h($edge['type']) . '</td><td>' . h($edge['to']) . '</td></tr>'; } return '<!doctype html><html><head><meta charset="utf-8"><title>SIMON Scan Report</title><style>body{font-family:Arial;background:#070912;color:#eef;padding:30px}.card{background:#11172b;border:1px solid #32406c;border-radius:18px;padding:20px;margin:14px 0}table{width:100%;border-collapse:collapse}td,th{border-bottom:1px solid #2b365d;padding:9px;text-align:left}.sev{text-transform:uppercase;font-weight:800} .modal{position:fixed;inset:0;z-index:100;display:none;place-items:center;padding:24px;background:rgba(0,0,0,.72);backdrop-filter:blur(16px)} .modal.open{display:grid} .modalCard{width:min(920px,96vw);max-height:90vh;overflow:auto;border-radius:26px;padding:22px;background:linear-gradient(180deg,#141b19,#080d0c);border:1px solid rgba(84,241,255,.22);box-shadow:0 40px 140px #000,0 0 80px rgba(84,241,255,.12)} .modalHead{display:flex;justify-content:space-between;align-items:center;gap:12px;position:sticky;top:-22px;background:#111816;padding:14px 0;z-index:2} .closeModal{font-size:20px;min-width:44px} .tabPanel{display:none} .tabPanel.active{display:block} .reportGrid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px} .reportBox{padding:14px;border:1px solid var(--line2);border-radius:18px;background:rgba(255,255,255,.035)} .aiResult{white-space:pre-wrap;line-height:1.6;padding:16px;border-radius:18px;background:#07100d;border:1px solid rgba(50,255,115,.18);max-height:420px;overflow:auto} .formGrid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px} .formGrid .full{grid-column:1/-1} .statusDot{width:10px;height:10px;border-radius:50%;display:inline-block;background:var(--red);box-shadow:0 0 16px currentColor} .statusDot.on{background:var(--green)} @media(max-width:760px){.formGrid,.reportGrid{grid-template-columns:1fr}.formGrid .full{grid-column:auto}} </style></head><body><h1>SIMON Scan Report</h1><div class="card"><b>File:</b> ' . h($record['original_name'] ?? '') . '<br><b>Score:</b> ' . (int)($record['score'] ?? 0) . '/100<br><b>Severity:</b> <span class="sev">' . h($record['severity'] ?? '') . '</span><br><b>SHA256:</b> ' . h($record['sha256'] ?? '') . '<br><b>Created:</b> ' . h($record['created_at'] ?? '') . '</div><div class="card"><h2>Flags</h2><ul>' . $flags . '</ul></div><div class="card"><h2>Hits</h2><table><tr><th>Category</th><th>Rule</th><th>Count</th></tr>' . $hits . '</table></div><div class="card"><h2>Function + Link Map</h2><table><tr><th>From</th><th>Type</th><th>To</th></tr>' . $mapRows . '</table></div></body></html>'; } ensure_dirs(); $settings = app_settings(); $action = $_GET['action'] ?? $_POST['action'] ?? ''; if (isset($_GET['logout'])) { session_destroy(); header('Location: ?login=1'); exit; } if (isset($_POST['do_login'])) { $submittedUsername = trim((string)($_POST['username'] ?? '')); $submittedPassword = (string)($_POST['password'] ?? ''); if (hash_equals($scannerUsername, $submittedUsername) && password_verify($submittedPassword, $scannerPasswordHash)) { $_SESSION['scanner_auth'] = true; log_event('login_success'); header('Location: ?'); exit; } $loginError = 'Invalid username or password.'; log_event('login_failed'); } if ($action === 'preview') { require_login(); $id = (string)($_GET['id'] ?? ''); $path = controlled_file_path($id); if (!$path) { http_response_code(404); exit('Not found'); } $mime = mime_for($path); header('Content-Type: ' . $mime); header('Content-Length: ' . filesize($path)); header('X-Content-Type-Options: nosniff'); log_event('preview_opened', ['id' => $id]); readfile($path); exit; } if ($action === 'report') { require_login(); $id = (string)($_GET['id'] ?? ''); $kind = (string)($_GET['kind'] ?? 'json'); $record = find_record($id); if (!$record) { http_response_code(404); exit('Report not found'); } if ($kind === 'html') { header('Content-Type:text/html; charset=utf-8'); echo make_html_report($record); } else { header('Content-Type:application/json'); header('Content-Disposition: attachment; filename="scan-' . $id . '.json"'); echo json_encode($record, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); } exit; } if (!is_logged_in() || isset($_GET['login'])): ?><!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title><?= h(APP_NAME) ?> Login</title> <style> body{margin:0;min-height:100vh;display:grid;place-items:center;background:radial-gradient(circle at 20% 10%,#341a82,transparent 36%),radial-gradient(circle at 82% 28%,#0c8a4b,transparent 30%),#050711;color:#eef;font-family:system-ui,-apple-system,Segoe UI,Roboto}.box{width:min(460px,92vw);padding:32px;border-radius:30px;background:rgba(8,12,28,.82);border:1px solid rgba(255,255,255,.18);box-shadow:0 30px 100px #000}h1{margin:0 0 8px}.muted{color:#aab6da;line-height:1.6}input,button{width:100%;box-sizing:border-box;border-radius:16px;border:1px solid rgba(255,255,255,.18);padding:14px;margin-top:12px;background:#0d1328;color:#fff}button{background:linear-gradient(135deg,#19e86f,#00d5ff,#7b5cff);font-weight:900;cursor:pointer}.passwordWrap{position:relative}.passwordWrap input{padding-right:88px}.showPass{position:absolute;right:8px;top:19px;width:auto;margin:0;padding:8px 12px;background:rgba(255,255,255,.08);font-size:11px}.err{color:#ff8c8c}.brand{letter-spacing:.12em;font-weight:1000;color:#7dffad} </style> </head> <body> <form class="box" method="post"> <div class="brand">SIMON BUG COMMAND</div> <h1>Nebula Scanner Login</h1> <p class="muted">Private admin access required. Default login is <b>admin</b> / <b>admin</b>. Override it with <code>SCANNER_USERNAME</code> and <code>SCANNER_PASSWORD_HASH</code>.</p> <?php if (!empty($loginError)): ?><p class="err"><?= h($loginError) ?></p><?php endif; ?> <input type="text" name="username" value="admin" placeholder="Username" autocomplete="username" autofocus required> <div class="passwordWrap"><input id="loginPassword" type="password" name="password" value="admin" placeholder="Password" autocomplete="current-password" required><button class="showPass" type="button" id="showPass" aria-label="Show or hide password">SHOW</button></div> <button name="do_login" value="1">Enter Scanner</button> </form> <script>const p=document.getElementById('loginPassword'),b=document.getElementById('showPass');b.addEventListener('click',()=>{const show=p.type==='password';p.type=show?'text':'password';b.textContent=show?'HIDE':'SHOW'});</script> </body> </html><?php exit; endif; $notice = ''; $selected = null; $remoteEntries = []; $remotePath = (string)($_SESSION['sftp_path'] ?? '/'); $sftpBrowserOpen = false; if ($_SERVER['REQUEST_METHOD'] === 'POST') { check_csrf(); if ($action === 'upload' && isset($_FILES['scan_file'])) { $file = $_FILES['scan_file']; if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { $notice = 'Upload failed.'; } elseif (($file['size'] ?? 0) > MAX_UPLOAD_BYTES) { $notice = 'File exceeds ' . human_bytes(MAX_UPLOAD_BYTES) . '.'; } else { $name = safe_name((string)$file['name']); $id = now_id(); $destination = UPLOAD_DIR . '/' . $id . '_' . $name; log_event('upload_started', ['name' => $name, 'size' => (int)$file['size']]); if (move_uploaded_file((string)$file['tmp_name'], $destination)) { @chmod($destination, 0600); $record = scan_file($destination, $name, $keywordRules, $riskExtensions); $history = history(); array_unshift($history, $record); save_history($history); $selected = $record; $notice = 'Scan completed: ' . $name; log_event('scan_completed', ['id' => $record['id'], 'score' => $record['score'], 'severity' => $record['severity']]); if (($record['score'] ?? 0) >= 50) { log_event('flagged_file', ['id' => $record['id'], 'score' => $record['score'], 'name' => $name]); } } else { $notice = 'Could not move upload.'; } } } if ($action === 'delete') { $id = (string)($_POST['id'] ?? ''); $history = history(); $newHistory = []; foreach ($history as $record) { if (($record['id'] ?? '') === $id) { $path = controlled_file_path($id); if ($path) { @unlink($path); } log_event('file_deleted', ['id' => $id]); } else { $newHistory[] = $record; } } save_history($newHistory); $notice = 'Deleted selected file/report from history.'; } if ($action === 'clear') { foreach (history() as $record) { $path = controlled_file_path((string)($record['id'] ?? '')); if ($path) { @unlink($path); } } save_history([]); $notice = 'History and uploaded files cleared.'; log_event('history_cleared'); } if ($action === 'email_report') { $id = (string)($_POST['id'] ?? ''); $record = find_record($id); if ($record) { $ok = @mail( ($settings['email_to'] ?? $GLOBALS['emailTo']), 'SIMON Scan Report: ' . ($record['original_name'] ?? 'file'), strip_tags(make_html_report($record)), 'Content-Type: text/plain; charset=UTF-8' ); $notice = $ok ? 'Report emailed to ' . ($settings['email_to'] ?? $GLOBALS['emailTo']) : 'Server mail() failed. Download report instead.'; log_event('report_email', ['id' => $id, 'ok' => $ok]); } } if ($action === 'save_settings') { $settings['ai_endpoint'] = trim((string)($_POST['ai_endpoint'] ?? $settings['ai_endpoint'])); $settings['ai_model'] = trim((string)($_POST['ai_model'] ?? $settings['ai_model'])); $settings['ai_enabled'] = isset($_POST['ai_enabled']); $settings['idle_seconds'] = max(60, min(3600, (int)($_POST['idle_seconds'] ?? 180))); $settings['email_to'] = filter_var((string)($_POST['email_to'] ?? DEFAULT_EMAIL_TO), FILTER_VALIDATE_EMAIL) ? (string)$_POST['email_to'] : DEFAULT_EMAIL_TO; $settings['sftp_require_fingerprint'] = true; $settings['sftp_host'] = trim((string)($_POST['sftp_host'] ?? $settings['sftp_host'])); $settings['sftp_port'] = max(1, min(65535, (int)($_POST['sftp_port'] ?? 22))); $settings['sftp_user'] = trim((string)($_POST['sftp_user'] ?? $settings['sftp_user'])); $settings['sftp_fingerprint'] = trim((string)($_POST['sftp_fingerprint'] ?? $settings['sftp_fingerprint'])); $settings['accent'] = preg_match('/^#[0-9a-f]{6}$/i', (string)($_POST['accent'] ?? '')) ? (string)$_POST['accent'] : $settings['accent']; $settings['accent_2'] = preg_match('/^#[0-9a-f]{6}$/i', (string)($_POST['accent_2'] ?? '')) ? (string)$_POST['accent_2'] : $settings['accent_2']; $settings['accent_3'] = preg_match('/^#[0-9a-f]{6}$/i', (string)($_POST['accent_3'] ?? '')) ? (string)$_POST['accent_3'] : $settings['accent_3']; $settings['background'] = preg_match('/^#[0-9a-f]{6}$/i', (string)($_POST['background'] ?? '')) ? (string)$_POST['background'] : $settings['background']; $settings['panel'] = preg_match('/^#[0-9a-f]{6}$/i', (string)($_POST['panel'] ?? '')) ? (string)$_POST['panel'] : $settings['panel']; $settings['sound_enabled'] = isset($_POST['sound_enabled']); $settings['sound_volume'] = max(0, min(1, (float)($_POST['sound_volume'] ?? 0.32))); $settings['sound_theme'] = in_array((string)($_POST['sound_theme'] ?? 'bug'), ['bug','scanner','arcade','silent'], true) ? (string)$_POST['sound_theme'] : 'bug'; $settings['spatial_enabled'] = isset($_POST['spatial_enabled']); $settings['ticker_seconds'] = max(30, min(240, (int)($_POST['ticker_seconds'] ?? 82))); save_app_settings($settings); $sessionKey = trim((string)($_POST['ai_api_key'] ?? '')); if ($sessionKey !== '') { $_SESSION['scanner_ai_key'] = $sessionKey; } $notice = 'Settings saved. API keys are kept in this login session only.'; log_event('settings_saved', ['ai_model' => $settings['ai_model']]); } if ($action === 'ai_test') { try { $apiKey = (string)($_SESSION['scanner_ai_key'] ?? getenv('OPENAI_API_KEY') ?: ''); $reply = ai_call($settings, $apiKey, 'Reply exactly: SIMON BUG AI connection verified.'); $notice = 'AI connected: ' . substr($reply, 0, 180); log_event('ai_test_success', ['model' => $settings['ai_model']]); } catch (Throwable $e) { $notice = $e->getMessage(); log_event('ai_test_failed', ['error' => $e->getMessage()]); } } if ($action === 'ai_analyze') { $id = (string)($_POST['id'] ?? ''); $record = find_record($id); if (!$record) { $notice = 'Select a scan before asking SIMON.'; } else { $question = trim((string)($_POST['ai_question'] ?? '')); $scope = (string)($_POST['ai_scope'] ?? 'full'); $allowedScopes = ['security', 'dependencies', 'quality', 'full']; if (!in_array($scope, $allowedScopes, true)) { $scope = 'full'; } if ($question === '') { $question = 'Explain the most important risks, dependencies, and recommended fixes.'; } append_ai_message($id, 'user', $question, 'user'); $record = find_record($id) ?? $record; $payload = [ 'analysis_scope' => $scope, 'user_question' => $question, 'file' => $record['original_name'] ?? '', 'mime' => $record['mime'] ?? '', 'size' => $record['size_human'] ?? '', 'score' => $record['score'] ?? 0, 'severity' => $record['severity'] ?? '', 'entropy' => $record['entropy'] ?? 0, 'flags' => $record['flags'] ?? [], 'rule_hits' => $record['hits'] ?? [], 'source_summary' => $record['source_map']['summary'] ?? [], 'functions' => array_slice($record['source_map']['functions'] ?? [], 0, 120), 'includes' => array_slice($record['source_map']['includes'] ?? [], 0, 120), 'api_calls' => array_slice($record['source_map']['api_calls'] ?? [], 0, 120), 'forms' => array_slice($record['source_map']['forms'] ?? [], 0, 80), 'links' => array_slice($record['source_map']['links'] ?? [], 0, 120), 'edges' => array_slice($record['source_map']['edges'] ?? [], 0, 160), 'code_excerpt' => substr((string)($record['text_preview'] ?? ''), 0, 12000), 'recent_conversation' => array_slice($record['ai_chat'] ?? [], -8), ]; $apiKey = (string)($_SESSION['scanner_ai_key'] ?? getenv('OPENAI_API_KEY') ?: ''); $providerUsed = 'SIMON Local'; $analysis = ''; if (!empty($settings['ai_enabled']) && $apiKey !== '') { try { $prompt = "Answer the user's latest question about the selected file. " . "Use the scan evidence and recent conversation. State what was analyzed, separate confirmed findings from possible concerns, and provide prioritized fixes.\n\n" . json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); $analysis = ai_call($settings, $apiKey, $prompt); $providerUsed = (string)($settings['ai_model'] ?? 'Connected AI'); } catch (Throwable $e) { $analysis = local_ai_analysis($record, $question, $scope) . "\n\nExternal AI error: " . $e->getMessage(); $providerUsed = 'SIMON Local fallback'; log_event('ai_external_fallback', ['id' => $id, 'error' => $e->getMessage()]); } } else { $analysis = local_ai_analysis($record, $question, $scope); } append_ai_message($id, 'assistant', $analysis, $providerUsed); $selected = update_history_record($id, function (array $item) use ($analysis, $question, $scope, $payload, $providerUsed): array { $item['ai_analysis'] = $analysis; $item['ai_question'] = $question; $item['ai_scope'] = $scope; $item['ai_provider_used'] = $providerUsed; $item['ai_input_summary'] = [ 'flags' => count($payload['flags']), 'rule_hits' => count($payload['rule_hits']), 'functions' => count($payload['functions']), 'includes' => count($payload['includes']), 'api_calls' => count($payload['api_calls']), 'links' => count($payload['links']), 'edges' => count($payload['edges']), 'code_excerpt_characters' => strlen($payload['code_excerpt']), ]; $item['ai_analyzed_at'] = date('c'); return $item; }); $notice = 'SIMON answered using ' . $providerUsed . '.'; log_event('ai_chat_response', ['id' => $id, 'provider' => $providerUsed, 'scope' => $scope]); } } if ($action === 'ai_clear_chat') { $id = (string)($_POST['id'] ?? ''); $selected = update_history_record($id, function (array $item): array { $item['ai_chat'] = []; unset($item['ai_analysis'], $item['ai_question'], $item['ai_scope'], $item['ai_provider_used'], $item['ai_analyzed_at']); return $item; }); $notice = 'AI conversation cleared for the selected file.'; } if ($action === 'sftp_browse') { try { $host = trim((string)($_POST['sftp_host'] ?? $_SESSION['sftp_host'] ?? $settings['sftp_host'])); $port = (int)($_POST['sftp_port'] ?? $_SESSION['sftp_port'] ?? $settings['sftp_port']); $user = trim((string)($_POST['sftp_user'] ?? $_SESSION['sftp_user'] ?? $settings['sftp_user'])); $fingerprint = trim((string)($_POST['sftp_fingerprint'] ?? $_SESSION['sftp_fingerprint'] ?? $settings['sftp_fingerprint'])); $password = (string)($_POST['sftp_pass'] ?? $_SESSION['sftp_pass'] ?? ''); $remotePath = normalize_remote_path((string)($_POST['sftp_path'] ?? $_SESSION['sftp_path'] ?? '/')); if ($password !== '') { $_SESSION['sftp_pass'] = $password; } $_SESSION['sftp_host'] = $host; $_SESSION['sftp_port'] = $port; $_SESSION['sftp_user'] = $user; $_SESSION['sftp_fingerprint'] = $fingerprint; $_SESSION['sftp_path'] = $remotePath; $connection = open_verified_sftp($host, $port, $user, $password, $fingerprint); $remoteEntries = browse_sftp_directory($connection, $remotePath); $sftpBrowserOpen = true; $notice = 'SFTP directory loaded: ' . $remotePath; log_event('sftp_browse_success', ['path' => $remotePath, 'count' => count($remoteEntries)]); } catch (Throwable $e) { $notice = $e->getMessage(); $sftpBrowserOpen = true; log_event('sftp_browse_failed', ['error' => $e->getMessage()]); } } if ($action === 'sftp_import_selected') { try { $host = (string)($_SESSION['sftp_host'] ?? $settings['sftp_host']); $port = (int)($_SESSION['sftp_port'] ?? $settings['sftp_port']); $user = (string)($_SESSION['sftp_user'] ?? $settings['sftp_user']); $password = (string)($_SESSION['sftp_pass'] ?? ''); $fingerprint = (string)($_SESSION['sftp_fingerprint'] ?? $settings['sftp_fingerprint']); $remoteFile = normalize_remote_path((string)($_POST['remote_file'] ?? '')); $transfer = secure_sftp_download( $host, $port, $user, $password, $fingerprint, $remoteFile ); $record = scan_file($transfer['path'], $transfer['name'], $keywordRules, $riskExtensions); $record['source'] = 'sftp'; $record['remote_path'] = $remoteFile; $record['sftp_host_fingerprint'] = $transfer['fingerprint']; $items = history(); array_unshift($items, $record); save_history($items); $selected = $record; $notice = 'Remote file imported, scanned, and mapped: ' . $remoteFile; log_event('sftp_import_selected_success', ['id' => $record['id'], 'path' => $remoteFile]); } catch (Throwable $e) { $notice = $e->getMessage(); $sftpBrowserOpen = true; log_event('sftp_import_selected_failed', ['error' => $e->getMessage()]); } } if ($action === 'sftp_import') { log_event('sftp_connect_attempt', ['host' => $_POST['sftp_host'] ?? '']); try { $transfer = secure_sftp_download( trim((string)($_POST['sftp_host'] ?? '')), (int)($_POST['sftp_port'] ?? 22), trim((string)($_POST['sftp_user'] ?? '')), (string)($_POST['sftp_pass'] ?? ''), trim((string)($_POST['sftp_fingerprint'] ?? '')), trim((string)($_POST['sftp_path'] ?? '')) ); $record = scan_file($transfer['path'], $transfer['name'], $keywordRules, $riskExtensions); $record['source'] = 'sftp'; $record['sftp_host_fingerprint'] = $transfer['fingerprint']; $items = history(); array_unshift($items, $record); save_history($items); $selected = $record; $notice = 'SFTP file imported, scanned, and mapped using ' . ($transfer['driver'] ?? 'SFTP') . '.'; log_event('sftp_import_success', ['id' => $record['id'], 'name' => $record['original_name']]); } catch (Throwable $e) { $notice = $e->getMessage(); log_event('sftp_import_failed', ['error' => $e->getMessage()]); } } } $history = history(); if (!$selected && isset($_GET['id'])) { $selected = find_record((string)$_GET['id']); } if (!$selected && $history) { $selected = $history[0]; } $csrf = csrf(); $events = array_slice(array_reverse(file_exists(EVENT_FILE) ? file(EVENT_FILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) : []), 0, 10); $selectedMap = $selected['source_map'] ?? ['functions' => [], 'classes' => [], 'includes' => [], 'routes' => [], 'forms' => [], 'links' => [], 'assets' => [], 'api_calls' => [], 'globals' => [], 'events' => [], 'edges' => [], 'summary' => []]; $mapJson = json_encode($selectedMap, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); $searchIndex = []; foreach ($history as $record) { $base = [ 'file_id' => (string)($record['id'] ?? ''), 'file' => (string)($record['original_name'] ?? ''), 'score' => (int)($record['score'] ?? 0), 'severity' => (string)($record['severity'] ?? ''), ]; $searchIndex[] = $base + [ 'type' => 'file', 'label' => $base['file'], 'detail' => 'File · ' . $base['severity'] . ' · score ' . $base['score'], 'url' => '?id=' . rawurlencode($base['file_id']), ]; foreach (($record['hits'] ?? []) as $hit) { $searchIndex[] = $base + [ 'type' => 'threat', 'label' => (string)($hit['label'] ?? ''), 'detail' => (string)($hit['category'] ?? '') . ' · ' . $base['file'], 'url' => '?id=' . rawurlencode($base['file_id']) . '#reports', ]; } $map = $record['source_map'] ?? []; foreach (($map['functions'] ?? []) as $item) { $searchIndex[] = $base + [ 'type' => 'function', 'label' => (string)($item['name'] ?? ''), 'detail' => 'Function · ' . $base['file'], 'url' => '?id=' . rawurlencode($base['file_id']) . '#mapping', ]; } foreach (($map['includes'] ?? []) as $item) { $searchIndex[] = $base + [ 'type' => 'include', 'label' => (string)($item['path'] ?? ''), 'detail' => 'Include · ' . $base['file'], 'url' => '?id=' . rawurlencode($base['file_id']) . '#mapping', ]; } foreach (($map['api_calls'] ?? []) as $item) { $searchIndex[] = $base + [ 'type' => 'api', 'label' => (string)($item['target'] ?? ''), 'detail' => 'API call · ' . $base['file'], 'url' => '?id=' . rawurlencode($base['file_id']) . '#mapping', ]; } foreach (($map['links'] ?? []) as $item) { $searchIndex[] = $base + [ 'type' => 'link', 'label' => (string)($item['url'] ?? ''), 'detail' => 'Link · ' . $base['file'], 'url' => '?id=' . rawurlencode($base['file_id']) . '#mapping', ]; } foreach (($map['classes'] ?? []) as $item) { $searchIndex[] = $base + [ 'type' => 'class', 'label' => (string)($item['name'] ?? ''), 'detail' => 'Class · ' . $base['file'], 'url' => '?id=' . rawurlencode($base['file_id']) . '#mapping', ]; } foreach (($map['routes'] ?? []) as $item) { $searchIndex[] = $base + [ 'type' => 'route', 'label' => (string)($item['path'] ?? $item['route'] ?? ''), 'detail' => 'Route · ' . $base['file'], 'url' => '?id=' . rawurlencode($base['file_id']) . '#mapping', ]; } foreach (($map['assets'] ?? []) as $item) { $searchIndex[] = $base + [ 'type' => 'asset', 'label' => (string)($item['path'] ?? $item['url'] ?? ''), 'detail' => 'Asset · ' . $base['file'], 'url' => '?id=' . rawurlencode($base['file_id']) . '#mapping', ]; } $preview = preg_replace('/\s+/', ' ', strip_tags((string)($record['text_preview'] ?? ''))); if ($preview !== '') { $searchIndex[] = $base + [ 'type' => 'content', 'label' => $base['file'] . ' content', 'detail' => substr($preview, 0, 600), 'url' => '?id=' . rawurlencode($base['file_id']) . '#preview', ]; } } $searchJson = json_encode($searchIndex, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); ?><!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(APP_NAME) ?></title> <meta name="theme-color" content="#07120c"> <script async src="https://www.googletagmanager.com/gtag/js?id=<?= GA4_ID ?>"></script> <script> window.dataLayer=window.dataLayer||[]; function gtag(){dataLayer.push(arguments)} gtag('js',new Date()); gtag('config','<?= GA4_ID ?>'); </script> <style> :root{ --bg:<?= h((string)$settings['background']) ?>; --panel:<?= h((string)$settings['panel']) ?>; --panel2:#171d1b; --panel3:#0b1110;--line:rgba(148,255,179,.14);--line2:rgba(255,255,255,.08);--text:#eefcf2;--muted:#8fa79b;--green:<?= h((string)$settings['accent']) ?>;--green2:#0ec95d;--orange:#ff9d2e;--red:#ff4d5e;--blue:#5e8cff;--purple:<?= h((string)$settings['accent_3']) ?>;--cyan:<?= h((string)$settings['accent_2']) ?>;--radius:22px}*{box-sizing:border-box}html,body{height:100%}body{margin:0;background:#040606;color:var(--text);font-family:Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto;overflow:hidden}.nebula{position:fixed;inset:-15%;background:radial-gradient(circle at 18% 18%,rgba(50,255,115,.2),transparent 28%),radial-gradient(circle at 72% 16%,rgba(84,241,255,.15),transparent 24%),radial-gradient(circle at 74% 82%,rgba(128,92,255,.17),transparent 31%),radial-gradient(circle at 30% 88%,rgba(255,157,46,.11),transparent 28%),#040606;filter:saturate(1.22);animation:drift 18s ease-in-out infinite alternate}.nebula:after{content:"";position:absolute;inset:0;background-image:radial-gradient(circle,rgba(145,255,182,.8) 0 1px,transparent 1.5px);background-size:52px 52px;opacity:.12;animation:stars 90s linear infinite}@keyframes drift{to{transform:scale(1.08) rotate(1.6deg)}}@keyframes stars{to{background-position:900px 700px}}.spaceLayer{position:fixed;inset:-8%;pointer-events:none;perspective:1200px;transform-style:preserve-3d;overflow:hidden}.spaceLayer i{position:absolute;width:var(--s);height:var(--s);left:var(--x);top:var(--y);border-radius:50%;background:radial-gradient(circle at 35% 30%,rgba(255,255,255,.95),rgba(84,241,255,.4) 25%,rgba(128,92,255,.1) 58%,transparent 72%);box-shadow:0 0 36px rgba(84,241,255,.28);opacity:var(--o);transform:translateZ(var(--z));animation:spaceFloat var(--d) ease-in-out infinite alternate}@keyframes spaceFloat{to{transform:translate3d(var(--tx),var(--ty),var(--z)) scale(1.18);filter:hue-rotate(55deg)}}.app{position:fixed;inset:18px;display:grid;grid-template-columns:230px 1fr 310px;grid-template-rows:56px 1fr;gap:14px;perspective:1600px;transform-style:preserve-3d;transition:transform .18s ease-out}.shell{background:linear-gradient(180deg,rgba(18,23,22,.9),rgba(9,14,13,.88));border:1px solid var(--line);border-radius:28px;box-shadow:0 24px 90px rgba(0,0,0,.46),inset 0 0 38px rgba(50,255,115,.025);backdrop-filter:blur(20px);transform-style:preserve-3d;transition:transform .22s ease,box-shadow .22s ease}.shell:hover{transform:translateZ(18px);box-shadow:0 34px 110px rgba(0,0,0,.56),0 0 44px rgba(50,255,115,.08),inset 0 0 38px rgba(50,255,115,.04)}.top{grid-column:2/4;display:flex;align-items:center;padding:0 16px;gap:12px}.brand{grid-row:1/3;padding:18px;display:flex;flex-direction:column}.logo{display:flex;align-items:center;gap:10px;font-weight:1000;letter-spacing:.04em}.mark{width:28px;height:28px;border-radius:10px;background:radial-gradient(circle,#94ffad,#20da67 50%,#07120c);display:grid;place-items:center;color:#06110a;box-shadow:0 0 28px rgba(50,255,115,.55)}.nav{margin-top:24px;display:grid;gap:8px}.nav a,.mini{display:flex;align-items:center;gap:10px;text-decoration:none;color:var(--muted);padding:11px 12px;border-radius:14px;border:1px solid transparent}.nav a.active,.nav a:hover{background:rgba(50,255,115,.09);border-color:rgba(50,255,115,.2);color:#dfffe8}.asideFoot{margin-top:auto;border-top:1px solid var(--line2);padding-top:16px}.aiBox{padding:14px;border-radius:18px;background:rgba(50,255,115,.08);border:1px solid rgba(50,255,115,.18);box-shadow:0 0 30px rgba(50,255,115,.09)}.search{flex:1;display:flex;align-items:center;gap:10px;background:rgba(0,0,0,.22);border:1px solid var(--line2);border-radius:14px;padding:10px 12px;color:var(--muted)}.ticker{flex:1.4;overflow:hidden;white-space:nowrap;border:1px solid var(--line2);border-radius:999px;background:rgba(0,0,0,.19)}.ticker span{display:inline-block;padding:9px 0;animation:ticker <?= (int)$settings['ticker_seconds'] ?>s linear infinite}.ticker:hover span{animation-play-state:paused}.ticker b{color:var(--green);margin-left:42px}@keyframes ticker{from{transform:translateX(95%)}to{transform:translateX(-100%)}}.pill{border-radius:999px;padding:8px 12px;background:rgba(50,255,115,.1);border:1px solid rgba(50,255,115,.2);font-weight:800;color:#afffc6;text-decoration:none}.main{grid-column:2;grid-row:2;padding:18px;overflow:auto}.right{grid-column:3;grid-row:2;padding:16px;overflow:auto}.hero{display:grid;grid-template-columns:1.15fr .85fr;gap:14px}.panel{border:1px solid var(--line2);background:rgba(255,255,255,.035);border-radius:22px;padding:16px}.mapPanel{min-height:282px;background:radial-gradient(circle at 50% 50%,rgba(50,255,115,.08),transparent 55%),#07100d;position:relative;overflow:hidden}.mapCanvas{width:100%;height:255px;display:block}.metricGrid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}.metric{padding:14px;border-radius:16px;background:rgba(0,0,0,.22);border:1px solid var(--line2)}.metric strong{display:block;font-size:26px}.metric small{color:var(--muted)}.cards{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:14px 0}.card{min-height:94px;border:1px solid var(--line2);border-radius:18px;background:rgba(255,255,255,.045);padding:13px}.card b{display:block;font-size:22px}.card .spark{height:3px;background:linear-gradient(90deg,var(--green),var(--orange),var(--red));border-radius:999px;margin-top:14px}.preview{margin-top:14px;min-height:300px;display:grid;place-items:center;border:1px solid var(--line2);border-radius:22px;background:rgba(0,0,0,.24);overflow:hidden}.preview img,.preview video,.preview audio{max-width:100%;max-height:56vh}.code{white-space:pre-wrap;font:12px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace;color:#d7ffe1;background:#050816;border-radius:18px;padding:14px;max-height:56vh;overflow:auto;width:100%}.files{display:grid;gap:8px}.file{display:block;text-decoration:none;color:var(--text);padding:12px;border-radius:16px;border:1px solid transparent;background:rgba(255,255,255,.035)}.file.active,.file:hover{border-color:rgba(50,255,115,.35);background:rgba(50,255,115,.07)}.sev{display:inline-flex;border-radius:999px;padding:3px 8px;font-size:10px;text-transform:uppercase;font-weight:1000}.critical{background:rgba(255,77,94,.22);color:#ff9ca7}.high{background:rgba(255,157,46,.2);color:#ffd09a}.medium{background:rgba(255,211,122,.18);color:#ffe3a8}.low{background:rgba(50,255,115,.15);color:#adffc7}button,.btn,input,select{border:1px solid var(--line2);border-radius:14px;padding:10px 12px;background:rgba(255,255,255,.06);color:#fff}button,.btn{cursor:pointer;text-decoration:none;font-weight:900}.primary{background:linear-gradient(135deg,#32ff73,#54f1ff);color:#031008;border:0}.danger{background:rgba(255,77,94,.12);border-color:rgba(255,77,94,.35)}.stack{display:grid;gap:10px}.row{display:flex;gap:8px;flex-wrap:wrap;align-items:center}.bars{display:grid;gap:8px}.bar{display:grid;grid-template-columns:125px 1fr 38px;gap:8px;align-items:center}.track{height:10px;border-radius:999px;background:rgba(255,255,255,.08);overflow:hidden}.fill{height:100%;background:linear-gradient(90deg,var(--green),var(--cyan),var(--purple));border-radius:999px}.table{width:100%;border-collapse:collapse}.table th,.table td{border-bottom:1px solid var(--line2);padding:8px;text-align:left;font-size:12px}.notice{position:fixed;left:50%;top:86px;transform:translateX(-50%);z-index:10;padding:10px 16px;border-radius:999px;background:rgba(50,255,115,.14);border:1px solid rgba(50,255,115,.38)}.screensaver{position:fixed;inset:0;display:none;place-items:center;background:radial-gradient(circle at 50% 50%,rgba(50,255,115,.2),transparent 38%),rgba(0,0,0,.85);z-index:50}.screensaver.on{display:grid}.orb{width:220px;height:220px;border-radius:50%;background:radial-gradient(circle at 35% 30%,#fff,#32ff73 18%,#54f1ff 44%,#805cff 68%,#050711 100%);box-shadow:0 0 120px rgba(50,255,115,.65);animation:pulse 3s ease-in-out infinite alternate}@keyframes pulse{to{transform:scale(1.18) rotate(18deg);filter:hue-rotate(75deg)}}@media(max-width:1150px){body{overflow:auto}.app{position:relative;inset:auto;margin:10px;min-height:100vh;grid-template-columns:1fr;grid-template-rows:auto}.brand,.top,.main,.right{grid-column:1;grid-row:auto}.hero,.cards{grid-template-columns:1fr}.metricGrid{grid-template-columns:1fr}} .modal{position:fixed;inset:0;z-index:100;display:none;place-items:center;padding:24px;background:rgba(0,0,0,.72);backdrop-filter:blur(16px)} .modal.open{display:grid} .modalCard{width:min(920px,96vw);max-height:90vh;overflow:auto;border-radius:26px;padding:22px;background:linear-gradient(180deg,#141b19,#080d0c);border:1px solid rgba(84,241,255,.22);box-shadow:0 40px 140px #000,0 0 80px rgba(84,241,255,.12)} .modalHead{display:flex;justify-content:space-between;align-items:center;gap:12px;position:sticky;top:-22px;background:#111816;padding:14px 0;z-index:2} .closeModal{font-size:20px;min-width:44px} .reportGrid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px} .reportBox{padding:14px;border:1px solid var(--line2);border-radius:18px;background:rgba(255,255,255,.035)} .aiResult{white-space:pre-wrap;line-height:1.6;padding:16px;border-radius:18px;background:#07100d;border:1px solid rgba(50,255,115,.18);max-height:420px;overflow:auto} .formGrid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px} .formGrid .full{grid-column:1/-1} .statusDot{width:10px;height:10px;border-radius:50%;display:inline-block;background:var(--red);box-shadow:0 0 16px currentColor} .statusDot.on{background:var(--green)} @media(max-width:760px){.formGrid,.reportGrid{grid-template-columns:1fr}.formGrid .full{grid-column:auto}} .searchBox{position:relative;padding:0;background:transparent;border:0;overflow:visible} .searchBox input{width:100%;background:rgba(0,0,0,.22);padding:11px 14px} .searchResults{position:absolute;left:0;right:0;top:48px;z-index:90;max-height:430px;overflow:auto;background:#0b100f;border:1px solid var(--line);border-radius:18px;box-shadow:0 30px 90px #000;padding:8px} .searchResult{display:block;padding:11px;border-radius:12px;color:var(--text);text-decoration:none;border:1px solid transparent} .searchResult:hover{background:rgba(50,255,115,.08);border-color:rgba(50,255,115,.2)} .searchResult small{display:block;color:var(--muted);margin-top:3px} textarea{width:100%;border:1px solid var(--line2);border-radius:14px;padding:12px;background:rgba(255,255,255,.06);color:#fff;resize:vertical} .analysisScope{margin:10px 0;color:var(--muted)} .analysisScope summary{cursor:pointer;color:var(--cyan)} .remoteBrowser{margin-top:12px;border:1px solid var(--line2);border-radius:18px;padding:10px;background:rgba(0,0,0,.18)} .remoteList{display:grid;gap:7px;margin-top:10px;max-height:420px;overflow:auto} .remoteItem{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px;border-radius:13px;background:rgba(255,255,255,.035);border:1px solid var(--line2)} .remoteItem small{display:block;color:var(--muted);margin-top:3px} .chatThread{display:grid;gap:12px;max-height:520px;overflow:auto;padding:12px;border:1px solid var(--line2);border-radius:18px;background:rgba(0,0,0,.22);scroll-behavior:smooth} .chatMessage{max-width:88%;padding:13px 15px;border-radius:18px;line-height:1.55;border:1px solid var(--line2)} .chatMessage.user{justify-self:end;background:rgba(84,241,255,.10);border-color:rgba(84,241,255,.24)} .chatMessage.assistant{justify-self:start;background:rgba(50,255,115,.08);border-color:rgba(50,255,115,.20)} .chatMeta{font-size:11px;color:var(--muted);margin-bottom:7px;text-transform:uppercase;letter-spacing:.08em} .searchResults:not([hidden]){display:block!important} .searchBox:focus-within{z-index:120} .searchCount{font-size:11px;color:var(--muted);padding:7px 10px} </style> </head> <body> <div class="nebula"></div><div class="spaceLayer" aria-hidden="true"><i style="--s:180px;--x:5%;--y:12%;--o:.35;--z:120px;--tx:40px;--ty:30px;--d:13s"></i><i style="--s:120px;--x:78%;--y:8%;--o:.28;--z:80px;--tx:-35px;--ty:55px;--d:17s"></i><i style="--s:240px;--x:70%;--y:70%;--o:.22;--z:160px;--tx:-55px;--ty:-35px;--d:21s"></i><i style="--s:90px;--x:22%;--y:78%;--o:.3;--z:60px;--tx:45px;--ty:-45px;--d:15s"></i></div> <div class="screensaver" id="screensaver"><div><div class="orb"></div><h1>SIMON BUG Idle Mode</h1><p>Move mouse, touch, or press a key to resume.</p></div></div> <?php if ($notice): ?><div class="notice"><?= h($notice) ?></div><?php endif; ?> <div class="app"> <aside class="brand shell"> <div class="logo"><div class="mark">⌘</div><div>SIMON BUG<br><small style="color:var(--muted)">File Intelligence</small></div></div> <nav class="nav"> <a class="active" href="#overview">Overview</a> <a href="#scanner">Scanner</a> <a href="#mapping">File Map</a> <a href="#preview">Preview</a> <a href="#" data-open-modal="reportsModal">Reports</a> <a href="#" data-open-modal="settingsModal">Settings</a> </nav> <div class="asideFoot"> <div class="aiBox"> <b><span class="statusDot <?= !empty($settings['ai_enabled']) ? 'on' : '' ?>"></span> AI Command Center</b> <p style="color:var(--muted);font-size:12px">Connect an OpenAI-compatible endpoint, test it, then analyze the selected scan.</p> <div class="row"> <button type="button" class="btn" data-open-modal="settingsModal">AI Settings</button> <?php if ($selected): ?> <form method="post" action="?action=ai_analyze"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <input type="hidden" name="id" value="<?= h($selected['id']) ?>"> <button class="primary">Analyze</button> </form> <?php endif; ?> </div> </div> <a class="mini" href="?logout=1">Logout</a> </div> </aside> <header class="top shell"> <div class="search searchBox"> <input id="globalSearch" type="search" placeholder="Search files, threats, functions, includes, APIs, links..." autocomplete="off"> <div id="searchResults" class="searchResults" hidden></div> </div> <div class="ticker"><span><b>LIVE</b> Scanner online. <b>AI</b> Connection controls available in Settings. <b>SFTP</b> Host fingerprint verification enforced. <?php foreach ($events as $event) echo '<b>EVENT</b> ' . h(substr($event, 0, 120)) . ' '; ?></span></div> <span class="pill"><?= class_exists('\\phpseclib3\\Net\\SFTP') ? 'SFTP READY' : (function_exists('ssh2_connect') ? 'SSH2 READY' : 'INSTALL PHPSECLIB') ?></span> <span class="pill"><?= count($history) ?> scans</span> </header> <main class="main shell" id="overview"> <?php if ($selected): $mime = $selected['mime']; $isImage = str_starts_with($mime, 'image/'); $isVideo = str_starts_with($mime, 'video/'); $isAudio = str_starts_with($mime, 'audio/'); $sourceMap = $selected['source_map'] ?? []; ?> <div class="row" style="justify-content:space-between"> <div> <h1 style="margin:.1rem 0"><?= h($selected['original_name']) ?></h1> <div style="color:var(--muted)"><?= h($selected['mime']) ?> · <?= h($selected['size_human']) ?> · <?= h($selected['created_at']) ?></div> </div> <span class="sev <?= h($selected['severity']) ?>"><?= h($selected['severity']) ?> · <?= h((string)$selected['score']) ?>/100</span> </div> <section class="hero" id="mapping"> <div class="panel mapPanel"> <div class="row" style="justify-content:space-between"><b>SIMON BUG Function Map</b><span class="pill">LIVE GRAPH</span></div> <canvas class="mapCanvas" id="mapCanvas"></canvas> </div> <div class="metricGrid"> <div class="metric"><small>Functions</small><strong><?= count($sourceMap['functions'] ?? []) ?></strong></div> <div class="metric"><small>Includes</small><strong><?= count($sourceMap['includes'] ?? []) ?></strong></div> <div class="metric"><small>API Calls</small><strong><?= count($sourceMap['api_calls'] ?? []) ?></strong></div> <div class="metric"><small>Inputs</small><strong><?= count($sourceMap['globals'] ?? []) ?></strong></div> <div class="metric"><small>Links</small><strong><?= count($sourceMap['links'] ?? []) ?></strong></div> <div class="metric"><small>Events</small><strong><?= count($sourceMap['events'] ?? []) ?></strong></div> </div> </section> <section class="cards"> <div class="card"><small>Risk Score</small><b><?= h((string)$selected['score']) ?></b><div class="spark"></div></div> <div class="card"><small>Entropy</small><b><?= h((string)$selected['entropy']) ?></b><div class="spark"></div></div> <div class="card"><small>Rule Hits</small><b><?= count($selected['hits'] ?? []) ?></b><div class="spark"></div></div> <div class="card"><small>Map Edges</small><b><?= count($sourceMap['edges'] ?? []) ?></b><div class="spark"></div></div> </section> <section id="preview"> <h2>Preview</h2> <div class="preview"> <?php if ($isImage): ?> <img src="?action=preview&id=<?= h($selected['id']) ?>" alt="preview"> <?php elseif ($isVideo): ?> <video src="?action=preview&id=<?= h($selected['id']) ?>" controls></video> <?php elseif ($isAudio): ?> <audio src="?action=preview&id=<?= h($selected['id']) ?>" controls></audio> <?php elseif (!empty($selected['text_preview'])): ?> <div class="code"><?= h($selected['text_preview']) ?></div> <?php else: ?> <div><h3>No inline preview</h3><p style="color:var(--muted)">Binary previews are blocked for safety.</p></div> <?php endif; ?> </div> </section> <section> <h2>Threat Categories</h2> <div class="bars"> <?php foreach (($selected['categories'] ?? []) as $category => $count): $width = min(100, (int)$count * 16); ?> <div class="bar"><b><?= h($category) ?></b><div class="track"><div class="fill" style="width:<?= $width ?>%"></div></div><span><?= h((string)$count) ?></span></div> <?php endforeach; ?> </div> </section> <section id="ai-chat"> <div class="row" style="justify-content:space-between"> <div> <h2 style="margin-bottom:4px">SIMON BUG Conversation</h2> <small style="color:var(--muted)">Selected file: <?= h($selected['original_name']) ?> · Provider: <?= h((string)($selected['ai_provider_used'] ?? (!empty($settings['ai_enabled']) ? 'AI configured, not yet used' : 'SIMON Local'))) ?></small> </div> <form method="post" action="?action=ai_clear_chat" onsubmit="return confirm('Clear this file conversation?')"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <input type="hidden" name="id" value="<?= h($selected['id']) ?>"> <button>Clear Chat</button> </form> </div> <div class="chatThread" id="chatThread"> <?php foreach (($selected['ai_chat'] ?? []) as $message): ?> <article class="chatMessage <?= h((string)($message['role'] ?? 'assistant')) ?>"> <div class="chatMeta"><?= h(($message['role'] ?? '') === 'user' ? 'You' : (string)($message['source'] ?? 'SIMON')) ?> · <?= h((string)($message['at'] ?? '')) ?></div> <div><?= nl2br(h((string)($message['content'] ?? ''))) ?></div> </article> <?php endforeach; ?> <?php if (empty($selected['ai_chat'])): ?> <article class="chatMessage assistant"> <div class="chatMeta">SIMON Local</div> <div>I am ready to analyze <?= h($selected['original_name']) ?>. I can answer immediately from the scanner evidence. Add an API key in Settings for deeper external AI reasoning.</div> </article> <?php endif; ?> </div> <form method="post" action="?action=ai_analyze#ai-chat" class="stack aiAskForm"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <input type="hidden" name="id" value="<?= h($selected['id']) ?>"> <div class="row"> <select name="ai_scope" aria-label="AI analysis scope"> <option value="full">Full file analysis</option> <option value="security">Security only</option> <option value="dependencies">Functions and dependencies</option> <option value="quality">Code quality and performance</option> </select> <span class="pill"><?= !empty($settings['ai_enabled']) && (!empty($_SESSION['scanner_ai_key']) || getenv('OPENAI_API_KEY')) ? 'External AI available' : 'SIMON Local active' ?></span> </div> <textarea name="ai_question" rows="4" placeholder="Ask a follow-up about this file..." required></textarea> <div class="row" style="justify-content:space-between"> <small style="color:var(--muted)">Context: metadata, findings, functions, includes, APIs, forms, links, dependency edges, code excerpt, and recent chat.</small> <button class="primary">Send to SIMON</button> </div> </form> <details class="analysisScope"> <summary>Show exact analysis scope</summary> <pre class="code" style="max-height:260px"><?= h(json_encode($selected['ai_input_summary'] ?? [ 'status' => 'No question asked yet', 'selected_file' => $selected['original_name'], 'available_functions' => count($selectedMap['functions'] ?? []), 'available_includes' => count($selectedMap['includes'] ?? []), 'available_api_calls' => count($selectedMap['api_calls'] ?? []), 'available_edges' => count($selectedMap['edges'] ?? []), ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) ?></pre> </details> </section> <?php else: ?> <h1>SIMON BUG File Intelligence</h1> <p style="color:var(--muted)">Upload a file to scan keywords, regex signatures, entropy, extension risk, image/video preview, and source-code function mapping.</p> <?php endif; ?> </main> <section class="right shell" id="scanner"> <h2>Scan Intake</h2> <form method="post" enctype="multipart/form-data" action="?action=upload" class="stack"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <input type="file" name="scan_file" required> <button class="primary">Upload + Scan + Map</button> </form> <h2>Secure SFTP Browser</h2> <form method="post" action="?action=sftp_browse" class="stack" autocomplete="off"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <div class="row"> <input name="sftp_host" value="<?= h((string)($_SESSION['sftp_host'] ?? $settings['sftp_host'])) ?>" placeholder="SFTP host" required style="flex:1"> <input name="sftp_port" type="number" value="<?= h((string)($_SESSION['sftp_port'] ?? $settings['sftp_port'])) ?>" min="1" max="65535" style="width:86px"> </div> <input name="sftp_user" value="<?= h((string)($_SESSION['sftp_user'] ?? $settings['sftp_user'])) ?>" placeholder="Username" required> <input type="password" name="sftp_pass" placeholder="<?= !empty($_SESSION['sftp_pass']) ? 'Password active for this session' : 'Password (session only)' ?>" autocomplete="new-password"> <input name="sftp_fingerprint" value="<?= h((string)($_SESSION['sftp_fingerprint'] ?? $settings['sftp_fingerprint'])) ?>" placeholder="Verified SHA-256 host fingerprint" required> <div class="row"> <input name="sftp_path" value="<?= h($remotePath) ?>" placeholder="/" style="flex:1"> <button class="primary">Connect / Open Folder</button> </div> <small style="color:var(--muted)">After connecting, folders and files appear below. Open folders, then choose Import + Scan on a file. Password remains in the PHP session only.</small> </form> <?php if ($sftpBrowserOpen): ?> <div class="remoteBrowser"> <div class="row" style="justify-content:space-between"> <b>Remote: <?= h($remotePath) ?></b> <form method="post" action="?action=sftp_browse"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <input type="hidden" name="sftp_path" value="<?= h(remote_parent_path($remotePath)) ?>"> <button>Up</button> </form> </div> <div class="remoteList"> <?php foreach ($remoteEntries as $entry): ?> <div class="remoteItem"> <div> <b><?= $entry['is_dir'] ? 'Folder' : 'File' ?> · <?= h($entry['name']) ?></b> <small><?= $entry['is_dir'] ? '' : h(human_bytes((int)$entry['size'])) ?> <?= h($entry['modified']) ?> <?= h($entry['permissions']) ?></small> </div> <?php if ($entry['is_dir']): ?> <form method="post" action="?action=sftp_browse"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <input type="hidden" name="sftp_path" value="<?= h($entry['path']) ?>"> <button>Open</button> </form> <?php else: ?> <form method="post" action="?action=sftp_import_selected"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <input type="hidden" name="remote_file" value="<?= h($entry['path']) ?>"> <button class="primary">Import + Scan</button> </form> <?php endif; ?> </div> <?php endforeach; ?> <?php if (!$remoteEntries): ?><p style="color:var(--muted)">No remote items returned.</p><?php endif; ?> </div> </div> <?php endif; ?> <h2>Files</h2> <div class="files"> <?php foreach ($history as $record): ?> <a class="file <?= ($selected && $selected['id'] === $record['id']) ? 'active' : '' ?>" href="?id=<?= h($record['id']) ?>"> <b><?= h($record['original_name']) ?></b><br> <span class="sev <?= h($record['severity']) ?>"><?= h($record['severity']) ?></span> <small><?= h($record['size_human']) ?> · <?= h((string)$record['score']) ?>/100</small> </a> <?php endforeach; ?> <?php if (!$history): ?><p style="color:var(--muted)">No scans yet.</p><?php endif; ?> </div> <?php if ($selected): ?> <h2 id="reports">Actions</h2> <div class="row"> <a class="btn" target="_blank" href="?action=report&id=<?= h($selected['id']) ?>&kind=json">JSON</a> <a class="btn" target="_blank" href="?action=report&id=<?= h($selected['id']) ?>&kind=html">HTML</a> <a class="btn" target="_blank" href="?action=preview&id=<?= h($selected['id']) ?>">Full Preview</a> </div> <form method="post" action="?action=email_report" class="row"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <input type="hidden" name="id" value="<?= h($selected['id']) ?>"> <button>Email Report</button> </form> <form method="post" action="?action=delete" class="row" onsubmit="return confirm('Delete this file?')"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <input type="hidden" name="id" value="<?= h($selected['id']) ?>"> <button class="danger">Delete File</button> </form> <h2>Flags</h2> <?php if (!empty($selected['flags'])): ?><ul><?php foreach ($selected['flags'] as $flag): ?><li><?= h($flag) ?></li><?php endforeach; ?></ul><?php else: ?><p style="color:var(--muted)">No special flags beyond rules.</p><?php endif; ?> <h2>Hits</h2> <table class="table"><tr><th>Category</th><th>Rule</th><th>#</th></tr><?php foreach (($selected['hits'] ?? []) as $hit): ?><tr><td><?= h($hit['category']) ?></td><td><?= h($hit['label']) ?></td><td><?= h((string)$hit['count']) ?></td></tr><?php endforeach; if (empty($selected['hits'])): ?><tr><td colspan="3">No keyword or regex hits.</td></tr><?php endif; ?></table> <h2>Function Links</h2> <table class="table"><tr><th>Type</th><th>Target</th></tr><?php foreach (($selectedMap['edges'] ?? []) as $edge): ?><tr><td><?= h($edge['type']) ?></td><td><?= h($edge['to']) ?></td></tr><?php endforeach; if (empty($selectedMap['edges'])): ?><tr><td colspan="2">No source map edges.</td></tr><?php endif; ?></table> <h2>SHA256</h2> <p class="code" style="word-break:break-all;max-height:none"><?= h($selected['sha256']) ?></p> <?php endif; ?> <form method="post" action="?action=clear" onsubmit="return confirm('Clear all scan history and uploaded files?')"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <button class="danger">Clear History</button> </form> </section> </div> <div class="modal" id="settingsModal" aria-hidden="true"> <div class="modalCard"> <div class="modalHead"> <div><h2 style="margin:0">Settings</h2><small style="color:var(--muted)">AI, alerts, email, idle mode, and security</small></div> <button type="button" class="closeModal" data-close-modal>×</button> </div> <form method="post" action="?action=save_settings" class="formGrid"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <label class="full">AI endpoint<input class="full" name="ai_endpoint" value="<?= h((string)$settings['ai_endpoint']) ?>" required></label> <label>Model<input name="ai_model" value="<?= h((string)$settings['ai_model']) ?>" required></label> <label>API key<input type="password" name="ai_api_key" placeholder="<?= !empty($_SESSION['scanner_ai_key']) || getenv('OPENAI_API_KEY') ? 'Key already available' : 'Session-only API key' ?>" autocomplete="new-password"></label> <label>Email reports to<input type="email" name="email_to" value="<?= h((string)$settings['email_to']) ?>"></label> <label>Idle screensaver seconds<input type="number" name="idle_seconds" min="60" max="3600" value="<?= h((string)$settings['idle_seconds']) ?>"></label> <label class="full"><input type="checkbox" name="ai_enabled" value="1" <?= !empty($settings['ai_enabled']) ? 'checked' : '' ?>> Enable AI controls</label> <h3 class="full">IONOS SFTP Defaults</h3> <label>Host<input name="sftp_host" value="<?= h((string)$settings['sftp_host']) ?>"></label> <label>Port<input type="number" name="sftp_port" value="<?= h((string)$settings['sftp_port']) ?>"></label> <label>Username<input name="sftp_user" value="<?= h((string)$settings['sftp_user']) ?>"></label> <label>Verified host fingerprint<input name="sftp_fingerprint" value="<?= h((string)$settings['sftp_fingerprint']) ?>"></label> <h3 class="full">Appearance</h3> <label>Primary accent<input type="color" name="accent" value="<?= h((string)$settings['accent']) ?>"></label> <label>Secondary accent<input type="color" name="accent_2" value="<?= h((string)$settings['accent_2']) ?>"></label> <label>Nebula accent<input type="color" name="accent_3" value="<?= h((string)$settings['accent_3']) ?>"></label> <label>Background<input type="color" name="background" value="<?= h((string)$settings['background']) ?>"></label> <label>Panel color<input type="color" name="panel" value="<?= h((string)$settings['panel']) ?>"></label> <label>Ticker duration (seconds)<input type="number" name="ticker_seconds" min="30" max="240" value="<?= h((string)$settings['ticker_seconds']) ?>"></label> <label class="full"><input type="checkbox" name="spatial_enabled" value="1" <?= !empty($settings['spatial_enabled']) ? 'checked' : '' ?>> Enable spatial motion</label> <h3 class="full">Bug Sounds</h3> <label>Sound theme <select name="sound_theme"> <?php foreach (['bug'=>'Bug Chitter','scanner'=>'Scanner Pulse','arcade'=>'Arcade','silent'=>'Silent'] as $key=>$label): ?> <option value="<?= h($key) ?>" <?= $settings['sound_theme']===$key?'selected':'' ?>><?= h($label) ?></option> <?php endforeach; ?> </select> </label> <label>Volume<input type="range" name="sound_volume" min="0" max="1" step="0.01" value="<?= h((string)$settings['sound_volume']) ?>"></label> <label class="full"><input type="checkbox" name="sound_enabled" value="1" <?= !empty($settings['sound_enabled']) ? 'checked' : '' ?>> Enable interface and bug sounds</label> <div class="full row"> <button type="button" id="testSound">Test Bug Sound</button> </div> <div class="full row"> <button class="primary">Save Settings</button> </div> </form> <form method="post" action="?action=ai_test" style="margin-top:12px"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <button>Test AI Connection</button> </form> <p style="color:var(--muted)">The API key is retained only in the current PHP session. For persistent production use, set <code>OPENAI_API_KEY</code> in the server environment.</p> </div> </div> <div class="modal" id="reportsModal" aria-hidden="true"> <div class="modalCard"> <div class="modalHead"> <div><h2 style="margin:0">Scan Reports</h2><small style="color:var(--muted)">Detailed results, exports, and report delivery</small></div> <button type="button" class="closeModal" data-close-modal>×</button> </div> <?php if ($selected): ?> <div class="reportGrid"> <div class="reportBox"><small>Risk</small><h2><?= h((string)$selected['score']) ?>/100</h2><span class="sev <?= h($selected['severity']) ?>"><?= h($selected['severity']) ?></span></div> <div class="reportBox"><small>Rule hits</small><h2><?= count($selected['hits'] ?? []) ?></h2></div> <div class="reportBox"><small>Function links</small><h2><?= count($selectedMap['edges'] ?? []) ?></h2></div> </div> <h3>Flags</h3> <ul><?php foreach (($selected['flags'] ?? []) as $flag): ?><li><?= h($flag) ?></li><?php endforeach; ?><?php if (empty($selected['flags'])): ?><li>No special flags.</li><?php endif; ?></ul> <h3>Findings</h3> <table class="table"><tr><th>Category</th><th>Rule</th><th>Count</th></tr><?php foreach (($selected['hits'] ?? []) as $hit): ?><tr><td><?= h($hit['category']) ?></td><td><?= h($hit['label']) ?></td><td><?= h((string)$hit['count']) ?></td></tr><?php endforeach; ?></table> <div class="row" style="margin-top:14px"> <a class="btn" target="_blank" href="?action=report&id=<?= h($selected['id']) ?>&kind=json">Download JSON</a> <a class="btn" target="_blank" href="?action=report&id=<?= h($selected['id']) ?>&kind=html">Open HTML Report</a> <form method="post" action="?action=email_report"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <input type="hidden" name="id" value="<?= h($selected['id']) ?>"> <button>Email Report</button> </form> </div> <?php else: ?> <p>No scan is selected. Upload or import a file first.</p> <?php endif; ?> </div> </div> <script> document.querySelectorAll('[data-open-modal]').forEach(el=>el.addEventListener('click',event=>{ event.preventDefault(); const modal=document.getElementById(el.dataset.openModal); if(modal){modal.classList.add('open');modal.setAttribute('aria-hidden','false')} })); document.querySelectorAll('[data-close-modal]').forEach(el=>el.addEventListener('click',()=>{ const modal=el.closest('.modal'); if(modal){modal.classList.remove('open');modal.setAttribute('aria-hidden','true')} })); document.querySelectorAll('.modal').forEach(modal=>modal.addEventListener('click',event=>{ if(event.target===modal){modal.classList.remove('open');modal.setAttribute('aria-hidden','true')} })); addEventListener('keydown',event=>{ if(event.key==='Escape')document.querySelectorAll('.modal.open').forEach(m=>m.classList.remove('open')); }); const UI_SETTINGS = { soundEnabled: <?= !empty($settings['sound_enabled']) ? 'true' : 'false' ?>, soundVolume: <?= json_encode((float)$settings['sound_volume']) ?>, soundTheme: <?= json_encode((string)$settings['sound_theme']) ?>, spatialEnabled: <?= !empty($settings['spatial_enabled']) ? 'true' : 'false' ?> }; let audioContext = null; function getAudioContext(){ if(!audioContext){ audioContext = new (window.AudioContext || window.webkitAudioContext)(); } return audioContext; } function tone(freq=440,duration=.08,type='sine',gain=.04,delay=0){ if(!UI_SETTINGS.soundEnabled || UI_SETTINGS.soundTheme==='silent') return; try{ const ctx=getAudioContext(); const osc=ctx.createOscillator(); const amp=ctx.createGain(); osc.type=type; osc.frequency.setValueAtTime(freq,ctx.currentTime+delay); amp.gain.setValueAtTime(0.0001,ctx.currentTime+delay); amp.gain.exponentialRampToValueAtTime(Math.max(.0001,gain*UI_SETTINGS.soundVolume),ctx.currentTime+delay+.01); amp.gain.exponentialRampToValueAtTime(.0001,ctx.currentTime+delay+duration); osc.connect(amp);amp.connect(ctx.destination); osc.start(ctx.currentTime+delay);osc.stop(ctx.currentTime+delay+duration+.02); }catch(e){} } function bugSound(kind='hover'){ if(kind==='hover'){tone(720,.04,'triangle',.05)} if(kind==='click'){tone(420,.05,'square',.05);tone(610,.06,'triangle',.04,.045)} if(kind==='success'){tone(440,.08,'sine',.05);tone(660,.1,'sine',.05,.08);tone(880,.12,'triangle',.04,.17)} if(kind==='warning'){tone(220,.12,'sawtooth',.06);tone(180,.15,'square',.05,.1)} if(kind==='bug'){tone(980,.025,'square',.035);tone(760,.03,'triangle',.03,.03);tone(1120,.025,'square',.025,.065)} } document.querySelectorAll('button,.btn,.nav a,.file').forEach(el=>{ el.addEventListener('mouseenter',()=>bugSound('hover')); el.addEventListener('click',()=>bugSound('click')); }); document.getElementById('testSound')?.addEventListener('click',()=>bugSound('bug')); if(UI_SETTINGS.spatialEnabled){ const app=document.querySelector('.app'); addEventListener('pointermove',event=>{ const x=(event.clientX/innerWidth-.5); const y=(event.clientY/innerHeight-.5); app.style.transform=`rotateX(${(-y*1.7).toFixed(2)}deg) rotateY(${(x*2.2).toFixed(2)}deg)`; },{passive:true}); addEventListener('pointerleave',()=>{app.style.transform='rotateX(0deg) rotateY(0deg)'}); } const SEARCH_INDEX = <?= $searchJson ?: '[]' ?>; const searchInput = document.getElementById('globalSearch'); const searchResults = document.getElementById('searchResults'); function escapeHtml(value){ return String(value).replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); } function renderSearch(query){ const q = query.trim().toLowerCase(); if(q.length < 1){ const recent = SEARCH_INDEX.filter(item => item.type === 'file').slice(0, 12); searchResults.innerHTML = '<div class="searchCount">' + SEARCH_INDEX.length + ' indexed items · recent files</div>' + recent.map(item => ` <a class="searchResult" href="${escapeHtml(item.url)}"> <b>${escapeHtml(item.label || item.file)}</b> <small>${escapeHtml(item.detail)}</small> </a>`).join(''); searchResults.hidden = false; return; } const terms = q.split(/\s+/).filter(Boolean); const matches = SEARCH_INDEX.filter(item => { const haystack = `${item.file} ${item.type} ${item.label} ${item.detail} ${item.severity}`.toLowerCase(); return terms.every(term => haystack.includes(term)); }).slice(0, 40); searchResults.innerHTML = matches.length ? '<div class="searchCount">' + matches.length + ' results from ' + SEARCH_INDEX.length + ' indexed items</div>' + matches.map(item => ` <a class="searchResult" href="${escapeHtml(item.url)}"> <b>${escapeHtml(item.label || item.file)}</b> <small>${escapeHtml(item.type.toUpperCase())} · ${escapeHtml(item.detail)}</small> </a> `).join('') : '<div class="searchResult"><b>No matches</b><small>Try a file, function, threat, include, API, or link name.</small></div>'; searchResults.hidden = false; } searchInput?.addEventListener('input', event => renderSearch(event.target.value)); searchInput?.addEventListener('focus', event => renderSearch(event.target.value)); searchInput?.addEventListener('keydown', event => { if(event.key === 'Escape'){ searchResults.hidden = true; searchInput.blur(); } if(event.key === 'Enter'){ const first = searchResults.querySelector('a'); if(first){ event.preventDefault(); location.href = first.href; } } }); document.addEventListener('click', event => { if(!event.target.closest('.searchBox')){ searchResults.hidden = true; } }); const chatThread=document.getElementById('chatThread');if(chatThread)chatThread.scrollTop=chatThread.scrollHeight; const SOURCE_MAP = <?= $mapJson ?: '{}' ?>; const CFG={WEBHOOK_BASE:'<?= h(getenv('SIMON_WEBHOOK_BASE') ?: '/connlink/test1/ui/webhook_inbox.php') ?>',PULSE_STREAM:'default',VISIT_STREAM:'visits',KEY:'<?= h(getenv('SIMON_WEBHOOK_KEY') ?: '') ?>'}; function simonEvent(title,summary,extra={}){try{const key=CFG.KEY?'&key='+encodeURIComponent(CFG.KEY):'';fetch(CFG.WEBHOOK_BASE+'?stream='+encodeURIComponent(CFG.PULSE_STREAM)+key,{method:'POST',headers:{'Content-Type':'application/json'},keepalive:true,body:JSON.stringify({title,summary,url:location.href,at:new Date().toISOString(),source:'simon-bug-nebula-file-mapper.php',...extra})}).catch(()=>{})}catch(e){}} <?php if ($notice): ?> setTimeout(()=>bugSound(<?= json_encode(str_contains(strtolower($notice),'failed') || str_contains(strtolower($notice),'required') || str_contains(strtolower($notice),'mismatch') ? 'warning' : 'success') ?>),250); <?php endif; ?> simonEvent('Scanner opened',document.title,{screen:innerWidth+'x'+innerHeight}); let idleTimer, saver=document.getElementById('screensaver');function resetIdle(){saver.classList.remove('on');clearTimeout(idleTimer);idleTimer=setTimeout(()=>saver.classList.add('on'),<?= (int)$settings['idle_seconds'] * 1000 ?>)}['mousemove','keydown','touchstart','scroll','click'].forEach(e=>addEventListener(e,resetIdle,{passive:true}));resetIdle(); document.querySelectorAll('a[href*="action=preview"]').forEach(a=>a.addEventListener('click',()=>simonEvent('Preview opened','User opened file preview'))); const appShell=document.querySelector('.app'); let spatialFrame=0; addEventListener('pointermove',event=>{ if(!appShell||matchMedia('(prefers-reduced-motion: reduce)').matches) return; cancelAnimationFrame(spatialFrame); spatialFrame=requestAnimationFrame(()=>{ const rx=((event.clientY/innerHeight)-.5)*-2.2; const ry=((event.clientX/innerWidth)-.5)*2.8; appShell.style.transform=`rotateX(${rx}deg) rotateY(${ry}deg) translateZ(0)`; }); },{passive:true}); addEventListener('pointerleave',()=>{if(appShell)appShell.style.transform='rotateX(0deg) rotateY(0deg)'},{passive:true}); function drawMap(){ const canvas=document.getElementById('mapCanvas'); if(!canvas) return; const dpr=window.devicePixelRatio||1; const rect=canvas.getBoundingClientRect(); canvas.width=rect.width*dpr; canvas.height=rect.height*dpr; const ctx=canvas.getContext('2d'); ctx.scale(dpr,dpr); const w=rect.width,h=rect.height; ctx.clearRect(0,0,w,h); const center={x:w*.5,y:h*.48,label:SOURCE_MAP.filename||'file',type:'file'}; let items=[]; function add(arr,type,key){(arr||[]).slice(0,18).forEach(o=>items.push({label:o[key]||o.name||o.path||o.url||o.target||o.event,type}))} add(SOURCE_MAP.functions,'fn','name'); add(SOURCE_MAP.classes,'class','name'); add(SOURCE_MAP.includes,'inc','path'); add(SOURCE_MAP.api_calls,'api','target'); add(SOURCE_MAP.links,'link','url'); add(SOURCE_MAP.forms,'form','action'); const nodes=[center]; const radius=Math.min(w,h)*.38; items.forEach((it,i)=>{const a=(Math.PI*2*i/Math.max(1,items.length))-Math.PI/2;nodes.push({x:center.x+Math.cos(a)*radius,y:center.y+Math.sin(a)*radius,label:it.label,type:it.type})}); ctx.lineWidth=1; ctx.strokeStyle='rgba(50,255,115,.25)'; nodes.slice(1).forEach(n=>{ctx.beginPath();ctx.moveTo(center.x,center.y);ctx.lineTo(n.x,n.y);ctx.stroke()}); nodes.forEach((n,i)=>{const r=i===0?26:14;let color=i===0?'#32ff73':'#54f1ff';if(n.type==='inc')color='#ff9d2e';if(n.type==='api')color='#ff4d5e';if(n.type==='class')color='#805cff';ctx.beginPath();ctx.arc(n.x,n.y,r,0,Math.PI*2);ctx.fillStyle=color;ctx.shadowColor=color;ctx.shadowBlur=18;ctx.fill();ctx.shadowBlur=0;ctx.fillStyle=i===0?'#031008':'#06110a';ctx.font=i===0?'700 10px system-ui':'700 8px system-ui';ctx.textAlign='center';ctx.fillText((n.type||'').toUpperCase().slice(0,4),n.x,n.y+3);ctx.fillStyle='rgba(238,252,242,.9)';ctx.font='10px system-ui';const short=(n.label||'').replace(/^.*\//,'').slice(0,22);ctx.fillText(short,n.x,Math.min(h-8,n.y+r+14));}); if(nodes.length===1){ctx.fillStyle='rgba(238,252,242,.65)';ctx.font='14px system-ui';ctx.textAlign='center';ctx.fillText('Upload source code to generate function map',w/2,h/2+52)} } addEventListener('resize',drawMap);drawMap(); </script> </body> </html>
Save file
Quick jump
open a path
Open