721 lines
39 KiB
PHP
Executable File
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']]);
|
|
}
|