SIMON Master Console Rebuild
True command center for SIMON + WEB360
This rebuild gives you a cleaner production spine: interactive tree, live viewer, guarded editor, dynamic graphics, architecture map, and note storage. It is designed to sit beside your existing
console.php
and become the stronger replacement path.
Dashboard
Tree
Viewer
System Map
File Registry
Notes
Files
15,312
Folders
410
Scanned Size
3.85 GB
PHP Files
891
Editable Text Files
12,573
File viewer
guarded to /htdocs
/simon_security_scanner_protected.php
<?php declare(strict_types=1); /** * SIMON Security Scanner * Production-oriented remote website audit + restricted local source scan. * * Deploy behind HTTPS and server authentication. * Recommended location: /tools/simon_security_scanner.php */ session_name('SIMON_SITEMAP_ADMIN'); if (session_status() !== PHP_SESSION_ACTIVE) { session_start([ 'cookie_httponly' => true, 'cookie_secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'), 'cookie_samesite' => 'Strict', 'use_strict_mode' => true, ]); } if (empty($_SESSION['simon_admin']) || $_SESSION['simon_admin'] !== true) { $_SESSION['simon_return'] = '/sitemap.php?mode=security_layer'; header('Location: /sitemap.php?mode=login', true, 302); exit; } header('Content-Type: text/html; charset=UTF-8'); header('X-Content-Type-Options: nosniff'); header('X-Frame-Options: DENY'); header('Referrer-Policy: no-referrer'); header('Permissions-Policy: camera=(), microphone=(), geolocation=()'); header("Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com; img-src 'self' data: https:; connect-src 'self' https://www.google-analytics.com https://region1.google-analytics.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"); const APP_NAME = 'SIMON Security Scanner'; const APP_VERSION = '2.0.0'; const ADMIN_EMAIL = 'admin@gaylordsinclair.com'; const DEFAULT_URL = 'https://www.gaylordsinclair.com/'; const EXPECTED_GA4 = 'G-S6MEHB4BEY'; const EXPECTED_IMPACT = 'e8ec6683-663c-421a-a30d-f8c0fe517345'; const EXPECTED_WEBHOOK = '/connlink/test1/ui/webhook_inbox.php'; const EXPECTED_VISIT_STREAM = 'visits'; const EXPECTED_PULSE_STREAM = 'default'; const ALLOWED_HOSTS = ['gaylordsinclair.com', 'www.gaylordsinclair.com']; const MAX_REMOTE_PAGES_HARD = 500; const MAX_LOCAL_FILES_HARD = 15000; const MAX_LOCAL_FILE_BYTES = 1500000; function h(string $value): string { return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } function clamp_int(mixed $value, int $min, int $max, int $fallback): int { $n = filter_var($value, FILTER_VALIDATE_INT); return $n === false ? $fallback : max($min, min($max, $n)); } function bytes_fmt(int $bytes): string { $units = ['B','KB','MB','GB']; $i = 0; $v = (float)$bytes; while ($v >= 1024 && $i < count($units)-1) { $v /= 1024; $i++; } return $i === 0 ? $bytes.' '.$units[$i] : rtrim(rtrim(number_format($v, 2), '0'), '.').' '.$units[$i]; } function csrf_token(): string { if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(32)); return (string)$_SESSION['csrf']; } function verify_csrf(): void { $token = (string)($_POST['csrf'] ?? ''); if ($token === '' || !hash_equals((string)($_SESSION['csrf'] ?? ''), $token)) { http_response_code(403); exit('Invalid request token.'); } } function normalize_url(string $url): string { $url = trim($url); if ($url === '') return ''; if (!preg_match('~^https?://~i', $url)) $url = 'https://'.$url; return $url; } function url_host(string $url): string { return strtolower((string)(parse_url($url, PHP_URL_HOST) ?? '')); } function base_origin(string $url): string { $p = parse_url($url); if (!is_array($p) || empty($p['host'])) return ''; $scheme = strtolower((string)($p['scheme'] ?? 'https')); $port = isset($p['port']) ? ':'.(int)$p['port'] : ''; return $scheme.'://'.strtolower((string)$p['host']).$port; } function is_allowed_host(string $host): bool { return in_array(strtolower($host), ALLOWED_HOSTS, true); } function is_public_ip(string $ip): bool { return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false; } function validate_remote_target(string $url): array { $p = parse_url($url); if (!is_array($p) || !in_array(strtolower((string)($p['scheme'] ?? '')), ['http','https'], true)) return [false, 'Only HTTP and HTTPS URLs are allowed.']; $host = strtolower((string)($p['host'] ?? '')); if (!is_allowed_host($host)) return [false, 'Host is not in the scanner allowlist.']; $ips = @gethostbynamel($host) ?: []; if (!$ips) return [false, 'Host DNS could not be resolved.']; foreach ($ips as $ip) if (!is_public_ip($ip)) return [false, 'Target resolves to a private or reserved network address.']; return [true, '']; } function resolve_url(string $base, string $ref): ?string { $ref = trim(html_entity_decode($ref, ENT_QUOTES | ENT_HTML5, 'UTF-8')); if ($ref === '' || str_starts_with($ref, '#') || preg_match('~^(mailto:|tel:|javascript:|data:)~i', $ref)) return null; $ref = preg_replace('~#.*$~', '', $ref) ?? $ref; if (preg_match('~^https?://~i', $ref)) return $ref; $bp = parse_url($base); if (!is_array($bp) || empty($bp['scheme']) || empty($bp['host'])) return null; if (str_starts_with($ref, '//')) return $bp['scheme'].':'.$ref; $path = str_starts_with($ref, '/') ? $ref : preg_replace('~/[^/]*$~', '/', (string)($bp['path'] ?? '/')).$ref; $segments = []; foreach (explode('/', (string)$path) as $segment) { if ($segment === '' || $segment === '.') continue; if ($segment === '..') { array_pop($segments); continue; } $segments[] = $segment; } $port = isset($bp['port']) ? ':'.(int)$bp['port'] : ''; return $bp['scheme'].'://'.$bp['host'].$port.'/'.implode('/', $segments); } function fetch_url(string $url, bool $headOnly, int $timeout): array { if (!function_exists('curl_init')) return ['ok'=>false,'error'=>'PHP cURL extension is unavailable.']; [$allowed, $reason] = validate_remote_target($url); if (!$allowed) return ['ok'=>false,'error'=>$reason]; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER=>true, CURLOPT_FOLLOWLOCATION=>false, CURLOPT_TIMEOUT=>$timeout, CURLOPT_CONNECTTIMEOUT=>min(6,$timeout), CURLOPT_HEADER=>true, CURLOPT_NOBODY=>$headOnly, CURLOPT_ENCODING=>'', CURLOPT_USERAGENT=>'SIMON-Security-Scanner/'.APP_VERSION, CURLOPT_SSL_VERIFYPEER=>true, CURLOPT_SSL_VERIFYHOST=>2, CURLOPT_PROTOCOLS=>CURLPROTO_HTTP|CURLPROTO_HTTPS, CURLOPT_REDIR_PROTOCOLS=>CURLPROTO_HTTP|CURLPROTO_HTTPS, ]); $start = microtime(true); $raw = curl_exec($ch); $elapsed = (microtime(true)-$start)*1000; if ($raw === false) { $e = curl_error($ch); $n = curl_errno($ch); curl_close($ch); return ['ok'=>false,'error'=>"cURL {$n}: {$e}",'ms'=>$elapsed]; } $info = curl_getinfo($ch); curl_close($ch); $headerSize = (int)($info['header_size'] ?? 0); return [ 'ok'=>true, 'status'=>(int)($info['http_code'] ?? 0), 'final_url'=>$url, 'redirects'=>0, 'content_type'=>(string)($info['content_type'] ?? ''), 'size'=>(int)($info['size_download'] ?? 0), 'ms'=>$elapsed, 'headers_raw'=>substr((string)$raw,0,$headerSize), 'body'=>$headOnly?'':substr((string)$raw,$headerSize), ]; } function headers_map(string $raw): array { $map=[]; foreach (preg_split('~\R~', trim($raw)) ?: [] as $line) { if (str_contains($line, ':')) { [$k,$v]=explode(':',$line,2); $map[strtolower(trim($k))]=trim($v); } } return $map; } function extract_title(string $html): string { return preg_match('~<title[^>]*>(.*?)</title>~is',$html,$m) ? trim(strip_tags((string)$m[1])) : ''; } function extract_meta_description(string $html): string { if (preg_match('~<meta\s+[^>]*name=["\']description["\'][^>]*content=["\']([^"\']*)["\']~i',$html,$m)) return trim($m[1]); if (preg_match('~<meta\s+[^>]*content=["\']([^"\']*)["\'][^>]*name=["\']description["\']~i',$html,$m)) return trim($m[1]); return ''; } function guess_type(string $url, string $ct): string { $u=strtolower($url); $ct=strtolower($ct); if (str_contains($ct,'text/html') || preg_match('~\.(php|html?)(\?|$)~',$u)) return 'page'; if (str_contains($ct,'text/css') || preg_match('~\.css(\?|$)~',$u)) return 'css'; if (str_contains($ct,'javascript') || preg_match('~\.m?js(\?|$)~',$u)) return 'js'; if (preg_match('~\.(png|jpe?g|gif|webp|svg|ico|avif)(\?|$)~',$u)) return 'image'; if (preg_match('~\.(pdf|zip|rar|7z|csv|json|xml|mp3|mp4|mov|webm|wav)(\?|$)~',$u)) return 'download'; return 'other'; } function parse_html(string $base, string $html): array { $out=['links'=>[],'assets'=>[],'forms'=>[]]; $patterns=[ ['links','~<a[^>]+href=["\']([^"\']+)["\']~i'], ['assets','~<script[^>]+src=["\']([^"\']+)["\']~i'], ['assets','~<link[^>]+href=["\']([^"\']+)["\']~i'], ['assets','~<img[^>]+(?:src|data-src)=["\']([^"\']+)["\']~i'], ['assets','~<(?:video|audio|source)[^>]+src=["\']([^"\']+)["\']~i'], ['forms','~<form[^>]+action=["\']([^"\']+)["\']~i'], ]; foreach ($patterns as [$bucket,$regex]) if (preg_match_all($regex,$html,$m)) foreach ($m[1] as $ref) { $u=resolve_url($base,(string)$ref); if ($u) $out[$bucket][]=$u; } foreach ($out as $k=>$v) $out[$k]=array_values(array_unique($v)); return $out; } function detect_trackers(string $html): array { $rules=[ 'Google Analytics 4'=>'~googletagmanager\.com/gtag/js|\bgtag\s*\(~i', 'Google Tag Manager'=>'~googletagmanager\.com/gtm\.js|GTM-[A-Z0-9]+~i', 'Impact'=>'~impact\.com|impactradius|irclickid|e8ec6683-663c-421a-a30d-f8c0fe517345~i', 'Facebook Pixel'=>'~connect\.facebook\.net/.*/fbevents\.js|\bfbq\s*\(~i', 'Microsoft Clarity'=>'~clarity\.ms|\bclarity\s*\(~i', 'Hotjar'=>'~static\.hotjar\.com|\bhj\s*\(~i', 'TikTok Pixel'=>'~analytics\.tiktok\.com|\bttq\s*\(~i', 'LinkedIn Insight'=>'~snap\.licdn\.com/li\.lms-analytics~i', 'Pinterest Tag'=>'~s\.pinimg\.com/ct/core\.js~i', 'Cloudflare Beacon'=>'~static\.cloudflareinsights\.com/beacon\.min\.js~i', 'SIMON Webhook'=>'~webhook_inbox\.php|SIMON_CONFIG|PULSE_STREAM|VISIT_STREAM~i', ]; $found=[]; foreach ($rules as $name=>$regex) if (preg_match($regex,$html)) $found[]=$name; preg_match_all('~G-[A-Z0-9]+~i',$html,$ga); preg_match_all('~GTM-[A-Z0-9]+~i',$html,$gtm); $checks=[ ['name'=>'GA4','expected'=>EXPECTED_GA4,'present'=>str_contains($html,EXPECTED_GA4)], ['name'=>'Impact verification','expected'=>EXPECTED_IMPACT,'present'=>str_contains($html,EXPECTED_IMPACT)], ['name'=>'SIMON webhook','expected'=>EXPECTED_WEBHOOK,'present'=>str_contains($html,EXPECTED_WEBHOOK)], ['name'=>'Visit stream','expected'=>EXPECTED_VISIT_STREAM,'present'=>preg_match('~VISIT_STREAM[^\n]{0,100}["\']'.preg_quote(EXPECTED_VISIT_STREAM,'~').'["\']~i',$html)===1], ['name'=>'Pulse stream','expected'=>EXPECTED_PULSE_STREAM,'present'=>preg_match('~PULSE_STREAM[^\n]{0,100}["\']'.preg_quote(EXPECTED_PULSE_STREAM,'~').'["\']~i',$html)===1], ]; return ['found'=>$found,'ga4_ids'=>array_values(array_unique($ga[0]??[])),'gtm_ids'=>array_values(array_unique($gtm[0]??[])),'expected'=>$checks]; } function remote_threats(string $html, array $headers, string $url): array { $findings=[]; $tests=[ ['critical','Dynamic PHP/JS execution','~\beval\s*\(|new\s+Function\s*\(|assert\s*\(~i'], ['high','Encoded or packed payload','~base64_decode\s*\(|\batob\s*\(|gzinflate\s*\(|fromCharCode\s*\(~i'], ['high','Possible hidden iframe','~<iframe[^>]+(display\s*:\s*none|opacity\s*:\s*0|width=["\']?0)|createElement\s*\(\s*["\']iframe~i'], ['high','Possible crypto-miner reference','~coinhive|cryptonight|webassembly.{0,80}miner|wasm.{0,80}miner~i'], ['medium','Inline event-handler usage','~\son(?:click|load|error|mouseover|focus)\s*=~i'], ['medium','Insecure HTTP resource on HTTPS page','~(?:src|href)=["\']http://~i'], ['medium','Target blank without noopener','~target=["\']_blank["\'](?![^>]*rel=["\'][^"\']*noopener)~i'], ['medium','Password form may post insecurely','~<form[^>]+action=["\']http://[^"\']+["\'][^>]*>.*?<input[^>]+type=["\']password~is'], ]; foreach ($tests as [$severity,$title,$regex]) if (preg_match($regex,$html)) $findings[]=['severity'=>$severity,'title'=>$title,'location'=>$url]; $required=['content-security-policy','strict-transport-security','x-content-type-options','referrer-policy','permissions-policy']; foreach ($required as $header) if (empty($headers[$header])) $findings[]=['severity'=>'medium','title'=>'Missing security header: '.$header,'location'=>$url]; if (isset($headers['server'])) $findings[]=['severity'=>'info','title'=>'Server header disclosed: '.$headers['server'],'location'=>$url]; return $findings; } function local_scan(string $requestedRoot, int $maxFiles): array { $docRoot=realpath((string)($_SERVER['DOCUMENT_ROOT']??getcwd())); $root=realpath($requestedRoot); if (!$docRoot || !$root || ($root!==$docRoot && !str_starts_with($root,$docRoot.DIRECTORY_SEPARATOR))) return ['ok'=>false,'error'=>'Local root must be inside DOCUMENT_ROOT.']; $findings=[]; $files=[]; $locked=[]; $byExt=[]; $count=0; $rules=[ ['critical','Encoded execution','~eval\s*\(\s*base64_decode\s*\(|assert\s*\(\s*base64_decode~i'], ['critical','Known webshell signature','~\b(c99|r57|b374k|filesman|wso)\b~i'], ['high','Command execution function','~\b(shell_exec|system|passthru|proc_open|popen)\s*\(~i'], ['high','Packed payload','~\b(gzinflate|gzuncompress|str_rot13)\s*\(~i'], ['high','Remote code/file inclusion pattern','~(?:include|require)(?:_once)?\s*\(\s*\$_(?:GET|POST|REQUEST|COOKIE)~i'], ['high','Unrestricted upload move','~move_uploaded_file\s*\([^;]+\)(?![\s\S]{0,300}(mime|extension|pathinfo|finfo))~i'], ['medium','Direct SQL query with request data','~(?:query|exec)\s*\([^;]*\$_(?:GET|POST|REQUEST|COOKIE)~i'], ['medium','Direct HTML output of request data','~(?:echo|print)\s+\$_(?:GET|POST|REQUEST|COOKIE)~i'], ['medium','Weak hash algorithm','~\b(md5|sha1)\s*\(~i'], ['medium','Hard-coded credential marker','~(?:password|passwd|api[_-]?key|secret)\s*[=:]\s*["\'][^"\']{8,}["\']~i'], ]; try { $dir=new RecursiveDirectoryIterator($root,FilesystemIterator::SKIP_DOTS|FilesystemIterator::CURRENT_AS_FILEINFO); $it=new RecursiveIteratorIterator($dir,RecursiveIteratorIterator::SELF_FIRST,RecursiveIteratorIterator::CATCH_GET_CHILD); foreach ($it as $node) { if ($count >= $maxFiles) break; $path=$node->getPathname(); if ($node->isLink()) { $findings[]=['severity'=>'info','title'=>'Symbolic link skipped','location'=>$path]; continue; } if ($node->isDir()) { if (!is_readable($path)) $locked[]=$path; continue; } if (!$node->isFile()) continue; $count++; $size=(int)$node->getSize(); $ext=strtolower(pathinfo($path,PATHINFO_EXTENSION)); $rel=ltrim(substr($path,strlen($root)),DIRECTORY_SEPARATOR); $byExt[$ext?:'(none)']=($byExt[$ext?:'(none)']??0)+1; $row=['path'=>$rel,'size'=>$size,'mtime'=>$node->getMTime(),'ext'=>$ext]; if ($size <= MAX_LOCAL_FILE_BYTES && in_array($ext,['php','phtml','inc','js','html','htm','env','ini'],true)) { $content=@file_get_contents($path); if (is_string($content)) { foreach ($rules as [$sev,$title,$regex]) if (preg_match($regex,$content,$m,PREG_OFFSET_CAPTURE)) { $line=substr_count(substr($content,0,(int)$m[0][1]),"\n")+1; $findings[]=['severity'=>$sev,'title'=>$title,'location'=>$rel.':'.$line]; } if (preg_match('~[A-Za-z0-9+/]{1800,}={0,2}~',$content,$m,PREG_OFFSET_CAPTURE)) { $line=substr_count(substr($content,0,(int)$m[0][1]),"\n")+1; $findings[]=['severity'=>'high','title'=>'Large encoded-looking payload','location'=>$rel.':'.$line]; } } } if (preg_match('~/(uploads?|media|images?)/.*\.(php|phtml|phar)$~i',str_replace('\\','/',$path))) $findings[]=['severity'=>'critical','title'=>'Executable PHP file inside upload/media directory','location'=>$rel]; $files[]=$row; } } catch (Throwable $e) { return ['ok'=>false,'error'=>$e->getMessage()]; } arsort($byExt); return ['ok'=>true,'root'=>$root,'files_scanned'=>$count,'max_reached'=>$count>=$maxFiles,'by_ext'=>$byExt,'locked_dirs'=>array_values(array_unique($locked)),'files'=>$files,'findings'=>$findings]; } function severity_rank(string $s): int { return ['critical'=>5,'high'=>4,'medium'=>3,'low'=>2,'info'=>1][$s]??0; } function report_score(array $findings): int { $score=100; foreach ($findings as $f) $score-=['critical'=>25,'high'=>12,'medium'=>5,'low'=>2,'info'=>0][$f['severity']??'info']??0; return max(0,$score); } function send_admin_report(array $report): array { $summary=$report['summary']; $subject='[SIMON Security] '.$summary['risk'].' — '.($report['host']??'site').' — '.gmdate('Y-m-d H:i').' UTC'; $lines=[APP_NAME.' '.APP_VERSION,'Generated: '.$report['generated_utc'],'Target: '.$report['start_url'],'Risk: '.$summary['risk'],'Score: '.$summary['score'].'/100','Pages: '.$summary['pages'],'Assets: '.$summary['assets'],'Findings: '.$summary['findings'],'Critical: '.$summary['severity']['critical'],'High: '.$summary['severity']['high'],'Medium: '.$summary['severity']['medium'],'','Top findings:']; foreach (array_slice($report['findings'],0,30) as $f) $lines[]='['.strtoupper($f['severity']).'] '.$f['title'].' — '.$f['location']; $body=implode("\r\n",$lines); $headers=['From: SIMON Security <'.ADMIN_EMAIL.'>','Reply-To: '.ADMIN_EMAIL,'Content-Type: text/plain; charset=UTF-8','X-Mailer: PHP/'.PHP_VERSION]; $sent=@mail(ADMIN_EMAIL,$subject,$body,implode("\r\n",$headers)); return ['attempted'=>true,'sent'=>$sent,'recipient'=>ADMIN_EMAIL,'error'=>$sent?'':'PHP mail() returned false. Configure IONOS SMTP or sendmail if delivery fails.']; } $startUrl=normalize_url((string)($_POST['url']??DEFAULT_URL)); $timeout=clamp_int($_POST['timeout']??12,3,30,12); $maxPages=clamp_int($_POST['max_pages']??100,1,MAX_REMOTE_PAGES_HARD,100); $maxDepth=clamp_int($_POST['max_depth']??3,0,8,3); $includeAssets=(string)($_POST['include_assets']??'1')==='1'; $seedSitemap=(string)($_POST['seed_sitemap']??'1')==='1'; $doLocal=(string)($_POST['do_local']??'0')==='1'; $sendEmail=(string)($_POST['send_email']??'1')==='1'; $localRoot=(string)($_POST['local_root']??($_SERVER['DOCUMENT_ROOT']??getcwd())); $report=null; $fatal=''; if (isset($_GET['export'])) { $saved=$_SESSION['SIMON_SECURITY_REPORT']??null; if (!is_array($saved)) { http_response_code(404); exit('No report is available.'); } header('Content-Type: application/json; charset=UTF-8'); header('Content-Disposition: attachment; filename="simon-security-report-'.gmdate('Ymd-His').'.json"'); echo json_encode($saved,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE); exit; } if (isset($_POST['run'])) { verify_csrf(); $last=(int)($_SESSION['last_scan_at']??0); if (time()-$last<5) { $fatal='Please allow at least five seconds between scans.'; } else { $_SESSION['last_scan_at']=time(); [$valid,$reason]=validate_remote_target($startUrl); if (!$valid) $fatal=$reason; else { $began=microtime(true); $host=url_host($startUrl); $origin=base_origin($startUrl); $queue=[['url'=>$startUrl,'depth'=>0,'from'=>'(start)','kind'=>'page']]; $seen=[]; $pages=[]; $assets=[]; $external=[]; $failures=[]; $findings=[]; $trackerPages=[]; if ($seedSitemap) { foreach ([$origin.'/sitemap.xml',$origin.'/robots.txt'] as $seedUrl) { $seed=fetch_url($seedUrl,false,$timeout); if (($seed['ok']??false) && (int)$seed['status']===200) { if (str_ends_with($seedUrl,'robots.txt')) preg_match_all('~^\s*Sitemap:\s*(\S+)~im',(string)$seed['body'],$m); else preg_match_all('~<loc>\s*([^<]+)\s*</loc>~i',(string)$seed['body'],$m); foreach (($m[1]??[]) as $u) if (url_host(trim($u))===$host) $queue[]=['url'=>trim($u),'depth'=>0,'from'=>'(seed)','kind'=>'page']; } } } while ($queue && count($pages)<$maxPages) { $item=array_shift($queue); $url=(string)$item['url']; $canonical=preg_replace('~#.*$~','',$url)??$url; if (isset($seen[$canonical])) continue; $seen[$canonical]=true; if (url_host($canonical)!==$host) { $external[$canonical]=($external[$canonical]??0)+1; continue; } $res=fetch_url($canonical,false,$timeout); if (!($res['ok']??false)) { $failures[]=['url'=>$canonical,'error'=>$res['error']??'Unknown fetch error']; continue; } $headers=headers_map((string)$res['headers_raw']); $type=guess_type($canonical,(string)$res['content_type']); $entry=['url'=>$canonical,'from'=>$item['from'],'depth'=>$item['depth'],'kind'=>$item['kind'],'status'=>$res['status'],'type'=>$type,'content_type'=>$res['content_type'],'size_bytes'=>$res['size'],'ms'=>$res['ms'],'title'=>'','description'=>'','trackers'=>[]]; if ($type==='page' && (int)$res['status']>=200 && (int)$res['status']<400) { $html=(string)$res['body']; $entry['title']=extract_title($html); $entry['description']=extract_meta_description($html); $parsed=parse_html($canonical,$html); $track=detect_trackers($html); $entry['trackers']=$track; $trackerPages[$canonical]=$track; foreach (remote_threats($html,$headers,$canonical) as $f) $findings[]=$f; if ((int)$item['depth']<$maxDepth) foreach ($parsed['links'] as $link) { if (url_host($link)===$host) $queue[]=['url'=>$link,'depth'=>(int)$item['depth']+1,'from'=>$canonical,'kind'=>'link']; else $external[$link]=($external[$link]??0)+1; } if ($includeAssets) foreach ($parsed['assets'] as $asset) { if (url_host($asset)===$host) $assets[$asset]=($assets[$asset]??0)+1; else $external[$asset]=($external[$asset]??0)+1; } foreach ($parsed['forms'] as $form) if (url_host($form)!==$host) $findings[]=['severity'=>'medium','title'=>'Form submits to an external host','location'=>$canonical.' -> '.$form]; } else $assets[$canonical]=($assets[$canonical]??0)+1; if ((int)$res['status']>=400) $findings[]=['severity'=>(int)$res['status']>=500?'high':'medium','title'=>'HTTP error '.$res['status'],'location'=>$canonical]; $pages[]=$entry; } $local=$doLocal?local_scan($localRoot,MAX_LOCAL_FILES_HARD):null; if (is_array($local)&&($local['ok']??false)) foreach ($local['findings'] as $f) $findings[]=$f; usort($findings,fn($a,$b)=>severity_rank($b['severity'])<=>severity_rank($a['severity'])); $sev=['critical'=>0,'high'=>0,'medium'=>0,'low'=>0,'info'=>0]; foreach ($findings as $f) $sev[$f['severity']]++; $score=report_score($findings); $risk=$sev['critical']?'CRITICAL':($sev['high']?'HIGH':($sev['medium']?'MEDIUM':'LOW')); arsort($assets); arsort($external); $report=['generated_utc'=>gmdate('c'),'application'=>APP_NAME,'version'=>APP_VERSION,'start_url'=>$startUrl,'host'=>$host,'summary'=>['score'=>$score,'risk'=>$risk,'pages'=>count($pages),'assets'=>count($assets),'external'=>count($external),'failures'=>count($failures),'findings'=>count($findings),'severity'=>$sev,'elapsed_ms'=>(int)round((microtime(true)-$began)*1000)],'pages'=>$pages,'assets'=>$assets,'external'=>$external,'tracker_pages'=>$trackerPages,'findings'=>$findings,'failures'=>$failures,'local_scan'=>$local,'email'=>['attempted'=>false,'sent'=>false,'recipient'=>ADMIN_EMAIL,'error'=>'Email disabled for this run.']]; $report['email']=['attempted'=>false,'sent'=>false,'recipient'=>ADMIN_EMAIL,'error'=>'Direct email disabled; midnight digest policy active.']; $_SESSION['SIMON_SECURITY_REPORT']=$report; } } } ?> <!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="#050914"> <style> :root{--bg:#050914;--panel:rgba(13,22,42,.86);--line:rgba(139,203,255,.2);--text:#edf7ff;--muted:#9bb0c8;--cyan:#6be5ff;--green:#55e39a;--yellow:#ffd166;--red:#ff6b7a;--radius:18px}*{box-sizing:border-box}html{color-scheme:dark}body{margin:0;background:radial-gradient(circle at 80% -10%,rgba(32,175,255,.18),transparent 35%),radial-gradient(circle at -10% 110%,rgba(96,70,255,.15),transparent 38%),var(--bg);color:var(--text);font:14px/1.45 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;min-height:100vh}.top{position:sticky;top:0;z-index:20;display:flex;align-items:center;gap:12px;padding:12px 16px;background:rgba(5,9,20,.88);backdrop-filter:blur(18px);border-bottom:1px solid var(--line)}.brand{font-weight:900;letter-spacing:.03em}.orb,.iBtn{width:34px;height:34px;border-radius:50%;border:1px solid rgba(107,229,255,.55);background:radial-gradient(circle at 32% 28%,#fff,#6be5ff 18%,#2569ff 55%,#08193a 100%);box-shadow:0 0 24px rgba(70,191,255,.45)}.iBtn{display:grid;place-items:center;color:white;font-weight:900;cursor:pointer}.spacer{flex:1}.wrap{max-width:1280px;margin:auto;padding:16px}.card{background:linear-gradient(180deg,rgba(18,31,57,.9),rgba(9,17,34,.88));border:1px solid var(--line);border-radius:var(--radius);box-shadow:0 22px 70px rgba(0,0,0,.28);margin-bottom:14px;overflow:hidden}.head{display:flex;align-items:center;gap:10px;padding:13px 15px;border-bottom:1px solid var(--line);font-weight:850}.body{padding:15px}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.metrics{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:10px}.metric{padding:12px;border:1px solid var(--line);border-radius:14px;background:rgba(0,0,0,.18)}.metric b{display:block;font-size:20px}.small{font-size:12px;color:var(--muted)}label{font-weight:750;color:var(--muted);display:block;margin-bottom:5px}input[type=text],input[type=number]{width:100%;padding:11px 12px;border:1px solid var(--line);border-radius:13px;background:rgba(0,0,0,.25);color:var(--text)}.row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}.btn{border:1px solid rgba(107,229,255,.45);background:rgba(107,229,255,.08);color:var(--text);border-radius:13px;padding:10px 13px;font-weight:850;text-decoration:none;cursor:pointer}.btn.primary{background:linear-gradient(135deg,#1679ff,#14b8d4)}.pill{display:inline-flex;padding:4px 9px;border:1px solid var(--line);border-radius:999px;font:12px ui-monospace,monospace}.critical,.high{color:var(--red)}.medium{color:var(--yellow)}.low{color:var(--green)}table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:9px;border-bottom:1px solid var(--line);vertical-align:top}th{color:var(--muted);font-size:12px}pre{white-space:pre-wrap;word-break:break-word;background:rgba(0,0,0,.26);border:1px solid var(--line);border-radius:14px;padding:12px;max-height:440px;overflow:auto}.menu{position:fixed;right:16px;top:58px;z-index:40;width:min(340px,calc(100vw - 32px));padding:14px;background:rgba(6,12,25,.97);border:1px solid var(--line);border-radius:17px;box-shadow:0 25px 80px rgba(0,0,0,.55);display:none}.menu.open{display:block}.menu a{display:block;color:var(--text);text-decoration:none;padding:10px;border-radius:10px}.menu a:hover{background:rgba(107,229,255,.08)}footer{text-align:center;color:var(--muted);padding:24px}.notice{padding:11px;border:1px solid var(--line);border-radius:13px;background:rgba(255,209,102,.07)}@media(max-width:900px){.grid{grid-template-columns:1fr}.metrics{grid-template-columns:repeat(2,1fr)}table{display:block;overflow:auto}.top .status{display:none}} </style> </head> <body> <header class="top"><div class="orb" aria-hidden="true"></div><div class="brand">SIMON — Security Scanner</div><div class="spacer"></div><div class="status small">v<?=h(APP_VERSION)?> · <?=date('Y')?></div><button class="iBtn" id="iBtn" type="button" aria-label="Open information menu" aria-expanded="false">I</button></header> <nav class="menu" id="iMenu" aria-hidden="true"><strong>SIMON Information</strong><div class="small">Security scanner controls and Gaylord Sinclair links.</div><hr style="border-color:var(--line)"><a href="https://www.gaylordsinclair.com/">Gaylord Sinclair Home</a><a href="/sitemap.php?mode=admin">Sitemap Dashboard</a><a href="/sitemap.php?mode=security_layer">Security Layer</a><a href="https://www.gaylordsinclair.com/message.php">Contact Admin</a><a href="?export=1">Export Latest JSON</a><div class="small" style="margin-top:10px">Expected telemetry: <?=h(EXPECTED_GA4)?> · Impact · SIMON webhook.</div></nav> <main class="wrap"> <?php if($fatal!==''):?><div class="card"><div class="body critical"><strong>Scan blocked:</strong> <?=h($fatal)?></div></div><?php endif;?> <section class="card"><div class="head">Run security scan</div><div class="body"><form method="post"><input type="hidden" name="csrf" value="<?=h(csrf_token())?>"><div class="grid"><div><label>Start URL</label><input type="text" name="url" value="<?=h($startUrl)?>"><div class="row" style="margin-top:10px"><div style="flex:1"><label>Timeout</label><input type="number" name="timeout" min="3" max="30" value="<?=$timeout?>"></div><div style="flex:1"><label>Max pages</label><input type="number" name="max_pages" min="1" max="<?=MAX_REMOTE_PAGES_HARD?>" value="<?=$maxPages?>"></div><div style="flex:1"><label>Max depth</label><input type="number" name="max_depth" min="0" max="8" value="<?=$maxDepth?>"></div></div></div><div><label>Local root</label><input type="text" name="local_root" value="<?=h($localRoot)?>"><div class="small">Restricted to DOCUMENT_ROOT and child directories.</div><div style="margin-top:12px"><label><input type="hidden" name="include_assets" value="0"><input type="checkbox" name="include_assets" value="1" <?=$includeAssets?'checked':''?>> Include internal assets</label><label><input type="hidden" name="seed_sitemap" value="0"><input type="checkbox" name="seed_sitemap" value="1" <?=$seedSitemap?'checked':''?>> Seed from sitemap.xml and robots.txt</label><label><input type="hidden" name="do_local" value="0"><input type="checkbox" name="do_local" value="1" <?=$doLocal?'checked':''?>> Scan local source files</label><div class="notice" style="margin-top:10px"><strong>Email policy:</strong> direct scan email is disabled. Security risks are included in the SIMON midnight digest.</div></div></div></div><div class="row" style="margin-top:14px"><button class="btn primary" name="run" value="1">Run full scan</button><?php if($report):?><a class="btn" href="?export=1">Export JSON</a><?php endif;?></div></form></div></section> <?php if($report): $s=$report['summary']; ?> <section class="card"><div class="head">Security result · <span class="<?=strtolower($s['risk'])?>"><?=h($s['risk'])?></span></div><div class="body"><div class="metrics"><div class="metric"><span class="small">Score</span><b><?=$s['score']?>/100</b></div><div class="metric"><span class="small">Pages</span><b><?=$s['pages']?></b></div><div class="metric"><span class="small">Assets</span><b><?=$s['assets']?></b></div><div class="metric"><span class="small">Findings</span><b><?=$s['findings']?></b></div><div class="metric"><span class="small">Critical / High</span><b><?=$s['severity']['critical']?> / <?=$s['severity']['high']?></b></div><div class="metric"><span class="small">Time</span><b><?=number_format($s['elapsed_ms']/1000,1)?>s</b></div></div><div class="notice" style="margin-top:12px">Email to <?=h(ADMIN_EMAIL)?>: <strong><?=$report['email']['sent']?'accepted by PHP mail transport':'not confirmed'?></strong><?php if($report['email']['error']):?> — <?=h($report['email']['error'])?><?php endif;?></div></div></section> <section class="card"><div class="head">Detailed threat findings</div><div class="body"><table><thead><tr><th>Severity</th><th>Finding</th><th>Evidence location</th></tr></thead><tbody><?php if(!$report['findings']):?><tr><td colspan="3">No heuristic threats were detected.</td></tr><?php endif;?><?php foreach($report['findings'] as $f):?><tr><td class="<?=h($f['severity'])?>"><strong><?=h(strtoupper($f['severity']))?></strong></td><td><?=h($f['title'])?></td><td><code><?=h($f['location'])?></code></td></tr><?php endforeach;?></tbody></table></div></section> <section class="card"><div class="head">Tracker and telemetry audit</div><div class="body"><table><thead><tr><th>Page</th><th>Detected</th><th>Required tracker status</th></tr></thead><tbody><?php foreach($report['tracker_pages'] as $page=>$t):?><tr><td><code><?=h($page)?></code></td><td><?=h(implode(', ',$t['found'])?:'None detected')?></td><td><?php foreach($t['expected'] as $check):?><span class="pill <?=$check['present']?'low':'medium'?>"><?=h($check['name'])?>: <?=$check['present']?'present':'missing'?></span> <?php endforeach;?></td></tr><?php endforeach;?></tbody></table></div></section> <section class="card"><div class="head">Crawled pages</div><div class="body"><table><thead><tr><th>HTTP</th><th>URL</th><th>Type</th><th>Size</th><th>Time</th><th>Found from</th></tr></thead><tbody><?php foreach($report['pages'] as $p):?><tr><td><?=$p['status']?></td><td><code><?=h($p['url'])?></code><?php if($p['title']):?><div class="small"><?=h($p['title'])?></div><?php endif;?></td><td><?=h($p['type'])?></td><td><?=h(bytes_fmt((int)$p['size_bytes']))?></td><td><?=number_format((float)$p['ms'])?> ms</td><td><code><?=h((string)$p['from'])?></code></td></tr><?php endforeach;?></tbody></table></div></section> <div class="grid"><section class="card"><div class="head">Internal assets</div><div class="body"><pre><?php foreach(array_slice($report['assets'],0,400,true) as $u=>$n) echo h($n.' '.$u."\n");?></pre></div></section><section class="card"><div class="head">External references</div><div class="body"><pre><?php foreach(array_slice($report['external'],0,400,true) as $u=>$n) echo h($n.' '.$u."\n");?></pre></div></section></div> <?php if(is_array($report['local_scan'])):?><section class="card"><div class="head">Local source scan</div><div class="body"><?php if($report['local_scan']['ok']):?><div class="row"><span class="pill">Root: <?=h($report['local_scan']['root'])?></span><span class="pill">Files: <?=$report['local_scan']['files_scanned']?></span><span class="pill">Locked: <?=count($report['local_scan']['locked_dirs'])?></span><span class="pill">Findings: <?=count($report['local_scan']['findings'])?></span></div><pre><?php foreach($report['local_scan']['by_ext'] as $ext=>$n) echo h($ext.': '.$n."\n");?></pre><?php else:?><div class="critical"><?=h($report['local_scan']['error'])?></div><?php endif;?></div></section><?php endif;?> <?php endif;?> </main> <footer>© <?=date('Y')?> Gaylord Sinclair LLC · Powered by SIMON · Security results are heuristic and require human validation.</footer> <script> (()=>{const b=document.getElementById('iBtn'),m=document.getElementById('iMenu');const set=v=>{m.classList.toggle('open',v);m.setAttribute('aria-hidden',String(!v));b.setAttribute('aria-expanded',String(v));};b.addEventListener('click',e=>{e.stopPropagation();set(!m.classList.contains('open'));});document.addEventListener('click',e=>{if(!m.contains(e.target)&&e.target!==b)set(false)});document.addEventListener('keydown',e=>{if(e.key==='Escape')set(false)});})(); </script> </body></html>
Save file
Quick jump
open a path
Open