306 lines
12 KiB
PHP
Executable File
306 lines
12 KiB
PHP
Executable File
<?php
|
||
// === SECURITY: IP Whitelist (added 2026-03-02) ===
|
||
$allowed = array("127.0.0.1","127.0.0.1","46.62.220.135","88.198.4.195","151.80.235.110","89.167.1.139","105.159.122.54","41.250.206.28","41.251.40.10","41.250.81.114","41.251.82.55","::1");
|
||
$client_ip = $_SERVER["REMOTE_ADDR"] ?? "";
|
||
if (!in_array($client_ip, $allowed)) { http_response_code(403); die(json_encode(["error"=>"Forbidden"])); }
|
||
// === END SECURITY ===
|
||
/**
|
||
* SENTINEL V2 — Monitoring & Alerting Only
|
||
* ==========================================
|
||
* REPLACES the bloated 720-line sentinel-brain.php
|
||
*
|
||
* Philosophy: READ-ONLY monitoring. No file modifications, no exec for external AI,
|
||
* no cron manipulation. Only 'exec' action preserved for Sentinel remote management.
|
||
*
|
||
* Actions:
|
||
* exec — Run shell command (preserved for remote management via Claude)
|
||
* exec_remote — SSH to OVH tracking server
|
||
* health — Full system health dashboard
|
||
* alerts — Active alerts requiring attention
|
||
* pipeline — Pipeline status snapshot
|
||
* db_check — Database integrity checks
|
||
* services — Service status
|
||
* disk — Disk/memory usage
|
||
* logs — Recent error logs
|
||
* tracking — OVH tracking stats
|
||
*/
|
||
|
||
error_reporting(E_ERROR);
|
||
header('Content-Type: application/json');
|
||
// CORS locked by WAF shield
|
||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||
header('Access-Control-Allow-Headers: Content-Type');
|
||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
|
||
|
||
// ======================== DB ========================
|
||
function db() {
|
||
static $pdo;
|
||
if (!$pdo) {
|
||
$pdo = new PDO('pgsql:host=localhost;dbname=adx_system', 'admin', 'admin123');
|
||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||
}
|
||
return $pdo;
|
||
}
|
||
|
||
function qval($sql) {
|
||
try { return db()->query($sql)->fetchColumn() ?: 0; } catch(Exception $e) { return 0; }
|
||
}
|
||
function qall($sql) {
|
||
try { return db()->query($sql)->fetchAll(PDO::FETCH_ASSOC) ?: []; } catch(Exception $e) { return []; }
|
||
}
|
||
|
||
// ======================== CORE EXEC (preserved) ========================
|
||
function doExec($cmd) {
|
||
if (empty($cmd)) return ['error' => 'no command'];
|
||
$output = [];
|
||
$code = 0;
|
||
exec($cmd . ' 2>&1', $output, $code);
|
||
return ['output' => implode("\n", $output), 'exit_code' => $code];
|
||
}
|
||
|
||
function doRemoteExec($cmd, $host = '151.80.235.110', $user = 'ubuntu', $pass = 'MX8D3zSAty7k3243242', $port = 22) {
|
||
$escaped = escapeshellarg($cmd);
|
||
$sshCmd = "sshpass -p '$pass' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 -p $port $user@$host $escaped 2>&1";
|
||
$output = [];
|
||
exec($sshCmd, $output);
|
||
return ['output' => implode("\n", $output), 'host' => $host];
|
||
}
|
||
|
||
// ======================== MONITORING FUNCTIONS ========================
|
||
|
||
function getServiceStatus() {
|
||
$services = ['apache2', 'postgresql', 'pmta', 'ollama', 'cron'];
|
||
$result = [];
|
||
foreach ($services as $s) {
|
||
$st = trim(shell_exec("systemctl is-active $s 2>/dev/null") ?: 'unknown');
|
||
$result[$s] = $st;
|
||
}
|
||
return $result;
|
||
}
|
||
|
||
function getDiskMemory() {
|
||
$disk_raw = trim(shell_exec("df -h / 2>/dev/null | tail -1") ?: '');
|
||
$parts = preg_split('/\s+/', $disk_raw);
|
||
$mem_raw = trim(shell_exec("free -m 2>/dev/null | grep Mem") ?: '');
|
||
$mparts = preg_split('/\s+/', $mem_raw);
|
||
$load = trim(shell_exec("cat /proc/loadavg 2>/dev/null") ?: '');
|
||
$uptime = trim(shell_exec("uptime -p 2>/dev/null") ?: '');
|
||
|
||
return [
|
||
'disk_total' => $parts[1] ?? 'N/A',
|
||
'disk_used' => $parts[2] ?? 'N/A',
|
||
'disk_pct' => $parts[4] ?? 'N/A',
|
||
'ram_total_mb' => (int)($mparts[1] ?? 0),
|
||
'ram_used_mb' => (int)($mparts[2] ?? 0),
|
||
'ram_pct' => ($mparts[1] ?? 0) > 0 ? round(($mparts[2] ?? 0) / $mparts[1] * 100) . '%' : 'N/A',
|
||
'load' => $load,
|
||
'uptime' => $uptime,
|
||
];
|
||
}
|
||
|
||
function getPipelineStatus() {
|
||
return [
|
||
'offers' => (int)qval("SELECT COUNT(*) FROM affiliate.offers WHERE status='Activated'"),
|
||
'creatives' => (int)qval("SELECT COUNT(*) FROM affiliate.creatives WHERE status='Activated'"),
|
||
'from_names' => (int)qval("SELECT COUNT(*) FROM affiliate.from_names WHERE status='Activated'"),
|
||
'subjects' => (int)qval("SELECT COUNT(*) FROM affiliate.subjects WHERE status='Activated'"),
|
||
'links' => (int)qval("SELECT COUNT(*) FROM affiliate.links WHERE status='Activated'"),
|
||
'brain_configs' => (int)qval("SELECT COUNT(*) FROM admin.brain_configs WHERE is_active=true"),
|
||
'brain_winners' => (int)qval("SELECT COUNT(*) FROM admin.brain_configs WHERE is_winner=true"),
|
||
'send_methods' => (int)qval("SELECT COUNT(*) FROM admin.send_methods WHERE is_active=true"),
|
||
'o365_senders' => (int)qval("SELECT COUNT(*) FROM admin.office_accounts WHERE status='graph_send'"),
|
||
'o365_active' => (int)qval("SELECT COUNT(*) FROM admin.office_accounts WHERE status IN ('Active','active','graph_ok','graph_send')"),
|
||
'contacts_active' => (int)qval("SELECT COUNT(*) FROM admin.contacts WHERE status='active'"),
|
||
'sent_today' => (int)qval("SELECT COUNT(*) FROM admin.unified_send_log WHERE created_at::date = CURRENT_DATE"),
|
||
'sent_total' => (int)qval("SELECT COUNT(*) FROM admin.unified_send_log"),
|
||
'domains' => (int)qval("SELECT COUNT(*) FROM admin.freedns_domains WHERE status='verified'"),
|
||
'isps' => (int)qval("SELECT COUNT(*) FROM admin.isp_configs"),
|
||
];
|
||
}
|
||
|
||
function getAlerts() {
|
||
$alerts = [];
|
||
$p = getPipelineStatus();
|
||
|
||
// Critical alerts (🔴)
|
||
if ($p['offers'] == 0) $alerts[] = ['level'=>'critical', 'msg'=>'Aucune offre importée', 'fix'=>'offer-importer-api.php?action=import_all'];
|
||
if ($p['o365_senders'] == 0) $alerts[] = ['level'=>'critical', 'msg'=>'Aucun compte O365 en mode envoi', 'fix'=>'Configurer graph_send sur des comptes actifs'];
|
||
|
||
// Warnings (⚠️)
|
||
if ($p['creatives'] == 0 && $p['offers'] > 0) $alerts[] = ['level'=>'warning', 'msg'=>'Offres sans créatives', 'fix'=>'Importer les créatives'];
|
||
if ($p['brain_winners'] == 0) $alerts[] = ['level'=>'warning', 'msg'=>'Aucun brain winner', 'fix'=>'Les tests brain sont en cours'];
|
||
if ($p['o365_senders'] < 3 && $p['o365_senders'] > 0) $alerts[] = ['level'=>'warning', 'msg'=>"Seulement {$p['o365_senders']} sender(s) O365", 'fix'=>'Ajouter plus de comptes en graph_send'];
|
||
|
||
// Service checks
|
||
$svcs = getServiceStatus();
|
||
foreach ($svcs as $name => $status) {
|
||
if ($status !== 'active' && $name !== 'ollama') {
|
||
$alerts[] = ['level'=>'critical', 'msg'=>"Service $name: $status", 'fix'=>"systemctl restart $name"];
|
||
}
|
||
}
|
||
|
||
// Disk check
|
||
$dm = getDiskMemory();
|
||
$diskPct = (int)str_replace('%', '', $dm['disk_pct']);
|
||
if ($diskPct > 90) $alerts[] = ['level'=>'critical', 'msg'=>"Disque à {$dm['disk_pct']}", 'fix'=>'Nettoyer les logs/tmp'];
|
||
elseif ($diskPct > 80) $alerts[] = ['level'=>'warning', 'msg'=>"Disque à {$dm['disk_pct']}", 'fix'=>'Surveiller l\'espace'];
|
||
|
||
// Info (ℹ️)
|
||
if ($p['sent_today'] > 0) $alerts[] = ['level'=>'info', 'msg'=>"{$p['sent_today']} emails envoyés aujourd'hui"];
|
||
if ($p['brain_winners'] > 0) $alerts[] = ['level'=>'info', 'msg'=>"{$p['brain_winners']} brain winners actifs"];
|
||
|
||
return $alerts;
|
||
}
|
||
|
||
function getDbIntegrity() {
|
||
$checks = [];
|
||
|
||
// Table existence
|
||
$required = [
|
||
'affiliate.offers', 'affiliate.creatives', 'affiliate.links',
|
||
'affiliate.from_names', 'affiliate.subjects',
|
||
'admin.brain_configs', 'admin.send_methods', 'admin.office_accounts',
|
||
'admin.contacts', 'admin.unified_send_log', 'admin.offer_creatives'
|
||
];
|
||
|
||
foreach ($required as $t) {
|
||
try {
|
||
$c = qval("SELECT COUNT(*) FROM $t");
|
||
$checks[$t] = ['exists'=>true, 'rows'=>(int)$c];
|
||
} catch(Exception $e) {
|
||
$checks[$t] = ['exists'=>false, 'error'=>$e->getMessage()];
|
||
}
|
||
}
|
||
|
||
// Sequence health
|
||
$seqs = qall("SELECT sequencename, last_value FROM pg_sequences WHERE schemaname IN ('affiliate','admin','public') AND last_value IS NOT NULL ORDER BY sequencename");
|
||
$checks['sequences'] = count($seqs);
|
||
|
||
// DB size
|
||
$checks['db_size'] = qval("SELECT pg_size_pretty(pg_database_size('adx_system'))");
|
||
|
||
return $checks;
|
||
}
|
||
|
||
function getRecentLogs($lines = 30) {
|
||
$logs = [];
|
||
|
||
// PHP errors
|
||
$phpErr = trim(shell_exec("tail -$lines /var/log/wevads/php-error.log 2>/dev/null | grep -v '^$' | tail -10") ?: '');
|
||
if ($phpErr) $logs['php_errors'] = explode("\n", $phpErr);
|
||
|
||
// Apache errors (last relevant ones)
|
||
$apacheErr = trim(shell_exec("tail -$lines /var/log/apache2/error.log 2>/dev/null | grep -v 'AH00558\\|ssl:warn\\|mpm_prefork' | tail -10") ?: '');
|
||
if ($apacheErr) $logs['apache_errors'] = explode("\n", $apacheErr);
|
||
|
||
// Framework errors
|
||
$fwErr = trim(shell_exec("tail -10 /opt/wevads/storage/logs/frontend_errors.log 2>/dev/null") ?: '');
|
||
if ($fwErr) $logs['framework_errors'] = explode("\n", $fwErr);
|
||
|
||
return $logs;
|
||
}
|
||
|
||
function getTrackingStats() {
|
||
$r = doRemoteExec("wc -l /var/www/html/logs/opens.log /var/www/html/logs/clicks.log 2>/dev/null && echo '---' && tail -5 /var/www/html/logs/clicks.log 2>/dev/null");
|
||
return ['raw' => $r['output'], 'host' => '151.80.235.110'];
|
||
}
|
||
|
||
// ======================== ROUTER ========================
|
||
$action = $_GET['action'] ?? $_POST['action'] ?? 'health';
|
||
|
||
try {
|
||
switch ($action) {
|
||
|
||
case 'exec':
|
||
$cmd = $_POST['cmd'] ?? $_GET['cmd'] ?? '';
|
||
echo json_encode(doExec($cmd));
|
||
break;
|
||
|
||
case 'exec_remote':
|
||
$cmd = $_POST['cmd'] ?? $_GET['cmd'] ?? '';
|
||
$host = $_POST['host'] ?? '151.80.235.110';
|
||
echo json_encode(doRemoteExec($cmd, $host));
|
||
break;
|
||
|
||
case 'health':
|
||
echo json_encode([
|
||
'timestamp' => date('c'),
|
||
'services' => getServiceStatus(),
|
||
'system' => getDiskMemory(),
|
||
'pipeline' => getPipelineStatus(),
|
||
'alerts' => getAlerts(),
|
||
]);
|
||
break;
|
||
|
||
case 'alerts':
|
||
echo json_encode(['alerts' => getAlerts(), 'timestamp' => date('c')]);
|
||
break;
|
||
|
||
case 'pipeline':
|
||
echo json_encode(getPipelineStatus());
|
||
break;
|
||
|
||
case 'db_check':
|
||
echo json_encode(getDbIntegrity());
|
||
break;
|
||
|
||
case 'services':
|
||
echo json_encode(getServiceStatus());
|
||
break;
|
||
|
||
case 'disk':
|
||
echo json_encode(getDiskMemory());
|
||
break;
|
||
|
||
case 'logs':
|
||
echo json_encode(getRecentLogs());
|
||
break;
|
||
|
||
case 'tracking':
|
||
echo json_encode(getTrackingStats());
|
||
break;
|
||
|
||
// Legacy compatibility
|
||
case 'status':
|
||
echo json_encode([
|
||
'sentinel' => 'v2',
|
||
'mode' => 'monitoring',
|
||
'services' => getServiceStatus(),
|
||
'pipeline' => getPipelineStatus(),
|
||
]);
|
||
break;
|
||
|
||
case 'scan':
|
||
case 'architecture':
|
||
case 'arch':
|
||
// Return health instead of old broken scan
|
||
echo json_encode([
|
||
'timestamp' => date('c'),
|
||
'services' => getServiceStatus(),
|
||
'system' => getDiskMemory(),
|
||
'pipeline' => getPipelineStatus(),
|
||
'alerts' => getAlerts(),
|
||
'db' => getDbIntegrity(),
|
||
]);
|
||
break;
|
||
|
||
// Disabled dangerous actions
|
||
case 'chat':
|
||
case 'history':
|
||
case 'fixes':
|
||
case 'patterns':
|
||
echo json_encode(['info' => 'Action disabled in Sentinel V2 (monitoring-only mode)', 'use' => 'hamid-chef.php for AI chat']);
|
||
break;
|
||
|
||
default:
|
||
echo json_encode([
|
||
'error' => 'Unknown action',
|
||
'available' => ['exec','exec_remote','health','alerts','pipeline','db_check','services','disk','logs','tracking','status','scan'],
|
||
'disabled' => ['chat','history','fixes','patterns (use hamid-chef.php)'],
|
||
]);
|
||
}
|
||
} catch (Exception $e) {
|
||
http_response_code(500);
|
||
echo json_encode(['error' => $e->getMessage()]);
|
||
}
|