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,026
Folders
377
Scanned Size
3.79 GB
PHP Files
791
Editable Text Files
12,310
File viewer
guarded to /htdocs
/simon/simon_bug.php
<?php /** * SIMON BUG Nebula File Scanner + Function Mapper * Single-file PHP 8.0+ admin scanner with AI reports, verified SFTP import, and deferred host discovery. * * Drop-in path: /htdocs/simon/index.php * Default login: admin / admin * Override with SCANNER_USERNAME and SCANNER_PASSWORD_HASH. */ declare(strict_types=1); session_start(); /* * SIMON production configuration bootstrap. * Loads the existing private config so the scanner can use: * SIMON_DB_DSN * SIMON_DB_USER * SIMON_DB_PASSWORD * simon_database_connection('simon') */ $scannerPrivateConfigCandidates = [ '/homepages/7/d4299367777/private/config.php', '/homepages/7/d4299367777/htdocs/private/config.php', '/kunden/homepages/7/d4299367777/private/config.php', '/kunden/homepages/7/d4299367777/htdocs/private/config.php', dirname(__DIR__, 2) . '/private/config.php', dirname(__DIR__, 3) . '/private/config.php', ]; foreach ($scannerPrivateConfigCandidates as $scannerPrivateConfig) { if (is_file($scannerPrivateConfig)) { require_once $scannerPrivateConfig; break; } } /* * SIMON BUG emergency runtime guard. * Logs fatal errors privately and shows a compact diagnostic instead of a blank page. */ @ini_set('display_errors', '0'); @ini_set('log_errors', '1'); @ini_set('memory_limit', getenv('SIMON_BUG_MEMORY_LIMIT') ?: '256M'); @set_time_limit(120); register_shutdown_function(static function (): void { $error = error_get_last(); if (!$error || !in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) { return; } $message = sprintf("[%s] %s in %s:%d ", date('c'), $error['message'], $error['file'], $error['line']); $target = __DIR__ . '/simon_bug_fatal.log'; @file_put_contents($target, $message, FILE_APPEND | LOCK_EX); if (!headers_sent()) { http_response_code(500); header('Content-Type: text/html; charset=utf-8'); } echo '<!doctype html><meta charset="utf-8"><title>SIMON BUG Recovery</title>'; echo '<style>body{margin:0;min-height:100vh;display:grid;place-items:center;background:#050711;color:#eef;font:16px system-ui}.box{max-width:760px;padding:28px;border:1px solid #35406c;border-radius:22px;background:#0d1328}code{color:#8fffb0}</style>'; echo '<div class="box"><h1>SIMON BUG could not finish loading</h1><p>A fatal PHP error was captured instead of showing a blank page.</p><p>Check <code>simon_bug_fatal.log</code> beside this PHP file.</p><p><b>' . htmlspecialchars($error['message'], ENT_QUOTES, 'UTF-8') . '</b></p></div>'; }); /* * Vendor loading * * Supported layouts: * /vendor/autoload.php * /vendor/phpseclib/Net/SFTP.php * /vendor/phpseclib/phpseclib/Net/SFTP.php */ $vendorDiagnostics = [ 'autoload_found' => false, 'autoload_path' => '', 'phpseclib_source' => '', 'phpseclib_loaded' => false, 'errors' => [], ]; $autoloadCandidates = [ __DIR__ . '/vendor/autoload.php', dirname(__DIR__) . '/vendor/autoload.php', ]; foreach ($autoloadCandidates as $autoloadFile) { if (!is_file($autoloadFile)) { continue; } try { require_once $autoloadFile; $vendorDiagnostics['autoload_found'] = true; $vendorDiagnostics['autoload_path'] = $autoloadFile; break; } catch (Throwable $autoloadError) { $vendorDiagnostics['errors'][] = $autoloadError->getMessage(); } } /* * Safety fallback: register the exact phpseclib layout visible on this host * even if the manually uploaded autoload file is absent or incomplete. */ if (!class_exists('\phpseclib3\Net\SFTP')) { $sourceCandidates = [ __DIR__ . '/vendor/phpseclib', __DIR__ . '/vendor/phpseclib/phpseclib', __DIR__ . '/vendor/phpseclib/src', ]; foreach ($sourceCandidates as $sourceRoot) { if (!is_file($sourceRoot . '/Net/SFTP.php')) { continue; } $vendorDiagnostics['phpseclib_source'] = $sourceRoot; spl_autoload_register( static function (string $className) use ($sourceRoot): void { $prefix = 'phpseclib3\\'; if (!str_starts_with($className, $prefix)) { return; } $relativeClass = substr($className, strlen($prefix)); $file = $sourceRoot . '/' . str_replace('\\', '/', $relativeClass) . '.php'; if (is_file($file)) { require_once $file; } }, true, true ); $bootstrap = $sourceRoot . '/bootstrap.php'; if (is_file($bootstrap)) { require_once $bootstrap; } break; } } $vendorDiagnostics['phpseclib_loaded'] = class_exists('\phpseclib3\Net\SFTP'); const APP_NAME = 'SIMON BUG — Retrieval Intelligence Studio'; const APP_VERSION = '12.2.11.1'; const MAX_UPLOAD_BYTES = 120_000_000; const GA4_ID = 'G-S6MEHB4BEY'; const DEFAULT_EMAIL_TO = 'admin@gaylordsinclair.com'; const IONOS_SFTP_HOST = 'access-5018739336.webspace-host.com'; const IONOS_SFTP_FINGERPRINT_ED25519 = 'SHA256:1gx2w8Rtv3wCgi7Jh8myf/KVd72cRQbow03UP8P095Q'; const IONOS_SFTP_FINGERPRINT_ECDSA = 'SHA256:J4oM+B2g7zZWAI3DolXR1e4vdIMrGO301kEN14/slsQ'; const IONOS_SFTP_FINGERPRINT_RSA = 'SHA256:psLDE8kfhoS9GWrc/GTrdlPvyKPcWcGZv2JxOCvwF3w'; $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'); define('PROJECTS_FILE', DATA_DIR . '/projects.json'); define('CHECKPOINTS_DIR', DATA_DIR . '/checkpoints'); define('REVIEWS_FILE', DATA_DIR . '/ai_reviews.json'); define('FUNCTION_INTEL_FILE', DATA_DIR . '/function_intelligence.json'); define('PROJECT_DNA_FILE', DATA_DIR . '/project_dna.json'); define('HOST_SCAN_STATE_FILE', DATA_DIR . '/host_scan_state.json'); define('HOST_SCAN_EXPORT_FILE', DATA_DIR . '/host_scan_latest.json'); define('HOST_SCAN_REPORT_DIR', DATA_DIR . '/host_scan_reports'); define('HOST_SCAN_FAILURE_DIR', DATA_DIR . '/host_scan_failures'); define('SIMON_5W1H_STREAM_DIR', DATA_DIR . '/5w1h_streams'); define('SCAN_ACCURACY_FILE', DATA_DIR . '/scan_accuracy_latest.json'); define('BEAST_AGENTS_FILE', DATA_DIR . '/beast_agents.json'); define('BEAST_JOBS_FILE', DATA_DIR . '/beast_jobs.json'); define('BEAST_FLOWS_FILE', DATA_DIR . '/beast_flows.json'); define('BEAST_CRITERIA_FILE', DATA_DIR . '/beast_scan_criteria.json'); define('BEAST_RUNS_FILE', DATA_DIR . '/beast_runs.json'); define('DATA_DICTIONARY_PREFERRED_FILE', __DIR__ . '/SIMON_BUG_MASTER_v3_OPERATIONS_CODEBASE_HOST_COMPATIBLE.xlsx'); $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, CHECKPOINTS_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' => implode(" ", [ IONOS_SFTP_FINGERPRINT_ED25519, IONOS_SFTP_FINGERPRINT_ECDSA, IONOS_SFTP_FINGERPRINT_RSA, ]), 'accent' => '#32ff73', 'accent_2' => '#54f1ff', 'accent_3' => '#805cff', 'color_mode' => 'rainbow', 'background' => '#040606', 'panel' => '#111514', 'sound_enabled' => true, 'sound_volume' => 0.32, 'sound_theme' => 'bug', 'spatial_enabled' => true, 'ticker_seconds' => 160, 'operation_mode' => 'assisted', 'auto_scan_on_save' => true, 'auto_checkpoint_on_save' => true, 'publish_requires_approval' => true, 'project_root' => __DIR__, ]; 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 build_function_criteria(array $record): array { $map = $record['source_map'] ?? []; $functions = $map['functions'] ?? []; $edges = $map['edges'] ?? []; $hits = $record['hits'] ?? []; $criteria = []; $incoming = []; $outgoing = []; foreach ($edges as $edge) { $from = (string)($edge['from'] ?? ''); $to = (string)($edge['to'] ?? ''); if ($from !== '') $outgoing[$from] = ($outgoing[$from] ?? 0) + 1; if ($to !== '') $incoming[$to] = ($incoming[$to] ?? 0) + 1; } foreach ($functions as $function) { $name = trim((string)($function['name'] ?? 'anonymous')); $signature = trim((string)($function['signature'] ?? $name . '()')); $start = (int)($function['line'] ?? $function['start_line'] ?? 0); $end = (int)($function['end_line'] ?? $start); $lineCount = max(1, $end - $start + 1); $inCount = (int)($incoming[$name] ?? 0); $outCount = (int)($outgoing[$name] ?? 0); $riskEvidence = []; foreach ($hits as $hit) { $label = strtolower((string)($hit['label'] ?? '')); $category = strtolower((string)($hit['category'] ?? '')); if ( str_contains($label, strtolower($name)) || in_array($category, ['execution','database','xss','upload','secrets','authentication'], true) ) { $riskEvidence[] = (string)($hit['label'] ?? $hit['category'] ?? 'scanner finding'); } } $complexity = 1; if ($lineCount > 80) $complexity += 3; elseif ($lineCount > 40) $complexity += 2; elseif ($lineCount > 20) $complexity += 1; $complexity += min(4, $outCount); $classification = 'project-specific'; $confidence = 0.68; $reasons = []; if ($inCount === 0) { $classification = 'unused-review'; $confidence = 0.72; $reasons[] = 'No resolved incoming calls were detected in the current scan scope.'; } else { $reasons[] = $inCount . ' incoming reference(s) detected.'; } if ($lineCount <= 60 && $inCount >= 2 && empty($riskEvidence)) { $classification = 'shared-candidate'; $confidence = 0.78; $reasons[] = 'Compact function with multiple callers and no direct scanner risk evidence.'; } if (!empty($riskEvidence)) { $classification = 'security-review'; $confidence = 0.82; $reasons[] = 'Security-related scanner evidence intersects the file or function context.'; } $criteria[] = [ 'function_name' => $name, 'signature' => $signature, 'start_line' => $start ?: null, 'end_line' => $end ?: null, 'line_count' => $lineCount, 'incoming_references' => $inCount, 'outgoing_references' => $outCount, 'complexity_estimate' => $complexity, 'classification' => $classification, 'purpose' => '', 'business_role' => '', 'inputs' => [], 'outputs' => [], 'side_effects' => [], 'security_evidence' => array_values(array_unique($riskEvidence)), 'modernization_recommendation' => '', 'canonical_candidate' => $inCount >= 2 && empty($riskEvidence), 'shared_module_candidate' => $classification === 'shared-candidate', 'archive_candidate' => $classification === 'unused-review', 'requires_human_review' => in_array($classification, ['unused-review','security-review'], true), 'confidence' => $confidence, 'reasons' => $reasons, 'ai_status' => 'not_enriched', ]; } return $criteria; } function enrich_function_criteria_with_ai( array $settings, string $apiKey, array $record, array $criteria ): array { if ($apiKey === '' || empty($settings['ai_enabled'])) { return $criteria; } $prompt = "Complete the missing function criteria fields using only the supplied source excerpt and scanner evidence. " . "Return valid JSON with a top-level key functions. Each item must include function_name, purpose, business_role, " . "inputs, outputs, side_effects, modernization_recommendation, classification, confidence, and evidence. " . "Do not invent runtime behavior. Use empty arrays or 'unknown' when evidence is insufficient.\n\n" . json_encode([ 'file' => $record['original_name'] ?? '', 'source_excerpt' => substr((string)($record['text_preview'] ?? ''), 0, 18000), 'scanner_criteria' => $criteria, 'source_map' => [ 'includes' => $record['source_map']['includes'] ?? [], 'api_calls' => $record['source_map']['api_calls'] ?? [], 'forms' => $record['source_map']['forms'] ?? [], 'edges' => $record['source_map']['edges'] ?? [], ], 'findings' => $record['hits'] ?? [], ], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); $text = ai_call($settings, $apiKey, $prompt); $jsonText = trim($text); if (str_starts_with($jsonText, '```')) { $jsonText = preg_replace('/^```(?:json)?\s*|\s*```$/', '', $jsonText) ?? $jsonText; } $decoded = json_decode($jsonText, true); if (!is_array($decoded) || !is_array($decoded['functions'] ?? null)) { throw new RuntimeException('AI function criteria response was not valid structured JSON.'); } $byName = []; foreach ($decoded['functions'] as $item) { $name = (string)($item['function_name'] ?? ''); if ($name !== '') $byName[$name] = $item; } foreach ($criteria as &$criterion) { $name = (string)$criterion['function_name']; if (!isset($byName[$name])) continue; $item = $byName[$name]; foreach (['purpose','business_role','modernization_recommendation'] as $field) { if (isset($item[$field]) && is_string($item[$field])) { $criterion[$field] = trim($item[$field]); } } foreach (['inputs','outputs','side_effects'] as $field) { if (isset($item[$field]) && is_array($item[$field])) { $criterion[$field] = array_values($item[$field]); } } if (!empty($item['classification']) && is_string($item['classification'])) { $criterion['ai_classification'] = $item['classification']; } if (isset($item['confidence']) && is_numeric($item['confidence'])) { $criterion['ai_confidence'] = max(0, min(1, (float)$item['confidence'])); } if (isset($item['evidence']) && is_array($item['evidence'])) { $criterion['ai_evidence'] = array_values($item['evidence']); } $criterion['ai_status'] = 'enriched'; } unset($criterion); return $criteria; } function report_empty_state(string $message): string { return '<div class="emptyState">' . h($message) . '</div>'; } 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 trusted_fingerprints_for_host(string $host, string $configured): string { $configuredValues = parse_trusted_fingerprints($configured); if (strcasecmp(trim($host), IONOS_SFTP_HOST) === 0) { $configuredValues = array_merge($configuredValues, [ IONOS_SFTP_FINGERPRINT_ED25519, IONOS_SFTP_FINGERPRINT_ECDSA, IONOS_SFTP_FINGERPRINT_RSA, ]); } return implode("\n", array_values(array_unique($configuredValues))); } function parse_trusted_fingerprints(string $value): array { $parts = preg_split('/[\s,;]+/', trim($value)) ?: []; $trusted = []; foreach ($parts as $part) { $part = trim($part); if ($part === '') { continue; } if (!str_starts_with($part, 'SHA256:')) { $part = 'SHA256:' . $part; } $trusted[] = rtrim($part, '='); } return array_values(array_unique($trusted)); } function openssh_sha256_fingerprint(string $publicHostKey): array { $publicHostKey = trim($publicHostKey); $parts = preg_split('/\s+/', $publicHostKey) ?: []; if (count($parts) < 2) { throw new RuntimeException('The SFTP server returned an unrecognized public host key.'); } $algorithm = (string)$parts[0]; $blob = base64_decode((string)$parts[1], true); if ($blob === false || $blob === '') { throw new RuntimeException('The SFTP server host key could not be decoded.'); } return [ 'algorithm' => $algorithm, 'fingerprint' => 'SHA256:' . rtrim(base64_encode(hash('sha256', $blob, true)), '='), ]; } function assert_trusted_host_key(string $host, string $publicHostKey, string $expectedFingerprints): array { $presented = openssh_sha256_fingerprint($publicHostKey); $trusted = parse_trusted_fingerprints(trusted_fingerprints_for_host($host, $expectedFingerprints)); if (!$trusted) { throw new RuntimeException('At least one verified SHA-256 host fingerprint is required.'); } foreach ($trusted as $trustedFingerprint) { if (hash_equals($trustedFingerprint, $presented['fingerprint'])) { return $presented; } } throw new RuntimeException( 'Host fingerprint mismatch. Connection blocked. ' . 'Host: ' . $host . '. ' . 'Algorithm: ' . $presented['algorithm'] . '. ' . 'Presented: ' . $presented['fingerprint'] . '. ' . 'Trusted: ' . implode(', ', $trusted) ); } function sftp_capability_status(): array { global $vendorDiagnostics; if (class_exists('\\phpseclib3\\Net\\SFTP')) { return [ 'ready' => true, 'driver' => 'phpseclib', 'message' => 'SFTP READY', 'detail' => $vendorDiagnostics['phpseclib_source'] ?: ($vendorDiagnostics['autoload_path'] ?: 'Composer autoloader'), ]; } if (function_exists('ssh2_connect')) { return [ 'ready' => true, 'driver' => 'ssh2', 'message' => 'SFTP READY', 'detail' => 'Native PHP SSH2 extension', ]; } $details = []; $details[] = $vendorDiagnostics['autoload_found'] ? 'vendor/autoload.php found' : 'vendor/autoload.php missing'; $details[] = $vendorDiagnostics['phpseclib_source'] ? 'phpseclib source found at ' . $vendorDiagnostics['phpseclib_source'] : 'Net/SFTP.php not found in supported layouts'; foreach ($vendorDiagnostics['errors'] as $error) { $details[] = 'Loader error: ' . $error; } return [ 'ready' => false, 'driver' => 'none', 'message' => 'SFTP NOT READY', 'detail' => implode(' · ', $details), ]; } function scan_server_path( string $path, array $keywordRules, array $riskExtensions ): array { $path = trim($path); if ($path === '') { throw new RuntimeException('Enter a server file path.'); } $real = realpath($path); if ($real === false || !is_file($real) || !is_readable($real)) { throw new RuntimeException('The server file path does not exist or is not readable.'); } $allowedRoots = array_filter([ realpath(__DIR__), realpath(dirname(__DIR__)), realpath($_SERVER['DOCUMENT_ROOT'] ?? ''), ]); $allowed = false; foreach ($allowedRoots as $root) { if ($root && str_starts_with($real, $root)) { $allowed = true; break; } } if (!$allowed) { throw new RuntimeException('The requested path is outside the permitted website roots.'); } $size = filesize($real); if ($size === false || $size <= 0 || $size > MAX_UPLOAD_BYTES) { throw new RuntimeException('The server file is empty or exceeds the scan limit.'); } $copyName = now_id() . '_server_' . safe_name(basename($real)); $destination = UPLOAD_DIR . '/' . $copyName; if (!copy($real, $destination)) { throw new RuntimeException('Could not copy the server file into private scanner storage.'); } @chmod($destination, 0600); $record = scan_file($destination, basename($real), $keywordRules, $riskExtensions); $record['source'] = 'server_path'; $record['server_path'] = $real; return $record; } function open_verified_sftp( string $host, int $port, string $username, string $password, string $expectedFingerprint ): array { if (!class_exists('\\phpseclib3\\Net\\SFTP')) { throw new RuntimeException( 'Full SFTP browsing is unavailable because phpseclib is not bundled on this server. Use the Manual Remote Path mode below, or upload the vendor folder later.' ); } if ($host === '' || $username === '' || $password === '' || $expectedFingerprint === '') { throw new RuntimeException('Host, username, password, and verified fingerprint are required.'); } $sftp = new \phpseclib3\Net\SFTP($host, $port, 18); if (method_exists($sftp, 'setPreferredAlgorithms')) { $sftp->setPreferredAlgorithms([ 'hostkey' => [ 'ssh-ed25519', 'ecdsa-sha2-nistp256', 'rsa-sha2-512', 'rsa-sha2-256', 'ssh-rsa', ], ]); } $serverKey = $sftp->getServerPublicHostKey(); if (!is_string($serverKey) || $serverKey === '') { throw new RuntimeException('Could not retrieve the SFTP server host key.'); } $verifiedHostKey = assert_trusted_host_key($host, $serverKey, $expectedFingerprint); if (!$sftp->login($username, $password)) { throw new RuntimeException('SFTP authentication failed. The host fingerprint is verified; re-enter the current IONOS SFTP password for user ' . $username . '.'); } return [$sftp, $verifiedHostKey['fingerprint']]; } 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; $requested = trim($path); $workingDirectory = method_exists($sftp, 'pwd') ? (string)$sftp->pwd() : ''; /* * IONOS SFTP accounts may be chrooted. In that case /htdocs can be * invalid even though the website is stored in an htdocs directory on * the hosting filesystem. Try the requested path first, then the actual * SFTP working directory and common chroot-relative roots. */ $candidates = array_values(array_unique(array_filter([ $requested, $workingDirectory, '.', '/', 'htdocs', '/htdocs', ], static fn($value) => $value !== ''))); $list = false; $resolvedPath = ''; foreach ($candidates as $candidate) { $attempt = $sftp->rawlist($candidate); if (is_array($attempt)) { $list = $attempt; $resolvedPath = $candidate; break; } } if (!is_array($list)) { throw new RuntimeException( 'SFTP login succeeded, but no readable starting directory was found. ' . 'Requested: ' . ($requested ?: '(empty)') . '. ' . 'Server working directory: ' . ($workingDirectory ?: '(unknown)') . '.' ); } $GLOBALS['resolvedSftpPath'] = $resolvedPath; $rows = []; foreach ($list as $name => $meta) { if ($name === '.' || $name === '..') { continue; } $isDir = (($meta['type'] ?? 0) === 2); $base = rtrim($resolvedPath, '/'); if ($base === '' || $base === '.') { $fullPath = $name; } elseif ($base === '/') { $fullPath = '/' . $name; } else { $fullPath = $base . '/' . $name; } $rows[] = [ 'name' => (string)$name, 'path' => $fullPath, '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); if (method_exists($sftp, 'setPreferredAlgorithms')) { $sftp->setPreferredAlgorithms([ 'hostkey' => [ 'ssh-ed25519', 'ecdsa-sha2-nistp256', 'rsa-sha2-512', 'rsa-sha2-256', 'ssh-rsa', ], ]); } $serverKey = $sftp->getServerPublicHostKey(); if (!is_string($serverKey) || $serverKey === '') { throw new RuntimeException('Could not retrieve the SFTP server host key.'); } $verifiedHostKey = assert_trusted_host_key($host, $serverKey, $expectedFingerprint); $actualFingerprint = $verifiedHostKey['fingerprint']; if (!$sftp->login($username, $password)) { throw new RuntimeException('SFTP authentication failed. The host fingerprint is verified; re-enter the current IONOS SFTP password for user ' . $username . '.'); } $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. The host fingerprint is verified; re-enter the current IONOS SFTP password for user ' . $username . '.'); } $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 build_project_graph(array $history): array { $nodes = []; $edges = []; $fileNames = []; foreach ($history as $record) { $fileId = (string)($record['id'] ?? ''); $fileName = (string)($record['original_name'] ?? 'unknown'); $fileNames[strtolower(basename($fileName))] = $fileId; $nodes['file:' . $fileId] = [ 'id' => 'file:' . $fileId, 'label' => $fileName, 'type' => 'file', 'file_id' => $fileId, 'score' => (int)($record['score'] ?? 0), 'connected' => false, ]; } foreach ($history as $record) { $fileId = (string)($record['id'] ?? ''); $fileNode = 'file:' . $fileId; $map = $record['source_map'] ?? []; foreach (($map['includes'] ?? []) as $item) { $target = (string)($item['path'] ?? ''); if ($target === '') continue; $base = strtolower(basename(parse_url($target, PHP_URL_PATH) ?: $target)); $targetNode = isset($fileNames[$base]) ? 'file:' . $fileNames[$base] : 'external:' . sha1($target); if (!isset($nodes[$targetNode])) { $nodes[$targetNode] = [ 'id' => $targetNode, 'label' => $target, 'type' => isset($fileNames[$base]) ? 'file' : 'unresolved', 'connected' => isset($fileNames[$base]), ]; } $nodes[$fileNode]['connected'] = true; $nodes[$targetNode]['connected'] = true; $edges[] = ['from' => $fileNode, 'to' => $targetNode, 'type' => 'include']; } foreach (($map['api_calls'] ?? []) as $item) { $target = (string)($item['target'] ?? ''); if ($target === '') continue; $targetNode = 'endpoint:' . sha1($target); $nodes[$targetNode] ??= [ 'id' => $targetNode, 'label' => $target, 'type' => 'endpoint', 'connected' => true, ]; $nodes[$fileNode]['connected'] = true; $edges[] = ['from' => $fileNode, 'to' => $targetNode, 'type' => 'api']; } foreach (($map['forms'] ?? []) as $item) { $target = (string)($item['action'] ?? ''); if ($target === '') continue; $targetNode = 'endpoint:' . sha1($target); $nodes[$targetNode] ??= [ 'id' => $targetNode, 'label' => $target, 'type' => 'endpoint', 'connected' => true, ]; $nodes[$fileNode]['connected'] = true; $edges[] = ['from' => $fileNode, 'to' => $targetNode, 'type' => 'form']; } foreach (($map['links'] ?? []) as $item) { $target = (string)($item['url'] ?? ''); if ($target === '' || str_starts_with($target, '#')) continue; $targetNode = 'link:' . sha1($target); $nodes[$targetNode] ??= [ 'id' => $targetNode, 'label' => $target, 'type' => 'link', 'connected' => true, ]; $nodes[$fileNode]['connected'] = true; $edges[] = ['from' => $fileNode, 'to' => $targetNode, 'type' => 'link']; } } $connectedCount = 0; $unconnectedCount = 0; foreach ($nodes as $node) { if (($node['type'] ?? '') === 'file') { if (!empty($node['connected'])) $connectedCount++; else $unconnectedCount++; } } return [ 'nodes' => array_values($nodes), 'edges' => $edges, 'connected_files' => $connectedCount, 'unconnected_files' => $unconnectedCount, ]; } function normalize_upload_files(array $files): array { $normalized = []; if (!isset($files['name'])) return $normalized; if (!is_array($files['name'])) { return [$files]; } foreach ($files['name'] as $i => $name) { $normalized[] = [ 'name' => $name, 'type' => $files['type'][$i] ?? '', 'tmp_name' => $files['tmp_name'][$i] ?? '', 'error' => $files['error'][$i] ?? UPLOAD_ERR_NO_FILE, 'size' => $files['size'][$i] ?? 0, ]; } return $normalized; } function safe_project_root(string $root): string { $real = realpath($root); if (!$real || !is_dir($real)) { throw new RuntimeException('Project root does not exist.'); } $allowedRoots = array_filter([ realpath($_SERVER['DOCUMENT_ROOT'] ?? ''), realpath(__DIR__), realpath(dirname(__DIR__)), ]); foreach ($allowedRoots as $allowedRoot) { if (str_starts_with($real, $allowedRoot)) { return $real; } } throw new RuntimeException('Project root is outside the permitted website directories.'); } function project_file_category(string $path): string { $name = strtolower(basename($path)); $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); $p = strtolower(str_replace('\\', '/', $path)); if (str_contains($p, '/vendor/')) return 'Vendor'; if (str_contains($p, '/api/') || str_contains($name, 'webhook')) return 'Endpoint'; if (str_contains($name, 'auth') || str_contains($name, 'login') || str_contains($name, 'session')) return 'Authentication'; if (str_contains($name, 'config') || in_array($ext, ['env','ini','yaml','yml'], true)) return 'Configuration'; if (str_contains($name, 'db') || str_contains($name, 'database') || $ext === 'sql') return 'Database'; if (in_array($ext, ['css','scss','sass'], true)) return 'Styles'; if (in_array($ext, ['js','mjs','ts','tsx','jsx'], true)) return 'JavaScript'; if (in_array($ext, ['png','jpg','jpeg','gif','webp','svg','mp4','mov','mp3','wav'], true)) return 'Assets'; if (in_array($ext, ['php','phtml'], true)) return 'Application'; if (in_array($ext, ['html','htm'], true)) return 'Frontend'; if ($ext === 'log') return 'Logs'; return 'Other'; } function discover_project_files(string $root, int $limit = 1200): array { $root = safe_project_root($root); $skip = ['.git','node_modules','_simon_private_scanner','_scanner_private','cache']; $files = []; $iterator = new RecursiveIteratorIterator( new RecursiveCallbackFilterIterator( new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS), static function (SplFileInfo $current) use ($skip): bool { return !($current->isDir() && in_array($current->getFilename(), $skip, true)); } ) ); foreach ($iterator as $item) { if (!$item->isFile()) continue; if (count($files) >= $limit) break; $path = $item->getPathname(); $relative = ltrim(str_replace($root, '', $path), DIRECTORY_SEPARATOR); $files[] = [ 'path' => $path, 'relative' => str_replace(DIRECTORY_SEPARATOR, '/', $relative), 'name' => $item->getFilename(), 'size' => $item->getSize(), 'modified' => $item->getMTime(), 'extension' => strtolower($item->getExtension()), 'category' => project_file_category($path), ]; } usort($files, static fn($a,$b) => strcasecmp($a['relative'],$b['relative'])); return $files; } function project_tree_from_files(array $files): array { $tree = []; foreach ($files as $file) { $relative = trim(str_replace('\\', '/', (string)($file['relative'] ?? '')), '/'); if ($relative === '') continue; $parts = explode('/', $relative); $cursor =& $tree; foreach ($parts as $index => $part) { $isFile = $index === count($parts) - 1; if ($isFile) { $cursor['__files'][] = $file; } else { if (!isset($cursor[$part]) || !is_array($cursor[$part])) { $cursor[$part] = []; } $cursor =& $cursor[$part]; } } unset($cursor); } return $tree; } function render_project_tree(array $tree, string $csrf, int $depth = 0): string { $html = '<div class="hostTreeLevel depth-' . $depth . '">'; $directories = array_filter(array_keys($tree), static fn(string $key): bool => $key !== '__files'); natcasesort($directories); foreach ($directories as $directory) { $children = $tree[$directory]; $html .= '<details class="hostTreeFolder"' . ($depth < 1 ? ' open' : '') . '>'; $html .= '<summary><span class="treeChevron">›</span><span class="treeFolderIcon">◈</span><b>' . h($directory) . '</b><small>' . count($children['__files'] ?? []) . ' direct</small></summary>'; $html .= render_project_tree($children, $csrf, $depth + 1); $html .= '</details>'; } $files = $tree['__files'] ?? []; usort($files, static fn(array $a, array $b): int => strcasecmp((string)$a['name'], (string)$b['name'])); foreach ($files as $file) { $path = (string)$file['path']; $relative = (string)$file['relative']; $html .= '<div class="hostTreeFile projectFileRow" data-search="' . h(strtolower($relative . ' ' . ($file['category'] ?? ''))) . '">'; $html .= '<div class="treeFileMeta"><span class="treeFileIcon">✦</span><div><b>' . h((string)$file['name']) . '</b><small>' . h((string)($file['category'] ?? 'File')) . ' · ' . h(human_bytes((int)($file['size'] ?? 0))) . '</small></div></div>'; $html .= '<div class="treeFileActions">'; $html .= '<form method="post" action="?action=open_project_file#editorPanel"><input type="hidden" name="csrf" value="' . h($csrf) . '"><input type="hidden" name="file_path" value="' . h($path) . '"><button type="submit">Open</button></form>'; $html .= '<form method="post" action="?action=scan_project_file#overview"><input type="hidden" name="csrf" value="' . h($csrf) . '"><input type="hidden" name="file_path" value="' . h($path) . '"><button type="submit" class="primary">Scan</button></form>'; $html .= '</div></div>'; } return $html . '</div>'; } function create_checkpoint(string $projectId, string $filePath, string $reason = 'manual'): array { $real = realpath($filePath); if (!$real || !is_file($real)) { throw new RuntimeException('Checkpoint source file was not found.'); } $dir = CHECKPOINTS_DIR . '/' . preg_replace('/[^A-Za-z0-9_-]/', '_', $projectId); if (!is_dir($dir)) mkdir($dir, 0700, true); $id = now_id(); $target = $dir . '/' . $id . '_' . safe_name(basename($real)); if (!copy($real, $target)) { throw new RuntimeException('Checkpoint copy failed.'); } @chmod($target, 0600); $meta = [ 'id' => $id, 'project_id' => $projectId, 'file_path' => $real, 'checkpoint_path' => $target, 'reason' => $reason, 'sha256' => hash_file('sha256', $target), 'created_at' => date('c'), ]; write_json_file($target . '.json', $meta); return $meta; } function list_checkpoints(string $projectId, int $limit = 30): array { $dir = CHECKPOINTS_DIR . '/' . preg_replace('/[^A-Za-z0-9_-]/', '_', $projectId); if (!is_dir($dir)) return []; $items = []; foreach (glob($dir . '/*.json') ?: [] as $metaFile) { $meta = read_json_file($metaFile, []); if ($meta) $items[] = $meta; } usort($items, static fn($a,$b) => strcmp((string)($b['created_at']??''),(string)($a['created_at']??''))); return array_slice($items, 0, $limit); } function ai_review_queue(): array { return read_json_file(REVIEWS_FILE, []); } function add_ai_review_candidate(array $candidate): array { $queue = ai_review_queue(); $candidate['id'] = now_id(); $candidate['created_at'] = date('c'); array_unshift($queue, $candidate); write_json_file(REVIEWS_FILE, array_slice($queue, 0, 300)); return $candidate; } function endpoint_inventory_from_history(array $history): array { $rows = []; foreach ($history as $record) { $file = (string)($record['original_name'] ?? 'unknown'); $map = $record['source_map'] ?? []; foreach (($map['forms'] ?? []) as $form) { $target = (string)($form['action'] ?? ''); if ($target === '') continue; $rows[] = ['source'=>$file,'endpoint'=>$target,'kind'=>'form','auth'=>'unknown','csrf'=>'unknown','status'=>'review']; } foreach (($map['api_calls'] ?? []) as $api) { $target = (string)($api['target'] ?? ''); if ($target === '') continue; $rows[] = ['source'=>$file,'endpoint'=>$target,'kind'=>'api','auth'=>'unknown','csrf'=>'n/a','status'=>'review']; } } return $rows; } function grade_unified_project(array $history, array $endpoints): array { if (!$history) return ['score'=>0,'grade'=>'N/A','summary'=>'No project scans yet.']; $score = 100; $critical = 0; $high = 0; foreach ($history as $record) { $severity = $record['severity'] ?? 'low'; if ($severity === 'critical') { $critical++; $score -= 18; } elseif ($severity === 'high') { $high++; $score -= 10; } elseif ($severity === 'medium') { $score -= 4; } } $score -= min(20, count($endpoints) * 2); $score = max(0, min(100, $score)); $grade = match (true) { $score >= 95 => 'A+', $score >= 90 => 'A', $score >= 85 => 'B+', $score >= 80 => 'B', $score >= 70 => 'C', $score >= 60 => 'D', default => 'F', }; return [ 'score'=>$score, 'grade'=>$grade, 'summary'=>$critical > 0 ? 'Release blocked by critical findings.' : ($high > 0 ? 'Resolve high-risk findings before publishing.' : 'Eligible for assisted review.'), ]; } function beast_id(string $prefix): string { return $prefix . '_' . bin2hex(random_bytes(8)); } function beast_now(): string { return date('c'); } function beast_default_agents(): array { return [ [ 'id'=>'agent_architect','name'=>'Architecture Agent','role'=>'architecture', 'provider'=>'openai-compatible','model'=>'configured-default','authority'=>'proposal', 'enabled'=>true,'instructions'=>'Map modules, dependencies, duplicated responsibilities, and evolution paths.', 'capabilities'=>['architecture.review','dependency.map','modernization.plan'] ], [ 'id'=>'agent_security','name'=>'Security Agent','role'=>'security', 'provider'=>'openai-compatible','model'=>'configured-default','authority'=>'proposal', 'enabled'=>true,'instructions'=>'Require evidence for every finding. Never classify unknown as safe.', 'capabilities'=>['security.review','endpoint.audit','secret.detect'] ], [ 'id'=>'agent_function','name'=>'Function Intelligence Agent','role'=>'function', 'provider'=>'simon-local+ai','model'=>'hybrid','authority'=>'proposal', 'enabled'=>true,'instructions'=>'Classify canonical, duplicate, unused, broken, shared, archive, and project-specific functions.', 'capabilities'=>['function.graph','function.criteria','duplicate.detect'] ], [ 'id'=>'agent_builder','name'=>'WEB360 Builder Agent','role'=>'builder', 'provider'=>'openai-compatible','model'=>'configured-default','authority'=>'approval_required', 'enabled'=>true,'instructions'=>'Return complete production-ready files. Preserve working behavior and create checkpoints.', 'capabilities'=>['code.generate','file.modify','responsive.fix'] ], [ 'id'=>'agent_qa','name'=>'Verification Agent','role'=>'qa', 'provider'=>'simon-local+ai','model'=>'hybrid','authority'=>'advisory', 'enabled'=>true,'instructions'=>'Validate requirements, security gates, links, layouts, and rollback readiness.', 'capabilities'=>['scan.validate','report.audit','deployment.verify'] ], [ 'id'=>'agent_orchestrator','name'=>'SIMON Orchestrator','role'=>'orchestrator', 'provider'=>'policy-engine','model'=>'rules+ai','authority'=>'approval_required', 'enabled'=>true,'instructions'=>'Assign jobs, enforce authority, route data, compare results, and stop unsafe runs.', 'capabilities'=>['job.route','flow.execute','agent.coordinate','approval.enforce'] ], ]; } function beast_agents(): array { $stored=read_json_file(BEAST_AGENTS_FILE, []); return $stored ?: beast_default_agents(); } function beast_jobs(): array { return read_json_file(BEAST_JOBS_FILE, []); } function beast_flows(): array { $stored=read_json_file(BEAST_FLOWS_FILE, []); if ($stored) return $stored; return [[ 'id'=>'flow_project_intelligence','name'=>'Project Intelligence Pipeline','enabled'=>true, 'trigger'=>'manual_or_changed_files','authority'=>'approval_required', 'steps'=>[ ['order'=>1,'type'=>'inventory','agent'=>'agent_orchestrator','action'=>'host.scan'], ['order'=>2,'type'=>'analysis','agent'=>'agent_function','action'=>'function.graph'], ['order'=>3,'type'=>'analysis','agent'=>'agent_security','action'=>'security.review'], ['order'=>4,'type'=>'analysis','agent'=>'agent_architect','action'=>'architecture.review'], ['order'=>5,'type'=>'verification','agent'=>'agent_qa','action'=>'scan.validate'], ['order'=>6,'type'=>'approval','agent'=>'agent_orchestrator','action'=>'owner.approval'], ['order'=>7,'type'=>'build','agent'=>'agent_builder','action'=>'approved.change'], ['order'=>8,'type'=>'verification','agent'=>'agent_qa','action'=>'deployment.verify'], ], 'created_at'=>beast_now(),'updated_at'=>beast_now() ]]; } function beast_criteria(): array { $stored=read_json_file(BEAST_CRITERIA_FILE, []); if ($stored) return $stored; return [ 'profile_name'=>'SIMON Complete Project Intelligence', 'version'=>1, 'enabled'=>true, 'scope'=>['php','html','css','javascript','typescript','swift','sql','json','xml','assets'], 'rules'=>[ ['id'=>'functions','label'=>'Function call graph','enabled'=>true,'severity'=>'info','evidence_required'=>true], ['id'=>'routes','label'=>'Pages, routes, forms, APIs, redirects','enabled'=>true,'severity'=>'medium','evidence_required'=>true], ['id'=>'database','label'=>'SQL tables, columns, reads, writes','enabled'=>true,'severity'=>'medium','evidence_required'=>true], ['id'=>'security','label'=>'Input, SQLi, XSS, CSRF, upload, auth','enabled'=>true,'severity'=>'high','evidence_required'=>true], ['id'=>'responsive','label'=>'Desktop, tablet, mobile layout','enabled'=>true,'severity'=>'medium','evidence_required'=>true], ['id'=>'maintainability','label'=>'Complexity, duplication, dead code','enabled'=>true,'severity'=>'medium','evidence_required'=>true], ['id'=>'performance','label'=>'Blocking work, large assets, repeated calls','enabled'=>true,'severity'=>'medium','evidence_required'=>true], ['id'=>'ai_modernization','label'=>'AI modernization recommendations','enabled'=>true,'severity'=>'info','evidence_required'=>true], ], 'minimum_confidence'=>0.70, 'block_on_critical'=>true, 'unknown_is_safe'=>false, 'updated_at'=>beast_now(), ]; } function beast_save_json(string $path, array $data): void { write_json_file($path,$data); @chmod($path,0600); } function beast_find_agent(string $id): ?array { foreach (beast_agents() as $agent) if (($agent['id']??'')===$id) return $agent; return null; } function beast_create_job(array $input): array { $agentId=trim((string)($input['agent_id']??'')); $agent=beast_find_agent($agentId); if (!$agent) throw new RuntimeException('Selected agent does not exist.'); $title=trim((string)($input['title']??'')); if ($title==='') throw new RuntimeException('Job title is required.'); $risk=(string)($input['risk']??'medium'); if (!in_array($risk,['low','medium','high','critical'],true)) $risk='medium'; $status=($risk==='low' && ($agent['authority']??'')!=='approval_required')?'queued':'pending_approval'; return [ 'id'=>beast_id('job'),'title'=>$title,'description'=>trim((string)($input['description']??'')), 'agent_id'=>$agentId,'flow_id'=>trim((string)($input['flow_id']??'')), 'job_type'=>trim((string)($input['job_type']??'analysis')),'risk'=>$risk,'status'=>$status, 'authority'=>(string)($agent['authority']??'proposal'),'input'=>['project_root'=>$input['project_root']??null], 'result'=>null,'error'=>null,'created_at'=>beast_now(),'updated_at'=>beast_now(),'approved_at'=>null,'completed_at'=>null, ]; } function beast_run_job(array $job, array $context=[]): array { if (($job['status']??'')==='pending_approval') throw new RuntimeException('Job requires approval before execution.'); $job['status']='running'; $job['updated_at']=beast_now(); try { $type=(string)($job['job_type']??'analysis'); if ($type==='function_intelligence') { $files=$context['project_files']??[]; $result=build_function_intelligence($files); save_function_intelligence_cache($result); $job['result']=['summary'=>$result['summary']??[],'message'=>'Function intelligence rebuilt.']; } elseif ($type==='host_scan') { $root=(string)($job['input']['project_root']??$context['project_root']??__DIR__); $state=start_host_scan($root); $job['result']=['scan_uuid'=>$state['scan_uuid']??null,'files'=>$state['total']??0,'message'=>'Host scan initialized; continue batches in Host Scan + DB.']; } elseif ($type==='criteria_validate') { $dictionary=load_data_dictionary_contract(); $job['result']=validate_data_dictionary_contract($dictionary); } elseif ($type==='report_audit') { $history=history(); $job['result']=['scan_records'=>count($history),'blank_sections_prevented'=>true,'message'=>'Report coverage audit completed.']; } else { $job['result']=['message'=>'Job accepted by SIMON Orchestrator. Agent-specific work is queued for connected AI or manual review.','criteria'=>beast_criteria()]; } $job['status']='completed'; $job['completed_at']=beast_now(); } catch (Throwable $e) { $job['status']='failed'; $job['error']=$e->getMessage(); } $job['updated_at']=beast_now(); return $job; } function beast_stats(): array { $jobs=beast_jobs(); $agents=beast_agents(); $flows=beast_flows(); $count=function(string $status)use($jobs){return count(array_filter($jobs,fn($j)=>($j['status']??'')===$status));}; return ['agents'=>count($agents),'flows'=>count($flows),'jobs'=>count($jobs),'pending'=>$count('pending_approval'),'running'=>$count('running'),'completed'=>$count('completed'),'failed'=>$count('failed')]; } function project_dna_defaults(): array { return [ 'project_name' => 'SIMON Unified Project', 'purpose' => '', 'primary_goal' => 'Understand, secure, improve, and preserve the project.', 'intended_users' => '', 'business_objective' => '', 'required_features' => [], 'known_problems' => [], 'preserve_rules' => [ 'Preserve working behavior unless an approved change requires otherwise.', 'Create a checkpoint before modifying files.', 'Do not expose credentials or private configuration.', ], 'change_rules' => [ 'Prefer incremental changes over full rebuilds.', 'Reuse canonical functions before creating duplicates.', 'Re-scan affected files after a change.', ], 'security_requirements' => [ 'Protect administrative endpoints.', 'Require authentication, authorization, validation, and CSRF where applicable.', 'Treat public uploads, webhooks, and database writes as high-risk.', ], 'success_criteria' => [ 'No critical release blockers.', 'No broken required references.', 'Approved changes are checkpointed and verified.', ], 'scan_focus' => 'full', 'ai_instructions' => '', 'owner_notes' => '', 'updated_at' => null, ]; } function project_dna(): array { $stored = read_json_file(PROJECT_DNA_FILE, []); return array_replace_recursive(project_dna_defaults(), is_array($stored) ? $stored : []); } function normalize_text_list(string $value): array { $lines = preg_split('/\R+/', trim($value)) ?: []; return array_values(array_filter(array_map('trim', $lines), static fn($line) => $line !== '')); } function save_project_dna(array $dna): void { $dna['updated_at'] = date('c'); write_json_file(PROJECT_DNA_FILE, $dna); @chmod(PROJECT_DNA_FILE, 0600); } function function_intelligence_cache(): array { return read_json_file(FUNCTION_INTEL_FILE, [ 'generated_at' => null, 'summary' => [], 'functions' => [], 'duplicates' => [], 'broken_references' => [], ]); } function save_function_intelligence_cache(array $data): void { write_json_file(FUNCTION_INTEL_FILE, $data); @chmod(FUNCTION_INTEL_FILE, 0600); } function function_normalize_body(string $body): string { $body = preg_replace('#/\\*.*?\\*/#s', '', $body) ?? $body; $body = preg_replace('/\\/\\/.*$/m', '', $body) ?? $body; $body = preg_replace('/#.*$/m', '', $body) ?? $body; $body = preg_replace("~([\"'])(?:\\\\.|(?!\\1).)*\\1~s", 'STR', $body) ?? $body; $body = preg_replace('/\\b\\d+(?:\\.\\d+)?\\b/', 'NUM', $body) ?? $body; $body = preg_replace('/\\$[A-Za-z_][A-Za-z0-9_]*/', '$VAR', $body) ?? $body; $body = preg_replace('/\\s+/', '', $body) ?? $body; return strtolower($body); } function function_extract_block(string $source, int $braceAt): string { $depth = 0; $quote = null; $escaped = false; $limit = min(strlen($source), $braceAt + 120000); for ($i = $braceAt; $i < $limit; $i++) { $char = $source[$i]; if ($quote !== null) { if ($escaped) { $escaped = false; continue; } if ($char === '\\\\') { $escaped = true; continue; } if ($char === $quote) $quote = null; continue; } if ($char === '"' || $char === "'") { $quote = $char; continue; } if ($char === '{') $depth++; if ($char === '}') { $depth--; if ($depth === 0) return substr($source, $braceAt, $i - $braceAt + 1); } } return substr($source, $braceAt, max(0, $limit - $braceAt)); } function function_extract_from_file(array $file): array { $path = (string)($file['path'] ?? ''); $relative = (string)($file['relative'] ?? basename($path)); $ext = strtolower((string)($file['extension'] ?? pathinfo($path, PATHINFO_EXTENSION))); if (!in_array($ext, ['php','phtml','js','mjs','ts','tsx','jsx'], true)) return []; if (!is_file($path) || !is_readable($path) || filesize($path) > 2500000) return []; $source = @file_get_contents($path); if (!is_string($source)) return []; $patterns = []; if (in_array($ext, ['php','phtml'], true)) { $patterns[] = '/\\bfunction\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\(([^)]*)\\)\\s*(?::\\s*([^\\s{]+))?\\s*\\{/m'; } else { $patterns[] = '/\\bfunction\\s+([A-Za-z_$][A-Za-z0-9_$]*)\\s*\\(([^)]*)\\)\\s*\\{/m'; $patterns[] = '/\\b(?:const|let|var)\\s+([A-Za-z_$][A-Za-z0-9_$]*)\\s*=\\s*(?:async\\s*)?\\(([^)]*)\\)\\s*=>\\s*\\{/m'; } $rows = []; foreach ($patterns as $pattern) { if (!preg_match_all($pattern, $source, $matches, PREG_OFFSET_CAPTURE)) continue; foreach ($matches[1] as $i => $nameMatch) { $name = (string)$nameMatch[0]; $offset = (int)$matches[0][$i][1]; $full = (string)$matches[0][$i][0]; $braceAt = $offset + strrpos($full, '{'); $body = function_extract_block($source, $braceAt); $normalized = function_normalize_body($body); $start = substr_count(substr($source, 0, $offset), "\\n") + 1; $end = $start + substr_count($body, "\\n"); $params = (string)($matches[2][$i][0] ?? ''); $ret = (string)($matches[3][$i][0] ?? ''); $rows[] = [ 'id' => sha1($relative.'|'.$name.'|'.$start), 'name' => $name, 'signature' => $name.'('.trim($params).')'.($ret !== '' ? ': '.$ret : ''), 'language' => in_array($ext, ['php','phtml'], true) ? 'php' : 'javascript', 'file_path' => $path, 'relative_path' => $relative, 'start_line' => $start, 'end_line' => $end, 'body_hash' => hash('sha256', $body), 'normalized_hash' => hash('sha256', $normalized), 'normalized_source' => substr($normalized, 0, 15000), 'body_length' => strlen($body), 'incoming_references' => 0, 'callers' => [], 'runtime_errors' => 0, 'classification' => 'project-specific', 'confidence' => 0.70, 'canonical_function_id' => null, 'shared_module_candidate' => false, 'archive_safe' => false, 'reasons' => ['No strong reuse, duplicate, or archive signal'], ]; } } return $rows; } function build_function_intelligence(array $projectFiles): array { $functions = []; $sources = []; foreach (array_slice($projectFiles, 0, 700) as $file) { $path = (string)($file['path'] ?? ''); $ext = strtolower((string)($file['extension'] ?? '')); if (!in_array($ext, ['php','phtml','js','mjs','ts','tsx','jsx'], true)) continue; if (!is_file($path) || !is_readable($path) || filesize($path) > 2500000) continue; $content = @file_get_contents($path); if (!is_string($content)) continue; $sources[] = ['relative'=>(string)$file['relative'], 'content'=>$content]; foreach (function_extract_from_file($file) as $row) $functions[] = $row; } $nameMap = []; foreach ($functions as $i => $f) $nameMap[$f['name']][] = $i; $broken = []; foreach ($sources as $source) { if (!preg_match_all('/\\b([A-Za-z_$][A-Za-z0-9_$]*)\\s*\\(/', $source['content'], $calls, PREG_OFFSET_CAPTURE)) continue; foreach ($calls[1] as $call) { $name = (string)$call[0]; $offset = (int)$call[1]; $reserved = ['if','for','foreach','while','switch','catch','isset','empty','array','echo','print','include','require','return','new','function','match','count','strlen','substr','trim','json_encode','json_decode','preg_match','preg_match_all']; if (in_array(strtolower($name), $reserved, true)) continue; if (isset($nameMap[$name])) { foreach ($nameMap[$name] as $idx) { $functions[$idx]['incoming_references']++; $functions[$idx]['callers'][] = $source['relative']; } } else { $line = substr_count(substr($source['content'], 0, $offset), "\\n") + 1; $key = strtolower($source['relative'].'|'.$name.'|'.$line); $broken[$key] = ['source_file'=>$source['relative'],'target'=>$name,'line'=>$line,'type'=>'unresolved call']; } } } foreach ($functions as &$f) $f['callers'] = array_values(array_unique($f['callers'])); unset($f); $groups = []; foreach ($functions as $i => $f) $groups[$f['normalized_hash']][] = $i; $duplicates = []; foreach ($groups as $indexes) { if (count($indexes) < 2) continue; $canonical = $indexes[0]; foreach ($indexes as $idx) { if ($functions[$idx]['incoming_references'] > $functions[$canonical]['incoming_references']) $canonical = $idx; } foreach ($indexes as $idx) { $functions[$idx]['canonical_function_id'] = $functions[$canonical]['id']; $functions[$idx]['reasons'] = [$idx === $canonical ? 'Preferred exact-duplicate implementation' : 'Exact normalized body match']; $functions[$idx]['classification'] = $idx === $canonical ? 'canonical' : 'duplicate'; $functions[$idx]['confidence'] = $idx === $canonical ? 0.94 : 0.98; } $duplicates[] = ['type'=>'exact','similarity'=>1.0,'canonical_function_id'=>$functions[$canonical]['id'],'function_ids'=>array_map(fn($i)=>$functions[$i]['id'],$indexes)]; } foreach ($functions as &$f) { if ($f['incoming_references'] === 0 && $f['classification'] === 'project-specific') { $f['classification'] = 'unused'; $f['confidence'] = 0.72; $f['reasons'] = ['No static callers found']; } $path = strtolower($f['relative_path']); $signals = ['auth','security','csrf','validate','mail','email','http','logger','config','database','encrypt','decrypt','sanitize','cache']; $signal = false; foreach ($signals as $word) if (str_contains(strtolower($f['name'].' '.$path), $word)) { $signal = true; break; } if ($f['incoming_references'] >= 2 && $signal && !str_contains($path, '/vendor/')) { $f['shared_module_candidate'] = true; if ($f['classification'] === 'project-specific') { $f['classification'] = 'shared-candidate'; $f['confidence'] = 0.80; $f['reasons'] = ['Reusable utility with multiple callers']; } } if ($f['incoming_references'] === 0 && (str_contains($path,'legacy') || str_contains($path,'old') || str_contains($path,'archive'))) { $f['archive_safe'] = true; $f['classification'] = 'archive-candidate'; $f['confidence'] = 0.76; $f['reasons'] = ['Legacy path and no static callers']; } } unset($f); $summary = [ 'total'=>count($functions), 'canonical'=>count(array_filter($functions,fn($f)=>$f['classification']==='canonical')), 'duplicates'=>count(array_filter($functions,fn($f)=>$f['classification']==='duplicate')), 'unused'=>count(array_filter($functions,fn($f)=>$f['classification']==='unused')), 'broken_references'=>count($broken), 'high_error'=>count(array_filter($functions,fn($f)=>$f['classification']==='high-error')), 'shared_candidates'=>count(array_filter($functions,fn($f)=>!empty($f['shared_module_candidate']))), 'archive_candidates'=>count(array_filter($functions,fn($f)=>!empty($f['archive_safe']))), 'project_specific'=>count(array_filter($functions,fn($f)=>$f['classification']==='project-specific')), ]; return [ 'generated_at'=>date('c'), 'summary'=>$summary, 'functions'=>$functions, 'duplicates'=>$duplicates, 'broken_references'=>array_values($broken), 'source_files_scanned'=>count($sources), ]; } function simon_uuid(): string { $data = random_bytes(16); $data[6] = chr((ord($data[6]) & 0x0f) | 0x40); $data[8] = chr((ord($data[8]) & 0x3f) | 0x80); $hex = bin2hex($data); return substr($hex, 0, 8) . '-' . substr($hex, 8, 4) . '-' . substr($hex, 12, 4) . '-' . substr($hex, 16, 4) . '-' . substr($hex, 20); } function simon_db_connection(): ?PDO { static $cacheKey = null; static $pdo = null; /* * Preferred connection: the centralized private config helper. */ if (function_exists('simon_database_connection')) { try { $candidatePdo = simon_database_connection('simon'); if ($candidatePdo instanceof PDO) { $selectedDatabase = (string)$candidatePdo->query('SELECT DATABASE()')->fetchColumn(); if ($selectedDatabase !== 'dbs15883515') { throw new RuntimeException( 'SIMON scanner connected to the wrong database: ' . $selectedDatabase ); } return $candidatePdo; } } catch (Throwable $configError) { $_SESSION['simon_db_last_error'] = $configError->getMessage(); } } $sessionPassword = (string)($_SESSION['simon_db_password'] ?? ''); $effectivePassword = (string)( getenv('SIMON_DB_PASSWORD') ?: (defined('SIMON_DB_PASSWORD') ? SIMON_DB_PASSWORD : '') ?: $sessionPassword ); $effectiveHost = trim((string)( getenv('SIMON_DB_HOST') ?: 'db5020893348.hosting-data.io' )); $effectivePort = (int)( getenv('SIMON_DB_PORT') ?: 3306 ); $effectiveDatabase = trim((string)( getenv('SIMON_DB_NAME') ?: 'dbs15883515' )); $effectiveUser = (string)( getenv('SIMON_DB_USER') ?: (defined('SIMON_DB_USER') ? SIMON_DB_USER : '') ?: 'dbu5579662' ); $newCacheKey = hash('sha256', implode('|', [ (string)getenv('SIMON_DB_DSN'), $effectiveHost, (string)$effectivePort, $effectiveDatabase, $effectiveUser, $effectivePassword !== '' ? 'password-present' : 'password-missing', ])); if ($cacheKey === $newCacheKey) { return $pdo; } $cacheKey = $newCacheKey; $pdo = null; $sharedCandidates = [ dirname(__DIR__) . '/_inc/simon_db.php', dirname(__DIR__) . '/_inc/db.php', __DIR__ . '/config/db.php', ]; foreach ($sharedCandidates as $candidate) { if (!is_file($candidate)) { continue; } try { require_once $candidate; } catch (Throwable $ignored) { // Continue to environment-based connection. } if (function_exists('simonDb')) { try { $candidatePdo = simonDb(); if ($candidatePdo instanceof PDO) { $pdo = $candidatePdo; return $pdo; } } catch (Throwable $ignored) { // Continue to environment-based connection. } } } $dsn = trim((string)(getenv('SIMON_DB_DSN') ?: (defined('SIMON_DB_DSN') ? SIMON_DB_DSN : ''))); $user = $effectiveUser; $password = $effectivePassword; if ($dsn === '') { $host = $effectiveHost; $database = $effectiveDatabase; $port = $effectivePort; if ($host !== '' && $database !== '') { $dsn = 'mysql:host=' . $host . ';port=' . $port . ';dbname=' . $database . ';charset=utf8mb4'; } } if ($dsn === '' || $user === '' || $password === '') { return null; } try { $pdo = new PDO($dsn, $user, $password, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_TIMEOUT => 12, ]); } catch (Throwable $error) { $_SESSION['simon_db_last_error'] = $error->getMessage(); log_event('database_connection_failed', ['error' => $error->getMessage()]); $pdo = null; } if ($pdo instanceof PDO) { try { $selectedDatabase = (string)$pdo->query('SELECT DATABASE()')->fetchColumn(); if ($selectedDatabase !== 'dbs15883515') { throw new RuntimeException( 'SIMON scanner connected to the wrong database: ' . $selectedDatabase ); } unset($_SESSION['simon_db_last_error']); } catch (Throwable $databaseCheckError) { $_SESSION['simon_db_last_error'] = $databaseCheckError->getMessage(); $pdo = null; } } return $pdo; } function simon_db_table_exists(PDO $pdo, string $table): bool { static $cache = []; if (array_key_exists($table, $cache)) { return $cache[$table]; } try { $stmt = $pdo->prepare( 'SELECT COUNT(*) FROM information_schema.tables ' . 'WHERE table_schema = DATABASE() AND table_name = ?' ); $stmt->execute([$table]); $cache[$table] = ((int)$stmt->fetchColumn()) > 0; } catch (Throwable $error) { $cache[$table] = false; } return $cache[$table]; } function xlsx_column_number(string $letters): int { $number = 0; foreach (str_split(strtoupper($letters)) as $letter) { $number = ($number * 26) + (ord($letter) - 64); } return max(0, $number - 1); } function xlsx_shared_strings(ZipArchive $zip): array { $xml = $zip->getFromName('xl/sharedStrings.xml'); if (!is_string($xml) || $xml === '') { return []; } $document = @simplexml_load_string($xml); if (!$document) { return []; } $document->registerXPathNamespace( 'x', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main' ); $strings = []; foreach ($document->xpath('//x:si') ?: [] as $item) { $parts = []; foreach ($item->xpath('.//x:t') ?: [] as $textNode) { $parts[] = (string)$textNode; } $strings[] = implode('', $parts); } return $strings; } function resolve_data_dictionary_file(): ?string { $configured = trim((string)(getenv('SIMON_BUG_CONTRACT_PATH') ?: '')); $candidates = array_values(array_filter([ $configured !== '' ? $configured : null, DATA_DICTIONARY_PREFERRED_FILE, __DIR__ . '/SIMON_BUG_MASTER_COMBINED_5W1H_HOST_COMPATIBLE.xlsx', __DIR__ . '/SIMON_BUG_Retrieval_Data_Dictionary.xlsx', __DIR__ . '/SIMON_BUG_Retrieval_Data_Dictionary_v2_5W1H_Stream_COMPATIBLE.xlsx', __DIR__ . '/SIMON_BUG_Retrieval_Data_Dictionary_v2_5W1H_Stream.xlsx', __DIR__ . '/SIMON_BUG_Retrieval_Data_Dictionary_v4_Accuracy_Function_Criteria.xlsx', __DIR__ . '/SIMON_BUG_Retrieval_Data_Dictionary_v3_Host_DB.xlsx', ])); foreach ($candidates as $candidate) { if (is_file($candidate) && is_readable($candidate)) { return $candidate; } } $matches = glob(__DIR__ . '/*.xlsx') ?: []; usort($matches, static fn(string $a, string $b): int => (filemtime($b) ?: 0) <=> (filemtime($a) ?: 0)); foreach ($matches as $candidate) { $name = strtolower(basename($candidate)); if ( is_file($candidate) && is_readable($candidate) && (str_contains($name, 'simon') || str_contains($name, 'retrieval') || str_contains($name, 'dictionary')) ) { return $candidate; } } return null; } function embedded_dictionary_contract(): array { $sheets = [ 'Overview','Scan Runs','Sources','Files','Signal Catalog','Signals','Functions', 'Relationships','Endpoints','Findings','5W1H Events','Population Workflow', 'Host Scan Contract','Database Mapping' ]; $fields = []; foreach ([ 'Files' => ['file_id','path','filename','extension','mime_type','size_bytes','sha256','modified_at'], 'Functions' => ['function_name','signature','line_start','line_end','incoming_references','outgoing_references','complexity','classification'], 'Endpoints' => ['route','method','handler','authentication','authorization','csrf','validation','risk_score'], 'Findings' => ['severity','category','title','evidence','recommendation','confidence'], '5W1H Events' => ['who','what','when','where','why','how','event_time','truth_state','make_true','make_false'], ] as $sheet => $names) { foreach ($names as $name) { $fields[] = [ 'sheet_name' => $sheet, 'field_name' => $name, 'required' => 'yes', 'retrieve_from' => 'SIMON embedded scanner contract', 'validation_normalization' => 'Non-empty when applicable; evidence required for inferred values', ]; } } return [ 'available' => true, 'source' => 'embedded_fallback', 'path' => null, 'modified_at' => null, 'hash' => hash('sha256', json_encode([$sheets,$fields])), 'sheets' => $sheets, 'fields' => $fields, 'signal_kinds' => [], 'error' => null, 'warning' => 'Excel workbook not found. The embedded validated contract was used so scanning can continue.', ]; } function read_xlsx_workbook(string $path): array { if (!class_exists('ZipArchive')) { throw new RuntimeException('PHP ZipArchive is required to read the Excel data dictionary.'); } if (!is_file($path) || !is_readable($path)) { throw new RuntimeException('Excel data dictionary was not found or is not readable: ' . $path); } $zip = new ZipArchive(); if ($zip->open($path) !== true) { throw new RuntimeException('Could not open the Excel data dictionary: ' . $path); } try { $workbookXml = $zip->getFromName('xl/workbook.xml'); $relsXml = $zip->getFromName('xl/_rels/workbook.xml.rels'); if (!is_string($workbookXml) || !is_string($relsXml)) { throw new RuntimeException('Excel workbook structure is incomplete.'); } libxml_use_internal_errors(true); $workbook = simplexml_load_string($workbookXml, 'SimpleXMLElement', LIBXML_NONET | LIBXML_COMPACT); $relationships = simplexml_load_string($relsXml, 'SimpleXMLElement', LIBXML_NONET | LIBXML_COMPACT); if (!$workbook || !$relationships) { $messages = array_map( static fn(LibXMLError $error): string => trim($error->message), libxml_get_errors() ); libxml_clear_errors(); throw new RuntimeException( 'Excel workbook XML could not be parsed' . ($messages ? ': ' . implode(' | ', array_slice($messages, 0, 3)) : '.') ); } $relationshipMap = []; foreach ($relationships->xpath('//*[local-name()="Relationship"]') ?: [] as $relationship) { $relationshipMap[(string)$relationship['Id']] = (string)$relationship['Target']; } $shared = xlsx_shared_strings($zip); $result = []; foreach ($workbook->xpath('//*[local-name()="sheets"]/*[local-name()="sheet"]') ?: [] as $sheetNode) { $name = trim((string)$sheetNode['name']); if ($name === '') { continue; } $relationId = ''; foreach ($sheetNode->attributes() as $attributeName => $attributeValue) { if (strtolower((string)$attributeName) === 'id') { $relationId = (string)$attributeValue; } } foreach ($sheetNode->getNamespaces(true) as $prefix => $namespace) { foreach ($sheetNode->attributes($namespace) as $attributeName => $attributeValue) { if (strtolower((string)$attributeName) === 'id') { $relationId = (string)$attributeValue; } } } $target = $relationshipMap[$relationId] ?? ''; if ($target === '') { continue; } // OOXML relationship targets may be absolute package paths such as // /xl/worksheets/sheet1.xml or workbook-relative paths such as // worksheets/sheet1.xml. Normalize both without duplicating xl/. $target = str_replace('\\', '/', (string)$target); $target = preg_replace('#^(?:\.\./)+#', '', $target); $target = ltrim((string)$target, '/'); $sheetPath = str_starts_with($target, 'xl/') ? $target : 'xl/' . $target; $sheetXml = $zip->getFromName($sheetPath); if (!is_string($sheetXml)) { continue; } $sheet = simplexml_load_string($sheetXml, 'SimpleXMLElement', LIBXML_NONET | LIBXML_COMPACT); if (!$sheet) { continue; } $rows = []; foreach ($sheet->xpath('//*[local-name()="sheetData"]/*[local-name()="row"]') ?: [] as $rowNode) { $row = []; foreach ($rowNode->xpath('./*[local-name()="c"]') ?: [] as $cell) { $reference = (string)$cell['r']; if (!preg_match('/^([A-Z]+)(\d+)$/i', $reference, $match)) { continue; } $column = xlsx_column_number(strtoupper($match[1])); $type = (string)$cell['t']; $value = ''; if ($type === 'inlineStr') { $parts = []; foreach ($cell->xpath('.//*[local-name()="t"]') ?: [] as $textNode) { $parts[] = (string)$textNode; } $value = implode('', $parts); } else { $valueNodes = $cell->xpath('./*[local-name()="v"]') ?: []; $rawValue = $valueNodes ? (string)$valueNodes[0] : ''; if ($type === 's') { $value = $shared[(int)$rawValue] ?? ''; } elseif ($type === 'b') { $value = $rawValue === '1'; } else { $value = $rawValue; } } $row[$column] = $value; } if ($row) { ksort($row); $maximum = max(array_keys($row)); $normalized = []; for ($column = 0; $column <= $maximum; $column++) { $normalized[] = $row[$column] ?? null; } $rows[] = $normalized; } } $result[$name] = $rows; } if (!$result) { throw new RuntimeException('Excel workbook was opened but no worksheets could be resolved.'); } return $result; } finally { $zip->close(); } } function load_data_dictionary_contract(): array { static $cached = null; if (is_array($cached)) { return $cached; } $cached = [ 'available' => false, 'path' => null, 'modified_at' => null, 'hash' => null, 'sheets' => [], 'fields' => [], 'signal_kinds' => [], 'error' => null, ]; try { $dictionaryPath = resolve_data_dictionary_file(); if ($dictionaryPath === null) { $cached = embedded_dictionary_contract(); return $cached; } $sheets = read_xlsx_workbook($dictionaryPath); $fields = []; $signalKinds = []; foreach ($sheets as $sheetName => $rows) { $headerIndex = null; $headers = []; foreach ($rows as $rowIndex => $row) { $normalized = array_map( static fn($value) => strtolower(trim((string)$value)), $row ); if (in_array('field name', $normalized, true)) { $headerIndex = $rowIndex; $headers = $normalized; break; } } if ($headerIndex !== null) { $fieldColumn = array_search('field name', $headers, true); foreach (array_slice($rows, $headerIndex + 1) as $row) { $fieldName = trim((string)($row[$fieldColumn] ?? '')); if ($fieldName === '') { continue; } $record = ['sheet_name' => $sheetName]; foreach ($headers as $column => $header) { if ($header === '') { continue; } $key = preg_replace('/[^a-z0-9]+/', '_', $header); $record[trim((string)$key, '_')] = $row[$column] ?? null; } $fields[] = $record; } } if ($sheetName === 'Signal Catalog') { foreach ($rows as $row) { $kind = strtolower(trim((string)($row[0] ?? ''))); if ($kind === '' || $kind === 'signal kind' || str_contains($kind, 'simon bug')) { continue; } $signalKinds[] = [ 'signal_kind' => $kind, 'description' => trim((string)($row[1] ?? '')), 'example' => trim((string)($row[2] ?? '')), 'parser_evidence' => trim((string)($row[3] ?? '')), 'normalization' => trim((string)($row[4] ?? '')), 'sensitivity' => trim((string)($row[5] ?? '')), 'risk_basis' => trim((string)($row[6] ?? '')), 'evolution_path' => trim((string)($row[7] ?? '')), ]; } } } $cached = [ 'available' => true, 'source' => 'excel', 'path' => $dictionaryPath, 'modified_at' => date('c', (int)filemtime($dictionaryPath)), 'hash' => hash_file('sha256', $dictionaryPath), 'sheets' => array_keys($sheets), 'fields' => $fields, 'signal_kinds' => $signalKinds, 'error' => null, ]; } catch (Throwable $error) { $cached['error'] = $error->getMessage(); } return $cached; } function validate_data_dictionary_contract(array $dictionary): array { $coreRequiredSheets = [ 'Overview','Scan Runs','Sources','Files','Signal Catalog','Signals','Functions', 'Relationships','Endpoints','Findings','5W1H Events','Population Workflow' ]; $operationalSheets = [ 'Unified 5W1H Stream','Stream Event Mapping','Stream Loading Contract', 'Host File Tree','Scan Selections','Access Permissions','Scan Rules', 'Scan Safety Controls','Scan Jobs','Parser Registry','Rule Registry', 'Baselines','Report Definitions','Roles','Permissions','Tests','Backups', 'Error Catalog','Audit Log','Code Base Registry','Module Registry' ]; $errors = []; $warnings = []; $sheetNames = array_values(array_filter(array_map('trim', $dictionary['sheets'] ?? []))); $fields = $dictionary['fields'] ?? []; if (empty($dictionary['available'])) { $errors[] = (string)($dictionary['error'] ?? 'Workbook could not be opened.'); } foreach ($coreRequiredSheets as $sheet) { if (!in_array($sheet, $sheetNames, true)) { $errors[] = 'Missing required worksheet: ' . $sheet; } } foreach ($operationalSheets as $sheet) { if (!in_array($sheet, $sheetNames, true)) { $warnings[] = 'Optional operational worksheet not present: ' . $sheet; } } if (empty($dictionary['hash']) || !preg_match('/^[a-f0-9]{64}$/', (string)$dictionary['hash'])) { $errors[] = 'Workbook SHA-256 hash is missing or invalid.'; } $seen = []; $requiredFieldCount = 0; foreach ($fields as $index => $field) { $sheet = trim((string)($field['sheet_name'] ?? '')); $name = trim((string)($field['field_name'] ?? '')); if ($sheet === '' || $name === '') { continue; } $key = strtolower($sheet . '|' . $name); if (isset($seen[$key])) { $warnings[] = 'Duplicate dictionary field: ' . $sheet . ' / ' . $name; } $seen[$key] = true; $required = strtolower(trim((string)($field['required'] ?? ''))); if (in_array($required, ['yes','true','1','required'], true)) { $requiredFieldCount++; } } if (count($fields) < 25) { $errors[] = 'Dictionary contains too few field definitions to be considered complete.'; } if ($requiredFieldCount === 0) { $warnings[] = 'No fields are marked required.'; } return [ 'valid' => count($errors) === 0, 'errors' => array_values(array_unique($errors)), 'warnings' => array_values(array_unique($warnings)), 'required_sheets' => count($coreRequiredSheets), 'present_sheets' => count(array_intersect($coreRequiredSheets, $sheetNames)), 'operational_sheets' => count($operationalSheets), 'present_operational_sheets' => count(array_intersect($operationalSheets, $sheetNames)), 'field_count' => count($fields), 'required_field_count' => $requiredFieldCount, 'duplicate_count' => max(0, count($fields) - count($seen)), 'workbook_hash' => $dictionary['hash'] ?? null, 'workbook_path' => $dictionary['path'] ?? null, 'validated_at' => date('c'), ]; } function scan_accuracy_report(array $state): array { $total = (int)($state['total'] ?? 0); $processed = (int)($state['processed'] ?? 0); $errors = (int)($state['errors'] ?? 0); $deep = (int)($state['deep_scanned'] ?? 0); $metadata = (int)($state['metadata_only'] ?? 0); $directories = (int)($state['directories'] ?? 0); $reconciled = (int)($state['next_index'] ?? 0) >= $total; $eligibleText = (int)($state['eligible_text_items'] ?? $deep); $coverageDenominator = max(1, $eligibleText); $deepCoverage = min(1, $deep / $coverageDenominator); $errorRate = $total > 0 ? min(1, $errors / $total) : 0; $dictionaryValid = (bool)($state['dictionary_valid'] ?? false); $databaseConnected = (bool)($state['database_connected'] ?? false); $completeness = ($total > 0 ? min(1, $processed / $total) : 0) * 0.45 + $deepCoverage * 0.25 + ($dictionaryValid ? 0.20 : 0) + ($databaseConnected ? 0.10 : 0); $confidence = max( 0, min( 1, 0.95 - ($errorRate * 0.55) - ($dictionaryValid ? 0 : 0.20) - ($reconciled ? 0 : 0.25) ) ); $gates = [ [ 'key' => 'dictionary_contract', 'label' => 'Excel contract validates', 'status' => $dictionaryValid ? 'pass' : 'fail', 'evidence' => (int)($state['dictionary_fields'] ?? 0) . ' fields, ' . (int)($state['dictionary_sheets'] ?? 0) . ' sheets', ], [ 'key' => 'manifest_reconciliation', 'label' => 'Manifest counts reconcile', 'status' => $reconciled ? 'pass' : 'fail', 'evidence' => (int)($state['next_index'] ?? $processed) . ' handled of ' . $total, ], [ 'key' => 'database_persistence', 'label' => 'MariaDB persistence available', 'status' => $databaseConnected ? 'pass' : 'warning', 'evidence' => $databaseConnected ? (int)($state['database_updates'] ?? 0) . ' database updates' : 'JSON fallback only', ], [ 'key' => 'error_rate', 'label' => 'Error rate below 2%', 'status' => $errorRate <= 0.02 ? 'pass' : ($errorRate <= 0.10 ? 'warning' : 'fail'), 'evidence' => number_format($errorRate * 100, 2) . '%', ], [ 'key' => 'deep_scan_coverage', 'label' => 'Eligible text/code deep-scan coverage', 'status' => $deepCoverage >= 0.80 ? 'pass' : ($deepCoverage >= 0.50 ? 'warning' : 'fail'), 'evidence' => number_format($deepCoverage * 100, 1) . '% deep, ' . $metadata . ' metadata-only', ], ]; $failures = count(array_filter($gates, static fn($gate) => $gate['status'] === 'fail')); $warnings = count(array_filter($gates, static fn($gate) => $gate['status'] === 'warning')); return [ 'scan_uuid' => $state['scan_uuid'] ?? null, 'valid' => $failures === 0, 'status' => $failures > 0 ? 'failed_validation' : ($warnings > 0 ? 'completed_with_warnings' : 'validated'), 'confidence' => round($confidence, 4), 'completeness' => round(min(1, $completeness), 4), 'ambiguity' => round(max(0, 1 - $confidence), 4), 'risk' => round(min(1, $errorRate + ($dictionaryValid ? 0 : 0.25)), 4), 'error_rate' => round($errorRate, 6), 'deep_scan_coverage' => round($deepCoverage, 4), 'gates' => $gates, 'failure_count' => $failures, 'warning_count' => $warnings, 'generated_at' => date('c'), ]; } function persist_scan_accuracy(PDO $pdo, array $report, ?int $projectId): void { if (!simon_db_table_exists($pdo, 'simon_scan_validation_runs')) { return; } $stmt = $pdo->prepare( 'INSERT INTO simon_scan_validation_runs ' . '(validation_uuid,scan_uuid,project_id,status,confidence,completeness,ambiguity,risk,' . 'failure_count,warning_count,report_json,validated_at) ' . 'VALUES (?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP) ' . 'ON DUPLICATE KEY UPDATE status=VALUES(status),confidence=VALUES(confidence),' . 'completeness=VALUES(completeness),ambiguity=VALUES(ambiguity),risk=VALUES(risk),' . 'failure_count=VALUES(failure_count),warning_count=VALUES(warning_count),' . 'report_json=VALUES(report_json),validated_at=CURRENT_TIMESTAMP' ); $stmt->execute([ simon_uuid(), $report['scan_uuid'], $projectId, $report['status'], $report['confidence'], $report['completeness'], $report['ambiguity'], $report['risk'], $report['failure_count'], $report['warning_count'], json_encode($report, JSON_UNESCAPED_SLASHES), ]); if (simon_db_table_exists($pdo, 'simon_scan_validation_issues')) { $issue = $pdo->prepare( 'INSERT INTO simon_scan_validation_issues ' . '(scan_uuid,gate_key,title,status,evidence,created_at) VALUES (?,?,?,?,?,CURRENT_TIMESTAMP) ' . 'ON DUPLICATE KEY UPDATE status=VALUES(status),evidence=VALUES(evidence),created_at=CURRENT_TIMESTAMP' ); foreach ($report['gates'] as $gate) { $issue->execute([ $report['scan_uuid'], $gate['key'], $gate['label'], $gate['status'], $gate['evidence'], ]); } } } function sync_data_dictionary_to_database(PDO $pdo, array $dictionary): array { $result = ['inserted' => 0, 'updated' => 0, 'skipped' => 0, 'validation' => null]; $validation = validate_data_dictionary_contract($dictionary); $result['validation'] = $validation; if (!$validation['valid']) { throw new RuntimeException( 'Excel data dictionary validation failed: ' . implode(' | ', array_slice($validation['errors'], 0, 8)) ); } if (!simon_db_table_exists($pdo, 'simon_data_dictionary_fields')) { return $result; } $pdo->beginTransaction(); $sql = <<<'SQL' INSERT INTO simon_data_dictionary_fields ( field_uuid, workbook_hash, sheet_name, category_name, field_name, description, retrieve_from, population_rule, data_format, is_required, example_value, validation_rule, security_privacy, evolution_path, implementation_notes, updated_at ) VALUES ( ?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP ) ON DUPLICATE KEY UPDATE workbook_hash=VALUES(workbook_hash), category_name=VALUES(category_name), description=VALUES(description), retrieve_from=VALUES(retrieve_from), population_rule=VALUES(population_rule), data_format=VALUES(data_format), is_required=VALUES(is_required), example_value=VALUES(example_value), validation_rule=VALUES(validation_rule), security_privacy=VALUES(security_privacy), evolution_path=VALUES(evolution_path), implementation_notes=VALUES(implementation_notes), updated_at=CURRENT_TIMESTAMP SQL; $stmt = $pdo->prepare($sql); foreach ($dictionary['fields'] as $field) { $sheetName = (string)($field['sheet_name'] ?? ''); $fieldName = (string)($field['field_name'] ?? ''); if ($sheetName === '' || $fieldName === '') { $result['skipped']++; continue; } $stmt->execute([ simon_uuid(), $dictionary['hash'], $sheetName, (string)($field['category'] ?? ''), $fieldName, (string)($field['description'] ?? ''), (string)($field['retrieve_from'] ?? ''), (string)($field['population_rule'] ?? ''), (string)($field['data_format'] ?? ''), strtolower(trim((string)($field['required'] ?? ''))) === 'yes' ? 1 : 0, (string)($field['example_value'] ?? ''), (string)($field['validation_normalization'] ?? ''), (string)($field['security_privacy'] ?? ''), (string)($field['evolution_path'] ?? ''), (string)($field['implementation_notes'] ?? ''), ]); $result['updated']++; } if (simon_db_table_exists($pdo, 'simon_data_dictionary_imports')) { $import = $pdo->prepare( 'INSERT INTO simon_data_dictionary_imports ' . '(import_uuid, workbook_path, workbook_hash, sheet_count, field_count, status, imported_at) ' . 'VALUES (?,?,?,?,?,\'completed\',CURRENT_TIMESTAMP) ' . 'ON DUPLICATE KEY UPDATE sheet_count=VALUES(sheet_count), field_count=VALUES(field_count), ' . 'status=\'completed\', imported_at=CURRENT_TIMESTAMP' ); $import->execute([ simon_uuid(), basename((string)$dictionary['path']), $dictionary['hash'], count($dictionary['sheets']), count($dictionary['fields']), ]); } $pdo->commit(); return $result; } function host_scan_file_type(string $path, string $mime): string { if (is_dir($path)) { return 'directory'; } $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION)); return match (true) { in_array($extension, ['php', 'phtml'], true) => 'php', in_array($extension, ['html', 'htm'], true) => 'html', $extension === 'css' => 'css', in_array($extension, ['js', 'mjs', 'ts', 'tsx', 'jsx'], true) => 'javascript', $extension === 'json' => 'json', $extension === 'sql' => 'sql', in_array($extension, ['xml', 'svg'], true) => 'xml', in_array($extension, ['md', 'markdown'], true) => 'markdown', str_starts_with($mime, 'image/') => 'image', str_starts_with($mime, 'video/') => 'video', str_starts_with($mime, 'audio/') => 'audio', in_array($extension, ['zip', 'rar', '7z', 'tar', 'gz'], true) => 'archive', in_array($extension, ['env', 'ini', 'yaml', 'yml', 'conf', 'htaccess'], true) => 'config', str_starts_with($mime, 'text/') => 'text', default => 'other', }; } function host_scan_is_text_file(string $path, string $mime, int $size): bool { if ($size > 6_000_000) { return false; } $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION)); $textExtensions = [ 'php','phtml','html','htm','css','js','mjs','ts','tsx','jsx','json', 'xml','svg','txt','md','markdown','csv','sql','env','ini','log','yaml', 'yml','swift','java','py','rb','go','vue','svelte','c','cpp','h','hpp', 'cs','rs','sh','bash','htaccess','conf' ]; return in_array($extension, $textExtensions, true) || str_starts_with($mime, 'text/'); } function normalize_host_scan_root(string $root): string { $real = realpath(trim($root)); if (!$real || !is_dir($real)) { throw new RuntimeException('Host scan root does not exist.'); } $real = rtrim(str_replace('\\', '/', $real), '/'); // IONOS paths are frequently entered as .../htdocs/htdocs by mistake. if (basename($real) === 'htdocs' && basename(dirname($real)) === 'htdocs') { $parent = realpath(dirname($real)); if ($parent && is_dir($parent)) { $real = rtrim(str_replace('\\', '/', $parent), '/'); } } $approvedCandidates = [ '/homepages/7/d4299367777', '/homepages/7/d4299367777/htdocs', (string)($_SERVER['DOCUMENT_ROOT'] ?? ''), dirname((string)($_SERVER['DOCUMENT_ROOT'] ?? '')), ]; foreach ($approvedCandidates as $candidate) { $approved = realpath($candidate); if ($approved === false || !is_dir($approved)) { continue; } $approved = rtrim(str_replace('\\', '/', $approved), '/'); if ($real === $approved || str_starts_with($real . '/', $approved . '/')) { return $real; } } throw new RuntimeException('Host scan root is outside the approved IONOS hosting paths.'); } function build_host_manifest(string $root): array { $root = normalize_host_scan_root($root); $manifest = []; $skipped = []; $stack = [[$root, '']]; $maxItems = 250000; while ($stack && count($manifest) < $maxItems) { [$directory, $relativeBase] = array_pop($stack); if (!is_dir($directory) || !is_readable($directory)) { $skipped[] = [ 'path' => $relativeBase !== '' ? $relativeBase : '.', 'reason' => 'permission_denied_or_unreadable', ]; continue; } $items = @scandir($directory); if ($items === false) { $skipped[] = [ 'path' => $relativeBase !== '' ? $relativeBase : '.', 'reason' => 'directory_listing_failed', ]; continue; } foreach ($items as $name) { if ($name === '.' || $name === '..') continue; if (in_array($name, ['.git','.svn'], true)) continue; $path = $directory . DIRECTORY_SEPARATOR . $name; $relative = ltrim($relativeBase . '/' . $name, '/'); if (is_link($path)) { $skipped[] = ['path' => $relative, 'reason' => 'symbolic_link_skipped']; continue; } $isDirectory = is_dir($path); $manifest[] = [ 'path' => $path, 'relative_path' => str_replace(DIRECTORY_SEPARATOR, '/', $relative), 'is_directory' => $isDirectory, 'readable' => is_readable($path), ]; if ($isDirectory) { if (is_readable($path)) { $stack[] = [$path, $relative]; } else { $skipped[] = ['path' => $relative, 'reason' => 'permission_denied_or_unreadable']; } } } } usort( $manifest, static fn(array $a, array $b): int => strcasecmp((string)$a['relative_path'], (string)$b['relative_path']) ); return [ 'items' => $manifest, 'skipped' => $skipped, 'limit_reached' => count($manifest) >= $maxItems, ]; } function host_scan_state(): array { return read_json_file(HOST_SCAN_STATE_FILE, [ 'scan_uuid' => null, 'root' => null, 'status' => 'not_started', 'phase' => 'idle', 'manifest' => [], 'next_index' => 0, 'total' => 0, 'processed' => 0, 'deep_scanned' => 0, 'eligible_text_items' => 0, 'metadata_only' => 0, 'directories' => 0, 'errors' => 0, 'warnings' => 0, 'skipped_files' => 0, 'failure_count' => 0, 'database_attempts' => 0, 'database_failures' => 0, 'database_updates' => 0, 'dictionary_fields' => 0, 'started_at' => null, 'updated_at' => null, 'completed_at' => null, 'last_file' => null, 'error_message' => null, ]); } function save_host_scan_state(array $state): void { $state['updated_at'] = date('c'); write_json_file(HOST_SCAN_STATE_FILE, $state); @chmod(HOST_SCAN_STATE_FILE, 0600); } function resolve_host_scan_project(PDO $pdo, string $root): int { $slug = 'host-' . substr(hash('sha256', $root), 0, 16); $stmt = $pdo->prepare('SELECT id FROM simon_projects WHERE slug=? LIMIT 1'); $stmt->execute([$slug]); $existing = $stmt->fetchColumn(); if ($existing) { $update = $pdo->prepare( 'UPDATE simon_projects SET root_path=?, last_opened_at=CURRENT_TIMESTAMP, updated_at=CURRENT_TIMESTAMP WHERE id=?' ); $update->execute([$root, (int)$existing]); return (int)$existing; } $insert = $pdo->prepare( 'INSERT INTO simon_projects ' . '(project_uuid,name,slug,description,project_type,status,environment,root_path,objective,metadata) ' . 'VALUES (?,?,?,?,\'system\',\'active\',\'production\',?,?,?)' ); $insert->execute([ simon_uuid(), 'Host Webspace — ' . basename($root), $slug, 'Complete SIMON BUG inventory and intelligence scan of the active web host.', $root, 'Inventory every host file and use the Excel retrieval data dictionary as the collection contract.', json_encode(['scanner' => APP_NAME, 'scanner_version' => APP_VERSION], JSON_UNESCAPED_SLASHES), ]); return (int)$pdo->lastInsertId(); } function persist_host_scan_file( PDO $pdo, int $projectId, array $item, ?array $scanRecord, string $scanUuid ): int { $path = (string)$item['path']; $relative = (string)$item['relative_path']; $isDirectory = (bool)$item['is_directory']; $size = $isDirectory ? 0 : (int)(filesize($path) ?: 0); $mime = $isDirectory ? 'inode/directory' : (string)((new finfo(FILEINFO_MIME_TYPE))->file($path) ?: 'application/octet-stream'); $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION)); $contentHash = $isDirectory ? null : hash_file('sha256', $path); $modified = date('Y-m-d H:i:s', (int)filemtime($path)); $fileType = host_scan_file_type($path, $mime); $binary = !$isDirectory && !host_scan_is_text_file($path, $mime, $size); $stmt = $pdo->prepare( 'INSERT INTO simon_project_files ' . '(file_uuid,project_id,path,filename,extension,mime_type,file_type,is_binary,size_bytes,content_hash,disk_modified_at) ' . 'VALUES (?,?,?,?,?,?,?,?,?,?,?) ' . 'ON DUPLICATE KEY UPDATE filename=VALUES(filename),extension=VALUES(extension),' . 'mime_type=VALUES(mime_type),file_type=VALUES(file_type),is_binary=VALUES(is_binary),' . 'is_deleted=0,size_bytes=VALUES(size_bytes),content_hash=VALUES(content_hash),' . 'disk_modified_at=VALUES(disk_modified_at),updated_at=CURRENT_TIMESTAMP' ); $stmt->execute([ simon_uuid(), $projectId, $relative, basename($path), $extension !== '' ? $extension : null, $mime, $fileType, $binary ? 1 : 0, $size, $contentHash, $modified, ]); $fileLookup = $pdo->prepare( 'SELECT id FROM simon_project_files WHERE project_id=? AND path=? LIMIT 1' ); $fileLookup->execute([$projectId, $relative]); $fileId = (int)$fileLookup->fetchColumn(); if ( simon_db_table_exists($pdo, 'simon_host_scan_inventory') && $fileId > 0 ) { $inventory = $pdo->prepare( 'INSERT INTO simon_host_scan_inventory ' . '(scan_uuid,project_id,file_id,relative_path,is_directory,size_bytes,mime_type,content_hash,' . 'scan_depth,risk_score,severity,field_coverage_json,updated_at) ' . 'VALUES (?,?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP) ' . 'ON DUPLICATE KEY UPDATE file_id=VALUES(file_id),size_bytes=VALUES(size_bytes),' . 'mime_type=VALUES(mime_type),content_hash=VALUES(content_hash),scan_depth=VALUES(scan_depth),' . 'risk_score=VALUES(risk_score),severity=VALUES(severity),' . 'field_coverage_json=VALUES(field_coverage_json),updated_at=CURRENT_TIMESTAMP' ); $inventory->execute([ $scanUuid, $projectId, $fileId, $relative, $isDirectory ? 1 : 0, $size, $mime, $contentHash, $scanRecord ? 'deep' : 'metadata', (int)($scanRecord['score'] ?? 0), (string)($scanRecord['severity'] ?? 'info'), json_encode([ 'dictionary' => basename((string)(load_data_dictionary_contract()['path'] ?? 'embedded-contract')), 'metadata' => true, 'content' => $scanRecord !== null, 'functions' => count($scanRecord['source_map']['functions'] ?? []), 'endpoints' => count($scanRecord['source_map']['api_calls'] ?? []) + count($scanRecord['source_map']['forms'] ?? []), 'findings' => count($scanRecord['findings'] ?? []), ], JSON_UNESCAPED_SLASHES), ]); } if (!$scanRecord || $fileId <= 0) { return $fileId; } if (simon_db_table_exists($pdo, 'simon_code_symbols')) { $symbol = $pdo->prepare( 'INSERT INTO simon_code_symbols ' . '(project_id,file_id,symbol_type,symbol_name,signature,line_start,line_end,content_hash,metadata) ' . 'VALUES (?,?,?,?,?,?,?,?,?) ' . 'ON DUPLICATE KEY UPDATE signature=VALUES(signature),line_end=VALUES(line_end),' . 'content_hash=VALUES(content_hash),metadata=VALUES(metadata),indexed_at=CURRENT_TIMESTAMP' ); foreach (($scanRecord['source_map']['functions'] ?? []) as $function) { $symbol->execute([ $projectId, $fileId, 'function', (string)($function['name'] ?? 'anonymous'), (string)($function['signature'] ?? ''), (int)($function['line'] ?? 0) ?: null, (int)($function['end_line'] ?? 0) ?: null, hash('sha256', json_encode($function)), json_encode($function, JSON_UNESCAPED_SLASHES), ]); } foreach (($scanRecord['source_map']['classes'] ?? []) as $class) { $symbol->execute([ $projectId, $fileId, 'class', (string)($class['name'] ?? 'anonymous'), (string)($class['signature'] ?? ''), (int)($class['line'] ?? 0) ?: null, (int)($class['end_line'] ?? 0) ?: null, hash('sha256', json_encode($class)), json_encode($class, JSON_UNESCAPED_SLASHES), ]); } } if (simon_db_table_exists($pdo, 'simon_scan_results')) { $findingStmt = $pdo->prepare( 'INSERT INTO simon_scan_results ' . '(scan_uuid,project_id,file_id,scan_type,severity,rule_id,title,description,evidence,recommendation,fingerprint,status,last_seen_at) ' . 'VALUES (?,?,?,?,?,?,?,?,?,?,?,\'open\',CURRENT_TIMESTAMP) ' . 'ON DUPLICATE KEY UPDATE severity=VALUES(severity),description=VALUES(description),' . 'evidence=VALUES(evidence),recommendation=VALUES(recommendation),last_seen_at=CURRENT_TIMESTAMP' ); foreach (($scanRecord['findings'] ?? []) as $finding) { $title = (string)($finding['label'] ?? $finding['title'] ?? 'Scanner finding'); $description = (string)($finding['description'] ?? $title); $severity = strtolower((string)($finding['severity'] ?? $scanRecord['severity'] ?? 'info')); if (!in_array($severity, ['info','low','medium','high','critical'], true)) { $severity = 'info'; } $fingerprint = hash( 'sha256', $projectId . '|' . $fileId . '|' . $title . '|' . (string)($finding['line'] ?? '') . '|' . (string)($finding['match'] ?? '') ); $findingStmt->execute([ simon_uuid(), $projectId, $fileId, 'security', $severity, (string)($finding['category'] ?? 'scanner.rule'), $title, $description, json_encode($finding, JSON_UNESCAPED_SLASHES), (string)($finding['recommendation'] ?? 'Review the source and confirm the finding before modifying production.'), $fingerprint, ]); } } if (simon_db_table_exists($pdo, 'simon_endpoints')) { $endpointStmt = $pdo->prepare( 'INSERT INTO simon_endpoints ' . '(endpoint_uuid,project_id,file_id,route_path,normalized_route,http_method,endpoint_type,' . 'exposure,authentication_status,authorization_status,csrf_status,validation_status,risk_score,status,metadata) ' . 'VALUES (?,?,?,?,?,? ,?,?,?,?,?,?,?,?,?) ' . 'ON DUPLICATE KEY UPDATE file_id=VALUES(file_id),endpoint_type=VALUES(endpoint_type),' . 'exposure=VALUES(exposure),risk_score=VALUES(risk_score),status=VALUES(status),' . 'metadata=VALUES(metadata),last_seen_at=CURRENT_TIMESTAMP' ); $endpointRows = []; foreach (($scanRecord['source_map']['api_calls'] ?? []) as $endpoint) { $target = trim((string)($endpoint['target'] ?? '')); if ($target !== '') { $endpointRows[] = [ 'target' => $target, 'method' => strtoupper((string)($endpoint['method'] ?? 'ANY')), 'type' => 'api', 'metadata' => $endpoint, ]; } } foreach (($scanRecord['source_map']['forms'] ?? []) as $endpoint) { $target = trim((string)($endpoint['action'] ?? '')); if ($target !== '') { $endpointRows[] = [ 'target' => $target, 'method' => strtoupper((string)($endpoint['method'] ?? 'POST')), 'type' => 'form', 'metadata' => $endpoint, ]; } } foreach ($endpointRows as $endpoint) { $target = $endpoint['target']; $parsed = parse_url($target); $route = (string)($parsed['path'] ?? $target); if ($route === '') { continue; } $normalizedRoute = '/' . ltrim(preg_replace('#/+#', '/', $route), '/'); $isExternal = isset($parsed['host']); $endpointStmt->execute([ simon_uuid(), $projectId, $fileId, $route, $normalizedRoute, $endpoint['method'], $endpoint['type'], $isExternal ? 'public' : 'unknown', 'unknown', 'unknown', $endpoint['type'] === 'form' ? 'unknown' : 'not_applicable', 'unknown', (int)($scanRecord['score'] ?? 0), 'active', json_encode($endpoint['metadata'], JSON_UNESCAPED_SLASHES), ]); } } return $fileId; } function start_host_scan(string $root): array { $root = normalize_host_scan_root($root); $manifestResult = build_host_manifest($root); $manifest = $manifestResult['items']; $manifestSkipped = $manifestResult['skipped']; $dictionary = load_data_dictionary_contract(); $dictionaryValidation = validate_data_dictionary_contract($dictionary); if (!$dictionaryValidation['valid']) { log_event('dictionary_contract_warning', [ 'path' => $dictionary['path'] ?? null, 'errors' => $dictionaryValidation['errors'] ?? [], 'warnings' => $dictionaryValidation['warnings'] ?? [], ]); // Discovery remains available. Results are marked as contract-degraded // instead of hiding the host file tree or blocking the entire scan. } $pdo = simon_db_connection(); $databaseProjectId = null; $scanUuid = simon_uuid(); if ($pdo) { $databaseProjectId = resolve_host_scan_project($pdo, $root); if ($dictionaryValidation['valid']) { sync_data_dictionary_to_database($pdo, $dictionary); } if (simon_db_table_exists($pdo, 'simon_scan_runs')) { $scanRun = $pdo->prepare( 'INSERT INTO simon_scan_runs ' . '(scan_uuid,project_id,source_id,scan_mode,status,scanner_version,files_discovered,started_at,metadata) ' . 'VALUES (?,?,NULL,\'full\',\'running\',?,?,CURRENT_TIMESTAMP,?)' ); $scanRun->execute([ $scanUuid, $databaseProjectId, APP_VERSION, count($manifest), json_encode([ 'root' => $root, 'dictionary_hash' => $dictionary['hash'] ?? null, 'dictionary_path' => $dictionary['path'] ?? null, 'dictionary_file' => basename((string)($dictionary['path'] ?? 'embedded-contract')), ], JSON_UNESCAPED_SLASHES), ]); } } $state = [ 'scan_uuid' => $scanUuid, 'root' => $root, 'status' => 'running', 'phase' => 'discovering_complete', 'manifest' => $manifest, 'manifest_skipped' => $manifestSkipped, 'skipped_directories' => count($manifestSkipped), 'manifest_limit_reached' => (bool)($manifestResult['limit_reached'] ?? false), 'next_index' => 0, 'total' => count($manifest), 'processed' => 0, 'deep_scanned' => 0, 'eligible_text_items' => 0, 'metadata_only' => 0, 'directories' => 0, 'errors' => 0, 'warnings' => 0, 'skipped_files' => 0, 'failure_count' => count($manifestSkipped), 'database_attempts' => 0, 'database_failures' => 0, 'database_updates' => 0, 'recent_files' => [], 'started_microtime' => microtime(true), 'database_connected' => $pdo instanceof PDO, 'database_project_id' => $databaseProjectId, 'dictionary_available' => (bool)($dictionary['available'] ?? false), 'dictionary_valid' => (bool)$dictionaryValidation['valid'], 'dictionary_validation' => $dictionaryValidation, 'dictionary_hash' => $dictionary['hash'] ?? null, 'dictionary_path' => $dictionary['path'] ?? null, 'dictionary_fields' => count($dictionary['fields'] ?? []), 'dictionary_sheets' => count($dictionary['sheets'] ?? []), 'dictionary_error' => $dictionary['error'] ?? null, 'started_at' => date('c'), 'updated_at' => date('c'), 'completed_at' => null, 'last_file' => null, 'error_message' => null, ]; save_host_scan_state($state); log_event('host_scan_started', [ 'scan_uuid' => $scanUuid, 'root' => $root, 'files' => count($manifest), 'database_connected' => $pdo instanceof PDO, 'dictionary_fields' => $state['dictionary_fields'], ]); simon_emit_5w1h([ 'scan_uuid'=>$scanUuid,'project_id'=>$databaseProjectId,'event_type'=>'scan_started', 'who'=>['actor_type'=>'system','actor_id'=>'simon_bug'], 'what'=>['action'=>'complete_host_scan','entity_type'=>'host','entity_name'=>basename($root),'result'=>'started'], 'where'=>['host'=>$_SERVER['HTTP_HOST']??'unknown','absolute_path'=>$root,'relative_path'=>'.','environment'=>'production'], 'why'=>['trigger'=>'operator_request','reason'=>'Complete host intelligence scan requested'], 'how'=>['method'=>'recursive_resumable_batch_scan','scanner_version'=>APP_VERSION], 'health'=>['confidence'=>0.99,'ambiguity'=>0.01,'completeness'=>0.0,'risk'=>0.0], 'truth'=>true,'makeTrue'=>['complete_manifest','produce_host_report'],'meta'=>['total_items'=>count($manifest)] ],$pdo); return $state; } function process_host_scan_batch( array $state, array $rules, array $riskExtensions, int $batchSize = 25 ): array { if (($state['status'] ?? '') !== 'running') { return $state; } $manifest = $state['manifest'] ?? []; $total = count($manifest); $start = (int)($state['next_index'] ?? 0); $end = min($total, $start + max(1, min(100, $batchSize))); $pdo = simon_db_connection(); $projectId = $pdo ? (int)($state['database_project_id'] ?? resolve_host_scan_project($pdo, (string)$state['root'])) : 0; $historyItems = history(); for ($index = $start; $index < $end; $index++) { $item = $manifest[$index]; $path = (string)$item['path']; $state['last_file'] = (string)$item['relative_path']; try { if (!file_exists($path)) { throw new RuntimeException('File disappeared before scanning.'); } if ((bool)$item['is_directory']) { $state['directories']++; $scanRecord = null; } else { $size = (int)(filesize($path) ?: 0); $mime = (string)((new finfo(FILEINFO_MIME_TYPE))->file($path) ?: 'application/octet-stream'); if (host_scan_is_text_file($path, $mime, $size)) { $state['eligible_text_items']=(int)($state['eligible_text_items']??0)+1; $scanRecord = scan_file($path, basename($path), $rules, $riskExtensions); $scanRecord['host_relative_path'] = (string)$item['relative_path']; $scanRecord['host_scan_uuid'] = (string)$state['scan_uuid']; array_unshift($historyItems, $scanRecord); $state['deep_scanned']++; } else { $scanRecord = null; $state['metadata_only']++; } } if ($pdo && $projectId > 0) { $state['database_attempts']=(int)($state['database_attempts']??0)+1; try { persist_host_scan_file( $pdo, $projectId, $item, $scanRecord, (string)$state['scan_uuid'] ); $state['database_updates']++; } catch (Throwable $databaseError) { $state['database_failures']=(int)($state['database_failures']??0)+1; simon_record_scan_failure($state,$item,'database_persist',$databaseError,'warning'); } } $state['processed']++; $state['recent_files']=array_slice(array_merge([(string)$item['relative_path']],(array)($state['recent_files']??[])),0,30); simon_emit_5w1h([ 'scan_uuid'=>(string)$state['scan_uuid'],'project_id'=>$projectId?:null,'event_type'=>(bool)$item['is_directory']?'directory_scanned':'file_scanned', 'who'=>['actor_type'=>'system','actor_id'=>'host_scanner'], 'what'=>['action'=>$scanRecord?'deep_scan':((bool)$item['is_directory']?'directory_inventory':'metadata_scan'),'entity_type'=>(bool)$item['is_directory']?'directory':'file','entity_name'=>basename($path),'result'=>'completed'], 'when'=>['event_time'=>date('c')], 'where'=>['host'=>$_SERVER['HTTP_HOST']??'unknown','absolute_path'=>$path,'relative_path'=>(string)$item['relative_path'],'environment'=>'production'], 'why'=>['trigger'=>'complete_host_scan','reason'=>'Item discovered in active scan manifest'], 'how'=>['method'=>'resumable_batch_scan','scanner_version'=>APP_VERSION,'batch_number'=>(int)floor($index/max(1,$batchSize))+1], 'health'=>['confidence'=>0.98,'ambiguity'=>0.02,'completeness'=>$scanRecord?1.0:0.65,'risk'=>min(1,((int)($scanRecord['score']??0))/100)], 'truth'=>true,'makeTrue'=>[],'makeFalse'=>[], 'meta'=>['size_bytes'=>isset($size)?$size:0,'mime'=>isset($mime)?$mime:null,'severity'=>$scanRecord['severity']??'info','sha256'=>$scanRecord['sha256']??null] ],$pdo); } catch (Throwable $error) { $message=strtolower($error->getMessage()); $classification=(str_contains($message,'permission')||str_contains($message,'readable'))?'skipped':(str_contains($message,'disappeared')?'warning':'error'); simon_record_scan_failure($state,$item,'scan_item',$error,$classification); log_event('host_scan_file_failed', [ 'scan_uuid' => $state['scan_uuid'], 'file' => $item['relative_path'], 'classification'=>$classification, 'error' => $error->getMessage(), ]); simon_emit_5w1h([ 'scan_uuid'=>(string)$state['scan_uuid'],'project_id'=>$projectId?:null,'event_type'=>$classification==='skipped'?'file_skipped':'file_failed', 'who'=>['actor_type'=>'system','actor_id'=>'host_scanner'], 'what'=>['action'=>'scan_item','entity_type'=>(bool)($item['is_directory']??false)?'directory':'file','entity_name'=>basename((string)$path),'result'=>$classification], 'where'=>['host'=>$_SERVER['HTTP_HOST']??'unknown','absolute_path'=>$path,'relative_path'=>(string)$item['relative_path'],'environment'=>'production'], 'why'=>['trigger'=>'complete_host_scan','reason'=>$error->getMessage()], 'how'=>['method'=>'resumable_batch_scan','scanner_version'=>APP_VERSION], 'health'=>['confidence'=>0.99,'ambiguity'=>0.01,'completeness'=>0.2,'risk'=>$classification==='error'?0.8:0.3], 'truth'=>true,'makeTrue'=>['review_failure'],'meta'=>['classification'=>$classification] ],$pdo); } $state['next_index'] = $index + 1; } save_history(array_slice($historyItems, 0, 2000)); if ((int)$state['next_index'] >= $total) { $state['status'] = 'completed'; $state['phase'] = 'completed'; $state['completed_at'] = date('c'); unset($state['manifest']); if ($pdo && $projectId > 0 && simon_db_table_exists($pdo, 'simon_scan_runs')) { $finish = $pdo->prepare( 'UPDATE simon_scan_runs SET status=\'completed\',files_scanned=?,files_changed=?,' . 'files_skipped=?,completed_at=CURRENT_TIMESTAMP,duration_ms=TIMESTAMPDIFF(MICROSECOND,started_at,CURRENT_TIMESTAMP)/1000,' . 'metadata=? WHERE scan_uuid=?' ); $finish->execute([ (int)$state['processed'], (int)$state['deep_scanned'], (int)$state['metadata_only'], json_encode([ 'directories' => (int)$state['directories'], 'errors' => (int)$state['errors'], 'dictionary_fields' => (int)$state['dictionary_fields'], 'database_updates' => (int)$state['database_updates'], ], JSON_UNESCAPED_SLASHES), (string)$state['scan_uuid'], ]); } // Reconciliation is intentionally operator-triggered. Automatic reconciliation // during the final scan batch can exceed shared-hosting request limits. $state['5w1h_reconciliation']=['ok'=>null,'status'=>'pending_operator_reconcile']; $accuracy = scan_accuracy_report($state); $state['accuracy'] = $accuracy; $state['validation_status'] = $accuracy['status']; if (!$accuracy['valid']) { $state['status'] = 'partial'; } write_json_file(SCAN_ACCURACY_FILE, $accuracy); write_json_file(HOST_SCAN_EXPORT_FILE, $state); if ($pdo && $projectId > 0) { persist_scan_accuracy($pdo, $accuracy, $projectId); } $state['report_generated_at']=date('c'); $report=simon_save_host_scan_reports($state); simon_emit_5w1h([ 'scan_uuid'=>(string)$state['scan_uuid'],'project_id'=>$projectId?:null,'event_type'=>'scan_completed', 'who'=>['actor_type'=>'system','actor_id'=>'simon_bug'], 'what'=>['action'=>'complete_host_scan','entity_type'=>'scan_run','entity_name'=>(string)$state['scan_uuid'],'result'=>(string)$state['status']], 'when'=>['event_time'=>date('c'),'started_at'=>$state['started_at']??null,'completed_at'=>$state['completed_at']??null], 'where'=>['host'=>$_SERVER['HTTP_HOST']??'unknown','absolute_path'=>$state['root']??null,'relative_path'=>'.','environment'=>'production'], 'why'=>['trigger'=>'manifest_exhausted','reason'=>'All queued scan items were handled'], 'how'=>['method'=>'run_level_aggregation','scanner_version'=>APP_VERSION], 'health'=>['confidence'=>$accuracy['confidence']??0,'ambiguity'=>$accuracy['ambiguity']??1,'completeness'=>$accuracy['completeness']??0,'risk'=>$accuracy['risk']??0], 'truth'=>true,'makeTrue'=>['review_complete_host_report'],'meta'=>['counts'=>$report['counts']??[],'validation_status'=>$accuracy['status']??null] ],$pdo); log_event('host_scan_completed', [ 'scan' => $state, 'accuracy' => $accuracy, ]); } save_host_scan_state($state); return $state; } function host_scan_public_status(array $state): array { $processed = (int)($state['processed'] ?? 0); $total = (int)($state['total'] ?? 0); return [ 'ok' => true, 'scan_uuid' => $state['scan_uuid'] ?? null, 'status' => $state['status'] ?? 'not_started', 'phase' => $state['phase'] ?? 'idle', 'root' => $state['root'] ?? null, 'processed' => $processed, 'total' => $total, 'percent' => $total > 0 ? round(($processed / $total) * 100, 1) : 0, 'deep_scanned' => (int)($state['deep_scanned'] ?? 0), 'metadata_only' => (int)($state['metadata_only'] ?? 0), 'directories' => (int)($state['directories'] ?? 0), 'skipped_directories' => (int)($state['skipped_directories'] ?? 0), 'manifest_limit_reached' => (bool)($state['manifest_limit_reached'] ?? false), 'errors' => (int)($state['errors'] ?? 0), 'warnings' => (int)($state['warnings'] ?? 0), 'skipped_files' => (int)($state['skipped_files'] ?? 0), 'failure_count' => (int)($state['failure_count'] ?? 0), 'database_attempts' => (int)($state['database_attempts'] ?? 0), 'database_failures' => (int)($state['database_failures'] ?? 0), 'database_updates' => (int)($state['database_updates'] ?? 0), 'database_connected' => (bool)($state['database_connected'] ?? false), 'database_project_id' => $state['database_project_id'] ?? null, 'dictionary_available' => (bool)($state['dictionary_available'] ?? false), 'dictionary_valid' => (bool)($state['dictionary_valid'] ?? false), 'dictionary_validation' => $state['dictionary_validation'] ?? null, 'dictionary_fields' => (int)($state['dictionary_fields'] ?? 0), 'dictionary_sheets' => (int)($state['dictionary_sheets'] ?? 0), 'dictionary_error' => $state['dictionary_error'] ?? null, 'last_file' => $state['last_file'] ?? null, 'started_at' => $state['started_at'] ?? null, 'completed_at' => $state['completed_at'] ?? null, 'error_message' => $state['error_message'] ?? null, 'validation_status' => $state['validation_status'] ?? null, 'accuracy' => $state['accuracy'] ?? null, 'recent_files' => array_values((array)($state['recent_files'] ?? [])), 'queue_remaining' => max(0,$total-(int)($state['next_index']??0)), 'files_per_second' => !empty($state['started_at']) ? round($processed/max(1,time()-(strtotime((string)$state['started_at'])?:time())),2) : 0, 'estimated_seconds_remaining' => (!empty($state['started_at']) && $processed>0) ? (int)round(max(0,$total-$processed)/max(0.01,$processed/max(1,time()-(strtotime((string)$state['started_at'])?:time())))) : null, 'report_ready' => !empty($state['scan_uuid']) && is_file(simon_scan_artifact_path(HOST_SCAN_REPORT_DIR,(string)$state['scan_uuid'],'json')), ]; } function simon_scan_artifact_path(string $directory, string $scanUuid, string $extension): string { if (!preg_match('/^[a-f0-9-]{16,64}$/i', $scanUuid)) { throw new RuntimeException('Invalid scan UUID.'); } if (!is_dir($directory) && !@mkdir($directory, 0750, true) && !is_dir($directory)) { throw new RuntimeException('Could not create scan artifact directory.'); } return rtrim($directory, '/\\') . '/' . $scanUuid . '.' . ltrim($extension, '.'); } function simon_canonical_relative_path(string $path): string { $path=str_replace('\\','/',trim($path)); $path=(string)preg_replace('#/+#','/',$path); $path=ltrim($path,'/'); $segments=[]; foreach(explode('/',$path) as $segment){ if($segment===''||$segment==='.') continue; if($segment==='..'){array_pop($segments);continue;} $segments[]=$segment; } return implode('/',$segments); } function simon_terminal_event_type(string $eventType): bool { return in_array($eventType,['file_scanned','directory_scanned','file_skipped','file_failed'],true); } function simon_deterministic_event_uuid(string $scanUuid,string $entityType,string $relativePath): string { $hex=substr(hash('sha256',strtolower($scanUuid).'|'.strtolower($entityType).'|'.simon_canonical_relative_path($relativePath)),0,32); return substr($hex,0,8).'-'.substr($hex,8,4).'-4'.substr($hex,13,3).'-a'.substr($hex,17,3).'-'.substr($hex,20,12); } function simon_reconcile_5w1h_scan(string $scanUuid, ?PDO $pdo=null): array { if(!preg_match('/^[a-f0-9-]{16,64}$/i',$scanUuid)) throw new RuntimeException('Invalid scan UUID.'); $pdo=$pdo instanceof PDO?$pdo:simon_db_connection(); if(!$pdo instanceof PDO) throw new RuntimeException('MariaDB is not connected.'); if(!simon_db_table_exists($pdo,'simon_5w1h_events')) throw new RuntimeException('simon_5w1h_events table is missing.'); try{$pdo->exec("ALTER TABLE simon_5w1h_events ADD COLUMN item_key CHAR(64) NULL AFTER event_type");}catch(Throwable $e){} try{$pdo->exec("ALTER TABLE simon_5w1h_events ADD KEY idx_simon_5w1h_item (scan_uuid,item_key)");}catch(Throwable $e){} $terminal=['file_scanned','directory_scanned','file_skipped','file_failed']; $marks=implode(',',array_fill(0,count($terminal),'?')); $q=$pdo->prepare("SELECT id,event_uuid,event_type,what_entity_type,where_relative_path FROM simon_5w1h_events WHERE scan_uuid=? AND event_type IN ($marks) ORDER BY id ASC"); $q->execute(array_merge([$scanUuid],$terminal)); $rows=$q->fetchAll(PDO::FETCH_ASSOC)?:[]; $groups=[];$normalized=0; $update=$pdo->prepare('UPDATE simon_5w1h_events SET where_relative_path=?,item_key=? WHERE id=?'); foreach($rows as $row){ $entity=(string)($row['what_entity_type']??'item'); $relative=simon_canonical_relative_path((string)($row['where_relative_path']??'')); $key=hash('sha256',strtolower($entity).'|'.$relative); $groups[$key][]=(int)$row['id']; $update->execute([$relative,$key,(int)$row['id']]); $normalized++; } $duplicatesRemoved=0; $delete=$pdo->prepare('DELETE FROM simon_5w1h_events WHERE id=?'); foreach($groups as $ids){ if(count($ids)<2) continue; $keep=max($ids); foreach($ids as $id){if($id!==$keep){$delete->execute([$id]);$duplicatesRemoved++;}} } $inventory=[]; if(simon_db_table_exists($pdo,'simon_host_scan_inventory')){ $iq=$pdo->prepare('SELECT relative_path,is_directory,size_bytes,mime_type,content_hash,scan_depth,risk_score,severity FROM simon_host_scan_inventory WHERE scan_uuid=?'); $iq->execute([$scanUuid]);$inventory=$iq->fetchAll(PDO::FETCH_ASSOC)?:[]; } $existing=[]; $eq=$pdo->prepare("SELECT what_entity_type,where_relative_path FROM simon_5w1h_events WHERE scan_uuid=? AND event_type IN ($marks)"); $eq->execute(array_merge([$scanUuid],$terminal)); foreach($eq->fetchAll(PDO::FETCH_ASSOC)?:[] as $row){$existing[strtolower((string)$row['what_entity_type']).'|'.simon_canonical_relative_path((string)$row['where_relative_path'])]=true;} $recovered=0; foreach($inventory as $row){ $entity=!empty($row['is_directory'])?'directory':'file'; $relative=simon_canonical_relative_path((string)$row['relative_path']); $key=strtolower($entity).'|'.$relative; if(isset($existing[$key])) continue; simon_emit_5w1h([ 'scan_uuid'=>$scanUuid,'event_type'=>$entity==='directory'?'directory_scanned':'file_scanned', 'who'=>['actor_type'=>'system','actor_id'=>'reconciliation_engine'], 'what'=>['action'=>'recovered_from_inventory','entity_type'=>$entity,'entity_name'=>basename($relative),'result'=>'completed'], 'when'=>['event_time'=>date('c')], 'where'=>['host'=>$_SERVER['HTTP_HOST']??'unknown','absolute_path'=>null,'relative_path'=>$relative,'environment'=>'production'], 'why'=>['trigger'=>'5w1h_reconciliation','reason'=>'Terminal event was missing but inventory persistence succeeded'], 'how'=>['method'=>'inventory_backfill','scanner_version'=>APP_VERSION], 'health'=>['confidence'=>0.97,'ambiguity'=>0.03,'completeness'=>0.90,'risk'=>min(1,((int)($row['risk_score']??0))/100)], 'truth'=>true,'meta'=>['recovered'=>true,'size_bytes'=>(int)($row['size_bytes']??0),'mime'=>$row['mime_type']??null,'sha256'=>$row['content_hash']??null,'scan_depth'=>$row['scan_depth']??null] ],$pdo); $existing[$key]=true;$recovered++; } $completion=$pdo->prepare("SELECT COUNT(*) FROM simon_5w1h_events WHERE scan_uuid=? AND event_type='scan_completed'"); $completion->execute([$scanUuid]);$completionAdded=false; if((int)$completion->fetchColumn()===0){ simon_emit_5w1h([ 'scan_uuid'=>$scanUuid,'event_type'=>'scan_completed', 'who'=>['actor_type'=>'system','actor_id'=>'reconciliation_engine'], 'what'=>['action'=>'complete_host_scan','entity_type'=>'scan_run','entity_name'=>$scanUuid,'result'=>'reconciled'], 'when'=>['event_time'=>date('c')], 'where'=>['host'=>$_SERVER['HTTP_HOST']??'unknown','relative_path'=>'.','environment'=>'production'], 'why'=>['trigger'=>'5w1h_reconciliation','reason'=>'Unique terminal items reconciled against persisted inventory'], 'how'=>['method'=>'idempotent_completion','scanner_version'=>APP_VERSION], 'health'=>['confidence'=>0.99,'ambiguity'=>0.01,'completeness'=>1.0,'risk'=>0.0], 'truth'=>true,'meta'=>['duplicates_removed'=>$duplicatesRemoved,'missing_events_recovered'=>$recovered,'unique_terminal_items'=>count($existing),'inventory_items'=>count($inventory)] ],$pdo);$completionAdded=true; } return ['ok'=>true,'scan_uuid'=>$scanUuid,'terminal_rows_examined'=>count($rows),'normalized'=>$normalized,'duplicates_removed'=>$duplicatesRemoved,'missing_events_recovered'=>$recovered,'unique_terminal_items'=>count($existing),'inventory_items'=>count($inventory),'scan_completed_added'=>$completionAdded]; } function simon_emit_5w1h(array $event, ?PDO $pdo = null): array { $scanUuid = (string)($event['scan_uuid'] ?? 'unassigned'); $event['event_type'] = (string)($event['event_type'] ?? 'unknown'); $relativePath=simon_canonical_relative_path((string)($event['where']['relative_path']??'')); if(isset($event['where'])&&is_array($event['where'])) $event['where']['relative_path']=$relativePath; $entityType=(string)($event['what']['entity_type']??'item'); $event['event_id'] = (string)($event['event_id'] ?? (simon_terminal_event_type($event['event_type'])?simon_deterministic_event_uuid($scanUuid,$entityType,$relativePath):simon_uuid())); $event['event_type'] = (string)($event['event_type'] ?? 'unknown'); $event['when'] = is_array($event['when'] ?? null) ? $event['when'] : []; $event['when']['event_time'] = (string)($event['when']['event_time'] ?? date('c')); $event['truth'] = (bool)($event['truth'] ?? true); $event['health'] = array_merge([ 'confidence'=>0.90,'ambiguity'=>0.10,'completeness'=>0.80,'risk'=>0.0, ], is_array($event['health'] ?? null) ? $event['health'] : []); $event['makeFalse'] = array_values((array)($event['makeFalse'] ?? $event['make_false'] ?? [])); $event['makeTrue'] = array_values((array)($event['makeTrue'] ?? $event['make_true'] ?? [])); $event['meta'] = is_array($event['meta'] ?? null) ? $event['meta'] : []; if ($scanUuid !== '' && $scanUuid !== 'unassigned') { $path = simon_scan_artifact_path(SIMON_5W1H_STREAM_DIR, $scanUuid, 'ndjson'); @file_put_contents($path, json_encode($event, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE) . PHP_EOL, FILE_APPEND|LOCK_EX); } if ($pdo instanceof PDO) { try { $pdo->exec("CREATE TABLE IF NOT EXISTS simon_5w1h_events ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, event_uuid CHAR(36) NOT NULL, scan_uuid CHAR(36) NULL, job_uuid CHAR(36) NULL, project_id BIGINT UNSIGNED NULL, event_type VARCHAR(100) NOT NULL, item_key CHAR(64) NULL, who_type VARCHAR(50) NULL, who_id VARCHAR(191) NULL, what_action VARCHAR(100) NULL, what_entity_type VARCHAR(80) NULL, what_entity_name VARCHAR(255) NULL, what_result VARCHAR(80) NULL, when_event DATETIME(6) NOT NULL, where_host VARCHAR(255) NULL, where_path TEXT NULL, where_relative_path TEXT NULL, why_trigger VARCHAR(191) NULL, why_reason TEXT NULL, how_method VARCHAR(191) NULL, how_version VARCHAR(80) NULL, confidence DECIMAL(5,4) NOT NULL DEFAULT 0, ambiguity DECIMAL(5,4) NOT NULL DEFAULT 1, completeness DECIMAL(5,4) NOT NULL DEFAULT 0, risk DECIMAL(5,4) NOT NULL DEFAULT 0, truth TINYINT(1) NOT NULL DEFAULT 0, make_false JSON NULL, make_true JSON NULL, meta JSON NULL, created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), UNIQUE KEY uq_simon_5w1h_event_uuid (event_uuid), KEY idx_simon_5w1h_scan (scan_uuid), KEY idx_simon_5w1h_item (scan_uuid,item_key), KEY idx_simon_5w1h_type_time (event_type, when_event) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"); try{$pdo->exec("ALTER TABLE simon_5w1h_events ADD COLUMN item_key CHAR(64) NULL AFTER event_type");}catch(Throwable $e){} try{$pdo->exec("ALTER TABLE simon_5w1h_events ADD KEY idx_simon_5w1h_item (scan_uuid,item_key)");}catch(Throwable $e){} $stmt=$pdo->prepare("INSERT INTO simon_5w1h_events (event_uuid,scan_uuid,job_uuid,project_id,event_type,item_key,who_type,who_id,what_action,what_entity_type,what_entity_name,what_result,when_event,where_host,where_path,where_relative_path,why_trigger,why_reason,how_method,how_version,confidence,ambiguity,completeness,risk,truth,make_false,make_true,meta) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE event_type=VALUES(event_type),item_key=VALUES(item_key),what_action=VALUES(what_action),what_result=VALUES(what_result),when_event=VALUES(when_event),meta=VALUES(meta)"); $when=date('Y-m-d H:i:s.u', strtotime((string)$event['when']['event_time']) ?: time()); $stmt->execute([ $event['event_id'],$scanUuid ?: null,$event['job_uuid']??null,$event['project_id']??null,$event['event_type'], simon_terminal_event_type($event['event_type'])?hash('sha256',strtolower($entityType).'|'.$relativePath):null, $event['who']['actor_type']??$event['who']['type']??null,$event['who']['actor_id']??$event['who']['id']??null, $event['what']['action']??null,$event['what']['entity_type']??null,$event['what']['entity_name']??null,$event['what']['result']??null, $when,$event['where']['host']??null,$event['where']['absolute_path']??null,$event['where']['relative_path']??null, $event['why']['trigger']??null,$event['why']['reason']??null,$event['how']['method']??null,$event['how']['scanner_version']??APP_VERSION, (float)$event['health']['confidence'],(float)$event['health']['ambiguity'],(float)$event['health']['completeness'],(float)$event['health']['risk'],$event['truth']?1:0, json_encode($event['makeFalse'],JSON_UNESCAPED_SLASHES),json_encode($event['makeTrue'],JSON_UNESCAPED_SLASHES),json_encode($event['meta'],JSON_UNESCAPED_SLASHES) ]); } catch (Throwable $e) { log_event('5w1h_database_write_failed',['scan_uuid'=>$scanUuid,'error'=>$e->getMessage()]); } } return $event; } function simon_record_scan_failure(array &$state, array $item, string $operation, Throwable|string $error, string $classification='error'): void { $message=$error instanceof Throwable ? $error->getMessage() : (string)$error; $scanUuid=(string)($state['scan_uuid']??''); $record=[ 'time'=>date('c'),'scan_uuid'=>$scanUuid,'path'=>(string)($item['path']??''), 'relative_path'=>(string)($item['relative_path']??''),'operation'=>$operation, 'classification'=>$classification,'reason'=>$message, 'exception_class'=>$error instanceof Throwable ? get_class($error) : null, ]; if ($scanUuid!=='') { $path=simon_scan_artifact_path(HOST_SCAN_FAILURE_DIR,$scanUuid,'ndjson'); @file_put_contents($path,json_encode($record,JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE).PHP_EOL,FILE_APPEND|LOCK_EX); } $state['failure_count']=(int)($state['failure_count']??0)+1; if ($classification==='skipped') $state['skipped_files']=(int)($state['skipped_files']??0)+1; elseif ($classification==='warning') $state['warnings']=(int)($state['warnings']??0)+1; else $state['errors']=(int)($state['errors']??0)+1; } function simon_read_ndjson(string $path, int $limit=200000): array { if (!is_file($path)) return []; $rows=[];$h=@fopen($path,'rb'); if(!$h) return []; while (($line=fgets($h))!==false && count($rows)<$limit) { $row=json_decode(trim($line),true); if(is_array($row)) $rows[]=$row; } fclose($h); return $rows; } function simon_host_scan_report(array $state): array { $scanUuid=(string)($state['scan_uuid']??''); $pdo=simon_db_connection(); $inventory=[];$previous=[];$previousUuid=null; if ($pdo instanceof PDO && $scanUuid!=='' && simon_db_table_exists($pdo,'simon_host_scan_inventory')) { $q=$pdo->prepare('SELECT relative_path,is_directory,size_bytes,mime_type,content_hash,scan_depth,risk_score,severity FROM simon_host_scan_inventory WHERE scan_uuid=? ORDER BY relative_path'); $q->execute([$scanUuid]); $inventory=$q->fetchAll(PDO::FETCH_ASSOC)?:[]; if (simon_db_table_exists($pdo,'simon_scan_runs')) { $pq=$pdo->prepare("SELECT scan_uuid FROM simon_scan_runs WHERE scan_uuid<>? AND status IN ('completed','validated','partial') ORDER BY started_at DESC LIMIT 1"); $pq->execute([$scanUuid]); $previousUuid=$pq->fetchColumn()?:null; if($previousUuid){$q->execute([$previousUuid]);$previous=$q->fetchAll(PDO::FETCH_ASSOC)?:[];} } } $failures=$scanUuid!==''?simon_read_ndjson(simon_scan_artifact_path(HOST_SCAN_FAILURE_DIR,$scanUuid,'ndjson')):[]; $events=$scanUuid!==''?simon_read_ndjson(simon_scan_artifact_path(SIMON_5W1H_STREAM_DIR,$scanUuid,'ndjson')):[]; $ext=[];$sev=[];$totalBytes=0;$largest=[];$hashes=[]; foreach($inventory as $row){ $path=(string)$row['relative_path'];$e=strtolower(pathinfo($path,PATHINFO_EXTENSION))?:'[none]';$ext[$e]=($ext[$e]??0)+1; $sv=(string)($row['severity']??'info');$sev[$sv]=($sev[$sv]??0)+1;$totalBytes+=(int)($row['size_bytes']??0); if(empty($row['is_directory'])) $largest[]=['path'=>$path,'size_bytes'=>(int)$row['size_bytes']]; $h=(string)($row['content_hash']??''); if($h!=='') $hashes[$h][]=$path; } arsort($ext); arsort($sev); usort($largest,fn($a,$b)=>$b['size_bytes']<=>$a['size_bytes']);$largest=array_slice($largest,0,50); $duplicates=array_values(array_filter($hashes,fn($paths)=>count($paths)>1)); $currentMap=[];foreach($inventory as $r)$currentMap[(string)$r['relative_path']]=$r; $previousMap=[];foreach($previous as $r)$previousMap[(string)$r['relative_path']]=$r; $changes=['new'=>[],'deleted'=>[],'modified'=>[],'unchanged'=>0]; foreach($currentMap as $path=>$row){if(!isset($previousMap[$path]))$changes['new'][]=$path;elseif(($row['content_hash']??null)!==($previousMap[$path]['content_hash']??null))$changes['modified'][]=$path;else$changes['unchanged']++;} foreach($previousMap as $path=>$row)if(!isset($currentMap[$path]))$changes['deleted'][]=$path; $failureGroups=[];foreach($failures as $f){$k=(string)($f['classification']??'error').' | '.(string)($f['reason']??'unknown');$failureGroups[$k]=($failureGroups[$k]??0)+1;}arsort($failureGroups); return [ 'generated_at'=>date('c'),'scanner_version'=>APP_VERSION,'scan_uuid'=>$scanUuid,'previous_scan_uuid'=>$previousUuid, 'root'=>$state['root']??null,'status'=>$state['status']??null,'validation_status'=>$state['validation_status']??null, 'started_at'=>$state['started_at']??null,'completed_at'=>$state['completed_at']??null, 'counts'=>['total'=>(int)($state['total']??0),'processed'=>(int)($state['processed']??0),'directories'=>(int)($state['directories']??0),'deep_scanned'=>(int)($state['deep_scanned']??0),'metadata_only'=>(int)($state['metadata_only']??0),'skipped'=>(int)($state['skipped_files']??0)+(int)($state['skipped_directories']??0),'warnings'=>(int)($state['warnings']??0),'errors'=>(int)($state['errors']??0),'database_updates'=>(int)($state['database_updates']??0),'database_failures'=>(int)($state['database_failures']??0),'bytes'=>$totalBytes,'5w1h_events'=>count($events)], 'validation'=>$state['accuracy']??scan_accuracy_report($state),'extension_counts'=>$ext,'severity_counts'=>$sev,'largest_files'=>$largest, 'duplicates'=>$duplicates,'failures'=>$failures,'failure_groups'=>$failureGroups,'changes'=>$changes, 'database'=>['connected'=>(bool)($state['database_connected']??false),'writes_attempted'=>(int)($state['database_attempts']??0),'writes_succeeded'=>(int)($state['database_updates']??0),'writes_failed'=>(int)($state['database_failures']??0)], ]; } function simon_save_host_scan_reports(array $state): array { $report=simon_host_scan_report($state);$uuid=(string)$report['scan_uuid']; $jsonPath=simon_scan_artifact_path(HOST_SCAN_REPORT_DIR,$uuid,'json'); @file_put_contents($jsonPath,json_encode($report,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE),LOCK_EX); $html=simon_make_host_scan_html($report);@file_put_contents(simon_scan_artifact_path(HOST_SCAN_REPORT_DIR,$uuid,'html'),$html,LOCK_EX); $csv=simon_scan_artifact_path(HOST_SCAN_REPORT_DIR,$uuid,'inventory.csv'); $pdo=simon_db_connection();$out=@fopen($csv,'wb');if($out){fputcsv($out,['relative_path','is_directory','size_bytes','mime_type','content_hash','scan_depth','risk_score','severity']);if($pdo instanceof PDO&&simon_db_table_exists($pdo,'simon_host_scan_inventory')){$q=$pdo->prepare('SELECT relative_path,is_directory,size_bytes,mime_type,content_hash,scan_depth,risk_score,severity FROM simon_host_scan_inventory WHERE scan_uuid=? ORDER BY relative_path');$q->execute([$uuid]);while($r=$q->fetch(PDO::FETCH_ASSOC))fputcsv($out,$r);}fclose($out);} $fcsv=simon_scan_artifact_path(HOST_SCAN_REPORT_DIR,$uuid,'failures.csv');$out=@fopen($fcsv,'wb');if($out){fputcsv($out,['time','classification','operation','relative_path','path','reason','exception_class']);foreach($report['failures'] as $r)fputcsv($out,[$r['time']??'',$r['classification']??'',$r['operation']??'',$r['relative_path']??'',$r['path']??'',$r['reason']??'',$r['exception_class']??'']);fclose($out);} return $report; } function simon_make_host_scan_html(array $r): string { $h=fn($v)=>htmlspecialchars((string)$v,ENT_QUOTES,'UTF-8');$c=$r['counts'];$changes=$r['changes']; $rows='';foreach(array_slice($r['failures'],0,500) as $f){$rows.='<tr><td>'.$h($f['classification']??'').'</td><td>'.$h($f['operation']??'').'</td><td>'.$h($f['relative_path']??'').'</td><td>'.$h($f['reason']??'').'</td></tr>';} $largest='';foreach($r['largest_files'] as $f)$largest.='<tr><td>'.$h($f['path']).'</td><td>'.number_format((int)$f['size_bytes']).'</td></tr>'; $gates='';foreach(($r['validation']['gates']??[]) as $g)$gates.='<li><b>'.$h($g['label']??'').'</b> — '.$h($g['status']??'').' — '.$h($g['evidence']??'').'</li>'; return '<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SIMON Host Report '.$h($r['scan_uuid']).'</title><style>body{margin:0;background:#070914;color:#eef;font-family:system-ui;padding:24px}main{max-width:1200px;margin:auto}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px}.card,section{background:#10162a;border:1px solid #2c3655;border-radius:16px;padding:16px;margin:12px 0}h1{background:linear-gradient(90deg,#ff4fd8,#ffd54a,#39e6ff,#9c6bff);-webkit-background-clip:text;color:transparent}table{width:100%;border-collapse:collapse}td,th{padding:9px;border-bottom:1px solid #27304a;text-align:left;word-break:break-word}.muted{color:#aab5d4}</style></head><body><main><h1>SIMON BUG Complete Host Scan Report</h1><p class="muted">Scan '.$h($r['scan_uuid']).' · '.$h($r['root']).'</p><div class="grid">'.implode('',array_map(fn($k)=>'<div class="card"><small>'.$h(str_replace('_',' ',$k)).'</small><h2>'.$h($c[$k]??0).'</h2></div>',['total','processed','directories','deep_scanned','metadata_only','skipped','warnings','errors','database_updates','5w1h_events'])).'</div><section><h2>Validation</h2><ul>'.$gates.'</ul></section><section><h2>Previous Scan Comparison</h2><p>New: '.count($changes['new']).' · Modified: '.count($changes['modified']).' · Deleted: '.count($changes['deleted']).' · Unchanged: '.$h($changes['unchanged']).'</p></section><section><h2>Largest Files</h2><table><tr><th>Path</th><th>Bytes</th></tr>'.$largest.'</table></section><section><h2>Failure Diagnostics</h2><table><tr><th>Class</th><th>Operation</th><th>Path</th><th>Reason</th></tr>'.$rows.'</table></section></main></body></html>'; } function make_html_report(array $record): string { $map = $record['source_map'] ?? []; $hitsArray = $record['hits'] ?? []; $flagsArray = $record['flags'] ?? []; $edgesArray = $map['edges'] ?? []; $functionsArray = $map['functions'] ?? []; $includesArray = $map['includes'] ?? []; $apisArray = $map['api_calls'] ?? []; $formsArray = $map['forms'] ?? []; $linksArray = $map['links'] ?? []; $criteriaArray = $record['function_criteria'] ?? build_function_criteria($record); $aiAnalysis = trim((string)($record['ai_analysis'] ?? '')); $hits = ''; foreach ($hitsArray as $hit) { $hits .= '<tr><td>' . h((string)($hit['category'] ?? 'Unknown')) . '</td><td>' . h((string)($hit['label'] ?? 'Unnamed rule')) . '</td><td>' . (int)($hit['count'] ?? 1) . '</td></tr>'; } if ($hits === '') { $hits = '<tr><td colspan="3">No rule hits were recorded for this scan.</td></tr>'; } $flags = ''; foreach ($flagsArray as $flag) { $flags .= '<li>' . h((string)$flag) . '</li>'; } if ($flags === '') { $flags = '<li>No file-level flags were recorded.</li>'; } $mapRows = ''; foreach ($edgesArray as $edge) { $mapRows .= '<tr><td>' . h((string)($edge['from'] ?? 'Unknown')) . '</td><td>' . h((string)($edge['type'] ?? 'related')) . '</td><td>' . h((string)($edge['to'] ?? 'Unknown')) . '</td></tr>'; } if ($mapRows === '') { $mapRows = '<tr><td colspan="3">No resolved relationship edges were detected in the current file scope.</td></tr>'; } $functionRows = ''; foreach ($functionsArray as $function) { $functionRows .= '<tr><td>' . h((string)($function['name'] ?? 'anonymous')) . '</td><td>' . h((string)($function['signature'] ?? '')) . '</td><td>' . h((string)($function['line'] ?? $function['start_line'] ?? 'Unknown')) . '</td></tr>'; } if ($functionRows === '') { $functionRows = '<tr><td colspan="3">No functions were parsed from this file.</td></tr>'; } $criteriaRows = ''; foreach ($criteriaArray as $criterion) { $purpose = trim((string)($criterion['purpose'] ?? '')); $recommendation = trim((string)($criterion['modernization_recommendation'] ?? '')); $criteriaRows .= '<tr><td>' . h((string)($criterion['function_name'] ?? 'unknown')) . '</td><td>' . h((string)($criterion['classification'] ?? 'unknown')) . '</td><td>' . h($purpose !== '' ? $purpose : 'Not yet established from available evidence.') . '</td><td>' . h($recommendation !== '' ? $recommendation : 'Run AI enrichment or complete manual review.') . '</td><td>' . number_format(((float)($criterion['confidence'] ?? 0)) * 100, 0) . '%</td></tr>'; } if ($criteriaRows === '') { $criteriaRows = '<tr><td colspan="5">No function criteria are available because no functions were parsed.</td></tr>'; } $dependencies = [ 'Includes' => $includesArray, 'API calls' => $apisArray, 'Forms' => $formsArray, 'Links' => $linksArray, ]; $dependencyHtml = ''; foreach ($dependencies as $label => $items) { $dependencyHtml .= '<div class="metric"><b>' . h($label) . '</b><span>' . count($items) . '</span></div>'; } $aiHtml = $aiAnalysis !== '' ? '<pre>' . h($aiAnalysis) . '</pre>' : report_empty_state( !empty($record['ai_input_summary']) ? 'AI input was prepared, but no completed AI response is stored.' : 'AI has not analyzed this scan yet. Use AI Review or Populate Function Criteria.' ); $accuracy = $record['accuracy'] ?? null; $accuracyHtml = is_array($accuracy) ? '<div class="metric"><b>Confidence</b><span>' . number_format(((float)($accuracy['confidence'] ?? 0)) * 100, 1) . '%</span></div>' . '<div class="metric"><b>Completeness</b><span>' . number_format(((float)($accuracy['completeness'] ?? 0)) * 100, 1) . '%</span></div>' . '<div class="metric"><b>Status</b><span>' . h((string)($accuracy['status'] ?? 'Unknown')) . '</span></div>' : report_empty_state('This individual file record does not contain a completed host-scan accuracy audit.'); return '<!doctype html><html><head><meta charset="utf-8"><title>SIMON Scan Report</title><style> body{font-family:Arial,sans-serif;background:#070912;color:#eef;padding:30px;line-height:1.45} .card{background:#11172b;border:1px solid #32406c;border-radius:18px;padding:20px;margin:14px 0} .grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px} .metric{padding:12px;border:1px solid #2b365d;border-radius:12px;background:#0b1020} .metric b,.metric span{display:block}.metric span{font-size:22px;margin-top:4px} table{width:100%;border-collapse:collapse}td,th{border-bottom:1px solid #2b365d;padding:9px;text-align:left;vertical-align:top} .sev{text-transform:uppercase;font-weight:800}.emptyState{padding:14px;border:1px dashed #47557e;border-radius:12px;color:#aeb8db} pre{white-space:pre-wrap;overflow-wrap:anywhere;background:#080d19;border-radius:12px;padding:14px} @media(max-width:760px){.grid{grid-template-columns:1fr}} </style></head><body> <h1>SIMON Scan Report</h1> <div class="card"><b>File:</b> ' . h((string)($record['original_name'] ?? 'Unknown')) . '<br><b>Score:</b> ' . (int)($record['score'] ?? 0) . '/100' . '<br><b>Severity:</b> <span class="sev">' . h((string)($record['severity'] ?? 'unknown')) . '</span>' . '<br><b>SHA256:</b> ' . h((string)($record['sha256'] ?? 'Not recorded')) . '<br><b>Created:</b> ' . h((string)($record['created_at'] ?? 'Unknown')) . '</div> <div class="card"><h2>Coverage Summary</h2><div class="grid"> <div class="metric"><b>Functions</b><span>' . count($functionsArray) . '</span></div> <div class="metric"><b>Relationship edges</b><span>' . count($edgesArray) . '</span></div> <div class="metric"><b>Findings</b><span>' . count($hitsArray) . '</span></div> </div><div class="grid" style="margin-top:12px">' . $dependencyHtml . '</div></div> <div class="card"><h2>Flags</h2><ul>' . $flags . '</ul></div> <div class="card"><h2>Rule Findings</h2><table><tr><th>Category</th><th>Rule</th><th>Count</th></tr>' . $hits . '</table></div> <div class="card"><h2>Functions</h2><table><tr><th>Name</th><th>Signature</th><th>Line</th></tr>' . $functionRows . '</table></div> <div class="card"><h2>Function Criteria</h2><table><tr><th>Function</th><th>Classification</th><th>Purpose</th><th>Modernization</th><th>Confidence</th></tr>' . $criteriaRows . '</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> <div class="card"><h2>AI Review</h2>' . $aiHtml . '</div> <div class="card"><h2>Accuracy Audit</h2>' . $accuracyHtml . '</div> </body></html>'; } ensure_dirs(); $settings = app_settings(); $sftpCapability = sftp_capability_status(); $projectDna = project_dna(); $beastAgents = beast_agents(); $beastJobs = beast_jobs(); $beastFlows = beast_flows(); $beastCriteria = beast_criteria(); $beastStats = beast_stats(); $operationMode = (string)($settings['operation_mode'] ?? 'assisted'); /* Default to the application directory, not the entire hosting account. */ $projectRoot = (string)($settings['project_root'] ?? __DIR__); $activeProject = ['id'=>'default','name'=>'SIMON Unified Project','root'=>$projectRoot]; /* Discovery is deferred until after authentication to prevent a white screen on large hosts. */ $projectFiles = []; $projectErrorMessage = ''; $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') { try { $html = make_html_report($record); if (trim($html) === '') { throw new RuntimeException('HTML report generator returned an empty document.'); } header('Content-Type: text/html; charset=utf-8'); header('Cache-Control: no-store, no-cache, must-revalidate'); header('X-Content-Type-Options: nosniff'); if ((string)($_GET['download'] ?? '') === '1') { $safeReportName = preg_replace('/[^A-Za-z0-9._-]+/', '_', (string)($record['original_name'] ?? 'scan')) ?: 'scan'; header('Content-Disposition: attachment; filename="SIMON-report-' . $safeReportName . '-' . $id . '.html"'); } echo $html; } catch (Throwable $reportError) { http_response_code(500); header('Content-Type: text/html; charset=utf-8'); echo '<!doctype html><html><head><meta charset="utf-8"><title>SIMON Report Error</title></head><body style="background:#090b13;color:#fff;font-family:system-ui;padding:30px"><h1>HTML report failed</h1><p>' . h($reportError->getMessage()) . '</p><p>Record: <code>' . h($id) . '</code></p></body></html>'; log_event('html_report_failed', ['id' => $id, 'error' => $reportError->getMessage()]); } } 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; /* * Authenticated, bounded project discovery. * The full host scan remains available through the resilient batch scanner. */ try { $treeLimit = max(50, min(500, (int)(getenv('SIMON_BUG_TREE_LIMIT') ?: 250))); $projectFiles = discover_project_files($projectRoot, $treeLimit); } catch (Throwable $projectError) { $projectFiles = []; $projectErrorMessage = $projectError->getMessage(); @file_put_contents(__DIR__ . '/simon_bug_runtime.log', '[' . date('c') . '] Project tree: ' . $projectErrorMessage . " ", FILE_APPEND | LOCK_EX); } if (in_array($action, ['host_report_view','host_report_download','host_inventory_csv','host_failures_csv','host_5w1h_stream','host_5w1h_reconcile'], true)) { $scanUuid=(string)($_GET['scan_uuid']??(host_scan_state()['scan_uuid']??'')); try { if ($scanUuid==='') throw new RuntimeException('No host scan is selected.'); if ($action==='host_5w1h_reconcile') { check_csrf(); header('Content-Type: application/json; charset=utf-8'); echo json_encode(simon_reconcile_5w1h_scan($scanUuid),JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);exit; } if ($action==='host_report_view') { $path=simon_scan_artifact_path(HOST_SCAN_REPORT_DIR,$scanUuid,'html'); if(!is_file($path)){ $state=host_scan_state(); if(($state['scan_uuid']??'')===$scanUuid)simon_save_host_scan_reports($state); } header('Content-Type: text/html; charset=utf-8');header('Cache-Control: no-store');readfile($path);exit; } $map=[ 'host_report_download'=>[HOST_SCAN_REPORT_DIR,'json','application/json','SIMON-host-report-'.$scanUuid.'.json'], 'host_inventory_csv'=>[HOST_SCAN_REPORT_DIR,'inventory.csv','text/csv','SIMON-host-inventory-'.$scanUuid.'.csv'], 'host_failures_csv'=>[HOST_SCAN_REPORT_DIR,'failures.csv','text/csv','SIMON-host-failures-'.$scanUuid.'.csv'], 'host_5w1h_stream'=>[SIMON_5W1H_STREAM_DIR,'ndjson','application/x-ndjson','SIMON-5W1H-'.$scanUuid.'.ndjson'], ]; [$dir,$ext,$type,$name]=$map[$action];$path=simon_scan_artifact_path($dir,$scanUuid,$ext); if(!is_file($path) && $action!=='host_5w1h_stream'){ $state=host_scan_state();if(($state['scan_uuid']??'')===$scanUuid)simon_save_host_scan_reports($state); } if(!is_file($path)) throw new RuntimeException('Requested report is not ready.'); header('Content-Type: '.$type.'; charset=utf-8');header('Content-Disposition: attachment; filename="'.$name.'"');header('Cache-Control: no-store');readfile($path);exit; } catch(Throwable $e){http_response_code(404);header('Content-Type:text/plain;charset=utf-8');echo 'SIMON report error: '.$e->getMessage();exit;} } if ($_SERVER['REQUEST_METHOD'] === 'POST' && in_array($action, [ 'host_scan_start', 'host_scan_batch', 'host_scan_status' ], true)) { check_csrf(); header('Content-Type: application/json; charset=utf-8'); try { if ($action === 'host_scan_start') { $root = (string)($_POST['host_root'] ?? ($_SERVER['DOCUMENT_ROOT'] ?? __DIR__)); $state = start_host_scan($root); } elseif ($action === 'host_scan_batch') { $state = process_host_scan_batch( host_scan_state(), $keywordRules, $riskExtensions, (int)($_POST['batch_size'] ?? 25) ); } else { $state = host_scan_state(); } echo json_encode( host_scan_public_status($state), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); } catch (Throwable $error) { http_response_code(500); echo json_encode([ 'ok' => false, 'error' => $error->getMessage(), ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } exit; } $notice = ''; $selected = null; $remoteEntries = []; $remotePath = (string)($_SESSION['sftp_path'] ?? '.'); $sftpBrowserOpen = false; if ($_SERVER['REQUEST_METHOD'] === 'POST') { check_csrf(); if ($action === 'db_connect') { $submittedDbPassword = (string)($_POST['db_password'] ?? ''); if ($submittedDbPassword === '') { $notice = 'Enter the MariaDB password.'; } else { $_SESSION['simon_db_password'] = $submittedDbPassword; unset($_SESSION['simon_db_last_error']); $dbTest = simon_db_connection(); if ($dbTest instanceof PDO) { $notice = 'MariaDB connected: dbs15883515 on db5020893348.hosting-data.io.'; log_event('database_connected', [ 'host' => 'db5020893348.hosting-data.io', 'database' => 'dbs15883515', 'user' => 'dbu5579662', ]); } else { $notice = 'MariaDB connection failed. Re-enter the current database password.'; } } } if ($action === 'db_disconnect') { unset($_SESSION['simon_db_password'], $_SESSION['simon_db_last_error']); $notice = 'MariaDB password removed from this session.'; log_event('database_disconnected'); } if ($action === 'upload' && isset($_FILES['scan_file'])) { $uploadedFiles = normalize_upload_files($_FILES['scan_file']); $completed = 0; $failed = 0; $latestRecord = null; foreach ($uploadedFiles as $file) { if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { $failed++; continue; } if (($file['size'] ?? 0) > MAX_UPLOAD_BYTES) { $failed++; continue; } $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)) { $failed++; continue; } @chmod($destination, 0600); $record = scan_file($destination, $name, $keywordRules, $riskExtensions); $historyItems = history(); array_unshift($historyItems, $record); save_history($historyItems); $latestRecord = $record; $completed++; log_event('scan_completed', [ 'id' => $record['id'], 'score' => $record['score'], 'severity' => $record['severity'], ]); } if ($latestRecord) { $selected = $latestRecord; } $notice = $completed . ' file(s) scanned and mapped'; if ($failed > 0) { $notice .= ' · ' . $failed . ' failed'; } $notice .= '. Project graph rebuilt.'; } 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'] = trusted_fingerprints_for_host( $settings['sftp_host'], 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['color_mode'] = in_array((string)($_POST['color_mode'] ?? 'rainbow'), ['rainbow','custom','green'], true) ? (string)$_POST['color_mode'] : 'rainbow'; $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_disconnect') { unset( $_SESSION['sftp_host'], $_SESSION['sftp_port'], $_SESSION['sftp_user'], $_SESSION['sftp_pass'], $_SESSION['sftp_fingerprint'], $_SESSION['sftp_path'] ); $remoteEntries = []; $remotePath = '.'; $sftpBrowserOpen = false; $notice = 'SFTP session disconnected.'; log_event('sftp_disconnected'); } if ($action === 'beast_save_agent') { $agents=beast_agents(); $id=trim((string)($_POST['agent_id']??'')); $found=false; foreach ($agents as &$agent) { if (($agent['id']??'')!==$id) continue; $agent['name']=trim((string)($_POST['name']??$agent['name'])); $agent['provider']=trim((string)($_POST['provider']??$agent['provider'])); $agent['model']=trim((string)($_POST['model']??$agent['model'])); $authority=(string)($_POST['authority']??$agent['authority']); if (in_array($authority,['observe','advisory','proposal','approval_required','autonomous'],true)) $agent['authority']=$authority; $agent['enabled']=!empty($_POST['enabled']); $agent['instructions']=trim((string)($_POST['instructions']??$agent['instructions'])); $agent['updated_at']=beast_now(); $found=true; break; } unset($agent); if (!$found) $notice='Agent was not found.'; else { beast_save_json(BEAST_AGENTS_FILE,$agents); $notice='Agent policy updated.'; } $beastAgents=beast_agents(); $beastStats=beast_stats(); } if ($action === 'beast_create_job') { try { $jobs = beast_jobs(); $job = beast_create_job($_POST); $runNow = (string)($_POST['submit_job'] ?? 'route') === 'run_now'; if ($runNow) { // The user explicitly requested execution. Treat this click as approval. if (($job['status'] ?? '') === 'pending_approval') { $job['status'] = 'queued'; $job['approved_at'] = beast_now(); } $job = beast_run_job($job, [ 'project_files' => $projectFiles, 'project_root' => (string)($job['input']['project_root'] ?? $projectRoot), ]); } array_unshift($jobs, $job); beast_save_json(BEAST_JOBS_FILE, array_slice($jobs, 0, 1000)); log_event('beast_job_created', [ 'job_id' => $job['id'] ?? null, 'job_type' => $job['job_type'] ?? null, 'status' => $job['status'] ?? null, 'run_now' => $runNow, 'error' => $job['error'] ?? null, ]); if (($job['status'] ?? '') === 'failed') { $notice = 'Job failed: ' . (string)($job['error'] ?? 'Unknown execution error.'); } elseif (($job['status'] ?? '') === 'completed') { $notice = 'Job completed: ' . (string)($job['result']['message'] ?? $job['title'] ?? 'Completed'); } else { $notice = 'Job created with status ' . (string)($job['status'] ?? 'unknown') . '. Use the Job Board actions to continue.'; } } catch (Throwable $e) { $notice = 'Job creation failed: ' . $e->getMessage(); log_event('beast_job_create_failed', ['error' => $e->getMessage()]); } $beastJobs = beast_jobs(); $beastStats = beast_stats(); } if ($action === 'beast_job_action') { $id = (string)($_POST['job_id'] ?? ''); $verb = (string)($_POST['job_action'] ?? ''); $jobs = beast_jobs(); $matched = false; try { foreach ($jobs as &$job) { if (($job['id'] ?? '') !== $id) continue; $matched = true; if ($verb === 'approve') { $job['status'] = 'queued'; $job['approved_at'] = beast_now(); $notice = 'Job approved and queued. Click Run to execute it.'; } elseif ($verb === 'approve_run') { $job['status'] = 'queued'; $job['approved_at'] = beast_now(); $job = beast_run_job($job, [ 'project_files' => $projectFiles, 'project_root' => (string)($job['input']['project_root'] ?? $projectRoot), ]); $notice = ($job['status'] ?? '') === 'completed' ? 'Job approved and completed.' : 'Job execution failed: ' . (string)($job['error'] ?? 'Unknown error.'); } elseif ($verb === 'reject') { $job['status'] = 'rejected'; $job['completed_at'] = beast_now(); $notice = 'Job rejected.'; } elseif ($verb === 'run') { $job = beast_run_job($job, [ 'project_files' => $projectFiles, 'project_root' => (string)($job['input']['project_root'] ?? $projectRoot), ]); $notice = ($job['status'] ?? '') === 'completed' ? 'Job completed: ' . (string)($job['result']['message'] ?? $job['title'] ?? 'Completed') : 'Job failed: ' . (string)($job['error'] ?? 'Unknown error.'); } elseif ($verb === 'cancel') { $job['status'] = 'cancelled'; $job['completed_at'] = beast_now(); $notice = 'Job cancelled.'; } else { throw new RuntimeException('Unknown job action.'); } $job['updated_at'] = beast_now(); log_event('beast_job_action', [ 'job_id' => $id, 'action' => $verb, 'status' => $job['status'] ?? null, 'error' => $job['error'] ?? null, ]); break; } unset($job); if (!$matched) throw new RuntimeException('Job was not found.'); beast_save_json(BEAST_JOBS_FILE, $jobs); } catch (Throwable $e) { unset($job); $notice = 'Job action failed: ' . $e->getMessage(); log_event('beast_job_action_failed', ['job_id' => $id, 'action' => $verb, 'error' => $e->getMessage()]); } $beastJobs = beast_jobs(); $beastStats = beast_stats(); } if ($action === 'beast_save_criteria') { $criteria=beast_criteria(); $criteria['profile_name']=trim((string)($_POST['profile_name']??$criteria['profile_name'])); $criteria['minimum_confidence']=max(0,min(1,(float)($_POST['minimum_confidence']??0.70))); $criteria['block_on_critical']=!empty($_POST['block_on_critical']); $criteria['unknown_is_safe']=!empty($_POST['unknown_is_safe']); $enabled=(array)($_POST['rule_enabled']??[]); foreach ($criteria['rules'] as &$rule) $rule['enabled']=in_array($rule['id'],$enabled,true); unset($rule); $criteria['version']=(int)($criteria['version']??1)+1; $criteria['updated_at']=beast_now(); beast_save_json(BEAST_CRITERIA_FILE,$criteria); $beastCriteria=beast_criteria(); $notice='Scan criteria version '.$criteria['version'].' saved.'; } if ($action === 'beast_save_flow') { $flows=beast_flows(); $id=(string)($_POST['flow_id']??''); foreach ($flows as &$flow) { if (($flow['id']??'')!==$id) continue; $flow['enabled']=!empty($_POST['enabled']); $flow['trigger']=trim((string)($_POST['trigger']??$flow['trigger'])); $flow['authority']=trim((string)($_POST['authority']??$flow['authority'])); $flow['updated_at']=beast_now(); break; } unset($flow); beast_save_json(BEAST_FLOWS_FILE,$flows); $beastFlows=beast_flows(); $notice='Data flow policy updated.'; } if ($action === 'save_project_dna') { $allowedFocus = ['quick','full','security','architecture','performance','deployment','documentation','function','endpoint']; $scanFocus = (string)($_POST['scan_focus'] ?? 'full'); if (!in_array($scanFocus, $allowedFocus, true)) { $scanFocus = 'full'; } $projectDna = [ 'project_name' => trim((string)($_POST['project_name'] ?? 'SIMON Unified Project')), 'purpose' => trim((string)($_POST['purpose'] ?? '')), 'primary_goal' => trim((string)($_POST['primary_goal'] ?? '')), 'intended_users' => trim((string)($_POST['intended_users'] ?? '')), 'business_objective' => trim((string)($_POST['business_objective'] ?? '')), 'required_features' => normalize_text_list((string)($_POST['required_features'] ?? '')), 'known_problems' => normalize_text_list((string)($_POST['known_problems'] ?? '')), 'preserve_rules' => normalize_text_list((string)($_POST['preserve_rules'] ?? '')), 'change_rules' => normalize_text_list((string)($_POST['change_rules'] ?? '')), 'security_requirements' => normalize_text_list((string)($_POST['security_requirements'] ?? '')), 'success_criteria' => normalize_text_list((string)($_POST['success_criteria'] ?? '')), 'scan_focus' => $scanFocus, 'ai_instructions' => trim((string)($_POST['ai_instructions'] ?? '')), 'owner_notes' => trim((string)($_POST['owner_notes'] ?? '')), ]; save_project_dna($projectDna); $notice = 'Project Intent saved. Future scans and AI reviews will use this mission.'; log_event('project_dna_saved', [ 'project_name' => $projectDna['project_name'], 'scan_focus' => $projectDna['scan_focus'], ]); } if ($action === 'set_mode') { $mode = (string)($_POST['operation_mode'] ?? 'assisted'); if (!in_array($mode, ['legacy','assisted','automated'], true)) $mode = 'assisted'; $settings['operation_mode'] = $mode; save_app_settings($settings); $operationMode = $mode; $notice = 'Operating mode changed to ' . ucfirst($mode) . '.'; } if ($action === 'set_project_root') { try { $root = safe_project_root((string)($_POST['project_root'] ?? __DIR__)); $settings['project_root'] = $root; save_app_settings($settings); $projectRoot = $root; $projectFiles = discover_project_files($root); $notice = 'Project root loaded: ' . $root; } catch (Throwable $e) { $notice = $e->getMessage(); } } if ($action === 'scan_project_file') { try { $root = safe_project_root($projectRoot); $real = realpath((string)($_POST['file_path'] ?? '')); if (!$real || !is_file($real) || !str_starts_with($real, $root)) { throw new RuntimeException('Selected file is outside the active project root.'); } if (!is_readable($real)) { throw new RuntimeException('Selected file is not readable by the host scanner.'); } $record = scan_file($real, basename($real), $keywordRules, $riskExtensions); $record['source_type'] = 'host_project_tree'; $record['source_path'] = $real; $record['relative_path'] = ltrim(str_replace('\\', '/', substr($real, strlen($root))), '/'); $record['five_w_one_h'] = [ 'who' => 'operator', 'what' => 'selected_file_scan', 'when' => date('c'), 'where' => $record['relative_path'], 'why' => 'operator selected the file from the host tree', 'how' => 'direct host file scan', 'truth' => true, 'health' => [ 'confidence' => 0.96, 'ambiguity' => 0.04, 'completeness' => 1.0, 'risk' => min(1, ((int)($record['score'] ?? 0)) / 100), ], ]; $historyItems = history(); array_unshift($historyItems, $record); save_history($historyItems); $selected = $record; $_SESSION['editor_file'] = $real; $notice = 'Scanned selected host file: ' . basename($real) . '. Intelligence, relationships, risks, and 5W1H event were recorded.'; log_event('host_tree_file_scanned', [ 'id' => $record['id'] ?? null, 'path' => $record['relative_path'], 'score' => $record['score'] ?? null, ]); } catch (Throwable $e) { $notice = $e->getMessage(); log_event('host_tree_file_scan_failed', ['error' => $e->getMessage()]); } } if ($action === 'open_project_file') { try { $root = safe_project_root($projectRoot); $real = realpath((string)($_POST['file_path'] ?? '')); if (!$real || !is_file($real) || !str_starts_with($real, $root)) { throw new RuntimeException('Project file is outside the active project root.'); } $_SESSION['editor_file'] = $real; $notice = 'Opened in editor: ' . basename($real); } catch (Throwable $e) { $notice = $e->getMessage(); } } if ($action === 'save_editor_file') { try { $root = safe_project_root($projectRoot); $real = realpath((string)($_POST['file_path'] ?? '')); if (!$real || !is_file($real) || !str_starts_with($real, $root)) { throw new RuntimeException('Editor file is outside the active project root.'); } $content = (string)($_POST['file_content'] ?? ''); if (strlen($content) > 4_000_000) { throw new RuntimeException('Editor file exceeds the 4 MB editing limit.'); } if (!empty($settings['auto_checkpoint_on_save'])) { create_checkpoint($activeProject['id'], $real, 'before-save'); } if (file_put_contents($real, $content, LOCK_EX) === false) { throw new RuntimeException('Could not save the project file.'); } $_SESSION['editor_file'] = $real; $notice = 'Saved: ' . basename($real); if (!empty($settings['auto_scan_on_save'])) { $record = scan_file($real, basename($real), $keywordRules, $riskExtensions); $items = history(); array_unshift($items, $record); save_history($items); $selected = $record; $notice .= ' · Re-scanned.'; } } catch (Throwable $e) { $notice = $e->getMessage(); } } if ($action === 'create_checkpoint') { try { $checkpoint = create_checkpoint( $activeProject['id'], (string)($_POST['file_path'] ?? ''), 'manual' ); $notice = 'Checkpoint created: ' . $checkpoint['id']; } catch (Throwable $e) { $notice = $e->getMessage(); } } if ($action === 'restore_checkpoint') { try { $checkpoint = realpath((string)($_POST['checkpoint_path'] ?? '')); $target = realpath((string)($_POST['file_path'] ?? '')); if (!$checkpoint || !$target || !is_file($checkpoint) || !is_file($target)) { throw new RuntimeException('Checkpoint or target file is missing.'); } if (!str_starts_with($checkpoint, realpath(CHECKPOINTS_DIR))) { throw new RuntimeException('Invalid checkpoint path.'); } create_checkpoint($activeProject['id'], $target, 'before-restore'); if (!copy($checkpoint, $target)) { throw new RuntimeException('Checkpoint restore failed.'); } $_SESSION['editor_file'] = $target; $notice = 'Checkpoint restored.'; } catch (Throwable $e) { $notice = $e->getMessage(); } } if ($action === 'fill_function_criteria') { $id = (string)($_POST['id'] ?? ''); $record = find_record($id); if (!$record) { $notice = 'Selected scan record was not found.'; } else { try { $criteria = build_function_criteria($record); $provider = 'SIMON Local criteria engine'; $apiKey = (string)($_SESSION['scanner_ai_key'] ?? getenv('OPENAI_API_KEY') ?: ''); if (!empty($settings['ai_enabled']) && $apiKey !== '') { try { $criteria = enrich_function_criteria_with_ai($settings, $apiKey, $record, $criteria); $provider = (string)($settings['ai_model'] ?? 'Connected AI'); } catch (Throwable $aiError) { log_event('function_criteria_ai_fallback', [ 'id' => $id, 'error' => $aiError->getMessage(), ]); $provider = 'SIMON Local fallback'; } } $selected = update_history_record($id, function (array $item) use ($criteria, $provider): array { $item['function_criteria'] = $criteria; $item['function_criteria_provider'] = $provider; $item['function_criteria_updated_at'] = date('c'); return $item; }); $notice = 'Function criteria populated for ' . count($criteria) . ' function(s) using ' . $provider . '.'; } catch (Throwable $error) { $notice = 'Function criteria failed: ' . $error->getMessage(); } } } if ($action === 'queue_ai_review') { $candidate = add_ai_review_candidate([ 'project_id'=>$activeProject['id'], 'file_path'=>(string)($_POST['file_path'] ?? ''), 'review_type'=>(string)($_POST['review_type'] ?? 'full'), 'project_goal'=>(string)($projectDna['primary_goal'] ?? ''), 'scan_focus'=>(string)($projectDna['scan_focus'] ?? 'full'), 'provider'=>'SIMON Local', 'status'=>'queued', 'score'=>null, ]); $notice = 'AI review queued: ' . $candidate['review_type']; } if ($action === 'rebuild_function_intelligence') { try { $functionIntelligence = build_function_intelligence($projectFiles); save_function_intelligence_cache($functionIntelligence); $notice = 'Function Intelligence rebuilt: ' . (int)($functionIntelligence['summary']['total'] ?? 0) . ' functions analyzed.'; log_event('function_intelligence_rebuilt', $functionIntelligence['summary'] ?? []); } catch (Throwable $e) { $notice = 'Function Intelligence failed: ' . $e->getMessage(); log_event('function_intelligence_failed', ['error'=>$e->getMessage()]); } } if ($action === 'scan_server_path') { try { $record = scan_server_path( (string)($_POST['server_path'] ?? ''), $keywordRules, $riskExtensions ); $items = history(); array_unshift($items, $record); save_history($items); $selected = $record; $notice = 'Server file scanned and mapped: ' . ($record['server_path'] ?? ''); log_event('server_path_scan_success', [ 'id' => $record['id'], 'path' => $record['server_path'] ?? '', ]); } catch (Throwable $e) { $notice = $e->getMessage(); log_event('server_path_scan_failed', ['error' => $e->getMessage()]); } } 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 = trusted_fingerprints_for_host( $host, trim((string)($_POST['sftp_fingerprint'] ?? $_SESSION['sftp_fingerprint'] ?? $settings['sftp_fingerprint'])) ); $submittedPassword = (string)($_POST['sftp_pass'] ?? ''); $password = $submittedPassword !== '' ? $submittedPassword : (string)($_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); if (!empty($GLOBALS['resolvedSftpPath'])) { $remotePath = (string)$GLOBALS['resolvedSftpPath']; $_SESSION['sftp_path'] = $remotePath; } $sftpBrowserOpen = true; $notice = 'SFTP directory loaded: ' . $remotePath; $_SESSION['scroll_to_server_browser'] = true; 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]; } $editorFile = (string)($_SESSION['editor_file'] ?? ''); $editorContent = ($editorFile && is_file($editorFile)) ? (string)file_get_contents($editorFile) : ''; $editorExtension = $editorFile ? strtolower(pathinfo($editorFile, PATHINFO_EXTENSION)) : ''; $projectEndpoints = endpoint_inventory_from_history($history); $projectGrade = grade_unified_project($history, $projectEndpoints); $checkpoints = list_checkpoints($activeProject['id']); $reviewQueue = ai_review_queue(); $functionIntelligence = function_intelligence_cache(); if (empty($functionIntelligence['generated_at']) && $projectFiles) { try { $functionIntelligence = build_function_intelligence($projectFiles); save_function_intelligence_cache($functionIntelligence); } catch (Throwable $functionIntelError) { $functionIntelligence = function_intelligence_cache(); } } $functionIntelSummary = $functionIntelligence['summary'] ?? []; $functionIntelFunctions = $functionIntelligence['functions'] ?? []; $functionIntelBroken = $functionIntelligence['broken_references'] ?? []; $hostScanState = host_scan_public_status(host_scan_state()); $dataDictionaryStatus = load_data_dictionary_contract(); $dataDictionaryValidation = validate_data_dictionary_contract($dataDictionaryStatus); $scanAccuracyStatus = read_json_file(SCAN_ACCURACY_FILE, []); $databaseConnected = simon_db_connection() instanceof PDO; $databasePasswordPresent = !empty($_SESSION['simon_db_password']) || (string)getenv('SIMON_DB_PASSWORD') !== ''; $databaseLastError = (string)($_SESSION['simon_db_last_error'] ?? ''); $databaseStatusLabel = $databaseConnected ? 'CONNECTED' : ($databasePasswordPresent ? 'CONNECTION FAILED' : 'PASSWORD REQUIRED'); $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); $projectGraph = build_project_graph($history); $projectGraphJson = json_encode($projectGraph, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); $searchIndex = []; if ($selected) { $selectedId = (string)($selected['id'] ?? ''); $selectedName = (string)($selected['original_name'] ?? 'Selected file'); $searchIndex[] = [ 'file_id' => $selectedId, 'file' => $selectedName, 'score' => (int)($selected['score'] ?? 0), 'severity' => (string)($selected['severity'] ?? ''), 'type' => 'current', 'label' => $selectedName, 'detail' => 'Current file · click to view analysis', 'url' => '?id=' . rawurlencode($selectedId), ]; } 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', ]; } $rawPreview = (string)($record['text_preview'] ?? ''); if ($rawPreview !== '') { $lines = preg_split('/\R/', $rawPreview) ?: []; $seenSnippets = []; foreach ($lines as $lineNumber => $lineText) { $clean = trim(preg_replace('/\s+/', ' ', strip_tags($lineText)) ?? ''); if ($clean === '' || strlen($clean) < 4) continue; $keywords = preg_split('/[^A-Za-z0-9_.$\/:-]+/', strtolower($clean)) ?: []; $keywords = array_values(array_unique(array_filter($keywords, static fn($v) => strlen($v) >= 3))); $snippetKey = substr($clean, 0, 180); if (isset($seenSnippets[$snippetKey])) continue; $seenSnippets[$snippetKey] = true; $searchIndex[] = $base + [ 'type' => 'content', 'label' => $base['file'] . ' · line ' . ($lineNumber + 1), 'detail' => substr($clean, 0, 260), 'keywords' => implode(' ', array_slice($keywords, 0, 30)), 'url' => '?id=' . rawurlencode($base['file_id']) . '#preview', ]; if (count($seenSnippets) >= 120) break; } } } $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 <?= max(140, (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} .sftpStatus{padding:10px 12px;border-radius:14px;margin-bottom:10px;border:1px solid var(--line2)} .sftpStatus.ready{background:rgba(50,255,115,.08);border-color:rgba(50,255,115,.25)} .sftpStatus.offline{background:rgba(255,95,122,.08);border-color:rgba(255,95,122,.25)} .sftpStatus small{display:block;color:var(--muted);margin-top:3px} .manualScan{margin-top:10px;border:1px solid var(--line2);border-radius:16px;padding:10px;background:rgba(255,255,255,.025)} .manualScan summary{cursor:pointer;color:var(--cyan);font-weight:800;margin-bottom:10px} .screensaver{position:fixed;inset:0;display:none;background:#000;z-index:300;overflow:hidden} .screensaver.on{display:block} #bugCanvas{position:absolute;inset:0;width:100%;height:100%;display:block} .evolutionHud{position:absolute;left:50%;bottom:42px;transform:translateX(-50%);width:min(560px,86vw);text-align:center;padding:18px;border-radius:24px;background:rgba(5,5,12,.5);border:1px solid rgba(255,255,255,.16);backdrop-filter:blur(18px);box-shadow:0 20px 70px #000} .evoTitle{font-weight:1000;letter-spacing:.18em;margin-bottom:10px;background:linear-gradient(90deg,#ff2b2b,#ff9f1c,#ffe600,#31e981,#30c5ff,#7a5cff,#ff4fd8);-webkit-background-clip:text;background-clip:text;color:transparent} .evoMeter{height:12px;border-radius:999px;background:rgba(255,255,255,.08);overflow:hidden} #evoFill{height:100%;width:0;background:linear-gradient(90deg,#ff2b2b,#ff9f1c,#ffe600,#31e981,#30c5ff,#7a5cff,#ff4fd8);box-shadow:0 0 24px rgba(255,79,216,.7);transition:width .4s ease} #evoLabel{margin-top:9px;color:#fff;font-weight:800} .evolutionHud small{display:block;margin-top:5px;color:rgba(255,255,255,.65)} html,body{max-width:100%;overflow-x:hidden} .app,.shell,.main,.right,.panel,.metric,.card,.preview,.code,.chatThread,.modalCard{min-width:0} .main,.right{overflow-x:hidden} h1,h2,h3,p,li,td,th,.metric,.card,.file,.pill,.notice,.sftpStatus,.remoteItem{overflow-wrap:anywhere;word-break:break-word} .code{white-space:pre;overflow:auto;max-width:100%} .mapPanel{min-height:430px} .mapCanvas{height:390px} .graphToolbar{display:flex;gap:8px;flex-wrap:wrap;margin:10px 0} .graphLegend{display:flex;gap:12px;flex-wrap:wrap;font-size:12px;color:var(--muted)} .graphLegend span::before{content:"";display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:5px;background:var(--dot)} .graphTooltip{position:absolute;pointer-events:none;display:none;z-index:5;max-width:280px;padding:9px 11px;border-radius:12px;background:#07100d;border:1px solid var(--line);box-shadow:0 20px 60px #000;font-size:12px} .projectStats{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:12px 0} @media(max-width:900px){ .projectStats{grid-template-columns:repeat(2,1fr)} .top{flex-wrap:wrap;height:auto;padding:10px} .ticker{flex-basis:100%} .app{grid-template-columns:1fr} .brand,.top,.main,.right{grid-column:1!important;grid-row:auto!important} } @media(max-width:600px){ .cards,.metricGrid,.projectStats{grid-template-columns:1fr} .mapPanel{min-height:360px} .mapCanvas{height:320px} .notice{max-width:90vw;white-space:normal;text-align:center} } .screensaver::before, .screensaver::after{ content:""; position:absolute; left:50%; top:46%; width:clamp(110px,18vw,260px); height:clamp(70px,12vw,170px); border-radius:50% 46% 50% 46%; transform:translate(-50%,-50%); background: radial-gradient(circle at 24% 38%,#fff 0 3%,transparent 4%), radial-gradient(circle at 24% 62%,#fff 0 3%,transparent 4%), conic-gradient(from 0deg,#ff3158,#ff9f1c,#ffe83b,#31e981,#30c5ff,#7657ff,#ff4fd8,#ff3158); box-shadow:0 0 45px #30c5ff,0 0 100px rgba(255,79,216,.72); animation:cssBugEvolve 5s ease-in-out infinite alternate,cssBugHue 9s linear infinite; opacity:.94; } .screensaver::after{ width:clamp(220px,36vw,520px); height:clamp(220px,36vw,520px); border-radius:50%; background:repeating-conic-gradient(from 0deg,rgba(255,255,255,.14) 0 3deg,transparent 3deg 12deg); box-shadow:none; animation:cssOrbit 18s linear infinite; opacity:.35; } @keyframes cssBugEvolve{ 0%{transform:translate(-50%,-50%) scale(.72) rotate(-8deg)} 50%{transform:translate(-46%,-54%) scale(1.05) rotate(5deg)} 100%{transform:translate(-52%,-47%) scale(1.28) rotate(12deg)} } @keyframes cssBugHue{to{filter:hue-rotate(360deg)}} @keyframes cssOrbit{to{transform:translate(-50%,-50%) rotate(360deg)}} .screensaver.on .evolutionHud{animation:hudFloat 3.5s ease-in-out infinite alternate} @keyframes hudFloat{to{transform:translateX(-50%) translateY(-10px)}} .serverPanel{ margin-top:18px; padding:14px; border:1px solid var(--line2); border-radius:20px; background:rgba(255,255,255,.025); } .sftpConnectForm{ margin-top:12px; } .connectedBar{ display:flex; justify-content:space-between; align-items:center; gap:12px; margin-top:12px; padding:12px 14px; border:1px solid rgba(50,255,115,.25); border-radius:16px; background:rgba(50,255,115,.07); } .connectedBar small{ display:block; color:var(--muted); margin-top:3px; } .cleanBrowser{ margin-top:12px; } .browserToolbar{ display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:12px; } .pathBox{ min-width:0; flex:1; padding:10px 12px; border-radius:14px; border:1px solid var(--line2); background:rgba(0,0,0,.2); } .pathBox small{ display:block; color:var(--muted); } .pathBox b{ display:block; overflow-wrap:anywhere; } .remoteTableWrap{ overflow:auto; max-height:520px; border:1px solid var(--line2); border-radius:16px; } .remoteTable{ width:100%; border-collapse:collapse; min-width:760px; } .remoteTable th, .remoteTable td{ text-align:left; padding:11px 12px; border-bottom:1px solid var(--line2); vertical-align:middle; } .remoteTable th{ position:sticky; top:0; z-index:2; background:#0b100f; color:var(--muted); font-size:12px; } .remoteTable td small{ display:block; color:var(--muted); margin-top:3px; max-width:360px; overflow-wrap:anywhere; } .emptyRemote{ padding:20px; text-align:center; } .emptyRemote small{ display:block; color:var(--muted); margin-top:5px; } @media(max-width:700px){ .connectedBar, .browserToolbar{ align-items:stretch; flex-direction:column; } .remoteTableWrap{ max-height:420px; } } .modePanel{margin-top:18px;padding:12px;border:1px solid var(--line2);border-radius:16px;background:rgba(255,255,255,.03)} .modePanel small{color:var(--muted)} .modePanel p{font-size:12px;color:var(--muted);line-height:1.45;margin-bottom:0} .unifiedHeader{display:flex;justify-content:space-between;gap:20px;align-items:flex-start;margin-bottom:16px} .unifiedHeader h1{margin:.2rem 0} .unifiedHeader p{color:var(--muted);max-width:760px} .gradeCard{min-width:180px;padding:14px;border:1px solid var(--line);border-radius:18px;background:rgba(50,255,115,.06);text-align:center} .gradeCard b{display:block;font-size:42px} .gradeCard p{font-size:12px} .studioGrid{display:grid;grid-template-columns:300px minmax(0,1fr) 300px;gap:12px;min-height:620px} .studioPane{border:1px solid var(--line2);border-radius:20px;background:rgba(255,255,255,.025);padding:12px;min-width:0;overflow:hidden} .paneHead{display:flex;justify-content:space-between;gap:10px;align-items:center;margin-bottom:12px} .paneHead small{display:block;color:var(--muted);overflow-wrap:anywhere} .fileExplorerList{display:grid;gap:6px;margin-top:10px;max-height:500px;overflow:auto} .projectFileRow button{width:100%;text-align:left;background:rgba(255,255,255,.03)} .projectFileRow b,.projectFileRow small{display:block;overflow-wrap:anywhere} .projectFileRow small{color:var(--muted);margin-top:3px} .fileCategory{display:inline-block;font-size:9px;padding:2px 6px;border-radius:999px;background:rgba(84,241,255,.1);color:var(--cyan);margin-bottom:4px} .editorPane{display:flex;flex-direction:column} .editorForm{display:flex;flex-direction:column;min-height:540px;gap:10px} #codeEditor{flex:1;min-height:500px;font:12px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre;overflow:auto;tab-size:4} .editorActions{display:flex;justify-content:space-between;align-items:center;gap:10px;color:var(--muted)} .emptyPane{display:grid;place-items:center;text-align:center;min-height:520px;color:var(--muted)} .intelligenceStats{display:grid;grid-template-columns:repeat(2,1fr);gap:8px} .intelligenceStats div{padding:10px;border-radius:14px;background:rgba(0,0,0,.2);border:1px solid var(--line2)} .intelligenceStats b{display:block;font-size:22px} .quickActions,.checkpointList{display:grid;gap:8px;margin-top:14px} .quickActions form,.quickActions button{width:100%} .checkpointItem{padding:9px;border:1px solid var(--line2);border-radius:12px;background:rgba(255,255,255,.025)} .checkpointItem span,.checkpointItem small{display:block;overflow-wrap:anywhere} .checkpointItem small{color:var(--muted);margin:3px 0 6px} .bottomWorkspace{display:grid;grid-template-columns:1.2fr .8fr;gap:12px;margin-top:12px} .workspaceTab{border:1px solid var(--line2);border-radius:20px;background:rgba(255,255,255,.025);padding:12px;min-width:0} .endpointTableWrap{overflow:auto;max-height:360px} .reviewCards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;max-height:360px;overflow:auto} .reviewCard{padding:10px;border:1px solid var(--line2);border-radius:14px;background:rgba(0,0,0,.18)} .reviewCard b,.reviewCard small{display:block;overflow-wrap:anywhere} .reviewCard small{color:var(--muted);margin-top:4px} .legacyScanner{margin-top:14px} @media(max-width:1200px){.studioGrid{grid-template-columns:260px 1fr}.intelligencePane{grid-column:1/-1}.bottomWorkspace{grid-template-columns:1fr}} @media(max-width:800px){.studioGrid{grid-template-columns:1fr}.unifiedHeader{flex-direction:column}.gradeCard{width:100%}.reviewCards{grid-template-columns:1fr}} .functionIntelModule{margin-top:14px;padding:14px;border:1px solid var(--line2);border-radius:20px;background:rgba(255,255,255,.025)} .functionSummaryGrid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px;margin-bottom:12px} .functionSummaryCard{text-align:left;min-width:0;background:rgba(0,0,0,.22)}.functionSummaryCard small{display:block;color:var(--muted);overflow-wrap:anywhere}.functionSummaryCard b{display:block;margin-top:4px;font-size:24px} .functionIntelToolbar{display:grid;grid-template-columns:1fr 240px;gap:8px;margin-bottom:10px}.functionTableWrap{overflow:auto;max-height:620px;border:1px solid var(--line2);border-radius:16px} .functionTable{width:100%;min-width:1120px;border-collapse:collapse}.functionTable th,.functionTable td{padding:10px 11px;border-bottom:1px solid var(--line2);vertical-align:top;text-align:left;font-size:12px}.functionTable th{position:sticky;top:0;z-index:2;background:#0b100f;color:var(--muted)}.functionTable td small{display:block;color:var(--muted);margin-top:3px} .functionBadge{display:inline-flex;border-radius:999px;padding:4px 8px;font-size:10px;font-weight:900;text-transform:uppercase;background:rgba(255,255,255,.08)} .class-canonical{color:#9effbb;background:rgba(50,255,115,.12)}.class-duplicate{color:#ffd19b;background:rgba(255,157,46,.14)}.class-unused{color:#ffe69a;background:rgba(255,220,80,.14)}.class-high-error{color:#ff9aa7;background:rgba(255,77,94,.16)}.class-shared-candidate{color:#9deeff;background:rgba(84,241,255,.14)}.class-archive-candidate{color:#c7b9ff;background:rgba(128,92,255,.14)}.class-project-specific{color:#d6e1dc;background:rgba(180,200,190,.1)} .functionHint{margin-top:6px;color:var(--cyan);font-size:11px}.archiveHint{color:#c7b9ff}.brokenReferencePanel{margin-top:12px;padding:10px;border:1px solid var(--line2);border-radius:14px;background:rgba(255,255,255,.02)}.brokenReferencePanel summary{cursor:pointer;color:var(--orange);font-weight:800}.brokenReferenceList{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:10px;max-height:360px;overflow:auto}.brokenReferenceItem{padding:9px;border:1px solid var(--line2);border-radius:12px;background:rgba(0,0,0,.18)}.brokenReferenceItem b,.brokenReferenceItem span{display:block;overflow-wrap:anywhere}.brokenReferenceItem span{margin-top:3px;color:var(--muted);font-size:11px} @media(max-width:1100px){.functionSummaryGrid{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(max-width:700px){.functionSummaryGrid{grid-template-columns:repeat(2,minmax(0,1fr))}.functionIntelToolbar{grid-template-columns:1fr}.brokenReferenceList{grid-template-columns:1fr}} /* v11.2 workspace router and docking repair */ .workspaceView{display:none!important} .workspaceView.active{display:block!important} .nav a{position:relative;z-index:2} .nav a.active::after{ content:""; position:absolute; right:9px; width:7px; height:7px; border-radius:50%; background:var(--green); box-shadow:0 0 12px var(--green); } .modal{z-index:1000!important} .screensaver{z-index:2000!important} .notice{z-index:1500!important} .main,.right{scroll-behavior:smooth} .main section[id],.right section[id],.studioPane[id],.workspaceTab[id]{ scroll-margin-top:18px; } .projectIntentPanel{ margin-bottom:14px; padding:14px; border:1px solid var(--line2); border-radius:20px; background:linear-gradient(180deg,rgba(84,241,255,.05),rgba(255,255,255,.02)); } .projectIntentForm{display:grid;gap:12px} .intentGrid{ display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; } .intentGrid label{ display:grid; gap:6px; min-width:0; color:var(--muted); font-size:12px; font-weight:800; } .intentGrid .full{grid-column:1/-1} .intentGrid input, .intentGrid select, .intentGrid textarea{ width:100%; min-width:0; color:var(--text); } .intentActions{ display:flex; justify-content:space-between; align-items:center; gap:10px; } .intentActions small{color:var(--muted)} .missionSummary{ margin-bottom:12px; padding:12px; border:1px solid rgba(84,241,255,.18); border-radius:16px; background:rgba(84,241,255,.055); } .missionSummary small, .missionSummary b, .missionSummary p{display:block} .missionSummary small{color:var(--muted)} .missionSummary p{ color:var(--muted); line-height:1.45; overflow-wrap:anywhere; } @media(max-width:800px){ .intentGrid{grid-template-columns:1fr} .intentGrid .full{grid-column:auto} .intentActions{align-items:stretch;flex-direction:column} .intentActions button{width:100%} } /* Live host-scan theater: file river + SIMON Bug eater */ #scanTheater{ position:absolute;inset:0;z-index:4;pointer-events:none;overflow:hidden; } #scanTheaterHud{ position:absolute;left:50%;top:max(18px,env(safe-area-inset-top));transform:translateX(-50%); min-width:min(720px,84vw);padding:12px 16px;border:1px solid rgba(255,255,255,.16); border-radius:18px;background:rgba(5,6,18,.52);backdrop-filter:blur(18px) saturate(1.35); box-shadow:0 0 34px rgba(85,220,255,.18),0 0 58px rgba(255,65,210,.12); opacity:0;transition:opacity .35s ease; } #scanTheater.live #scanTheaterHud{opacity:1} .scanTheaterTop{display:flex;justify-content:space-between;gap:14px;align-items:center} .scanTheaterTop b{font-size:13px;letter-spacing:.12em;text-transform:uppercase} #scanTheaterStatus{font-size:11px;color:#8fffe0} .scanTheaterBar{height:7px;margin-top:9px;border-radius:999px;background:rgba(255,255,255,.08);overflow:hidden} #scanTheaterProgress{height:100%;width:0;background:linear-gradient(90deg,#ff3b86,#ffd43b,#3cff98,#3ae8ff,#8c62ff,#ff46d8);background-size:300% 100%;animation:simonRainbowShift 6s linear infinite;border-radius:999px} .scanTheaterStats{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px;margin-top:9px} .scanTheaterStats span{display:block;font-size:10px;color:rgba(255,255,255,.58);text-transform:uppercase;letter-spacing:.08em} .scanTheaterStats b{display:block;margin-top:2px;font-size:16px;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} #scanFileRiver{ position:absolute;left:0;right:0;bottom:18%;height:46px;overflow:hidden; border-top:1px solid rgba(255,255,255,.08);border-bottom:1px solid rgba(255,255,255,.08); background:linear-gradient(90deg,transparent,rgba(8,10,26,.56) 12%,rgba(8,10,26,.56) 88%,transparent); opacity:0;transition:opacity .35s ease; } #scanTheater.live #scanFileRiver{opacity:1} #scanFileRiverTrack{display:inline-flex;align-items:center;gap:32px;min-width:max-content;height:100%;padding-left:100vw;animation:scanRiver 24s linear infinite} .scanRiverItem{font:700 12px/1 ui-monospace,SFMono-Regular,Menlo,monospace;color:#9ff6ff;text-shadow:0 0 14px currentColor;white-space:nowrap} .scanRiverItem.warn{color:#ffd66b}.scanRiverItem.error{color:#ff8097}.scanRiverItem.meta{color:#d2a6ff} @keyframes scanRiver{to{transform:translateX(-70%)}} #scanChompLayer{position:absolute;inset:0;overflow:hidden} .scanChomp{ position:absolute;left:calc(100% + 80px);top:66%;max-width:360px;padding:9px 13px;border-radius:12px; border:1px solid rgba(255,255,255,.2);background:rgba(5,8,20,.76);color:#baf8ff; font:800 12px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; box-shadow:0 0 24px rgba(50,220,255,.22);animation:scanChomp 3.2s cubic-bezier(.2,.75,.22,1) forwards; } .scanChomp.error{color:#ff9aac;box-shadow:0 0 26px rgba(255,70,110,.35)} .scanChomp.warn{color:#ffe08a;box-shadow:0 0 26px rgba(255,210,80,.28)} @keyframes scanChomp{ 0%{transform:translate3d(0,0,0) scale(1);opacity:0} 8%{opacity:1} 72%{transform:translate3d(calc(-50vw + 170px),-18vh,0) scale(1);opacity:1} 86%{transform:translate3d(calc(-50vw + 80px),-23vh,0) scale(.42) rotate(-8deg);opacity:.9} 100%{transform:translate3d(calc(-50vw + 30px),-25vh,0) scale(.05) rotate(18deg);opacity:0} } #scanChompFlash{position:absolute;left:50%;top:43%;width:36px;height:36px;border-radius:50%;transform:translate(-50%,-50%) scale(0);opacity:0;background:radial-gradient(circle,#fff 0 8%,#6fffe8 18%,#ff4fd8 45%,transparent 72%)} #scanChompFlash.fire{animation:chompFlash .65s ease-out} @keyframes chompFlash{0%{transform:translate(-50%,-50%) scale(.2);opacity:0}25%{opacity:1}100%{transform:translate(-50%,-50%) scale(5);opacity:0}} @media(max-width:700px){#scanTheaterHud{min-width:92vw;top:10px}.scanTheaterStats{grid-template-columns:repeat(2,minmax(0,1fr))}.scanTheaterStats>div:last-child{display:none}#scanFileRiver{bottom:22%}} /* Procedural SIMON BUG screensaver */ .proceduralSaver{ position:fixed; inset:0; width:100%; height:100%; display:none; overflow:hidden; background:#020108; color:#f8f6ff; z-index:2000!important; cursor:none; touch-action:none; } .proceduralSaver.on{display:block} #proceduralScene{ position:absolute; inset:0; width:100%; height:100%; display:block; } #proceduralVignette{ position:absolute; inset:0; pointer-events:none; background: radial-gradient(circle at 50% 44%,transparent 0 30%,rgba(0,0,0,.1) 58%,rgba(0,0,0,.78) 100%), linear-gradient(180deg,rgba(0,0,0,.2),transparent 30%,rgba(0,0,0,.38)); } #proceduralClock{ position:absolute; top:max(18px,env(safe-area-inset-top)); right:max(18px,env(safe-area-inset-right)); z-index:5; padding:12px 15px; border:1px solid rgba(255,255,255,.14); border-radius:18px; background:rgba(8,8,20,.46); backdrop-filter:blur(18px) saturate(1.3); text-align:right; box-shadow:0 0 36px rgba(188,65,255,.24); transition:opacity .4s ease,transform .4s ease; } #proceduralTime{ font-size:clamp(24px,4vw,52px); font-weight:900; line-height:1; } #proceduralDate{ margin-top:5px; font-size:11px; letter-spacing:.12em; text-transform:uppercase; color:rgba(255,255,255,.64); } #proceduralHud{ position:absolute; left:max(18px,env(safe-area-inset-left)); right:max(18px,env(safe-area-inset-right)); bottom:max(18px,env(safe-area-inset-bottom)); z-index:5; display:flex; align-items:flex-end; justify-content:space-between; gap:16px; transition:opacity .4s ease,transform .4s ease; } .proceduralPanel{ padding:14px 16px; border:1px solid rgba(255,255,255,.14); border-radius:20px; background:rgba(8,8,20,.46); backdrop-filter:blur(18px) saturate(1.3); box-shadow:0 0 36px rgba(188,65,255,.24); } .proceduralKicker{ font-size:10px; font-weight:800; letter-spacing:.22em; text-transform:uppercase; color:#d8abff; } #proceduralMode{ margin:4px 0 0; font-size:clamp(22px,3vw,42px); line-height:1; font-weight:950; letter-spacing:.05em; } .proceduralMeta{ margin-top:8px; font-size:12px; color:rgba(255,255,255,.64); } .proceduralMission{ margin-top:9px; max-width:min(760px,72vw); font-size:clamp(12px,1.25vw,17px); line-height:1.45; color:rgba(255,255,255,.9); overflow-wrap:anywhere; } .proceduralControls{display:flex;gap:10px} .proceduralControls button{ width:52px; height:52px; border-radius:18px; border:1px solid rgba(255,255,255,.14); background:rgba(8,8,20,.46); color:#fff; font-size:20px; cursor:pointer; backdrop-filter:blur(18px); box-shadow:0 0 28px rgba(75,210,255,.18); } .proceduralControls button:active{transform:scale(.95)} .proceduralSaver.idle #proceduralHud, .proceduralSaver.idle #proceduralClock{ opacity:0; transform:translateY(14px); pointer-events:none; } #proceduralHint{ position:absolute; left:50%; top:max(16px,env(safe-area-inset-top)); transform:translateX(-50%); z-index:5; padding:8px 12px; border:1px solid rgba(255,255,255,.14); border-radius:999px; background:rgba(5,5,14,.4); backdrop-filter:blur(14px); font-size:10px; letter-spacing:.14em; text-transform:uppercase; color:rgba(255,255,255,.7); transition:opacity .4s ease; } .proceduralSaver.idle #proceduralHint{opacity:0} @media(max-width:700px){ .proceduralMeta{display:none} .proceduralPanel{max-width:calc(100vw - 106px)} .proceduralControls{flex-direction:column} .proceduralControls button{ width:48px; height:48px; border-radius:16px; } } .hostScanPanel{ margin-bottom:14px; padding:14px; border:1px solid var(--line2); border-radius:20px; background:linear-gradient(180deg,rgba(50,255,115,.05),rgba(255,255,255,.02)); } .hostScanStatusGrid{ display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:8px; } .hostScanStatusGrid>div{ min-width:0; padding:11px; border:1px solid var(--line2); border-radius:14px; background:rgba(0,0,0,.2); } .hostScanStatusGrid small, .hostScanStatusGrid b, .hostScanStatusGrid span{ display:block; overflow-wrap:anywhere; } .hostScanStatusGrid small{color:var(--muted)} .hostScanStatusGrid b{font-size:18px;margin:4px 0} .hostScanStatusGrid span{font-size:11px;color:var(--muted)} .hostScanProgress{ height:12px; margin:12px 0; overflow:hidden; border:1px solid var(--line2); border-radius:999px; background:rgba(0,0,0,.32); } .hostScanProgress>div{ height:100%; min-width:0; border-radius:999px; background:linear-gradient(90deg,#32ff73,#54f1ff,#805cff); box-shadow:0 0 18px rgba(84,241,255,.6); transition:width .25s ease; } .hostScanControls{ display:grid; grid-template-columns:minmax(0,1fr) 180px; gap:10px; align-items:end; } .hostScanControls label{ display:grid; gap:5px; color:var(--muted); font-size:12px; font-weight:800; } .hostScanButtons{ grid-column:1/-1; display:flex; gap:8px; flex-wrap:wrap; } .hostScanContract{ margin-top:12px; padding:11px; border:1px solid var(--line2); border-radius:14px; background:rgba(255,255,255,.025); } .contractTags{ display:flex; gap:6px; flex-wrap:wrap; margin-top:8px; } .contractTags span{ padding:5px 8px; border-radius:999px; background:rgba(84,241,255,.08); color:var(--cyan); font-size:10px; font-weight:900; } .hostScanError{color:#ff9aa7} @media(max-width:1000px){ .hostScanStatusGrid{grid-template-columns:repeat(2,minmax(0,1fr))} } @media(max-width:700px){ .hostScanStatusGrid, .hostScanControls{grid-template-columns:1fr} .hostScanButtons{grid-column:auto} .hostScanButtons button{width:100%} } .accuracyGatePanel{ margin-top:12px; padding:12px; border:1px solid var(--line2); border-radius:16px; background:rgba(84,241,255,.035); } .accuracyGateGrid{ display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:8px; } .accuracyGateGrid>div{ padding:9px; border:1px solid var(--line2); border-radius:12px; background:rgba(0,0,0,.18); } .accuracyGateGrid small, .accuracyGateGrid b{display:block} .accuracyGateGrid small{color:var(--muted)} .validationIssue{ margin-top:9px; padding:9px; border-radius:12px; font-size:12px; line-height:1.45; } .validationIssue.critical{background:rgba(255,77,94,.12);color:#ff9aa7} .validationIssue.warning{background:rgba(255,178,46,.12);color:#ffd19b} .accuracyGateList{ display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; margin-top:10px; } .accuracyGate{ padding:9px; border:1px solid var(--line2); border-radius:12px; background:rgba(255,255,255,.025); } .accuracyGate b, .accuracyGate span{display:block} .accuracyGate span{margin-top:3px;color:var(--muted);font-size:11px} .accuracyGate.pass{border-color:rgba(50,255,115,.35)} .accuracyGate.warning{border-color:rgba(255,178,46,.38)} .accuracyGate.fail{border-color:rgba(255,77,94,.45)} @media(max-width:900px){ .accuracyGateGrid{grid-template-columns:repeat(2,minmax(0,1fr))} } @media(max-width:650px){ .accuracyGateGrid,.accuracyGateList{grid-template-columns:1fr} } .functionCriteriaPanel{ margin:14px 0; padding:14px; border:1px solid var(--line2); border-radius:20px; background:rgba(84,241,255,.025); } .criteriaTableWrap{ overflow:auto; max-height:520px; border:1px solid var(--line2); border-radius:14px; } .criteriaTableWrap table{min-width:980px} .criteriaTableWrap small{display:block;color:var(--muted);margin-top:3px} .emptyState{ padding:14px; border:1px dashed var(--line2); border-radius:14px; color:var(--muted); background:rgba(255,255,255,.02); } .beastCommand{margin-bottom:14px;padding:14px;border:1px solid rgba(255,101,216,.28);border-radius:22px;background:linear-gradient(180deg,rgba(127,44,255,.08),rgba(84,241,255,.025))} .beastStats{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:8px;margin:12px 0}.beastStats>div{padding:10px;border:1px solid var(--line2);border-radius:14px;background:rgba(0,0,0,.22)}.beastStats small,.beastStats b{display:block}.beastStats b{font-size:24px}.beastGrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:10px}.beastPane{padding:12px;border:1px solid var(--line2);border-radius:16px;background:rgba(0,0,0,.18);min-width:0}.beastPane h3{margin:0 0 10px}.beastForm{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.beastForm label{display:grid;gap:5px;font-size:11px;color:var(--muted);font-weight:800}.beastForm .full{grid-column:1/-1}.criteriaChecks{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px}.criteriaChecks label{padding:8px;border:1px solid var(--line2);border-radius:12px;background:rgba(255,255,255,.025)}.criteriaChecks small{display:block;margin-left:22px}.agentList{display:grid;gap:7px}.agentList details{border:1px solid var(--line2);border-radius:13px;padding:8px}.agentList summary{cursor:pointer;display:flex;justify-content:space-between;gap:8px}.agentList summary span{color:var(--muted);font-size:11px}.flowCard{display:grid;gap:8px;padding:10px;border:1px solid var(--line2);border-radius:14px}.flowCard label{display:grid;gap:4px;color:var(--muted);font-size:11px}.flowCard ol{margin:0;padding-left:22px;color:var(--muted);font-size:12px}.jobBoard{margin-top:10px}.jobTableWrap{overflow:auto;max-height:520px}.jobTableWrap table{min-width:1100px}.jobActions{display:flex;gap:5px}.jobTableWrap small{display:block;color:var(--muted);margin-top:3px}\n/* SIMON rainbow spectrum mode */ @keyframes simonRainbowShift{0%{background-position:0% 50%;filter:hue-rotate(0deg)}50%{background-position:100% 50%;filter:hue-rotate(35deg)}100%{background-position:0% 50%;filter:hue-rotate(0deg)}} @keyframes simonRainbowGlow{0%,100%{box-shadow:0 0 16px rgba(255,0,128,.32),0 0 34px rgba(0,225,255,.18)}33%{box-shadow:0 0 16px rgba(255,210,0,.34),0 0 34px rgba(130,70,255,.2)}66%{box-shadow:0 0 16px rgba(0,255,145,.32),0 0 34px rgba(255,60,210,.2)}} .theme-rainbow .nebula{background:radial-gradient(circle at 15% 20%,rgba(255,40,130,.23),transparent 28%),radial-gradient(circle at 70% 12%,rgba(0,220,255,.22),transparent 26%),radial-gradient(circle at 82% 78%,rgba(125,65,255,.24),transparent 31%),radial-gradient(circle at 22% 82%,rgba(255,190,0,.18),transparent 29%),#05050b;animation:drift 18s ease-in-out infinite alternate,simonRainbowShift 16s linear infinite;background-size:140% 140%} .theme-rainbow .primary,.theme-rainbow .mark,.theme-rainbow .card .spark,.theme-rainbow .progress>span,.theme-rainbow .hostProgress>span{background:linear-gradient(90deg,#ff356f,#ffb800,#35ff8a,#29d9ff,#7857ff,#ff43cf,#ff356f);background-size:350% 100%;animation:simonRainbowShift 7s linear infinite,simonRainbowGlow 5s ease-in-out infinite;color:#05050b;border-color:transparent} .theme-rainbow .pill,.theme-rainbow .nav a.active,.theme-rainbow .nav a:hover,.theme-rainbow .file.active,.theme-rainbow .file:hover,.theme-rainbow .aiBox{border-color:rgba(91,219,255,.38);background:linear-gradient(120deg,rgba(255,50,130,.12),rgba(0,220,255,.12),rgba(126,87,255,.13));background-size:250% 250%;animation:simonRainbowShift 12s ease infinite;color:#fff} .theme-rainbow .ticker b,.theme-rainbow a,.theme-rainbow summary{background:linear-gradient(90deg,#ff5b8d,#ffd43b,#48ff9a,#4be7ff,#9b7bff,#ff5de1);background-size:300% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;animation:simonRainbowShift 8s linear infinite} .theme-rainbow .statusDot.on{background:#fff;animation:simonRainbowGlow 3s ease-in-out infinite} .theme-rainbow .shell:hover{box-shadow:0 34px 110px rgba(0,0,0,.56),0 0 44px rgba(84,241,255,.15),0 0 70px rgba(255,79,214,.08),inset 0 0 38px rgba(128,92,255,.05)} .theme-custom .primary{background:linear-gradient(135deg,var(--green),var(--cyan),var(--purple));background-size:220% 220%;animation:simonRainbowShift 10s ease infinite} @media(max-width:1100px){.beastStats{grid-template-columns:repeat(3,minmax(0,1fr))}.beastGrid{grid-template-columns:1fr}}@media(max-width:700px){.beastStats{grid-template-columns:repeat(2,minmax(0,1fr))}.beastForm,.criteriaChecks{grid-template-columns:1fr}.beastForm .full{grid-column:auto}} </style> </head> <body class="theme-<?= h((string)($settings['color_mode'] ?? 'rainbow')) ?>"> <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 proceduralSaver" id="screensaver" aria-hidden="true"> <canvas id="proceduralScene" aria-label="Animated SIMON BUG procedural screensaver"></canvas> <div id="proceduralVignette"></div> <div id="scanTheater" aria-hidden="true"> <section id="scanTheaterHud"> <div class="scanTheaterTop"><b>SIMON BUG · LIVE HOST SCAN</b><span id="scanTheaterStatus">IDLE</span></div> <div class="scanTheaterBar"><div id="scanTheaterProgress"></div></div> <div class="scanTheaterStats"> <div><span>Processed</span><b id="scanTheaterProcessed">0 / 0</b></div> <div><span>Deep</span><b id="scanTheaterDeep">0</b></div> <div><span>Metadata</span><b id="scanTheaterMeta">0</b></div> <div><span>Skipped</span><b id="scanTheaterSkipped">0</b></div> <div><span>Errors</span><b id="scanTheaterErrors">0</b></div> </div> </section> <div id="scanFileRiver"><div id="scanFileRiverTrack"><span class="scanRiverItem">Waiting for host scan…</span></div></div> <div id="scanChompLayer"></div><div id="scanChompFlash"></div> </div> <div id="proceduralHint">Tap or move to reveal controls</div> <section id="proceduralClock"> <div id="proceduralTime">--:--</div> <div id="proceduralDate">Loading</div> </section> <section id="proceduralHud"> <div class="proceduralPanel"> <div class="proceduralKicker">Gaylord Sinclair LLC · SI Intelligence</div> <h1 id="proceduralMode">SIMON BUG · GUARDIAN MODE</h1> <div id="proceduralMission" class="proceduralMission">Looking for open endpoints, missing protection, exposed secrets, unsafe uploads, and release blockers.</div> <div id="proceduralSource" class="proceduralMeta">Data contract: Endpoints · Configuration · Findings · 5W1H Events</div> </div> <div class="proceduralControls"> <button type="button" id="proceduralPrevious" aria-label="Previous mode">‹</button> <button type="button" id="proceduralPause" aria-label="Pause animation">Ⅱ</button> <button type="button" id="proceduralNext" aria-label="Next mode">›</button> </div> </section> </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="#studio" data-workspace="studio">Studio</a> <a href="#projectExplorer" data-workspace="studio" data-scroll-target="projectExplorer">Project Explorer</a> <a href="#editorPanel" data-workspace="studio" data-scroll-target="editorPanel">Editor</a> <a href="#projectIntent" data-workspace="studio" data-scroll-target="projectIntent">Project Intent</a> <a href="#beastCommand" data-workspace="studio" data-scroll-target="beastCommand">BEAST Command</a> <a href="#hostScan" data-workspace="studio" data-scroll-target="hostScan">Host Scan + DB</a> <a href="#endpointPanel" data-workspace="studio" data-scroll-target="endpointPanel">Endpoints</a> <a href="#functionIntelligence" data-workspace="studio" data-scroll-target="functionIntelligence">Function Intelligence</a> <a href="#reviewQueue" data-workspace="studio" data-scroll-target="reviewQueue">AI Reviews</a> <a href="#overview" data-workspace="overview">Legacy Scanner</a> <a href="#scanner" data-workspace="overview" data-scroll-target="scanner">Scanner</a> <a href="#mapping" data-workspace="overview" data-scroll-target="mapping">File Map</a> <a href="#preview" data-workspace="overview" data-scroll-target="preview">Preview</a> <a href="#" data-open-modal="reportsModal">Reports</a> <a href="#" data-open-modal="settingsModal">Settings</a> <a href="#" data-open-modal="diagnosticsModal">Diagnostics</a> </nav> <div class="modePanel"> <small>Operating Mode</small> <form method="post" action="?action=set_mode"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <select name="operation_mode" onchange="this.form.submit()"> <option value="legacy" <?= $operationMode==='legacy'?'selected':'' ?>>Legacy</option> <option value="assisted" <?= $operationMode==='assisted'?'selected':'' ?>>Assisted</option> <option value="automated" <?= $operationMode==='automated'?'selected':'' ?>>Automated</option> </select> </form> <p><?= $operationMode==='legacy' ? 'Manual control. No automatic changes.' : ($operationMode==='assisted' ? 'AI proposals and one-click actions with approval.' : 'Automated scans and queued proposals. Publishing still requires approval.') ?></p> </div> <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 workspaceView active" id="studio" data-workspace-view="studio"> <section class="unifiedHeader"> <div> <small>SIMON + WEB360</small> <h1>SIMON BUG BEAST — Project Intelligence Operating System</h1> <p>Create jobs, coordinate agents, change scan criteria and data flow, build, verify, approve, and publish from one governed system.</p> </div> <div class="gradeCard"> <small>Project Grade</small> <b><?= h($projectGrade['grade']) ?></b> <span><?= (int)$projectGrade['score'] ?>/100</span> <p><?= h($projectGrade['summary']) ?></p> </div> </section> <section class="beastCommand" id="beastCommand"> <div class="paneHead"> <div><b>BEAST Command Center</b><small>Jobs · Agents · Data Flow · Scan Criteria · Authority</small></div> <span class="pill">v<?= h(APP_VERSION) ?></span> </div> <div class="beastStats"> <div><small>Agents</small><b><?= (int)$beastStats['agents'] ?></b></div> <div><small>Flows</small><b><?= (int)$beastStats['flows'] ?></b></div> <div><small>Jobs</small><b><?= (int)$beastStats['jobs'] ?></b></div> <div><small>Pending approval</small><b><?= (int)$beastStats['pending'] ?></b></div> <div><small>Completed</small><b><?= (int)$beastStats['completed'] ?></b></div> <div><small>Failed</small><b><?= (int)$beastStats['failed'] ?></b></div> </div> <div class="beastGrid"> <section class="beastPane"> <h3>Create Job</h3> <form method="post" action="?action=beast_create_job#beastCommand" class="beastForm"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <label>Title<input name="title" required placeholder="Deep security and architecture review"></label> <label>Job type<select name="job_type"> <option value="host_scan">Complete host scan</option> <option value="function_intelligence">Rebuild function intelligence</option> <option value="criteria_validate">Validate Excel criteria</option> <option value="report_audit">Audit reports</option> <option value="analysis">AI/project analysis</option> <option value="modernization">Modernization proposal</option> <option value="deployment">Deployment verification</option> </select></label> <label>Assigned agent<select name="agent_id"><?php foreach($beastAgents as $agent): if(empty($agent['enabled']))continue; ?><option value="<?= h((string)$agent['id']) ?>"><?= h((string)$agent['name']) ?> · <?= h((string)$agent['authority']) ?></option><?php endforeach; ?></select></label> <label>Flow<select name="flow_id"><option value="">Direct job</option><?php foreach($beastFlows as $flow): ?><option value="<?= h((string)$flow['id']) ?>"><?= h((string)$flow['name']) ?></option><?php endforeach; ?></select></label> <label>Risk<select name="risk"><option>low</option><option selected>medium</option><option>high</option><option>critical</option></select></label> <label class="full">Description<textarea name="description" rows="4" placeholder="What must be analyzed, changed, preserved, validated, and returned."></textarea></label> <input type="hidden" name="project_root" value="<?= h($projectRoot) ?>"> <div class="row full"><button name="submit_job" value="route">Create and Route</button><button class="primary" name="submit_job" value="run_now">Create, Approve & Run Now</button></div> </form> </section> <section class="beastPane"> <h3>Scan Criteria</h3> <form method="post" action="?action=beast_save_criteria#beastCommand" class="beastForm"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <label>Profile<input name="profile_name" value="<?= h((string)$beastCriteria['profile_name']) ?>"></label> <label>Minimum confidence<input name="minimum_confidence" type="number" min="0" max="1" step="0.01" value="<?= h((string)$beastCriteria['minimum_confidence']) ?>"></label> <div class="criteriaChecks full"><?php foreach($beastCriteria['rules'] as $rule): ?><label><input type="checkbox" name="rule_enabled[]" value="<?= h((string)$rule['id']) ?>" <?= !empty($rule['enabled'])?'checked':'' ?>> <b><?= h((string)$rule['label']) ?></b><small><?= h((string)$rule['severity']) ?> · evidence <?= !empty($rule['evidence_required'])?'required':'optional' ?></small></label><?php endforeach; ?></div> <label><input type="checkbox" name="block_on_critical" value="1" <?= !empty($beastCriteria['block_on_critical'])?'checked':'' ?>> Block on critical</label> <label><input type="checkbox" name="unknown_is_safe" value="1" <?= !empty($beastCriteria['unknown_is_safe'])?'checked':'' ?>> Treat unknown as safe</label> <button class="primary">Save Criteria v<?= (int)$beastCriteria['version'] ?></button> </form> </section> </div> <div class="beastGrid"> <section class="beastPane"> <h3>Agents</h3> <div class="agentList"><?php foreach($beastAgents as $agent): ?><details><summary><b><?= h((string)$agent['name']) ?></b><span><?= h((string)$agent['role']) ?> · <?= h((string)$agent['authority']) ?></span></summary><form method="post" action="?action=beast_save_agent#beastCommand" class="beastForm"><input type="hidden" name="csrf" value="<?= h($csrf) ?>"><input type="hidden" name="agent_id" value="<?= h((string)$agent['id']) ?>"><label>Name<input name="name" value="<?= h((string)$agent['name']) ?>"></label><label>Provider<input name="provider" value="<?= h((string)$agent['provider']) ?>"></label><label>Model<input name="model" value="<?= h((string)$agent['model']) ?>"></label><label>Authority<select name="authority"><?php foreach(['observe','advisory','proposal','approval_required','autonomous'] as $a): ?><option value="<?= $a ?>" <?= $a===($agent['authority']??'')?'selected':'' ?>><?= $a ?></option><?php endforeach; ?></select></label><label class="full">Instructions<textarea name="instructions" rows="4"><?= h((string)$agent['instructions']) ?></textarea></label><label><input type="checkbox" name="enabled" value="1" <?= !empty($agent['enabled'])?'checked':'' ?>> Enabled</label><button>Save Agent</button></form></details><?php endforeach; ?></div> </section> <section class="beastPane"> <h3>Data Flows</h3> <?php foreach($beastFlows as $flow): ?><form method="post" action="?action=beast_save_flow#beastCommand" class="flowCard"><input type="hidden" name="csrf" value="<?= h($csrf) ?>"><input type="hidden" name="flow_id" value="<?= h((string)$flow['id']) ?>"><b><?= h((string)$flow['name']) ?></b><label>Trigger<input name="trigger" value="<?= h((string)$flow['trigger']) ?>"></label><label>Authority<select name="authority"><?php foreach(['observe','advisory','proposal','approval_required','autonomous'] as $a): ?><option value="<?= $a ?>" <?= $a===($flow['authority']??'')?'selected':'' ?>><?= $a ?></option><?php endforeach; ?></select></label><label><input type="checkbox" name="enabled" value="1" <?= !empty($flow['enabled'])?'checked':'' ?>> Enabled</label><ol><?php foreach($flow['steps'] as $step): ?><li><b><?= h((string)$step['action']) ?></b> → <?= h((string)$step['agent']) ?></li><?php endforeach; ?></ol><button>Save Flow Policy</button></form><?php endforeach; ?> </section> </div> <section class="beastPane jobBoard"> <div class="paneHead"><div><h3>Job Board</h3><small>Approval and execution history</small></div></div> <?php if(!$beastJobs): ?><div class="emptyState">No jobs yet. Create the first governed task above.</div><?php else: ?><div class="jobTableWrap"><table class="table"><tr><th>Job</th><th>Agent</th><th>Type</th><th>Risk</th><th>Status</th><th>Result</th><th>Actions</th></tr><?php foreach($beastJobs as $job): ?><tr><td><b><?= h((string)$job['title']) ?></b><small><?= h((string)$job['id']) ?></small></td><td><?= h((string)$job['agent_id']) ?></td><td><?= h((string)$job['job_type']) ?></td><td><?= h((string)$job['risk']) ?></td><td><span class="pill"><?= h((string)$job['status']) ?></span></td><td><small><?= h((string)($job['error']??($job['result']['message']??''))) ?></small></td><td><form method="post" action="?action=beast_job_action#beastCommand" class="jobActions"><input type="hidden" name="csrf" value="<?= h($csrf) ?>"><input type="hidden" name="job_id" value="<?= h((string)$job['id']) ?>"><?php if(($job['status']??'')==='pending_approval'): ?><button name="job_action" value="approve">Approve</button><button class="primary" name="job_action" value="approve_run">Approve & Run</button><button name="job_action" value="reject">Reject</button><?php elseif(($job['status']??'')==='queued'): ?><button class="primary" name="job_action" value="run">Run</button><button name="job_action" value="cancel">Cancel</button><?php endif; ?></form></td></tr><?php endforeach; ?></table></div><?php endif; ?> </section> </section> <section class="hostScanPanel" id="hostScan"> <div class="paneHead"> <div> <b>Complete Host Scan + Excel Contract + MariaDB</b> <small> Inventories every folder and file. Text/code receives deep analysis; binary and large assets receive metadata and hash analysis. </small> </div> <span class="pill" id="hostScanStatusBadge"> <?= h(strtoupper((string)$hostScanState['status'])) ?> </span> </div> <div class="hostScanStatusGrid"> <div> <small>Excel dictionary</small> <b><?= $dataDictionaryStatus['available'] ? (($dataDictionaryStatus['source'] ?? 'excel') === 'excel' ? 'EXCEL READY' : 'EMBEDDED FALLBACK') : 'MISSING' ?></b> <span><?= count($dataDictionaryStatus['fields'] ?? []) ?> fields · <?= count($dataDictionaryStatus['sheets'] ?? []) ?> sheets</span> </div> <div> <small>MariaDB</small> <b><?= h($databaseStatusLabel) ?></b> <span><?= $databaseConnected ? 'Writes canonical project intelligence tables' : 'Enter only the current database password below' ?></span> </div> <div> <small>Files processed</small> <b id="hostProcessed"><?= (int)$hostScanState['processed'] ?> / <?= (int)$hostScanState['total'] ?></b> <span id="hostLastFile"><?= h((string)($hostScanState['last_file'] ?? 'Not started')) ?></span> </div> <div> <small>Scan depth</small> <b><span id="hostDeep"><?= (int)$hostScanState['deep_scanned'] ?></span> deep</b> <span><span id="hostMetadata"><?= (int)$hostScanState['metadata_only'] ?></span> metadata-only · <span id="hostSkipped"><?= (int)($hostScanState['skipped_directories'] ?? 0) ?></span> unreadable skipped · <span id="hostErrors"><?= (int)$hostScanState['errors'] ?></span> errors</span> </div> </div> <div class="accuracyGatePanel" style="margin-top:14px"> <div class="paneHead"> <div> <b>SIMON MariaDB Connection</b> <small>Password is held only in this PHP session and is never written into this file or workbook.</small> </div> <span class="pill <?= $databaseConnected ? 'good' : ($databasePasswordPresent ? 'critical' : '') ?>"><?= h($databaseStatusLabel) ?></span> </div> <div class="accuracyGateGrid"> <div><small>Host</small><b>db5020893348.hosting-data.io</b></div> <div><small>Port</small><b>3306</b></div> <div><small>Database</small><b>dbs15883515</b></div> <div><small>User</small><b>dbu5579662</b></div> </div> <?php if ($databaseLastError !== ''): ?> <div class="validationIssue critical"><?= h($databaseLastError) ?></div> <?php endif; ?> <?php if (!$databaseConnected): ?> <form method="post" action="?action=db_connect#hostScan" class="beastForm" autocomplete="off"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <label class="full">MariaDB password <input type="password" name="db_password" placeholder="Enter current IONOS MariaDB password" autocomplete="current-password" required> </label> <button class="primary">Connect MariaDB</button> </form> <?php else: ?> <form method="post" action="?action=db_disconnect#hostScan" class="beastForm"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <button>Remove Password From Session</button> </form> <?php endif; ?> </div> <div class="accuracyGatePanel"> <div class="paneHead"> <div> <b>Accuracy and Upload Validation</b> <small>SIMON will not mark a scan validated unless the contract, counts, evidence, and persistence gates pass.</small> </div> <span class="pill <?= $dataDictionaryValidation['valid'] ? 'good' : 'critical' ?>"> <?= $dataDictionaryValidation['valid'] ? 'CONTRACT VALID' : 'CONTRACT BLOCKED' ?> </span> </div> <div class="accuracyGateGrid"> <div> <small>Required worksheets</small> <b><?= (int)$dataDictionaryValidation['present_sheets'] ?> / <?= (int)$dataDictionaryValidation['required_sheets'] ?></b> </div> <div> <small>Dictionary fields</small> <b><?= (int)$dataDictionaryValidation['field_count'] ?></b> </div> <div> <small>Duplicate fields</small> <b><?= (int)$dataDictionaryValidation['duplicate_count'] ?></b> </div> <div> <small>Last accuracy confidence</small> <b><?= isset($scanAccuracyStatus['confidence']) ? number_format(((float)$scanAccuracyStatus['confidence']) * 100, 1) . '%' : 'Not validated' ?></b> </div> </div> <?php if (!$dataDictionaryValidation['valid']): ?> <div class="validationIssue critical"> <?= h(implode(' · ', array_slice($dataDictionaryValidation['errors'], 0, 8))) ?> </div> <?php elseif (!empty($dataDictionaryValidation['warnings'])): ?> <div class="validationIssue warning"> <?= h(implode(' · ', array_slice($dataDictionaryValidation['warnings'], 0, 5))) ?> </div> <?php endif; ?> <?php if (!empty($scanAccuracyStatus['gates'])): ?> <div class="accuracyGateList"> <?php foreach ($scanAccuracyStatus['gates'] as $gate): ?> <div class="accuracyGate <?= h((string)$gate['status']) ?>"> <b><?= h((string)$gate['label']) ?></b> <span><?= h((string)$gate['evidence']) ?></span> </div> <?php endforeach; ?> </div> <?php endif; ?> </div> <div class="hostScanProgress"> <div id="hostScanProgressBar" style="width:<?= (float)$hostScanState['percent'] ?>%"></div> </div> <form id="hostScanForm" class="hostScanControls"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <label> Host root <input id="hostRoot" name="host_root" value="<?= h((string)($hostScanState['root'] ?? ($_SERVER['DOCUMENT_ROOT'] ?? __DIR__))) ?>" required> </label> <label> Files per batch <select id="hostBatchSize" name="batch_size"> <option value="10">10 — safest</option> <option value="25" selected>25 — recommended</option> <option value="50">50 — faster</option> </select> </label> <div class="hostScanButtons"> <button type="button" class="primary" id="startHostScan">Start Complete Host Scan</button> <button type="button" id="resumeHostScan">Resume / Continue</button> <button type="button" id="stopHostScan">Stop Automatic Batches</button> </div> </form> <div class="accuracyGatePanel" id="hostRunReports" style="margin-top:14px"> <div class="paneHead"> <div><b>Complete Host Reports + 5W1H Stream</b><small>Run-level aggregation by scan_uuid, failures, comparison, inventory, and event stream.</small></div> <span class="pill" id="hostReportReady"><?= !empty($hostScanState['report_ready']) ? 'REPORT READY' : 'RUN SCAN' ?></span> </div> <div class="hostScanButtons" style="display:flex;flex-wrap:wrap;gap:9px"> <?php $reportScanUuid=(string)($hostScanState['scan_uuid']??''); ?> <a class="button primary" id="viewHostReport" href="?action=host_report_view&scan_uuid=<?= h($reportScanUuid) ?>" target="_blank">View Complete Host Report</a> <a class="button" id="downloadHostReport" href="?action=host_report_download&scan_uuid=<?= h($reportScanUuid) ?>">Download Host Report JSON</a> <a class="button" id="downloadHostInventory" href="?action=host_inventory_csv&scan_uuid=<?= h($reportScanUuid) ?>">Download Full Inventory CSV</a> <a class="button" id="downloadHostFailures" href="?action=host_failures_csv&scan_uuid=<?= h($reportScanUuid) ?>">Download Failures CSV</a> <a class="button" id="download5W1H" href="?action=host_5w1h_stream&scan_uuid=<?= h($reportScanUuid) ?>">Download 5W1H NDJSON Stream</a> <button type="button" class="button" id="reconcile5W1H" data-scan-uuid="<?= h($reportScanUuid) ?>" data-csrf="<?= h($csrf) ?>">Repair / Reconcile 5W1H</button><span id="reconcile5W1HStatus" class="muted" role="status" aria-live="polite"></span> </div> <div class="accuracyGateGrid" style="margin-top:12px"> <div><small>Scan UUID</small><b id="hostReportUuid"><?= h($reportScanUuid ?: 'Not started') ?></b></div> <div><small>Queue remaining</small><b id="hostQueueRemaining"><?= (int)($hostScanState['queue_remaining']??0) ?></b></div> <div><small>Files / second</small><b id="hostFilesPerSecond"><?= h((string)($hostScanState['files_per_second']??0)) ?></b></div> <div><small>Database writes</small><b><span id="hostDbWrites"><?= (int)($hostScanState['database_updates']??0) ?></span> ok · <span id="hostDbFailures"><?= (int)($hostScanState['database_failures']??0) ?></span> failed</b></div> </div> </div> <div class="hostScanContract"> <b>Data loaded and updated</b> <div class="contractTags"> <span>Projects</span> <span>Files + hashes</span> <span>Functions + classes</span> <span>Endpoints</span> <span>Findings</span> <span>Scan runs</span> <span>Excel field dictionary</span> <span>5W1H evidence</span> </div> <?php if (!empty($dataDictionaryStatus['error'])): ?> <p class="hostScanError"><?= h((string)$dataDictionaryStatus['error']) ?></p> <?php endif; ?> </div> </section> <section class="projectIntentPanel" id="projectIntent"> <div class="paneHead"> <div> <b>Project Intent / Scan Mission</b> <small>Defines what SIMON should understand, protect, change, and verify.</small> </div> <span class="pill"><?= h(strtoupper((string)$projectDna['scan_focus'])) ?> FOCUS</span> </div> <form method="post" action="?action=save_project_dna#projectIntent" class="projectIntentForm"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <div class="intentGrid"> <label> Project name <input name="project_name" value="<?= h((string)$projectDna['project_name']) ?>" required> </label> <label> Scan focus <select name="scan_focus"> <?php foreach ([ 'quick'=>'Quick Scan', 'full'=>'Full Project Review', 'security'=>'Security', 'architecture'=>'Architecture', 'performance'=>'Performance', 'deployment'=>'Deployment Readiness', 'documentation'=>'Documentation', 'function'=>'Function Intelligence', 'endpoint'=>'Endpoint Intelligence' ] as $focusKey=>$focusLabel): ?> <option value="<?= h($focusKey) ?>" <?= $projectDna['scan_focus']===$focusKey?'selected':'' ?>> <?= h($focusLabel) ?> </option> <?php endforeach; ?> </select> </label> <label class="full"> What is this project? <textarea name="purpose" rows="3" placeholder="Describe the application and its role."><?= h((string)$projectDna['purpose']) ?></textarea> </label> <label class="full"> What do you want SIMON to accomplish? <textarea name="primary_goal" rows="3" placeholder="Example: review the whole project, preserve working behavior, secure endpoints, remove duplication, and prepare approved changes."><?= h((string)$projectDna['primary_goal']) ?></textarea> </label> <label> Intended users <textarea name="intended_users" rows="3"><?= h((string)$projectDna['intended_users']) ?></textarea> </label> <label> Business objective <textarea name="business_objective" rows="3"><?= h((string)$projectDna['business_objective']) ?></textarea> </label> <label> Required features — one per line <textarea name="required_features" rows="7"><?= h(implode("\n", $projectDna['required_features'] ?? [])) ?></textarea> </label> <label> Known problems — one per line <textarea name="known_problems" rows="7"><?= h(implode("\n", $projectDna['known_problems'] ?? [])) ?></textarea> </label> <label> Must preserve — one rule per line <textarea name="preserve_rules" rows="7"><?= h(implode("\n", $projectDna['preserve_rules'] ?? [])) ?></textarea> </label> <label> Allowed change rules — one per line <textarea name="change_rules" rows="7"><?= h(implode("\n", $projectDna['change_rules'] ?? [])) ?></textarea> </label> <label> Security requirements — one per line <textarea name="security_requirements" rows="7"><?= h(implode("\n", $projectDna['security_requirements'] ?? [])) ?></textarea> </label> <label> Success criteria — one per line <textarea name="success_criteria" rows="7"><?= h(implode("\n", $projectDna['success_criteria'] ?? [])) ?></textarea> </label> <label class="full"> Instructions for AI reviewers <textarea name="ai_instructions" rows="4" placeholder="Tell AI how to review and what not to change."><?= h((string)$projectDna['ai_instructions']) ?></textarea> </label> <label class="full"> Owner notes <textarea name="owner_notes" rows="4"><?= h((string)$projectDna['owner_notes']) ?></textarea> </label> </div> <div class="intentActions"> <small> Last updated: <?= h((string)($projectDna['updated_at'] ?? 'not yet')) ?> </small> <button class="primary">Save Project Intent</button> </div> </form> </section> <section class="studioGrid"> <aside class="studioPane explorerPane" id="projectExplorer"> <div class="paneHead"><div><b>Project Explorer</b><small><?= count($projectFiles) ?> files</small></div></div> <form method="post" action="?action=set_project_root" class="stack"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <input name="project_root" value="<?= h($projectRoot) ?>" placeholder="Project root path"> <button>Load Project</button> </form> <input id="fileFilter" type="search" placeholder="Filter host file tree..."> <div class="treeToolbarNote">Expand a folder, then choose <b>Open</b> or <b>Scan</b>. Scanning is read-only.</div> <style> .treeToolbarNote{margin:8px 0;padding:10px;border:1px solid rgba(84,241,255,.22);border-radius:12px;color:var(--muted);font-size:12px}.hostTreeLevel{display:grid;gap:5px}.hostTreeLevel .hostTreeLevel{margin-left:14px;padding-left:10px;border-left:1px solid rgba(84,241,255,.16)}.hostTreeFolder>summary{list-style:none;display:flex;gap:8px;align-items:center;padding:8px;border-radius:10px;cursor:pointer;background:rgba(255,255,255,.025)}.hostTreeFolder>summary::-webkit-details-marker{display:none}.hostTreeFolder[open]>summary .treeChevron{transform:rotate(90deg)}.treeChevron{display:inline-block;transition:.18s}.treeFolderIcon{color:#9f7cff}.hostTreeFolder summary small{margin-left:auto;color:var(--muted)}.hostTreeFile{display:flex!important;align-items:center;justify-content:space-between;gap:8px;padding:7px 8px!important;border-radius:10px}.treeFileMeta{display:flex;gap:8px;align-items:center;min-width:0}.treeFileMeta b{display:block;overflow:hidden;text-overflow:ellipsis}.treeFileMeta small{display:block;color:var(--muted)}.treeFileIcon{color:#54f1ff}.treeFileActions{display:flex;gap:5px;flex:0 0 auto}.treeFileActions form{margin:0}.treeFileActions button{padding:6px 9px;font-size:11px} </style> <div class="fileExplorerList hostFileTree"> <?= render_project_tree(project_tree_from_files($projectFiles), $csrf) ?> </div> </aside> <section class="studioPane editorPane" id="editorPanel"> <div class="paneHead"> <div><b>Editor / Preview</b><small><?= $editorFile ? h($editorFile) : 'Select a project file' ?></small></div> <?php if ($editorFile): ?> <div class="row"> <form method="post" action="?action=create_checkpoint"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <input type="hidden" name="file_path" value="<?= h($editorFile) ?>"> <button>Checkpoint</button> </form> <form method="post" action="?action=queue_ai_review"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <input type="hidden" name="file_path" value="<?= h($editorFile) ?>"> <input type="hidden" name="review_type" value="full"> <button>Queue AI Review</button> </form> </div> <?php endif; ?> </div> <?php if ($editorFile): ?> <form method="post" action="?action=save_editor_file#editorPanel" class="editorForm"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <input type="hidden" name="file_path" value="<?= h($editorFile) ?>"> <textarea name="file_content" id="codeEditor" spellcheck="false"><?= h($editorContent) ?></textarea> <div class="editorActions"> <span><?= h(strtoupper($editorExtension ?: 'TEXT')) ?> · <?= number_format(strlen($editorContent)) ?> chars</span> <button class="primary">Save + Re-scan</button> </div> </form> <?php else: ?> <div class="emptyPane"><b>Select a file from Project Explorer</b><p>Open it for manual editing, checkpointing, AI review, and re-scanning.</p></div> <?php endif; ?> </section> <aside class="studioPane intelligencePane"> <div class="paneHead"><div><b>SIMON Intelligence</b><small><?= ucfirst($operationMode) ?> mode</small></div></div> <div class="missionSummary"> <small>Active Mission</small> <b><?= h((string)$projectDna['project_name']) ?></b> <p><?= h((string)$projectDna['primary_goal']) ?></p> <span class="pill"><?= h(strtoupper((string)$projectDna['scan_focus'])) ?></span> </div> <div class="intelligenceStats"> <div><small>Endpoints</small><b><?= count($projectEndpoints) ?></b></div> <div><small>Reviews</small><b><?= count($reviewQueue) ?></b></div> <div><small>Checkpoints</small><b><?= count($checkpoints) ?></b></div> <div><small>Scans</small><b><?= count($history) ?></b></div> <div><small>Functions</small><b><?= (int)($functionIntelSummary['total'] ?? 0) ?></b></div> <div><small>Broken Calls</small><b><?= (int)($functionIntelSummary['broken_references'] ?? 0) ?></b></div> </div> <?php if ($editorFile): ?> <div class="quickActions"> <b>Review Selected File</b> <?php foreach (['security'=>'Security','architecture'=>'Architecture','quality'=>'Quality','full'=>'Full Review'] as $reviewKey=>$reviewLabel): ?> <form method="post" action="?action=queue_ai_review"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <input type="hidden" name="file_path" value="<?= h($editorFile) ?>"> <input type="hidden" name="review_type" value="<?= h($reviewKey) ?>"> <button><?= h($reviewLabel) ?></button> </form> <?php endforeach; ?> </div> <?php endif; ?> <div class="checkpointList"> <b>Recent Checkpoints</b> <?php foreach (array_slice($checkpoints,0,8) as $checkpoint): ?> <div class="checkpointItem"> <span><?= h(basename((string)$checkpoint['file_path'])) ?></span> <small><?= h((string)$checkpoint['created_at']) ?></small> <form method="post" action="?action=restore_checkpoint"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <input type="hidden" name="file_path" value="<?= h((string)$checkpoint['file_path']) ?>"> <input type="hidden" name="checkpoint_path" value="<?= h((string)$checkpoint['checkpoint_path']) ?>"> <button>Restore</button> </form> </div> <?php endforeach; ?> </div> </aside> </section> <section class="functionIntelModule" id="functionIntelligence"> <div class="paneHead"> <div><b>Function Intelligence</b><small>Generated <?= h((string)($functionIntelligence['generated_at'] ?? 'not yet')) ?> · <?= (int)($functionIntelligence['source_files_scanned'] ?? 0) ?> source files</small></div> <form method="post" action="?action=rebuild_function_intelligence#functionIntelligence"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <button class="primary">Rebuild Function Intelligence</button> </form> </div> <div class="functionSummaryGrid"> <?php foreach ([ 'total'=>'Total Functions','canonical'=>'Canonical','duplicates'=>'Duplicates','unused'=>'Unused', 'broken_references'=>'Broken References','high_error'=>'High Error','shared_candidates'=>'Shared Candidates', 'archive_candidates'=>'Archive Candidates','project_specific'=>'Project-Specific' ] as $summaryKey=>$summaryLabel): ?> <button type="button" class="functionSummaryCard" data-function-filter="<?= h($summaryKey) ?>"> <small><?= h($summaryLabel) ?></small><b><?= (int)($functionIntelSummary[$summaryKey] ?? 0) ?></b> </button> <?php endforeach; ?> </div> <div class="functionIntelToolbar"> <input id="functionIntelSearch" type="search" placeholder="Search function, file, classification, reason..."> <select id="functionIntelFilter"> <option value="all">All classifications</option><option value="canonical">Canonical</option> <option value="duplicate">Duplicates</option><option value="unused">Unused</option> <option value="high-error">High error</option><option value="shared-candidate">Shared candidates</option> <option value="archive-candidate">Archive candidates</option><option value="project-specific">Project-specific</option> </select> </div> <div class="functionTableWrap"><table class="functionTable"><thead><tr> <th>Function</th><th>Classification</th><th>File</th><th>Lines</th><th>Callers</th><th>Errors</th><th>Confidence</th><th>Recommendation</th> </tr></thead><tbody> <?php foreach ($functionIntelFunctions as $function): ?> <tr class="functionIntelRow" data-classification="<?= h((string)$function['classification']) ?>" data-search="<?= h(strtolower((string)$function['name'].' '.(string)$function['relative_path'].' '.(string)$function['classification'].' '.implode(' ',$function['reasons']??[]))) ?>"> <td><b><?= h((string)$function['name']) ?></b><small><?= h((string)$function['signature']) ?></small></td> <td><span class="functionBadge class-<?= h((string)$function['classification']) ?>"><?= h((string)$function['classification']) ?></span></td> <td><?= h((string)$function['relative_path']) ?></td><td><?= (int)$function['start_line'] ?>–<?= (int)$function['end_line'] ?></td> <td><?= (int)$function['incoming_references'] ?></td><td><?= (int)$function['runtime_errors'] ?></td> <td><?= number_format(((float)$function['confidence'])*100,0) ?>%</td> <td><?= h(implode(' · ',$function['reasons']??[])) ?><?php if (!empty($function['shared_module_candidate'])): ?><div class="functionHint">Candidate for shared module</div><?php endif; ?><?php if (!empty($function['archive_safe'])): ?><div class="functionHint archiveHint">Safe-to-archive candidate</div><?php endif; ?></td> </tr> <?php endforeach; ?> <?php if (!$functionIntelFunctions): ?><tr><td colspan="8">No function intelligence data yet.</td></tr><?php endif; ?> </tbody></table></div> <details class="brokenReferencePanel"><summary>Broken and unresolved references (<?= count($functionIntelBroken) ?>)</summary> <div class="brokenReferenceList"><?php foreach (array_slice($functionIntelBroken,0,300) as $broken): ?><div class="brokenReferenceItem"><b><?= h((string)$broken['target']) ?></b><span><?= h((string)$broken['source_file']) ?> · line <?= (int)$broken['line'] ?></span></div><?php endforeach; ?><?php if (!$functionIntelBroken): ?><p>No unresolved static calls detected.</p><?php endif; ?></div> </details> </section> <section class="bottomWorkspace"> <div class="workspaceTab" id="endpointPanel"> <div class="paneHead"><b>Endpoint Security Inventory</b><small><?= count($projectEndpoints) ?> discovered</small></div> <div class="endpointTableWrap"> <table class="table"> <tr><th>Source</th><th>Endpoint</th><th>Kind</th><th>Auth</th><th>CSRF</th><th>Status</th></tr> <?php foreach ($projectEndpoints as $endpoint): ?> <tr> <td><?= h($endpoint['source']) ?></td> <td><?= h($endpoint['endpoint']) ?></td> <td><?= h($endpoint['kind']) ?></td> <td><?= h($endpoint['auth']) ?></td> <td><?= h($endpoint['csrf']) ?></td> <td><span class="sev medium"><?= h($endpoint['status']) ?></span></td> </tr> <?php endforeach; ?> <?php if (!$projectEndpoints): ?><tr><td colspan="6">No endpoints discovered yet.</td></tr><?php endif; ?> </table> </div> </div> <div class="workspaceTab" id="reviewQueue"> <div class="paneHead"><b>Multi-AI Review Queue</b><small>Manual and automated candidates</small></div> <div class="reviewCards"> <?php foreach (array_slice($reviewQueue,0,12) as $review): ?> <article class="reviewCard"> <span class="pill"><?= h((string)($review['provider'] ?? 'AI')) ?></span> <b><?= h((string)($review['review_type'] ?? 'review')) ?></b> <small><?= h((string)($review['file_path'] ?? 'project')) ?></small> <p>Status: <?= h((string)($review['status'] ?? 'queued')) ?></p> </article> <?php endforeach; ?> <?php if (!$reviewQueue): ?><p>No AI review candidates queued yet.</p><?php endif; ?> </div> </div> </section> </main> <main class="main shell legacyScanner workspaceView" id="overview" data-workspace-view="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 Project Endpoint Map</b> <span class="pill">PROJECT GRAPH</span> </div> <div class="graphToolbar"> <button type="button" data-graph-filter="all">All</button> <button type="button" data-graph-filter="connected">Connected</button> <button type="button" data-graph-filter="unconnected">Unconnected</button> <button type="button" data-graph-filter="endpoint">Endpoints</button> <button type="button" id="graphFit">Fit</button> </div> <div class="graphLegend"> <span style="--dot:#32ff73">File</span> <span style="--dot:#54f1ff">Endpoint</span> <span style="--dot:#ff9d2e">Unresolved include</span> <span style="--dot:#805cff">External link</span> <span style="--dot:#ff4d5e">Unconnected file</span> </div> <canvas class="mapCanvas" id="mapCanvas"></canvas> <div class="graphTooltip" id="graphTooltip"></div> </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="projectStats"> <div class="card"><small>Project Files</small><b><?= count($history) ?></b><div class="spark"></div></div> <div class="card"><small>Connected Files</small><b><?= (int)($projectGraph['connected_files'] ?? 0) ?></b><div class="spark"></div></div> <div class="card"><small>Unconnected Files</small><b><?= (int)($projectGraph['unconnected_files'] ?? 0) ?></b><div class="spark"></div></div> <div class="card"><small>Project Edges</small><b><?= count($projectGraph['edges'] ?? []) ?></b><div class="spark"></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> <form method="post" action="?action=fill_function_criteria#functionCriteriaPanel"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <input type="hidden" name="id" value="<?= h((string)$selected['id']) ?>"> <button class="primary">Populate Function Criteria</button> </form> <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[]" multiple required accept=".php,.phtml,.js,.mjs,.css,.html,.htm,.json,.xml,.svg,.txt,.md,.csv,.sql,.env,.ini,.log,.yml,.yaml,.swift,.java,.py,.rb,.go,.ts,.tsx,.jsx,.vue,.svelte,.c,.cpp,.h,.hpp,.cs,.rs,.sh,.bash,.zip,.pdf,image/*,video/*,audio/*"> <button class="primary">Upload Files + Scan + Build Project Graph</button> </form> <section class="serverPanel" id="serverBrowser"> <div class="row" style="justify-content:space-between;align-items:flex-start"> <div> <h2 style="margin-bottom:4px">IONOS Server Files</h2> <small style="color:var(--muted)"> <?= $sftpCapability['ready'] ? 'Secure SFTP browser' : 'SFTP unavailable — local scanning only' ?> </small> </div> <span class="pill"><?= $sftpCapability['ready'] ? 'SFTP READY' : 'OFFLINE' ?></span> </div> <?php if ($sftpCapability['ready']): ?> <?php if (!$sftpBrowserOpen): ?> <form method="post" action="?action=sftp_browse#serverBrowser" class="stack sftpConnectForm" autocomplete="off"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <div class="formGrid"> <label> Host <input name="sftp_host" value="<?= h((string)($_SESSION['sftp_host'] ?? $settings['sftp_host'])) ?>" required> </label> <label> Port <input name="sftp_port" type="number" value="<?= h((string)($_SESSION['sftp_port'] ?? $settings['sftp_port'])) ?>" min="1" max="65535" required> </label> <label> Username <input name="sftp_user" value="<?= h((string)($_SESSION['sftp_user'] ?? $settings['sftp_user'])) ?>" required> </label> <label> Password <input type="password" name="sftp_pass" placeholder="<?= !empty($_SESSION['sftp_pass']) ? 'Password active for this session' : 'Enter SFTP password' ?>" autocomplete="new-password"> </label> <label class="full"> Starting folder <input name="sftp_path" value="<?= h($remotePath) ?>" placeholder="."> </label> <label class="full"> Verified host fingerprints <textarea name="sftp_fingerprint" rows="3" required><?= h((string)($_SESSION['sftp_fingerprint'] ?? $settings['sftp_fingerprint'])) ?></textarea> </label> </div> <button class="primary">Connect and Browse Files</button> </form> <?php else: ?> <div class="connectedBar"> <div> <b>Connected</b> <small><?= h((string)($_SESSION['sftp_host'] ?? $settings['sftp_host'])) ?> · <?= h($remotePath) ?></small> </div> <div class="row"> <form method="post" action="?action=sftp_browse#serverBrowser"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <input type="hidden" name="sftp_path" value="<?= h($remotePath) ?>"> <button>Refresh</button> </form> <form method="post" action="?action=sftp_disconnect#serverBrowser"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <button class="danger">Disconnect</button> </form> </div> </div> <div class="remoteBrowser cleanBrowser"> <div class="browserToolbar"> <form method="post" action="?action=sftp_browse#serverBrowser"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <input type="hidden" name="sftp_path" value="<?= h(remote_parent_path($remotePath)) ?>"> <button>Up One Folder</button> </form> <div class="pathBox"> <small>Current path</small> <b><?= h($remotePath) ?></b> </div> </div> <div class="remoteTableWrap"> <table class="remoteTable"> <thead> <tr> <th>Name</th> <th>Type</th> <th>Size</th> <th>Modified</th> <th>Action</th> </tr> </thead> <tbody> <?php foreach ($remoteEntries as $entry): ?> <tr> <td> <b><?= h($entry['name']) ?></b> <small><?= h($entry['path']) ?></small> </td> <td><?= $entry['is_dir'] ? 'Folder' : 'File' ?></td> <td><?= $entry['is_dir'] ? '—' : h(human_bytes((int)$entry['size'])) ?></td> <td><?= h($entry['modified']) ?></td> <td> <?php if ($entry['is_dir']): ?> <form method="post" action="?action=sftp_browse#serverBrowser"> <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#serverBrowser"> <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; ?> </td> </tr> <?php endforeach; ?> <?php if (!$remoteEntries): ?> <tr> <td colspan="5"> <div class="emptyRemote"> <b>This folder is empty or no entries were returned.</b> <small>Use Up One Folder or Refresh.</small> </div> </td> </tr> <?php endif; ?> </tbody> </table> </div> </div> <?php endif; ?> <?php else: ?> <div class="sftpStatus offline"> <b>SFTP is not available on this server.</b> <small><?= h((string)($sftpCapability['detail'] ?? '')) ?></small> </div> <details class="manualScan"> <summary>Use Manual Server Path Scan Instead</summary> <form method="post" action="?action=scan_server_path" class="stack"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <input name="server_path" placeholder="/homepages/.../htdocs/index.php" required> <button class="primary">Scan Server File</button> </form> </details> <?php endif; ?> </section> <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): ?> <section class="functionCriteriaPanel" id="functionCriteriaPanel"> <div class="paneHead"> <div> <h2 style="margin:0">Function Criteria</h2> <small style="color:var(--muted)"> <?= h((string)($selected['function_criteria_provider'] ?? 'SIMON Local criteria available on demand')) ?> </small> </div> <form method="post" action="?action=fill_function_criteria#functionCriteriaPanel"> <input type="hidden" name="csrf" value="<?= h($csrf) ?>"> <input type="hidden" name="id" value="<?= h((string)$selected['id']) ?>"> <button class="primary">Calculate / AI Enrich</button> </form> </div> <?php $selectedCriteria = $selected['function_criteria'] ?? []; ?> <?php if ($selectedCriteria): ?> <div class="criteriaTableWrap"> <table class="table"> <tr> <th>Function</th> <th>Class</th> <th>Purpose</th> <th>Calls</th> <th>Complexity</th> <th>Confidence</th> <th>Review</th> </tr> <?php foreach ($selectedCriteria as $criterion): ?> <tr> <td> <b><?= h((string)$criterion['function_name']) ?></b> <small><?= h((string)($criterion['signature'] ?? '')) ?></small> </td> <td><?= h((string)$criterion['classification']) ?></td> <td> <?= h(trim((string)($criterion['purpose'] ?? '')) !== '' ? (string)$criterion['purpose'] : 'Not established; AI or human review required.') ?> </td> <td> <?= (int)($criterion['incoming_references'] ?? 0) ?> in / <?= (int)($criterion['outgoing_references'] ?? 0) ?> out </td> <td><?= (int)($criterion['complexity_estimate'] ?? 0) ?></td> <td><?= number_format(((float)($criterion['confidence'] ?? 0))*100,0) ?>%</td> <td> <?= !empty($criterion['requires_human_review']) ? 'Required' : 'Standard' ?> <small><?= h((string)($criterion['ai_status'] ?? 'not_enriched')) ?></small> </td> </tr> <?php endforeach; ?> </table> </div> <?php else: ?> <div class="emptyState"> Function criteria have not been calculated. Click Calculate / AI Enrich. SIMON Local fills measurable criteria first; connected AI fills evidence-supported descriptive fields. </div> <?php endif; ?> </section> <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 class="full">Verified host fingerprints <textarea name="sftp_fingerprint" rows="4"><?= h((string)$settings['sftp_fingerprint']) ?></textarea> <small style="color:var(--muted)">One SHA256 fingerprint per line. IONOS RSA, ECDSA, and ED25519 keys are preloaded.</small> </label> <h3 class="full">Appearance</h3> <label>Color scheme<select name="color_mode"> <option value="rainbow" <?= ($settings['color_mode']??'rainbow')==='rainbow'?'selected':'' ?>>Animated rainbow spectrum</option> <option value="custom" <?= ($settings['color_mode']??'rainbow')==='custom'?'selected':'' ?>>Custom accent colors</option> <option value="green" <?= ($settings['color_mode']??'rainbow')==='green'?'selected':'' ?>>Classic SIMON green</option> </select></label> <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><button type="button" id="testScreensaver">Test Screensaver</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="diagnosticsModal" aria-hidden="true"> <div class="modalCard"> <div class="modalHead"> <div> <h2 style="margin:0">System Diagnostics</h2> <small style="color:var(--muted)">Version <?= h(APP_VERSION) ?> · Live server, library, storage, AI, and SFTP status</small> </div> <button type="button" class="closeModal" data-close-modal>×</button> </div> <div class="reportGrid"> <div class="reportBox"> <small>PHP</small> <h2><?= h(PHP_VERSION) ?></h2> <span class="sev low">READY</span> </div> <div class="reportBox"> <small>Autoloader</small> <h2><?= $vendorDiagnostics['autoload_found'] ? 'FOUND' : 'MISSING' ?></h2> <span class="sev <?= $vendorDiagnostics['autoload_found'] ? 'low' : 'high' ?>"> <?= $vendorDiagnostics['autoload_found'] ? 'READY' : 'CHECK' ?> </span> </div> <div class="reportBox"> <small>phpseclib</small> <h2><?= $vendorDiagnostics['phpseclib_loaded'] ? 'LOADED' : 'NOT LOADED' ?></h2> <span class="sev <?= $vendorDiagnostics['phpseclib_loaded'] ? 'low' : 'high' ?>"> <?= $vendorDiagnostics['phpseclib_loaded'] ? 'READY' : 'CHECK' ?> </span> </div> <div class="reportBox"> <small>SFTP</small> <h2><?= $sftpCapability['ready'] ? 'READY' : 'OFFLINE' ?></h2> <span class="sev <?= $sftpCapability['ready'] ? 'low' : 'high' ?>"> <?= h($sftpCapability['driver']) ?> </span> </div> <div class="reportBox"> <small>Private Data</small> <h2><?= is_writable(DATA_DIR) ? 'WRITABLE' : 'BLOCKED' ?></h2> <span class="sev <?= is_writable(DATA_DIR) ? 'low' : 'critical' ?>"> <?= is_writable(DATA_DIR) ? 'READY' : 'ERROR' ?> </span> </div> <div class="reportBox"> <small>AI</small> <h2><?= !empty($settings['ai_enabled']) ? 'ENABLED' : 'LOCAL' ?></h2> <span class="sev low">SIMON</span> </div> </div> <h3>Resolved paths</h3> <div class="code" style="max-height:none"> Application: <?= h(__DIR__) ?> Document root: <?= h((string)($_SERVER['DOCUMENT_ROOT'] ?? 'Unknown')) ?> Autoload: <?= h((string)($vendorDiagnostics['autoload_path'] ?: 'Not resolved')) ?> phpseclib source: <?= h((string)($vendorDiagnostics['phpseclib_source'] ?: 'Not resolved')) ?> SFTP detail: <?= h((string)($sftpCapability['detail'] ?? '')) ?> Private data: <?= h(DATA_DIR) ?> </div> <?php if (!empty($vendorDiagnostics['errors'])): ?> <h3>Loader errors</h3> <ul> <?php foreach ($vendorDiagnostics['errors'] as $loaderError): ?> <li><?= h($loaderError) ?></li> <?php endforeach; ?> </ul> <?php endif; ?> </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 primary" href="?action=report&id=<?= h($selected['id']) ?>&kind=html">View HTML Report</a> <a class="btn" href="?action=report&id=<?= h($selected['id']) ?>&kind=html&download=1">Download HTML</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')); document.getElementById('testScreensaver')?.addEventListener('click',()=>{ if(typeof startSaver==='function') startSaver(); }); 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.keywords||''} ${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 indexed results yet</b> <small>Upload or scan a file first. Search will then include files, functions, includes, APIs, links, classes, threats, and file content.</small> </div>`; searchResults.hidden = false; } searchInput?.addEventListener('focus', event => renderSearch(event.target.value)); searchInput?.addEventListener('click', event => renderSearch(event.target.value)); 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; function activateWorkspace(workspace, scrollTarget=null, updateHash=true){ const targetWorkspace = workspace === 'overview' ? 'overview' : 'studio'; document.querySelectorAll('[data-workspace-view]').forEach(view=>{ view.classList.toggle('active', view.dataset.workspaceView === targetWorkspace); }); document.querySelectorAll('.nav a[data-workspace]').forEach(link=>{ const exactTarget = link.dataset.scrollTarget || link.dataset.workspace; const requestedTarget = scrollTarget || targetWorkspace; link.classList.toggle( 'active', link.dataset.workspace === targetWorkspace && exactTarget === requestedTarget ); }); requestAnimationFrame(()=>{ const targetId = scrollTarget || targetWorkspace; const target = document.getElementById(targetId); if(target){ const scroller = target.closest('.main,.right'); if(scroller && scroller !== target){ const top = target.offsetTop - 12; scroller.scrollTo({top,behavior:'smooth'}); }else{ target.scrollIntoView({behavior:'smooth',block:'start'}); } } }); if(updateHash){ history.replaceState(null,'','#' + (scrollTarget || targetWorkspace)); } } document.querySelectorAll('.nav a[data-workspace]').forEach(link=>{ link.addEventListener('click',event=>{ event.preventDefault(); activateWorkspace( link.dataset.workspace, link.dataset.scrollTarget || null, true ); }); }); function workspaceFromHash(){ const target=(location.hash||'#studio').slice(1); const overviewTargets=new Set(['overview','scanner','mapping','preview']); const workspace=overviewTargets.has(target)?'overview':'studio'; activateWorkspace(workspace,target,false); } addEventListener('hashchange',workspaceFromHash); workspaceFromHash(); const functionIntelSearch=document.getElementById('functionIntelSearch'); const functionIntelFilter=document.getElementById('functionIntelFilter'); function applyFunctionIntelFilter(){const q=(functionIntelSearch?.value||'').trim().toLowerCase();const filter=functionIntelFilter?.value||'all';document.querySelectorAll('.functionIntelRow').forEach(row=>{const matchesText=q===''||(row.dataset.search||'').includes(q);const matchesClass=filter==='all'||(row.dataset.classification||'')===filter;row.hidden=!(matchesText&&matchesClass)})} functionIntelSearch?.addEventListener('input',applyFunctionIntelFilter);functionIntelFilter?.addEventListener('change',applyFunctionIntelFilter); document.querySelectorAll('[data-function-filter]').forEach(button=>button.addEventListener('click',()=>{const key=button.dataset.functionFilter;const map={canonical:'canonical',duplicates:'duplicate',unused:'unused',high_error:'high-error',shared_candidates:'shared-candidate',archive_candidates:'archive-candidate',project_specific:'project-specific'};if(key==='broken_references'){document.querySelector('.brokenReferencePanel')?.setAttribute('open','open');document.querySelector('.brokenReferencePanel')?.scrollIntoView({behavior:'smooth',block:'center'});return}if(functionIntelFilter){functionIntelFilter.value=map[key]||'all';applyFunctionIntelFilter()}})); const fileFilter=document.getElementById('fileFilter'); fileFilter?.addEventListener('input',()=>{ const q=fileFilter.value.trim().toLowerCase(); document.querySelectorAll('.projectFileRow').forEach(row=>{ row.hidden=q!=='' && !(row.dataset.search||'').includes(q); }); }); <?php if (!empty($_SESSION['scroll_to_server_browser'])): unset($_SESSION['scroll_to_server_browser']); ?> setTimeout(()=>{ document.getElementById('serverBrowser')?.scrollIntoView({behavior:'smooth',block:'start'}); },250); <?php endif; ?> 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}); 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}); const PROJECT_GRAPH = <?= $projectGraphJson ?: '{"nodes":[],"edges":[]}' ?>; let graphFilter = 'all'; let graphView = {scale:1, offsetX:0, offsetY:0}; let graphNodes = []; let graphDrag = null; function nodeColor(node){ if(node.type==='file') return node.connected ? '#32ff73' : '#ff4d5e'; if(node.type==='endpoint') return '#54f1ff'; if(node.type==='unresolved') return '#ff9d2e'; if(node.type==='link') return '#805cff'; return '#a7b0ad'; } function filteredGraph(){ const allowedNodes = PROJECT_GRAPH.nodes.filter(node=>{ if(graphFilter==='all') return true; if(graphFilter==='connected') return !!node.connected; if(graphFilter==='unconnected') return node.type==='file' && !node.connected; if(graphFilter==='endpoint') return node.type==='endpoint'; return true; }); const ids = new Set(allowedNodes.map(n=>n.id)); return { nodes: allowedNodes, edges: PROJECT_GRAPH.edges.filter(e=>ids.has(e.from)&&ids.has(e.to)) }; } function layoutGraph(width,height){ const graph=filteredGraph(); const files=graph.nodes.filter(n=>n.type==='file'); const endpoints=graph.nodes.filter(n=>n.type==='endpoint'); const unresolved=graph.nodes.filter(n=>n.type==='unresolved'); const links=graph.nodes.filter(n=>n.type==='link'); const placed=[]; function placeRing(items,radius,offset=0){ items.forEach((n,i)=>{ const a=(Math.PI*2*i/Math.max(1,items.length))+offset; placed.push({...n,x:width/2+Math.cos(a)*radius,y:height/2+Math.sin(a)*radius}); }); } placeRing(files,Math.min(width,height)*.22,-Math.PI/2); placeRing(endpoints,Math.min(width,height)*.36,0); placeRing(unresolved,Math.min(width,height)*.45,.35); placeRing(links,Math.min(width,height)*.50,.7); graphNodes=placed; return {nodes:placed,edges:graph.edges}; } function drawMap(){ const canvas=document.getElementById('mapCanvas'); if(!canvas) return; const rect=canvas.getBoundingClientRect(); const dpr=Math.min(devicePixelRatio||1,2); canvas.width=Math.floor(rect.width*dpr); canvas.height=Math.floor(rect.height*dpr); const ctx=canvas.getContext('2d'); ctx.setTransform(dpr,0,0,dpr,0,0); ctx.clearRect(0,0,rect.width,rect.height); const graph=layoutGraph(rect.width,rect.height); const byId=new Map(graph.nodes.map(n=>[n.id,n])); ctx.save(); ctx.translate(graphView.offsetX,graphView.offsetY); ctx.scale(graphView.scale,graphView.scale); graph.edges.forEach(edge=>{ const a=byId.get(edge.from),b=byId.get(edge.to); if(!a||!b)return; ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y); ctx.lineWidth=edge.type==='api'?2:1; ctx.strokeStyle=edge.type==='api'?'rgba(84,241,255,.7)':'rgba(180,255,210,.24)'; ctx.stroke(); }); graph.nodes.forEach(node=>{ const r=node.type==='file'?14:10; ctx.beginPath(); ctx.arc(node.x,node.y,r,0,Math.PI*2); ctx.fillStyle=nodeColor(node); ctx.shadowColor=nodeColor(node); ctx.shadowBlur=12; ctx.fill(); ctx.shadowBlur=0; const label=(node.label||'').replace(/^.*\//,''); ctx.font='11px system-ui'; ctx.textAlign='center'; ctx.fillStyle='rgba(238,252,242,.92)'; const max=22; const short=label.length>max?label.slice(0,max-1)+'…':label; ctx.fillText(short,node.x,node.y+r+14); }); ctx.restore(); if(graph.nodes.length===0){ ctx.fillStyle='rgba(238,252,242,.65)'; ctx.font='14px system-ui'; ctx.textAlign='center'; ctx.fillText('Upload multiple project files to build the connection graph',rect.width/2,rect.height/2); } } document.querySelectorAll('[data-graph-filter]').forEach(btn=>btn.addEventListener('click',()=>{ graphFilter=btn.dataset.graphFilter; drawMap(); })); document.getElementById('graphFit')?.addEventListener('click',()=>{ graphView={scale:1,offsetX:0,offsetY:0}; drawMap(); }); const graphCanvas=document.getElementById('mapCanvas'); const graphTooltip=document.getElementById('graphTooltip'); graphCanvas?.addEventListener('mousemove',event=>{ const rect=graphCanvas.getBoundingClientRect(); const x=(event.clientX-rect.left-graphView.offsetX)/graphView.scale; const y=(event.clientY-rect.top-graphView.offsetY)/graphView.scale; const hit=graphNodes.find(n=>Math.hypot(n.x-x,n.y-y)<18); if(hit){ graphTooltip.style.display='block'; graphTooltip.style.left=(event.clientX-rect.left+12)+'px'; graphTooltip.style.top=(event.clientY-rect.top+12)+'px'; graphTooltip.innerHTML=`<b>${escapeHtml(hit.label)}</b><br>${escapeHtml(hit.type)} · ${hit.connected?'connected':'unconnected'}`; }else{ graphTooltip.style.display='none'; } }); graphCanvas?.addEventListener('mouseleave',()=>graphTooltip.style.display='none'); addEventListener('resize',drawMap); drawMap(); /* Procedural SIMON BUG screensaver */ (() => { 'use strict'; const saver = document.getElementById('screensaver'); const canvas = document.getElementById('proceduralScene'); if (!saver || !canvas) return; const ctx = canvas.getContext('2d'); const modeLabel = document.getElementById('proceduralMode'); const scanTheater = document.getElementById('scanTheater'); const scanRiverTrack = document.getElementById('scanFileRiverTrack'); const scanChompLayer = document.getElementById('scanChompLayer'); const scanChompFlash = document.getElementById('scanChompFlash'); const scanRecentFiles = []; let scanLastAnimatedFile = ''; function scanText(id,value){ const el=document.getElementById(id); if(el) el.textContent=String(value ?? ''); } function scanClassFromStatus(status){ const text=String(status || '').toLowerCase(); if(text.includes('error') || text.includes('fail') || text.includes('critical')) return 'error'; if(text.includes('skip') || text.includes('warn')) return 'warn'; if(text.includes('metadata')) return 'meta'; return ''; } function pushScanFile(path,status='scanned'){ path=String(path || '').trim(); if(!path || path==='Waiting' || path==='Not started' || path===scanLastAnimatedFile) return; scanLastAnimatedFile=path; scanRecentFiles.unshift({path,status}); if(scanRecentFiles.length>18) scanRecentFiles.length=18; if(scanRiverTrack){ scanRiverTrack.innerHTML=scanRecentFiles.map(item=>`<span class="scanRiverItem ${scanClassFromStatus(item.status)}">${escapeHtml(item.path)}</span>`).join(''); } if(scanChompLayer){ const chip=document.createElement('div'); chip.className='scanChomp '+scanClassFromStatus(status); chip.textContent=path; chip.style.top=(58+Math.random()*18)+'%'; scanChompLayer.appendChild(chip); setTimeout(()=>chip.remove(),3400); } if(scanChompFlash){ scanChompFlash.classList.remove('fire'); void scanChompFlash.offsetWidth; scanChompFlash.classList.add('fire'); } } window.SIMON_SCAN_SAVER_UPDATE=function(status={}){ const live=['running','queued','initializing'].includes(String(status.status || '').toLowerCase()); if(scanTheater){ scanTheater.classList.toggle('live',live || Number(status.processed || 0)>0); scanTheater.setAttribute('aria-hidden',live?'false':'true'); } scanText('scanTheaterStatus',String(status.status || 'idle').toUpperCase().replaceAll('_',' ')); scanText('scanTheaterProcessed',`${status.processed || 0} / ${status.total || 0}`); scanText('scanTheaterDeep',status.deep_scanned || 0); scanText('scanTheaterMeta',status.metadata_only || 0); scanText('scanTheaterSkipped',status.skipped_directories || 0); scanText('scanTheaterErrors',status.errors || 0); const bar=document.getElementById('scanTheaterProgress'); if(bar) bar.style.width=`${Math.max(0,Math.min(100,Number(status.percent || 0)))}%`; pushScanFile(status.last_file,status.errors>0?'scanned':'scanned'); if(live){ if(modeLabel) modeLabel.textContent='SIMON BUG · FILE EATER MODE'; if(missionLabel) missionLabel.textContent='Eating files, mapping code, hashing assets, and logging anything that refuses to scan.'; if(sourceLabel) sourceLabel.textContent=`Live host scan · ${status.processed || 0} of ${status.total || 0} processed`; } }; const missionLabel = document.getElementById('proceduralMission'); const sourceLabel = document.getElementById('proceduralSource'); const pauseButton = document.getElementById('proceduralPause'); const previousButton = document.getElementById('proceduralPrevious'); const nextButton = document.getElementById('proceduralNext'); let width = 0; let height = 0; let dpr = 1; let paused = false; let running = false; let last = 0; let elapsed = 0; let modeIndex = 0; let hideTimer = null; let idleTimer = null; let autoTimer = null; let animationFrame = null; const modes = [ { id:'guardian', name:'SIMON BUG · GUARDIAN MODE', mission:'Looking for open endpoints, missing authentication or authorization, missing CSRF, exposed secrets, unsafe uploads, unsigned webhooks, and release blockers.', sources:'Endpoints · Configuration · Findings · 5W1H Events', palette:['#7f2cff','#d12cff','#23d7ff','#090015'] }, { id:'search', name:'SIMON BUG · SEARCH MODE', mission:'Looking for files, signals, keywords, missing includes, broken references, unresolved links, unused assets, and incomplete project connections.', sources:'Files · Signal Catalog · Signals · Relationships', palette:['#00ff88','#ff2aa8','#00b7ff','#080510'] }, { id:'energy', name:'SIMON BUG · ENERGY MODE', mission:'Looking for scan duration, latency, error rates, source health, endpoint health, retry activity, and incomplete coverage.', sources:'Scan Runs · Sources · Endpoints · Findings', palette:['#ffe600','#ff6b00','#00eaff','#08030a'] }, { id:'quantum', name:'SIMON BUG · QUANTUM MODE', mission:'Looking for canonical functions, exact and near duplicates, unused functions, copied logic, shared-module candidates, and archive candidates.', sources:'Functions · Relationships · Findings · Development Gaps', palette:['#ff35d3','#6d3cff','#00ffd0','#05000b'] }, { id:'organic', name:'SIMON BUG · ORGANIC MODE', mission:'Looking for project purpose, required features, known problems, intended users, 5W1H events, truth evidence, makeTrue and makeFalse factors, and the evolution path.', sources:'5W1H Events · Population Workflow · Development Gaps · Overview', palette:['#ff52c8','#7f4cff','#45d9ff','#07030d'] }, { id:'deployment', name:'SIMON BUG · DEPLOYMENT MODE', mission:'Looking for changed files, checkpoint status, unresolved blockers, approval state, rollback readiness, and post-deployment verification.', sources:'Population Workflow · Findings · Endpoints · Scan Runs · 5W1H Events', palette:['#32ff73','#54f1ff','#805cff','#030a09'] } ]; const particles = []; const stars = []; function hexToRgb(hex){ const n = parseInt(hex.slice(1),16); return {r:(n>>16)&255,g:(n>>8)&255,b:n&255}; } function rgba(hex,a){ const c = hexToRgb(hex); return `rgba(${c.r},${c.g},${c.b},${a})`; } function resize(){ dpr = Math.min(window.devicePixelRatio || 1, 2); width = window.innerWidth; height = window.innerHeight; canvas.width = Math.floor(width*dpr); canvas.height = Math.floor(height*dpr); canvas.style.width = width+'px'; canvas.style.height = height+'px'; ctx.setTransform(dpr,0,0,dpr,0,0); stars.length = 0; const starCount = Math.max(80,Math.min(220,Math.floor(width*height/8000))); for(let i=0;i<starCount;i++){ stars.push({ x:Math.random()*width, y:Math.random()*height, r:.2+Math.random()*1.5, a:.12+Math.random()*.65, s:.03+Math.random()*.15 }); } particles.length = 0; for(let i=0;i<150;i++){ particles.push({ a:Math.random()*Math.PI*2, d:70+Math.random()*220, s:(Math.random()-.5)*.004, r:.8+Math.random()*3.2, p:Math.random()*Math.PI*2 }); } } function roundRectPath(x,y,w,h,r){ const rr = Math.min(r,w/2,h/2); ctx.beginPath(); ctx.moveTo(x+rr,y); ctx.arcTo(x+w,y,x+w,y+h,rr); ctx.arcTo(x+w,y+h,x,y+h,rr); ctx.arcTo(x,y+h,x,y,rr); ctx.arcTo(x,y,x+w,y,rr); ctx.closePath(); } function glowCircle(x,y,r,color,alpha=1){ ctx.save(); const g = ctx.createRadialGradient(x,y,0,x,y,r); g.addColorStop(0,rgba(color,.95*alpha)); g.addColorStop(.28,rgba(color,.5*alpha)); g.addColorStop(1,rgba(color,0)); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(x,y,r,0,Math.PI*2); ctx.fill(); ctx.restore(); } function drawBackground(t,p){ const g = ctx.createRadialGradient( width*.5 + Math.cos(t*.18)*width*.08, height*.42 + Math.sin(t*.15)*height*.05, 20, width*.5, height*.5, Math.max(width,height)*.78 ); g.addColorStop(0,rgba(p[0],.22)); g.addColorStop(.32,rgba(p[1],.12)); g.addColorStop(.66,rgba(p[2],.06)); g.addColorStop(1,p[3]); ctx.fillStyle = g; ctx.fillRect(0,0,width,height); for(const star of stars){ star.y -= star.s; if(star.y < -3){ star.y=height+3; star.x=Math.random()*width; } ctx.fillStyle=`rgba(255,255,255,${star.a})`; ctx.beginPath(); ctx.arc(star.x,star.y,star.r,0,Math.PI*2); ctx.fill(); } ctx.save(); ctx.globalCompositeOperation='screen'; for(let i=0;i<5;i++){ const x=width*(.18+i*.16)+Math.sin(t*.2+i)*50; const y=height*(.28+Math.sin(t*.16+i)*.04); glowCircle(x,y,150+i*18,p[i%3],.08); } ctx.restore(); } function drawLeg(cx,cy,angle,len,color,t){ const wave=Math.sin(t*1.8+angle*3)*8; const x1=cx+Math.cos(angle)*len*.35; const y1=cy+Math.sin(angle)*len*.25; const x2=cx+Math.cos(angle)*len*.72-Math.sin(angle)*wave; const y2=cy+Math.sin(angle)*len*.62+Math.cos(angle)*wave; const x3=cx+Math.cos(angle)*len; const y3=cy+Math.sin(angle)*len*.88+18; ctx.save(); ctx.lineCap='round'; ctx.lineJoin='round'; ctx.shadowBlur=22; ctx.shadowColor=color; ctx.strokeStyle='rgba(8,8,16,.98)'; ctx.lineWidth=Math.max(16,len*.12); ctx.beginPath(); ctx.moveTo(cx,cy); ctx.quadraticCurveTo(x1,y1,x2,y2); ctx.quadraticCurveTo((x2+x3)/2,y3-18,x3,y3); ctx.stroke(); ctx.strokeStyle=rgba(color,.88); ctx.lineWidth=Math.max(4,len*.025); ctx.beginPath(); ctx.moveTo(cx,cy); ctx.quadraticCurveTo(x1,y1,x2,y2); ctx.quadraticCurveTo((x2+x3)/2,y3-18,x3,y3); ctx.stroke(); glowCircle(x3,y3,22,color,.48); ctx.restore(); } function drawAntenna(cx,cy,side,scale,p,t){ const sway=Math.sin(t*1.2+side)*12; const x1=cx+side*scale*.24; const y1=cy-scale*.53; const x2=cx+side*scale*.46+sway; const y2=cy-scale*.95; ctx.save(); ctx.lineCap='round'; ctx.shadowBlur=20; ctx.shadowColor=p[side>0?1:2]; ctx.strokeStyle='rgba(12,12,22,.98)'; ctx.lineWidth=scale*.055; ctx.beginPath(); ctx.moveTo(cx+side*scale*.12,cy-scale*.34); ctx.quadraticCurveTo(x1,y1,x2,y2); ctx.stroke(); ctx.strokeStyle=p[side>0?1:2]; ctx.lineWidth=scale*.012; ctx.stroke(); glowCircle(x2,y2,scale*.11,p[side>0?1:2],.9); ctx.restore(); } function drawBug(t,p){ const scale=Math.min(width,height)*.31; const cx=width*.5+Math.sin(t*.28)*width*.03; const cy=height*.53+Math.sin(t*.7)*10; const bob=Math.sin(t*.8)*8; ctx.save(); ctx.globalCompositeOperation='screen'; for(const pt of particles){ pt.a += pt.s; const pulse=1+Math.sin(t*1.6+pt.p)*.08; const x=cx+Math.cos(pt.a)*pt.d*pulse; const y=cy+Math.sin(pt.a)*pt.d*.62*pulse; ctx.fillStyle=rgba(p[Math.floor(pt.p)%3],.2); ctx.beginPath(); ctx.arc(x,y,pt.r,0,Math.PI*2); ctx.fill(); } ctx.restore(); const legAngles=[-2.7,-2.25,-1.85,-1.29,-.9,-.45]; legAngles.forEach((a,i)=>drawLeg(cx,cy+scale*.24,a,scale*.78,p[i%3],t+i*.2)); drawAntenna(cx,cy+bob,-1,scale,p,t); drawAntenna(cx,cy+bob,1,scale,p,t); ctx.save(); ctx.translate(cx,cy+bob); ctx.shadowBlur=55; ctx.shadowColor=rgba(p[0],.8); const bodyGrad=ctx.createRadialGradient(-scale*.12,-scale*.2,scale*.05,0,0,scale*.8); bodyGrad.addColorStop(0,'#252537'); bodyGrad.addColorStop(.42,'#090911'); bodyGrad.addColorStop(1,'#020205'); ctx.fillStyle=bodyGrad; ctx.beginPath(); ctx.ellipse(0,scale*.1,scale*.68,scale*.55,0,0,Math.PI*2); ctx.fill(); ctx.lineWidth=scale*.018; const rim=ctx.createLinearGradient(-scale,0,scale,0); rim.addColorStop(0,p[2]); rim.addColorStop(.5,p[0]); rim.addColorStop(1,p[1]); ctx.strokeStyle=rim; ctx.stroke(); const headGrad=ctx.createLinearGradient(0,-scale*.58,0,scale*.15); headGrad.addColorStop(0,'#20202d'); headGrad.addColorStop(1,'#05050a'); ctx.fillStyle=headGrad; ctx.beginPath(); ctx.ellipse(0,-scale*.22,scale*.62,scale*.47,0,0,Math.PI*2); ctx.fill(); ctx.strokeStyle=rgba('#ffffff',.11); ctx.lineWidth=2; ctx.stroke(); roundRectPath(-scale*.43,-scale*.49,scale*.86,scale*.5,scale*.16); const face=ctx.createLinearGradient(0,-scale*.49,0,0); face.addColorStop(0,'rgba(8,10,18,.96)'); face.addColorStop(1,'rgba(1,2,6,.98)'); ctx.fillStyle=face; ctx.fill(); ctx.strokeStyle=rgba(p[0],.58); ctx.lineWidth=scale*.012; ctx.stroke(); const eyeY=-scale*.28; const eyeDx=scale*.21; const blink=Math.max(.1,Math.abs(Math.sin(t*.75))*1.15); [-1,1].forEach((side,idx)=>{ glowCircle(side*eyeDx,eyeY,scale*.12,p[idx+1],.95); const eg=ctx.createRadialGradient( side*eyeDx-scale*.025, eyeY-scale*.035, 2, side*eyeDx, eyeY, scale*.09 ); eg.addColorStop(0,'#ffffff'); eg.addColorStop(.18,p[idx+1]); eg.addColorStop(1,'#090012'); ctx.fillStyle=eg; ctx.beginPath(); ctx.ellipse(side*eyeDx,eyeY,scale*.075,scale*.085*blink,0,0,Math.PI*2); ctx.fill(); }); ctx.strokeStyle=rgba(p[0],.9); ctx.lineWidth=scale*.018; ctx.lineCap='round'; ctx.beginPath(); ctx.arc(0,-scale*.16,scale*.13,.22*Math.PI,.78*Math.PI); ctx.stroke(); roundRectPath(-scale*.28,scale*.08,scale*.56,scale*.29,scale*.06); ctx.fillStyle='rgba(4,4,10,.88)'; ctx.fill(); ctx.strokeStyle=rgba(p[2],.45); ctx.lineWidth=scale*.009; ctx.stroke(); ctx.textAlign='center'; ctx.shadowBlur=18; ctx.shadowColor=p[0]; ctx.fillStyle='#ffffff'; ctx.font=`900 ${Math.max(20,scale*.11)}px Inter, sans-serif`; ctx.fillText('SIMON',0,scale*.205); ctx.shadowBlur=10; ctx.fillStyle=p[2]; ctx.font=`700 ${Math.max(9,scale*.038)}px Inter, sans-serif`; ctx.fillText('BUG',0,scale*.278); ctx.restore(); ctx.save(); ctx.globalCompositeOperation='screen'; glowCircle(cx,cy+scale*.35,scale*.62,p[0],.12); ctx.restore(); } function render(now){ if(!running) return; if(!last) last=now; const dt=Math.min((now-last)/1000,.05); last=now; if(!paused) elapsed+=dt; const palette=modes[modeIndex].palette; drawBackground(elapsed,palette); drawBug(elapsed,palette); animationFrame=requestAnimationFrame(render); } function setMode(next){ modeIndex=(next+modes.length)%modes.length; modeLabel.textContent=modes[modeIndex].name; if(missionLabel) missionLabel.textContent=modes[modeIndex].mission; if(sourceLabel) sourceLabel.textContent='Data contract: '+modes[modeIndex].sources; } function setPaused(value){ paused=value; pauseButton.textContent=paused?'▶':'Ⅱ'; pauseButton.setAttribute('aria-label',paused?'Resume animation':'Pause animation'); } function updateClock(){ const now=new Date(); const time=document.getElementById('proceduralTime'); const date=document.getElementById('proceduralDate'); if(time){ time.textContent=now.toLocaleTimeString([],{ hour:'2-digit', minute:'2-digit' }); } if(date){ date.textContent=now.toLocaleDateString([],{ weekday:'long', month:'long', day:'numeric', year:'numeric' }); } } function revealControls(){ saver.classList.remove('idle'); saver.style.cursor='default'; clearTimeout(hideTimer); hideTimer=setTimeout(()=>{ saver.classList.add('idle'); saver.style.cursor='none'; },3200); } function startSaver(){ if(running) return; saver.classList.add('on'); saver.classList.remove('idle'); saver.setAttribute('aria-hidden','false'); running=true; paused=false; last=0; elapsed=0; resize(); updateClock(); setPaused(false); setMode(modeIndex); revealControls(); animationFrame=requestAnimationFrame(render); clearInterval(autoTimer); autoTimer=setInterval(()=>{ if(running && !paused) setMode(modeIndex+1); },12000); } function stopSaver(){ if(!running) return; running=false; saver.classList.remove('on','idle'); saver.setAttribute('aria-hidden','true'); saver.style.cursor='none'; if(animationFrame){ cancelAnimationFrame(animationFrame); animationFrame=null; } clearInterval(autoTimer); autoTimer=null; } function resetIdle(){ if(running) stopSaver(); clearTimeout(idleTimer); idleTimer=setTimeout( startSaver, <?= max(10, (int)$settings['idle_seconds']) * 1000 ?> ); } previousButton?.addEventListener('click',event=>{ event.stopPropagation(); setMode(modeIndex-1); revealControls(); }); nextButton?.addEventListener('click',event=>{ event.stopPropagation(); setMode(modeIndex+1); revealControls(); }); pauseButton?.addEventListener('click',event=>{ event.stopPropagation(); setPaused(!paused); revealControls(); }); saver.addEventListener('pointermove',revealControls,{passive:true}); saver.addEventListener('pointerdown',event=>{ if(event.target.closest('.proceduralControls')) return; stopSaver(); resetIdle(); }); saver.addEventListener('touchstart',event=>{ if(event.target.closest('.proceduralControls')) return; stopSaver(); resetIdle(); },{passive:true}); document.addEventListener('keydown',event=>{ if(running){ if(event.key==='ArrowRight'){ setMode(modeIndex+1); revealControls(); return; } if(event.key==='ArrowLeft'){ setMode(modeIndex-1); revealControls(); return; } if(event.key===' '){ event.preventDefault(); setPaused(!paused); revealControls(); return; } if(event.key.toLowerCase()==='f' && document.fullscreenEnabled){ document.fullscreenElement ? document.exitFullscreen() : saver.requestFullscreen(); return; } stopSaver(); } resetIdle(); }); saver.addEventListener('dblclick',()=>{ if(!document.fullscreenEnabled) return; document.fullscreenElement ? document.exitFullscreen() : saver.requestFullscreen(); }); ['pointermove','pointerdown','touchstart','scroll','click'].forEach(type=>{ document.addEventListener(type,event=>{ if(running && saver.contains(event.target)) return; resetIdle(); },{passive:true}); }); window.addEventListener('resize',()=>{ if(running) resize(); },{passive:true}); document.getElementById('testScreensaver')?.addEventListener('click',()=>{ startSaver(); }); setInterval(updateClock,1000); updateClock(); if(typeof window.SIMON_SCAN_SAVER_UPDATE === 'function'){ window.SIMON_SCAN_SAVER_UPDATE(<?= json_encode($hostScanState, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>); } resetIdle(); })(); /* Complete host scan batch controller */ (() => { const startButton = document.getElementById('startHostScan'); const resumeButton = document.getElementById('resumeHostScan'); const stopButton = document.getElementById('stopHostScan'); const rootInput = document.getElementById('hostRoot'); const batchSize = document.getElementById('hostBatchSize'); const csrfInput = document.querySelector('#hostScanForm input[name="csrf"]'); if (!startButton || !resumeButton || !stopButton || !csrfInput) return; let autoRun = false; let requestActive = false; function bindReconcile5W1HButton(){ const button=document.getElementById('reconcile5W1H'); if(!button || button.dataset.bound==='1') return; button.dataset.bound='1'; button.addEventListener('click', async function(){ const statusEl=document.getElementById('reconcile5W1HStatus'); const visibleUuid=(document.getElementById('hostReportUuid')?.textContent||'').trim(); const uuid=(visibleUuid && visibleUuid!=='Not started') ? visibleUuid : String(button.dataset.scanUuid||'').trim(); const csrf=String(button.dataset.csrf||document.querySelector('input[name="csrf"]')?.value||'').trim(); if(!/^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i.test(uuid)){ alert('No valid scan is selected.'); return; } if(!csrf){ alert('Security token is missing. Reload the page and try again.'); return; } button.disabled=true; button.textContent='Reconciling…'; if(statusEl) statusEl.textContent='Repair started. Keep this page open.'; try{ const body=new URLSearchParams(); body.set('csrf',csrf); body.set('scan_uuid',uuid); const response=await fetch( '?action=host_5w1h_reconcile&scan_uuid='+encodeURIComponent(uuid), { method:'POST', headers:{ 'Content-Type':'application/x-www-form-urlencoded;charset=UTF-8', 'Accept':'application/json' }, body:body.toString(), credentials:'same-origin', cache:'no-store' } ); const raw=await response.text(); let result; try{ result=JSON.parse(raw); } catch(parseError){ throw new Error(raw ? raw.slice(0,500) : 'Server returned an empty response.'); } if(!response.ok || !result.ok){ throw new Error(result.error || ('Reconciliation failed with HTTP '+response.status)); } const message='5W1H repaired. Duplicates removed: '+(result.duplicates_removed??0)+ '; missing events recovered: '+(result.missing_events_recovered??0)+ '; unique items: '+(result.unique_terminal_items??0)+'.'; if(statusEl) statusEl.textContent=message; alert(message); window.location.reload(); }catch(error){ const message=error?.message || String(error); if(statusEl) statusEl.textContent='Repair failed: '+message; alert('Repair failed: '+message); }finally{ button.disabled=false; button.textContent='Repair / Reconcile 5W1H'; } }); } if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded',bindReconcile5W1HButton,{once:true}); }else{ bindReconcile5W1HButton(); } function updateHostScanUi(status){ document.getElementById('hostScanStatusBadge').textContent = String(status.status || 'unknown').toUpperCase(); document.getElementById('hostProcessed').textContent = `${status.processed || 0} / ${status.total || 0}`; document.getElementById('hostLastFile').textContent = status.last_file || 'Waiting'; document.getElementById('hostDeep').textContent = status.deep_scanned || 0; document.getElementById('hostMetadata').textContent = status.metadata_only || 0; document.getElementById('hostErrors').textContent = status.errors || 0; const skipped = document.getElementById('hostSkipped'); if(skipped) skipped.textContent = (status.skipped_directories || 0) + (status.skipped_files || 0); const qr=document.getElementById('hostQueueRemaining'); if(qr) qr.textContent=status.queue_remaining || 0; const fps=document.getElementById('hostFilesPerSecond'); if(fps) fps.textContent=status.files_per_second || 0; const dbw=document.getElementById('hostDbWrites'); if(dbw) dbw.textContent=status.database_updates || 0; const dbf=document.getElementById('hostDbFailures'); if(dbf) dbf.textContent=status.database_failures || 0; const ru=document.getElementById('hostReportUuid'); if(ru) ru.textContent=status.scan_uuid || 'Not started'; ['viewHostReport','downloadHostReport','downloadHostInventory','downloadHostFailures','download5W1H'].forEach(id=>{ const el=document.getElementById(id); if(!el || !status.scan_uuid) return; const reconcileButton=document.getElementById('reconcile5W1H'); if(reconcileButton&&!reconcileButton.dataset.bound){ reconcileButton.dataset.bound='1'; reconcileButton.addEventListener('click',async()=>{ const uuid=(document.getElementById('hostReportUuid')?.textContent||'').trim(); if(!uuid||uuid==='Not started'){alert('No scan selected.');return;} reconcileButton.disabled=true;reconcileButton.textContent='Reconciling…'; try{ const body=new URLSearchParams({csrf:document.querySelector('input[name="csrf"]')?.value||'',scan_uuid:uuid}); const response=await fetch('?action=host_5w1h_reconcile&scan_uuid='+encodeURIComponent(uuid),{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body}); const result=await response.json(); if(!response.ok||!result.ok) throw new Error(result.error||'Reconciliation failed'); alert('5W1H repaired. Duplicates removed: '+result.duplicates_removed+'; missing events recovered: '+result.missing_events_recovered+'; unique items: '+result.unique_terminal_items+'.'); location.reload(); }catch(error){alert(error.message||String(error));} finally{reconcileButton.disabled=false;reconcileButton.textContent='Repair / Reconcile 5W1H';} }); } const action={viewHostReport:'host_report_view',downloadHostReport:'host_report_download',downloadHostInventory:'host_inventory_csv',downloadHostFailures:'host_failures_csv',download5W1H:'host_5w1h_stream'}[id]; el.href=`?action=${action}&scan_uuid=${encodeURIComponent(status.scan_uuid)}`; }); const rr=document.getElementById('hostReportReady'); if(rr) rr.textContent=status.report_ready?'REPORT READY':'SCANNING'; document.getElementById('hostScanProgressBar').style.width = `${status.percent || 0}%`; if(typeof window.SIMON_SCAN_SAVER_UPDATE === 'function'){ window.SIMON_SCAN_SAVER_UPDATE(status); } if(status.accuracy && status.accuracy.status){ document.getElementById('hostScanStatusBadge').textContent = String(status.accuracy.status).toUpperCase().replaceAll('_',' '); } if(['completed','partial'].includes(status.status)){ autoRun = false; startButton.disabled = false; resumeButton.disabled = false; if(status.accuracy && !status.accuracy.valid){ alert('Scan finished, but accuracy validation failed. Review the Accuracy and Upload Validation panel before relying on the results.'); } } } async function hostScanRequest(action, values={}){ const body = new FormData(); body.set('csrf', csrfInput.value); Object.entries(values).forEach(([key,value])=>{ body.set(key,String(value)); }); const response = await fetch(`?action=${encodeURIComponent(action)}`,{ method:'POST', body, cache:'no-store', credentials:'same-origin' }); const data = await response.json(); if(!response.ok || data.ok === false){ throw new Error(data.error || `Host scan request failed (${response.status})`); } updateHostScanUi(data); return data; } async function runNextBatch(){ if(!autoRun || requestActive) return; requestActive = true; try{ const status = await hostScanRequest('host_scan_batch',{ batch_size:batchSize.value }); if(status.status === 'running' && autoRun){ setTimeout(runNextBatch,120); }else{ autoRun = false; } }catch(error){ autoRun = false; alert(error.message); }finally{ requestActive = false; } } startButton.addEventListener('click',async()=>{ if(!confirm('Start a complete inventory of every folder and file under this host root?')){ return; } startButton.disabled = true; autoRun = true; try{ await hostScanRequest('host_scan_start',{ host_root:rootInput.value }); runNextBatch(); }catch(error){ autoRun = false; startButton.disabled = false; alert(error.message); } }); resumeButton.addEventListener('click',()=>{ autoRun = true; runNextBatch(); }); stopButton.addEventListener('click',()=>{ autoRun = false; }); })(); </script> </body> </html>
Save file
Quick jump
open a path
Open