setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (Exception $e) { echo json_encode(['error' => 'DB: ' . $e->getMessage()]); exit; } // ═══════════════════════════════════════════════════════════════ // ARCHITECTURE — Complete system map // ═══════════════════════════════════════════════════════════════ $ARCH = [ 'servers' => [ 'hetzner' => ['ip'=>'89.167.40.150','name'=>'WEVAL Marketing','user'=>'root','ssh_port'=>49222,'os'=>'Ubuntu 24','ram'=>'16GB','disk'=>'150GB'], 'ovh' => ['ip'=>'151.80.235.110','name'=>'WEVAL Tracking','user'=>'ubuntu','ssh_port'=>22,'os'=>'Ubuntu','note'=>'NOT root - user is ubuntu'], 'consulting' => ['ip'=>'46.62.220.135','name'=>'WEVAL Consulting','user'=>'root'] ], 'paths' => [ 'adx' => '/opt/wevads/public', 'arsenal' => '/opt/wevads-arsenal/public', 'arsenal_api' => '/opt/wevads-arsenal/public/api', 'adx_views' => '/opt/wevads/app/views', 'backups' => '/opt/backups' ], 'ports' => [ 'adx'=>['port'=>5821,'name'=>'WEVADS ADX','root'=>'/opt/wevads/public','status'=>'unknown'], 'arsenal'=>['port'=>5890,'name'=>'Arsenal','root'=>'/opt/wevads-arsenal/public','status'=>'unknown'], 'fmg'=>['port'=>5822,'name'=>'File Manager','root'=>'/opt/fmgapp/public','status'=>'unknown'], 'bcg'=>['port'=>5823,'name'=>'Backup Config','root'=>'/opt/bcgapp/public','status'=>'unknown'], 'dkim'=>['port'=>5824,'name'=>'DKIM Manager','root'=>'/var/www/dkim','status'=>'unknown'], 'n8n'=>['port'=>8080,'name'=>'N8N Workflows','status'=>'unknown'], 'postgres'=>['port'=>5432,'name'=>'PostgreSQL','note'=>'localhost only','status'=>'unknown'], 'ollama'=>['port'=>11434,'name'=>'Ollama LLM','note'=>'localhost only','status'=>'unknown'] ], 'services' => ['apache2','postgresql','pmta','ollama'], 'databases' => ['adx_system'=>'Main (brain_*, hamid_*, office_*, sending_*)','adx_clients'=>'Client data'], 'exclude_files' => ['claude-exec.php','claude-exec2.php','index.php','sentinel-engine.php','sentinel-brain.php'] ]; // LLM Providers (ordered by speed) $LLM_PROVIDERS = [ ['name'=>'Cerebras','url'=>'https://api.cerebras.ai/v1/chat/completions','model'=>'llama-3.3-70b'], ['name'=>'Groq','url'=>'https://api.groq.com/openai/v1/chat/completions','model'=>'llama-3.3-70b-versatile'], ['name'=>'DeepSeek','url'=>'https://api.deepseek.com/v1/chat/completions','model'=>'deepseek-chat'], ['name'=>'Hyperbolic','url'=>'https://api.hyperbolic.xyz/v1/chat/completions','model'=>'meta-llama/Llama-3.3-70B-Instruct'] ]; // ═══════════════════════════════════════════════════════════════ // SSH EXECUTION // ═══════════════════════════════════════════════════════════════ // Remote SSH execution (OVH, Consulting) function remoteSsh($server, $cmd, $timeout = 20) { $servers = [ 'ovh' => ['ip'=>'151.80.235.110','user'=>'ubuntu','pass'=>'MX8D3zSAty7k3243242'], 'consulting' => ['ip'=>'46.62.220.135','user'=>'root','pass'=>'vr3xjMvwMtWW'] ]; $s = $servers[$server] ?? null; if (!$s) return ['ok'=>false,'output'=>'Unknown server: '.$server,'error'=>'']; $sshCmd = sprintf('sshpass -p %s ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 %s@%s %s', escapeshellarg($s['pass']), $s['user'], $s['ip'], escapeshellarg($cmd)); $desc = [0=>['pipe','r'], 1=>['pipe','w'], 2=>['pipe','w']]; $proc = proc_open($sshCmd, $desc, $pipes); if (!is_resource($proc)) return ['ok'=>false,'output'=>'SSH failed','error'=>'']; stream_set_timeout($pipes[1], $timeout); $out = stream_get_contents($pipes[1]); $err = stream_get_contents($pipes[2]); fclose($pipes[0]); fclose($pipes[1]); fclose($pipes[2]); return ['ok'=>proc_close($proc)===0, 'output'=>$out, 'error'=>$err]; } function sshExec($cmd, $timeout = 30) { $desc = [0=>['pipe','r'], 1=>['pipe','w'], 2=>['pipe','w']]; $proc = proc_open("sudo bash -c " . escapeshellarg($cmd), $desc, $pipes); if (!is_resource($proc)) return ['ok'=>false,'output'=>'proc_open failed','error'=>'']; stream_set_timeout($pipes[1], $timeout); $out = stream_get_contents($pipes[1]); $err = stream_get_contents($pipes[2]); fclose($pipes[0]); fclose($pipes[1]); fclose($pipes[2]); return ['ok'=>proc_close($proc)===0, 'output'=>$out, 'error'=>$err]; } // ═══════════════════════════════════════════════════════════════ // REAL LLM CHAT — Connected to HAMID providers // ═══════════════════════════════════════════════════════════════ function chatLLM($pdo, $userMessage) { global $ARCH, $LLM_PROVIDERS; // Build system context with FULL architecture knowledge $kb = $pdo->query("SELECT topic, content FROM admin.sentinel_knowledge")->fetchAll(PDO::FETCH_KEY_PAIR); $lastScan = $pdo->query("SELECT score, issues_found, issues_fixed, scan_date FROM admin.sentinel_scans ORDER BY scan_date DESC LIMIT 1")->fetch(PDO::FETCH_ASSOC); $patterns = $pdo->query("SELECT pattern_name, times_detected, severity FROM admin.sentinel_patterns WHERE times_detected > 0 ORDER BY times_detected DESC LIMIT 10")->fetchAll(PDO::FETCH_ASSOC); // Gather live system state $sysState = []; foreach ($ARCH['services'] as $svc) { $r = sshExec("systemctl is-active $svc 2>/dev/null"); $sysState[$svc] = trim($r['output']); } $diskR = sshExec("df -h / | tail -1 | awk '{print $5}'"); $memR = sshExec("free -m | grep Mem | awk '{printf \"%.0f/%.0fMB (%.0f%%)\", $3, $2, $3/$2*100}'"); $loadR = sshExec("cat /proc/loadavg | cut -d' ' -f1-3"); $systemPrompt = << - Fichiers critiques: sentinel-brain.php, brain-unified-send.php, auto-supply.php, ghost-network.php - FIX: Vault Guard (/api/vault-guard.php) auto-restore toutes les 6h - JAMAIS deployer via un seul cat heredoc pour fichiers >200 lignes ### Placeholders WEVADS - Format CORRECT: [domain], [email], [fname], [lname], [url], [unsub], [open], [subject], [company], [email_b64] - Format INTERDIT: {offer_link}, {unsubscribe}, {tracking}, {email_b64} - brain-inject.js Message-Id: <[a_7][n_5][n_3][a_3][email_b64]@[domain]> - 9 brain_configs body_template corrigees ### Tracking Pipeline - OVH track.php format: track.php?t=TRACKING_ID&e=open|click&u=BASE64_URL - ANCIEN format INTERDIT: /op/CID/EID/LID/0/0 - DB dual columns: old(email_id,type,ip) + new(tracking_id,event_type,ip_address,click_url,device_type) - PG: listen_addresses=*, pg_hba permet OVH 151.80.235.110, password=wevads2026 ### brain-unified-send.php - DOIT contenir: logSend(), detectISP(), injectTracking() format track.php - seedTest() DOIT etre complet (etait tronque ligne 192) ### Menu Protection - JAMAIS sed sur menu.html (sidebar) - JAMAIS rm -rf sur /opt/wevads - TOUJOURS backup avant modification ### Commandes SSH Speciales - OVH: user=ubuntu (PAS root), besoin sudo - Hetzner: user=root, port=49222 - Consulting: user=root ### Ports Critiques 5821=ADX, 5890=Arsenal, 5822=FMG, 5823=BCG, 5824=DKIM, 8080=N8N, 5432=PG, 11434=Ollama ### Scan V4 Capabilities Tu integres maintenant un scan de regression qui verifie: 1. Vault integrity (329+ fichiers, truncation <90%) 2. Placeholder format ([bracket] vs {curly}) 3. brain-inject.js headers format 4. brain-unified-send logSend/detectISP/injectTracking 5. PostgreSQL listen_addresses + pg_hba 6. OVH tracking health (HTTPS pixel test) 7. DB columns integrity (tracking_id, event_type, etc) Quand on te demande un scan, tu executes TOUTES ces verifications. Quand on te demande un fix, tu REPARES automatiquement depuis le vault. SYSPROMPT; foreach ($patterns as $p) { $systemPrompt .= "\n- {$p['pattern_name']} (sévérité: {$p['severity']}, détections: {$p['times_detected']})"; } $systemPrompt .= "\n\n## BASE DE CONNAISSANCES\n"; foreach ($kb as $topic => $content) { $systemPrompt .= "- $topic: $content\n"; } $systemPrompt .= <<query("SELECT provider_name, api_key, model, api_url FROM admin.hamid_providers WHERE is_active=true AND api_key IS NOT NULL AND LENGTH(api_key) > 10 ORDER BY priority")->fetchAll(PDO::FETCH_ASSOC); foreach ($providers as $prov) { $result = callLLM($prov, $systemPrompt, $userMessage); if ($result) { // Check if LLM wants to execute commands $response = $result; $executed = []; // Auto-execute ALL commands the LLM suggests // 1. Bash blocks -> local Hetzner if (preg_match_all('/```bash\n(.*?)\n```/s', $result, $cmds)) { foreach ($cmds[1] as $cmd) { $cmd = trim($cmd); if (strlen($cmd) > 3 && strlen($cmd) < 500) { $r = sshExec($cmd, 15); $executed[] = ['cmd'=>'[HETZNER] '.$cmd, 'output'=>substr($r['output'],0,800), 'ok'=>$r['ok']]; } } } // 2. remoteSsh() calls -> OVH/Consulting if (preg_match_all("/remoteSsh\\(['\"](\w+)['\"]\s*,\s*['\"](.*?)['\"]\\)/", $result, $rms)) { for ($i=0; $i'['.strtoupper($srv).'] '.$rcmd, 'output'=>substr($r['output'],0,800), 'ok'=>$r['ok']]; } } // 3. Detect inline: Commande: `xxx` if (preg_match_all('/[Cc]ommande\s*:\s*`([^`]+)`/', $result, $inl)) { foreach ($inl[1] as $icmd) { $icmd = trim($icmd); if (strpos($icmd, 'remoteSsh') !== false) continue; if (strlen($icmd) > 3 && strlen($icmd) < 500) { if (preg_match('/151\.80|ovh|tracking|ubuntu@/', $icmd)) { $cleanCmd = preg_replace('/^ssh\s+\S+\s+/', '', $icmd); $cleanCmd = preg_replace('/^sshpass.*?ubuntu@\S+\s+/', '', $cleanCmd); $r = remoteSsh("ovh", trim($cleanCmd), 15); $executed[] = ['cmd'=>'[OVH] '.$cleanCmd, 'output'=>substr($r['output'],0,800), 'ok'=>$r['ok']]; } else { $r = sshExec($icmd, 15); $executed[] = ['cmd'=>'[HETZNER] '.$icmd, 'output'=>substr($r['output'],0,800), 'ok'=>$r['ok']]; } } } } // ANTI-HALLUCINATION: Completely replace LLM response with REAL results if (!empty($executed)) { // Extract ONLY the first diagnostic line from LLM $firstLine = ""; foreach (explode("\n", $response) as $line) { $l = trim($line); if (mb_strlen($l) > 5 && preg_match('/Diagnostic|Vérif|Check|Analyse/i', $l)) { $firstLine = $l; break; } } // Build response ENTIRELY from real executed commands $response = ($firstLine ? $firstLine . "\n\n" : ""); foreach ($executed as $ex) { $icon = $ex['ok'] ? '✅' : '❌'; $server = ''; if (strpos($ex['cmd'], '[OVH]') === 0) $server = '📡 OVH'; elseif (strpos($ex['cmd'], '[HETZNER]') === 0) $server = '🖥️ Hetzner'; elseif (strpos($ex['cmd'], '[CONSULTING]') === 0) $server = '🏢 Consulting'; else $server = '💻'; $cmd = preg_replace('/^\[\w+\]\s*/', '', $ex['cmd']); $response .= $server . " `" . $cmd . "`\n"; $response .= $icon . " " . trim($ex['output']) . "\n\n"; } $response = trim($response); } // Log $pdo->prepare("INSERT INTO admin.sentinel_knowledge (category, topic, content, source) VALUES ('chat_log', ?, ?, ?)") ->execute([substr($userMessage, 0, 100), substr($response, 0, 1000), $prov['provider_name']]); return [ 'response' => $response, 'provider' => $prov['provider_name'], 'executed' => $executed, 'timestamp' => date('Y-m-d H:i:s') ]; } } // Fallback: rule-based if ALL providers fail return handleChatFallback($pdo, $userMessage); } function callLLM($provider, $system, $user) { $name = $provider['provider_name']; $key = $provider['api_key']; $url = $provider['api_url']; $model = $provider['model']; // Gemini has different API format if (stripos($name, 'gemini') !== false) { $url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$key}"; $body = json_encode([ 'contents' => [['parts' => [['text' => $system . "\n\nUser: " . $user]]]], 'generationConfig' => ['maxOutputTokens' => 1500] ]); $headers = ['Content-Type: application/json']; } // Claude has different format elseif (stripos($name, 'claude') !== false) { $body = json_encode([ 'model' => $model, 'max_tokens' => 1500, 'system' => $system, 'messages' => [['role'=>'user','content'=>$user]] ]); $headers = [ 'Content-Type: application/json', 'x-api-key: ' . $key, 'anthropic-version: 2023-06-01' ]; } // OpenAI-compatible (Cerebras, Groq, DeepSeek, Hyperbolic, etc) else { $body = json_encode([ 'model' => $model, 'messages' => [ ['role'=>'system','content'=>$system], ['role'=>'user','content'=>$user] ], 'max_tokens' => 1500, 'temperature' => 0.3 ]); $headers = [ 'Content-Type: application/json', 'Authorization: Bearer ' . $key ]; } $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => false ]); $resp = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode !== 200 || !$resp) return null; $data = json_decode($resp, true); if (!$data) return null; // Extract text based on provider format if (stripos($name, 'gemini') !== false) { return $data['candidates'][0]['content']['parts'][0]['text'] ?? null; } elseif (stripos($name, 'claude') !== false) { return $data['content'][0]['text'] ?? null; } else { return $data['choices'][0]['message']['content'] ?? null; } } function handleChatFallback($pdo, $msg) { // Minimal fallback if all LLM providers are down $m = strtolower($msg); $r = ''; if (preg_match('/scan|check|verify/', $m)) { $result = runFullScan($pdo, false); $r = "🔍 Scan: Score {$result['score']}%, {$result['issues_found']} issues sur {$result['total_files']} fichiers"; } elseif (preg_match('/fix|repair|corrige/', $m)) { $result = runFullScan($pdo, true); $r = "🔧 Fix: {$result['issues_fixed']}/{$result['issues_found']} réparées. Score: {$result['score']}%"; } elseif (preg_match('/exec\s+(.+)/i', $msg, $cm)) { $out = sshExec(trim($cm[1]), 15); $r = "💻 " . ($out['ok'] ? '✅' : '❌') . "\n" . substr($out['output'], 0, 1500); } else { $r = "⚠️ Providers IA indisponibles. Commandes disponibles: scan, fix, exec [cmd]"; } return ['response'=>$r, 'provider'=>'fallback', 'executed'=>[], 'timestamp'=>date('Y-m-d H:i:s')]; } // ═══════════════════════════════════════════════════════════════ // SCAN ENGINE (same as V2 but cleaner) // ═══════════════════════════════════════════════════════════════ function runFullScan($pdo, $autoFix = true) { // ═══════════════════════════════════════════════════════════════ // SENTINEL V4 — REGRESSION GUARD ENGINE // ═══════════════════════════════════════════════════════════════ function scanRegressions($pdo, $scanId, &$issues, &$fixed, $autoFix = true) { // 1. VAULT INTEGRITY $vaultDir = '/opt/wevads/vault'; foreach (glob("$vaultDir/*") as $vf) { if (is_dir($vf)) continue; $bn = basename($vf); $livePath = str_replace('__', '/', preg_replace('/\.gold$/', '', $bn)); if ($livePath[0] !== '/') $livePath = '/' . $livePath; if (!file_exists($livePath)) continue; $vSize = filesize($vf); $lSize = filesize($livePath); if ($lSize < $vSize * 0.9) { $lossPct = round((1-$lSize/$vSize)*100,1); $issues[] = ['file'=>basename($livePath),'path'=>$livePath,'type'=>'vault-truncation','detail'=>"Truncated {$lossPct}%"]; if ($autoFix) { copy($livePath,$livePath.'.broken_'.time()); copy($vf,$livePath); $fixed++; logFix($pdo,$scanId,$livePath,'vault-truncation',"Restored ({$lossPct}%)"); } } elseif (pathinfo($livePath,PATHINFO_EXTENSION)==='php') { $ck = shell_exec("php -l ".escapeshellarg($livePath)." 2>&1"); if (strpos($ck,'No syntax errors')===false) { $issues[] = ['file'=>basename($livePath),'path'=>$livePath,'type'=>'file-truncated-php','detail'=>substr(trim($ck),0,150)]; if ($autoFix) { copy($livePath,$livePath.'.broken_'.time()); copy($vf,$livePath); $fixed++; logFix($pdo,$scanId,$livePath,'file-truncated-php','Vault restore'); } } } } // 2. PLACEHOLDER REGRESSION $wrongPH = ['{offer_link}','{unsubscribe}','{tracking}','{optout}','{subject}','{company}','{name}','{email}','{domain}']; $phMap = ['{offer_link}'=>'http://[domain]/[url]','{unsubscribe}'=>'http://[domain]/[unsub]','{tracking}'=>'','{optout}'=>'http://[domain]/[unsub]','{subject}'=>'[subject]','{company}'=>'[company]','{name}'=>'[fname] [lname]','{email}'=>'[email]','{domain}'=>'[domain]']; try { $tpls = $pdo->query("SELECT id,body_template FROM admin.brain_configs WHERE body_template IS NOT NULL")->fetchAll(PDO::FETCH_ASSOC); foreach ($tpls as $t) { foreach ($wrongPH as $wp) { if (strpos($t['body_template'],$wp)!==false) { $issues[] = ['file'=>"brain_configs#{$t['id']}",'path'=>'database','type'=>'placeholder-regression','detail'=>"Found $wp"]; if ($autoFix) { $pdo->prepare("UPDATE admin.brain_configs SET body_template=REPLACE(body_template,?,?) WHERE id=?")->execute([$wp,$phMap[$wp],$t['id']]); $fixed++; logFix($pdo,$scanId,"brain_configs#{$t['id']}",'placeholder-regression',"Fixed $wp"); } }}} } catch(Exception $e){} // 3. BRAIN-INJECT.JS HEADERS $bi = '/opt/wevads/public/js/brain-inject.js'; if (file_exists($bi)) { $c=file_get_contents($bi); if (strpos($c,'{email_b64}')!==false) { $issues[]=['file'=>'brain-inject.js','path'=>$bi,'type'=>'brain-inject-headers','detail'=>'Uses {curly} not [bracket]']; if ($autoFix) { file_put_contents($bi,str_replace(['{email_b64}','@{domain}'],['[email_b64]','@[domain]'],$c)); $fixed++; logFix($pdo,$scanId,$bi,'brain-inject-headers','Fixed'); } }} // 4. BRAIN-UNIFIED-SEND CHECKS foreach (['/opt/wevads/public/brain-unified-send.php','/opt/wevads-arsenal/public/api/brain-unified-send.php'] as $bf) { if (!file_exists($bf)) continue; $c=file_get_contents($bf); if (strpos($c,'function logSend')===false) $issues[]=['file'=>basename($bf),'path'=>$bf,'type'=>'logSend-missing','detail'=>'Sends not logged']; if (strpos($c,"'/op/'")!==false) $issues[]=['file'=>basename($bf),'path'=>$bf,'type'=>'tracking-broken','detail'=>'Old /op/ format']; } // 5. PG REMOTE $pg=shell_exec("sudo -u postgres psql -t -c \"SHOW listen_addresses;\" 2>/dev/null"); if ($pg && strpos(trim($pg),'*')===false && strpos(trim($pg),'0.0.0.0')===false) $issues[]=['file'=>'postgresql.conf','path'=>'/etc/postgresql','type'=>'pg-remote-blocked','detail'=>'OVH blocked']; // 6. OVH TRACKING $ch=curl_init('https://culturellemejean.charity/track.php?t=sentinel_'.time().'&e=open'); curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>8,CURLOPT_SSL_VERIFYPEER=>false,CURLOPT_NOBODY=>true]); curl_exec($ch); $hc=curl_getinfo($ch,CURLINFO_HTTP_CODE); curl_close($ch); if ($hc!==200) $issues[]=['file'=>'track.php','path'=>'OVH','type'=>'tracking-broken','detail'=>"HTTP $hc"]; // 7. DB COLUMNS $req=['tracking_events'=>['tracking_id','event_type','click_url','ip_address','device_type'],'unified_send_log'=>['tracking_id','first_open','first_click']]; foreach ($req as $tbl=>$cols) { foreach ($cols as $col) { $ex=$pdo->query("SELECT COUNT(*) FROM information_schema.columns WHERE table_schema='admin' AND table_name='$tbl' AND column_name='$col'")->fetchColumn(); if (!$ex) $issues[]=['file'=>"admin.$tbl",'path'=>'database','type'=>'db-columns-missing','detail'=>"$col missing"]; }} } global $ARCH, $start; $scanId = $pdo->query("INSERT INTO admin.sentinel_scans (scan_type) VALUES ('full') RETURNING id")->fetchColumn(); $issues = []; $fixed = 0; $totalFiles = 0; foreach ($ARCH['paths'] as $name => $path) { if (!is_dir($path)) continue; $phpFiles = glob("$path/*.php") ?: []; $htmlFiles = glob("$path/*.html") ?: []; $allFiles = array_merge($phpFiles, $htmlFiles); $totalFiles += count($allFiles); // PHP syntax foreach ($phpFiles as $f) { if (isExcluded($f)) continue; $out = shell_exec("php -l " . escapeshellarg($f) . " 2>&1"); if (preg_match('/Parse error|Fatal/', $out)) { $issues[] = ['file'=>basename($f),'path'=>$f,'type'=>'php-syntax-error','detail'=>substr(trim($out),0,150)]; logFix($pdo, $scanId, $f, 'php-syntax-error', 'Logged'); } } // Content scans foreach ($allFiles as $f) { if (isExcluded($f)) continue; $content = @file_get_contents($f); if (!$content) continue; $changed = false; // NUKE if (preg_match('/WEVADS_NUKE|WEVADS_KILL|NUKE_JS_V|NUKE_CSS/', $content) && strpos(basename($f),'sentinel')===false) { $issues[] = ['file'=>basename($f),'path'=>$f,'type'=>'WEVADS_NUKE','detail'=>'Injection']; if ($autoFix) { $content = preg_replace('/]*NUKE[^>]*>[^<]*<\/style>/si','',$content); $content = preg_replace('/]*(?:NUKE|KILL)[^>]*>.*?<\/script>/si','',$content); $changed=true; $fixed++; logFix($pdo,$scanId,$f,'WEVADS_NUKE','Removed'); } } // Theme-v2 if (strpos($content,'weval-theme-system-v2')!==false) { $issues[] = ['file'=>basename($f),'path'=>$f,'type'=>'weval-theme-v2','detail'=>'Injection']; if ($autoFix) { $content = preg_replace('/.*?/s','',$content); $changed=true; $fixed++; logFix($pdo,$scanId,$f,'weval-theme-v2','Removed'); } } // Script balance (HTML) if (pathinfo($f,PATHINFO_EXTENSION)==='html') { $o=substr_count($content,''); if ($o>$c) { $d=$o-$c; $issues[]=['file'=>basename($f),'path'=>$f,'type'=>'unclosed-script','detail'=>"diff=$d"]; if ($autoFix) { $content.=str_repeat("\n",$d); $changed=true; $fixed++; logFix($pdo,$scanId,$f,'unclosed-script',"Added $d"); } } } // Junk after $pos=strpos($content,''); if ($pos!==false && strlen(trim(substr($content,$pos+14)))>50) { $issues[]=['file'=>basename($f),'path'=>$f,'type'=>'content-after-html','detail'=>'Junk']; if ($autoFix) { $content=substr($content,0,$pos+14)."\n"; $changed=true; $fixed++; logFix($pdo,$scanId,$f,'content-after-html','Truncated'); } } if ($changed && is_writable($f)) file_put_contents($f, $content); } } // V4 REGRESSION GUARD scanRegressions($pdo, $scanId, $issues, $fixed, $autoFix); // Port health foreach ($ARCH['ports'] as $k => $p) { $ch=curl_init("http://localhost:{$p['port']}/"); curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>3,CURLOPT_NOBODY=>true]); curl_exec($ch); $code=curl_getinfo($ch,CURLINFO_HTTP_CODE); curl_close($ch); if ($code==0) $issues[]=['file'=>"port:{$p['port']}",'path'=>'','type'=>'port-down','detail'=>"{$p['name']} down"]; } // Services foreach ($ARCH['services'] as $svc) { $r=sshExec("systemctl is-active $svc 2>/dev/null"); if (trim($r['output'])!=='active') $issues[]=['file'=>"svc:$svc",'path'=>'','type'=>'service-down','detail'=>"$svc inactive"]; } // Runtime errors $phpPages = array_slice(glob($ARCH['paths']['adx'].'/*.php')?:[],0,60); foreach ($phpPages as $f) { if (isExcluded($f)) continue; $bn=basename($f); $ch=curl_init("http://localhost:5821/$bn"); curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>3]); $out=curl_exec($ch); curl_close($ch); if ($out && preg_match('/(Warning|Fatal error|Parse error)<\/b>/',$out)) { $issues[]=['file'=>$bn,'path'=>$f,'type'=>'runtime-error','detail'=>"Error in $bn"]; if ($autoFix && strpos($out,'Permission denied')!==false && preg_match('/file_put_contents\(([^)]+)\)/',$out,$pm)) { $t=trim($pm[1],"'\" "); sshExec("touch '$t' && chmod 666 '$t'"); $fixed++; logFix($pdo,$scanId,$f,'permission-fix',"chmod $t"); } } } $duration = round((microtime(true)-$start)*1000); $score = $totalFiles>0 ? max(0,min(100,round((1-count($issues)/max($totalFiles,1))*100,1))) : 100; $pdo->prepare("UPDATE admin.sentinel_scans SET total_files=?,issues_found=?,issues_fixed=?,score=?,duration_ms=?,details=? WHERE id=?") ->execute([$totalFiles,count($issues),$fixed,$score,$duration,json_encode(['issues'=>$issues]),$scanId]); foreach ($issues as $i) $pdo->prepare("UPDATE admin.sentinel_patterns SET times_detected=times_detected+1,last_seen=NOW() WHERE pattern_name=?")->execute([$i['type']]); return ['scan_id'=>$scanId,'total_files'=>$totalFiles,'issues_found'=>count($issues),'issues_fixed'=>$fixed,'score'=>$score,'duration_ms'=>$duration,'issues'=>$issues]; } // ═══════════════════════════════════════════════════════════════ // DYNAMIC ARCHITECTURE MAP // ═══════════════════════════════════════════════════════════════ function getArchitectureMap($pdo) { global $ARCH; $map = ['servers'=>[],'ports'=>[],'services'=>[],'databases'=>[],'connections'=>[],'last_updated'=>date('Y-m-d H:i:s')]; // Servers foreach ($ARCH['servers'] as $k=>$s) { $map['servers'][$k] = $s; $map['servers'][$k]['reachable'] = false; } // Check Hetzner ports foreach ($ARCH['ports'] as $k=>$p) { $ch=curl_init("http://localhost:{$p['port']}/"); curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>2,CURLOPT_NOBODY=>true]); curl_exec($ch); $code=curl_getinfo($ch,CURLINFO_HTTP_CODE); curl_close($ch); $map['ports'][$k] = array_merge($p, ['status'=>$code>0?'up':'down','http_code'=>$code]); } $map['servers']['hetzner']['reachable'] = true; // Services foreach ($ARCH['services'] as $svc) { $r = sshExec("systemctl is-active $svc 2>/dev/null"); $map['services'][$svc] = trim($r['output']); } // DB stats $tables = $pdo->query("SELECT count(*) FROM information_schema.tables WHERE table_schema='admin'")->fetchColumn(); $brainW = $pdo->query("SELECT count(*) FROM admin.brain_winners")->fetchColumn(); $hamidC = $pdo->query("SELECT count(*) FROM admin.hamid_conversations")->fetchColumn(); $o365 = $pdo->query("SELECT count(*) FROM admin.office_accounts")->fetchColumn(); $sentScans = $pdo->query("SELECT count(*) FROM admin.sentinel_scans")->fetchColumn(); $sentFixes = $pdo->query("SELECT count(*) FROM admin.sentinel_fixes")->fetchColumn(); $sentPatterns = $pdo->query("SELECT count(*) FROM admin.sentinel_patterns")->fetchColumn(); $sentKB = $pdo->query("SELECT count(*) FROM admin.sentinel_knowledge")->fetchColumn(); $map['databases'] = [ 'tables'=>(int)$tables, 'brain_winners'=>(int)$brainW, 'hamid_conversations'=>(int)$hamidC, 'office_accounts'=>(int)$o365, 'sentinel_scans'=>(int)$sentScans, 'sentinel_fixes'=>(int)$sentFixes, 'sentinel_patterns'=>(int)$sentPatterns, 'sentinel_knowledge'=>(int)$sentKB ]; // System resources $disk = sshExec("df -h / | tail -1 | awk '{print $5}'"); $mem = sshExec("free -m | awk '/Mem/{printf \"%.0f/%.0fMB\", $3, $2}'"); $load = sshExec("cat /proc/loadavg | cut -d' ' -f1-3"); $uptime = sshExec("uptime -p"); $map['system'] = [ 'disk' => trim($disk['output']), 'memory' => trim($mem['output']), 'load' => trim($load['output']), 'uptime' => trim($uptime['output']) ]; // Connections $map['connections'] = [ ['from'=>'adx:5821','to'=>'arsenal:5890','type'=>'ProxyPass /api/','status'=>'active'], ['from'=>'arsenal:5890','to'=>'postgres:5432','type'=>'PDO','status'=>'active'], ['from'=>'hetzner','to'=>'ovh','type'=>'Tracking pixels','status'=>'configured'], ['from'=>'arsenal','to'=>'ollama:11434','type'=>'HAMID fallback','status'=>$map['services']['ollama']??'unknown'], ['from'=>'arsenal','to'=>'external','type'=>'7 AI providers','status'=>'active'] ]; // Recent sentinel activity $recentFixes = $pdo->query("SELECT issue_type, fix_applied, fix_date FROM admin.sentinel_fixes ORDER BY fix_date DESC LIMIT 5")->fetchAll(PDO::FETCH_ASSOC); $map['sentinel'] = [ 'total_scans' => (int)$sentScans, 'total_fixes' => (int)$sentFixes, 'patterns_known' => (int)$sentPatterns, 'recent_fixes' => $recentFixes, 'last_scan' => $pdo->query("SELECT score, scan_date FROM admin.sentinel_scans ORDER BY scan_date DESC LIMIT 1")->fetch(PDO::FETCH_ASSOC) ]; return $map; } function isExcluded($f) { global $ARCH; return in_array(basename($f), $ARCH['exclude_files']); } function logFix($pdo,$scanId,$file,$type,$fix) { $pdo->prepare("INSERT INTO admin.sentinel_fixes (file_path,issue_type,fix_applied,scan_id) VALUES(?,?,?,?)")->execute([$file,$type,$fix,$scanId]); } // ═══════════════════════════════════════════════════════════════ // API ROUTES // ═══════════════════════════════════════════════════════════════ switch ($action) { case 'scan': echo json_encode(runFullScan($pdo, ($_GET['fix']??'1')==='1'), JSON_PRETTY_PRINT); break; case 'chat': $msg = $_POST['message'] ?? $_GET['message'] ?? ''; if (!$msg) { echo json_encode(['error'=>'No message']); break; } echo json_encode(chatLLM($pdo, $msg), JSON_PRETTY_PRINT); break; case 'exec': $cmd = $_POST['cmd'] ?? $_GET['cmd'] ?? ''; if (!$cmd) { echo json_encode(['error'=>'No cmd']); break; } echo json_encode(sshExec($cmd)); break; case 'arch': case 'architecture': echo json_encode(getArchitectureMap($pdo), JSON_PRETTY_PRINT); break; case 'status': $last = $pdo->query("SELECT * FROM admin.sentinel_scans ORDER BY scan_date DESC LIMIT 1")->fetch(PDO::FETCH_ASSOC); $totalScans = $pdo->query("SELECT count(*) FROM admin.sentinel_scans")->fetchColumn(); $totalFixes = $pdo->query("SELECT count(*) FROM admin.sentinel_fixes")->fetchColumn(); $patterns = $pdo->query("SELECT pattern_name,times_detected,severity,last_seen FROM admin.sentinel_patterns ORDER BY times_detected DESC")->fetchAll(PDO::FETCH_ASSOC); echo json_encode(['status'=>'operational','last_scan'=>$last,'total_scans'=>(int)$totalScans,'total_fixes'=>(int)$totalFixes,'patterns'=>$patterns], JSON_PRETTY_PRINT); break; case 'history': $l = min((int)($_GET['limit']??20),100); echo json_encode(['scans'=>$pdo->query("SELECT * FROM admin.sentinel_scans ORDER BY scan_date DESC LIMIT $l")->fetchAll(PDO::FETCH_ASSOC)]); break; case 'fixes': $l = min((int)($_GET['limit']??50),200); echo json_encode(['fixes'=>$pdo->query("SELECT * FROM admin.sentinel_fixes ORDER BY fix_date DESC LIMIT $l")->fetchAll(PDO::FETCH_ASSOC)]); break; case 'patterns': echo json_encode(['patterns'=>$pdo->query("SELECT * FROM admin.sentinel_patterns ORDER BY times_detected DESC")->fetchAll(PDO::FETCH_ASSOC)]); break; case 'exec_remote': $server = $_GET['server'] ?? $_POST['server'] ?? ''; $cmd = $_POST['cmd'] ?? $_GET['cmd'] ?? ''; if (!$server || !$cmd) { echo json_encode(['error'=>'Need server and cmd']); break; } echo json_encode(remoteSsh($server, $cmd)); break; default: echo json_encode(['engine'=>'Sentinel V3','version'=>'3.0','actions'=>['scan','chat','exec','arch','status','history','fixes','patterns']]); }