Files
wevads-platform/scripts/api_sentinel-brain.php
2026-02-26 04:53:11 +01:00

721 lines
39 KiB
PHP
Executable File

<?php
/**
* ╔═══════════════════════════════════════════════════════════════╗
* ║ 🛡️ WEVAL MIND SENTINEL V4 ULTRA — Autonomous Regression Guard ║
* ║ LLM Chat • SSH 3 Servers • Vault Guard • Regression Scan • Auto-Repair ║
* ║ Knowledge: 3 months Claude sessions + anti-regression engine ║
* ╚═══════════════════════════════════════════════════════════════╝
*/
header('Content-Type: application/json');
error_reporting(0);
$start = microtime(true);
$action = $_GET['action'] ?? $_POST['action'] ?? 'status';
try {
$pdo = new PDO('pgsql:host=localhost;dbname=adx_system', 'admin', 'admin123');
$pdo->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 = <<<SYSPROMPT
Tu es SENTINEL V3, le gardien autonome de l'infrastructure WEVADS. Tu AGIS, tu ne donnes PAS de conseils génériques.
## TON IDENTITÉ
- Tu es un ingénieur système expert qui EXÉCUTE des commandes et RÉPARE les problèmes
- Tu as accès SSH root sur le serveur Hetzner (89.167.40.150)
- Tu peux exécuter n'importe quelle commande bash via la fonction sshExec()
- Tu connais TOUTE l'architecture du système
## ARCHITECTURE WEVADS
### Serveurs:
- HETZNER 89.167.40.150 (WEVAL Marketing) — Ubuntu 24, 16GB RAM, SSH port 49222, user: root
- OVH 151.80.235.110 (WEVAL Tracking) — Ubuntu, SSH port 22, user: ubuntu (⚠️ PAS root!)
- WEVAL Consulting 46.62.220.135
### Ports actifs sur Hetzner:
- 5821: WEVADS ADX (principal) → /opt/wevads/public — ProxyPass /api/ vers 5890
- 5890: Arsenal (APIs + screens) → /opt/wevads-arsenal/public
- 5822: FMGAPP, 5823: BCGAPP, 5824: DKIM Manager
- 8080: N8N Workflows, 5432: PostgreSQL (localhost), 11434: Ollama
### Bases de données:
- adx_system (schema admin): brain_*, hamid_*, office_accounts (1352+), sending_*, sentinel_*
- adx_clients: données clients
### Email Infrastructure:
- Office 365: 1352+ comptes, 176 tenants
- PMTA: multi-server, Brain Engine 7 winning configs (88-100% inbox)
- Tracking: OVH 151.80.235.110 (open/click/unsub)
## ÉTAT ACTUEL DU SYSTÈME
Services: apache2={$sysState['apache2']}, postgresql={$sysState['postgresql']}, pmta={$sysState['pmta']}, ollama={$sysState['ollama']}
Disque: {$diskR['output']}
Mémoire: {$memR['output']}
Load: {$loadR['output']}
Dernier scan: score={$lastScan['score']}%, issues={$lastScan['issues_found']}, fixed={$lastScan['issues_fixed']}
## EXPERTISE ANTI-REGRESSION (3 mois sessions Claude)
### Troncation Arsenal
- Le deploiement Arsenal tronque les fichiers PHP aux limites de string </body></html>
- 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 .= <<<RULES
## COMMANDES SSH DISPONIBLES
- sshExec(cmd) → Exécute sur HETZNER (local, root)
- remoteSsh('ovh', cmd) → Exécute sur OVH Tracking (ubuntu@151.80.235.110)
- remoteSsh('consulting', cmd) → Exécute sur WEVAL Consulting (root@46.62.220.135)
Pour le tracking OVH: utilise toujours remoteSsh('ovh', cmd). Le user est ubuntu, utilise sudo pour les commandes root.
Services OVH: nginx (web), php7.4-fpm (PHP). Fichiers tracking: /var/www/html/click.php, track.php, lead.php, optout.php
Bug connu: /var/www/scripts/help.php ligne 206 - appels HTTP récursifs → saturation PHP-FPM. Fix: sudo systemctl restart php7.4-fpm
## COMMUNICATION
- Tu es un assistant conversationnel ET un ingénieur système. Sois naturel et amical.
- Pour les salutations (hi, bonjour, salut) → Réponds chaleureusement avec un résumé rapide de état du système
- Pour les questions (comment ça va, quoi de neuf) → Donne un statut système avec les métriques clés
- Pour les demandes techniques → Exécute les commandes SSH et montre les résultats
- Tu comprends le langage naturel: "comment va le serveur" = demande de status
- NE JAMAIS dire "je ne comprends pas" → Trouve toujours une action utile ou donne un statut
## RÈGLES D'ACTION
1. Quand on te signale un problème → tu EXÉCUTES des commandes pour diagnostiquer et réparer
2. Tu donnes le RÉSULTAT concret de tes actions, pas des conseils
3. Pour le serveur tracking OVH: user=ubuntu, PAS root. Utilise sudo si besoin.
4. Tu réponds en français, concis, avec des résultats réels
5. Si tu identifies un problème → tu le fixes immédiatement
6. Tu montres les commandes exécutées ET leurs résultats
## FORMAT DE RÉPONSE
Utilise ce format quand tu exécutes des actions:
🔍 Diagnostic: [ce que tu vérifies]
💻 Commande: `[commande]`
📊 Résultat: [output]
✅ ou ❌ [conclusion]
🔧 Fix: [si nécessaire]
RULES;
// Try LLM providers in order
$providers = $pdo->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<count($rms[0]); $i++) {
$srv = $rms[1][$i]; $rcmd = $rms[2][$i];
$r = remoteSsh($srv, $rcmd, 15);
$executed[] = ['cmd'=>'['.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}'=>'<img src="http://[domain]/[open]" />','{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('/<style[^>]*NUKE[^>]*>[^<]*<\/style>/si','',$content); $content = preg_replace('/<script[^>]*(?: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('/<!-- weval-theme-system-v2 -->.*?<!-- \/weval-theme-system-v2 -->/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,'<script'); $c=substr_count($content,'</script>');
if ($o>$c) { $d=$o-$c; $issues[]=['file'=>basename($f),'path'=>$f,'type'=>'unclosed-script','detail'=>"diff=$d"];
if ($autoFix) { $content.=str_repeat("</script>\n",$d); $changed=true; $fixed++; logFix($pdo,$scanId,$f,'unclosed-script',"Added $d"); }
}
}
// Junk after </html>
$pos=strpos($content,'</body></html>');
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('/<b>(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']]);
}