diff --git a/servers/linux/monitoring/bin/alert-engine.php b/servers/linux/monitoring/bin/alert-engine.php new file mode 100644 index 0000000..6c80ec2 --- /dev/null +++ b/servers/linux/monitoring/bin/alert-engine.php @@ -0,0 +1,204 @@ +#!/usr/bin/env php += 2 && ($now - (int)$parts[1]) <= $DEDUP_WINDOW) { + $kept[] = $line; + } + } + file_put_contents($DEDUP_FILE, implode("\n", $kept) . (empty($kept) ? "" : "\n")); +} + +/** + * Vérifie si une alerte doit être envoyée (Déduplication) + */ +function should_notify_dedup($key) { + global $DEDUP_FILE, $DEDUP_WINDOW; + if (!file_exists($DEDUP_FILE)) return true; + + $now = time(); + $last_ts = 0; + + $handle = fopen($DEDUP_FILE, 'r'); + while (($line = fgets($handle)) !== false) { + $p = explode('|', trim($line)); + if (count($p) >= 5) { + $current_key = "{$p[0]}|{$p[2]}|{$p[3]}|{$p[4]}"; + if ($current_key === $key) { + $last_ts = (int)$p[1]; + } + } + } + fclose($handle); + + return ($now - $last_ts) >= $DEDUP_WINDOW; +} + +/** + * Envoi vers ntfy + */ +function send_ntfy($title, $body, $level) { + global $CONFIG; + if (!($CONFIG['ALERT_NTFY_ENABLED'] ?? true)) return true; + if (empty($CONFIG['NTFY_SERVER']) || empty($CONFIG['NTFY_TOPIC'])) return false; + + $priority = ['CRITICAL' => 'urgent', 'ERROR' => 'high', 'WARNING' => 'default'][$level] ?? 'default'; + $tags = ($CONFIG['NTFY_TAGS'][$level]) ?? 'warning'; + + $url = rtrim($CONFIG['NTFY_SERVER'], '/') . '/' . $CONFIG['NTFY_TOPIC']; + $headers = ["Title: $title", "Priority: $priority", "Tags: $tags"]; + + if (!empty($CONFIG['NTFY_TOKEN'])) { + $headers[] = "Authorization: Bearer " . $CONFIG['NTFY_TOKEN']; + } + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); + curl_exec($ch); + $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + return ($status >= 200 && $status < 300); +} + +/** + * Envoi par mail + */ +function send_mail($subject, $body) { + global $CONFIG; + if (!($CONFIG['ALERT_MAIL_ENABLED'] ?? true)) return true; + if (empty($CONFIG['DEST'])) return false; + + $prefix = $CONFIG['ALERT_MAIL_SUBJECT_PREFIX'] ?? '[monitoring]'; + $headers = "From: monitoring@" . gethostname() . "\r\n" . "Content-Type: text/plain; charset=UTF-8"; + + return mail($CONFIG['DEST'], "$prefix $subject", $body, $headers); +} + +/** + * Traitement d'une ligne de log + */ +function process_line($line) { + global $CONFIG, $DEDUP_FILE; + $data = json_decode($line, true); + if (!$data || !isset($data['level'], $data['event'])) return; + + $level = strtoupper($data['level']); + $event = $data['event']; + + if (in_array($level, ['DEBUG', 'INFO', 'NOTICE'])) return; + if (in_array($event, ($CONFIG['ALERT_IGNORE_EVENTS'] ?? []))) return; + + // Déduplication + $key = "{$data['host']}|{$data['app']}|{$level}|{$event}"; + if (!should_notify_dedup($key)) { + log_debug("alert_suppressed_dedup", "Alerte dédupliquée", ["event=$event", "host={$data['host']}"]); + return; + } + + // Détermination des canaux (Règle spécifique puis défaut) + $channels_str = $CONFIG['RULES'][$event] ?? $CONFIG['DEFAULT_CHANNELS'][$level] ?? ''; + if (empty($channels_str)) return; + $channels = explode(',', $channels_str); + + $title = "{$data['host']} [{$data['app']}] $level $event"; + $body = sprintf( + "Date: %s\nHôte: %s\nScript: %s\nNiveau: %s\nÉvénement: %s\n\nMessage:\n%s", + $data['ts'] ?? 'N/A', $data['host'], $data['app'], $level, $event, $data['message'] ?? '' + ); + + foreach ($channels as $ch) { + $ch = trim($ch); + $success = false; + if ($ch === 'ntfy') { + $success = send_ntfy($title, $body, $level); + $success ? log_info("alert_sent_ntfy", "Notification ntfy envoyée", ["event=$event"]) + : log_error("alert_ntfy_failed", "Échec ntfy", ["event=$event"]); + } elseif ($ch === 'mail') { + $success = send_mail($title, $body); + $success ? log_info("alert_sent_mail", "Mail envoyé", ["event=$event"]) + : log_error("alert_mail_failed", "Échec mail", ["event=$event"]); + } + } + + // Enregistrement déduplication + $entry = sprintf("%s|%s|%s|%s|%s\n", $data['host'], time(), $data['app'], $level, $event); + file_put_contents($DEDUP_FILE, $entry, FILE_APPEND); +} + +// --- Main --- + +lock_or_exit("alert-engine"); + +if (!file_exists($LOG_SOURCE)) { + log_notice("alert_log_missing", "Fichier de log absent", ["file=$LOG_SOURCE"]); + exit(0); +} + +$last_offset = file_exists($STATE_FILE) ? (int)file_get_contents($STATE_FILE) : 0; +$current_size = filesize($LOG_SOURCE); + +// Gestion de la rotation de log +if ($last_offset > $current_size) { + log_notice("alert_offset_reset", "Rotation détectée", ["old=$last_offset", "new=0"]); + $last_offset = 0; +} + +cleanup_dedup_file(); + +$fp = fopen($LOG_SOURCE, 'r'); +if ($last_offset > 0) fseek($fp, $last_offset); + +while (($line = fgets($fp)) !== false) { + if (trim($line) !== '') { + process_line($line); + } +} +fclose($fp); + +file_put_contents($STATE_FILE, $current_size); +exit_with_status(); \ No newline at end of file diff --git a/servers/linux/monitoring/bin/install-monitoring.sh b/servers/linux/monitoring/bin/install-monitoring.sh index 3499e16..ce9f152 100755 --- a/servers/linux/monitoring/bin/install-monitoring.sh +++ b/servers/linux/monitoring/bin/install-monitoring.sh @@ -1,18 +1,10 @@ #!/bin/bash # Copyright (C) 2026 Cédric Abonnel -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. +# License: GNU Affero General Public License v3 set -euo pipefail +# --- Configuration --- BASE_DIR="/opt/monitoring" CONF_DIR="${BASE_DIR}/conf" LOG_DIR="/var/log/monitoring" @@ -24,11 +16,12 @@ UPDATE_BASE_URL="https://git.abonnel.fr/cedricAbonnel/scripts-bash/raw/branch/ma MANIFEST_URL="${UPDATE_BASE_URL}/manifest.txt" INSTALL_DEPS="${INSTALL_DEPS:-true}" -CREATE_LOCAL_CONF="${CREATE_LOCAL_CONF:-true}" + +# --- Fonctions --- require_root() { if [ "${EUID}" -ne 0 ]; then - echo "Ce script doit être exécuté en root." >&2 + echo "ERREUR: Ce script doit être exécuté en root." >&2 exit 1 fi } @@ -38,35 +31,38 @@ install_deps() { return 0 fi + echo "--- Installation des dépendances ---" if command -v apt-get >/dev/null 2>&1; then apt-get update apt-get install -y curl coreutils findutils grep sed gawk util-linux ca-certificates + # Ajout des modules PHP nécessaires pour vos scripts (curl pour ntfy) + apt-get install -y php-cli php-curl php-common + else + echo "AVERTISSEMENT: Gestionnaire de paquets apt non détecté. Assurez-vous que php-cli et php-curl sont installés." fi } prepare_dirs() { - mkdir -p "${BASE_DIR}" "${CONF_DIR}" "${LOG_DIR}" "${STATE_DIR}" "${LOCK_DIR}" "${TMP_DIR}" + echo "--- Préparation des répertoires ---" + mkdir -p "${BASE_DIR}/bin" "${BASE_DIR}/lib" "${CONF_DIR}" "${LOG_DIR}" "${STATE_DIR}" "${LOCK_DIR}" "${TMP_DIR}" chmod 755 "${BASE_DIR}" "${CONF_DIR}" "${LOG_DIR}" "${STATE_DIR}" "${LOCK_DIR}" } fetch_manifest() { + echo "--- Récupération du manifeste ---" curl -fsS "${MANIFEST_URL}" -o "${TMP_DIR}/manifest.txt" } validate_manifest() { + # Validation du format : Hash Mode Chemin + # Exemple : a1b2... 755 bin/script.php awk ' NF == 3 && $1 ~ /^[0-9a-fA-F]{64}$/ && $2 ~ /^(644|755|600)$/ && $3 ~ /^(bin|lib|conf)\/[A-Za-z0-9._\/-]+$/ && $3 !~ /\.\./ - ' "${TMP_DIR}/manifest.txt" >/dev/null -} - -apply_mode() { - local mode="$1" - local file="$2" - chmod "$mode" "$file" + ' "${TMP_DIR}/manifest.txt" } download_one() { @@ -76,87 +72,85 @@ download_one() { local url="${UPDATE_BASE_URL}/${rel_path}" local dst="${BASE_DIR}/${rel_path}" + + # On ignore le téléchargement si c'est un fichier de conf qui existe déjà + if [[ "$rel_path" == conf/* ]] && [ -f "$dst" ]; then + echo "Skip: $rel_path (existe déjà)" + return 0 + fi + + echo "Téléchargement: $rel_path" local tmp_file tmp_file="$(mktemp "${TMP_DIR}/file.XXXXXX")" - curl -fsS "$url" -o "$tmp_file" + if ! curl -fsS "$url" -o "$tmp_file"; then + echo "ERREUR: Échec du téléchargement de ${url}" >&2 + rm -f "$tmp_file" + return 1 + fi local got_hash got_hash="$(sha256sum "$tmp_file" | awk '{print $1}')" if [ "$got_hash" != "$expected_hash" ]; then - echo "Hash invalide pour ${rel_path}" >&2 + echo "ERREUR: Hash invalide pour ${rel_path}" >&2 rm -f "$tmp_file" return 1 fi mkdir -p "$(dirname "$dst")" - apply_mode "$mode" "$tmp_file" mv -f "$tmp_file" "$dst" + chmod "$mode" "$dst" } install_from_manifest() { + echo "--- Installation des fichiers ---" while read -r hash mode rel_path; do [ -n "${hash:-}" ] || continue download_one "$hash" "$mode" "$rel_path" - done < "${TMP_DIR}/manifest.txt" -} - -create_local_conf_if_missing() { - if [ "${CREATE_LOCAL_CONF}" != "true" ]; then - return 0 - fi - - if [ ! -f "${CONF_DIR}/alert-engine.local.conf" ]; then - cat > "${CONF_DIR}/alert-engine.local.conf" <<'EOF' -#!/bin/bash - -NTFY_SERVER="https://ntfy.sh" -NTFY_TOPIC="FjdJ7qex2oGqZkV3OMaqNIxe" -NTFY_TOKEN="A_REMPLACER" - -DEST="root" -EOF - chmod 600 "${CONF_DIR}/alert-engine.local.conf" - fi + done < "${TMP_DIR}/manifest-valid.txt" } show_next_steps() { - cat <<'EOF' + cat <&2 + if ! validate_manifest > "${TMP_DIR}/manifest-valid.txt"; then + echo "ERREUR: Le manifeste est invalide ou corrompu." >&2 exit 1 fi install_from_manifest - create_local_conf_if_missing + + # Nettoyage + rm -rf "${TMP_DIR}" + show_next_steps } diff --git a/servers/linux/monitoring/bin/monitoring-update-config.php b/servers/linux/monitoring/bin/monitoring-update-config.php new file mode 100644 index 0000000..1909e7c --- /dev/null +++ b/servers/linux/monitoring/bin/monitoring-update-config.php @@ -0,0 +1,104 @@ +#!/usr/bin/env php + ou "KEY" => + preg_match_all('/[\'"]([A-Z0-9_]+)[\'"]\s*=>/i', $content, $matches); + $keys = $matches[1] ?? []; + sort($keys); + return array_unique($keys); +} + +/** + * Logique de logging + */ +function log_audit($level, $event, $msg) { + echo sprintf("[%s] %s: %s\n", strtoupper($level), $event, $msg); +} + +function check_config_drift() { + $found_issue = false; + $reviewed_files = 0; + $files_requiring_action = 0; + + log_audit('info', 'audit_start', "Début de l'audit des configurations PHP"); + + // On cherche les fichiers .php qui ne sont pas des .local.php + $base_files = glob(CONF_DIR . '/*.php'); + $base_files = array_filter($base_files, function($f) { + return !str_ends_with($f, '.local.php'); + }); + + foreach ($base_files as $base_conf) { + $reviewed_files++; + $base_name = basename($base_conf); + $local_conf = str_replace('.php', '.local.php', $base_conf); + $local_name = basename($local_conf); + + // 1. Si le fichier local n'existe pas + if (!file_exists($local_conf)) { + if (copy($base_conf, $local_conf)) { + chmod($local_conf, 0600); + log_audit('notice', 'audit_missing_local', "Le fichier $local_name a été créé par copie de $base_name"); + } else { + log_audit('error', 'audit_create_local_failed', "Impossible de créer $local_name"); + $found_issue = true; + $files_requiring_action++; + } + continue; + } + + // 2. Comparaison des clés + $keys_base = extract_keys($base_conf); + $keys_local = extract_keys($local_conf); + + $missing = array_diff($keys_base, $keys_local); // Présent dans base mais pas local + $obsolete = array_diff($keys_local, $keys_base); // Présent dans local mais plus dans base + + if (!empty($missing) || !empty($obsolete)) { + $found_issue = true; + $files_requiring_action++; + + log_audit('warning', 'audit_file_requires_action', "Vérification requise pour $local_name"); + + if (!empty($missing)) { + log_audit('warning', 'audit_keys_missing', "Options absentes de $local_name : " . implode(', ', $missing)); + } + + if (!empty($obsolete)) { + log_audit('info', 'audit_keys_obsolete', "Options obsolètes ou personnalisées dans $local_name : " . implode(', ', $obsolete)); + } + } else { + log_audit('info', 'audit_file_ok', "Le fichier $local_name est synchronisé avec $base_name"); + } + } + + // Résumé final + if (!$found_issue) { + log_audit('info', 'audit_success', "Toutes les configurations sont à jour ($reviewed_files fichiers vérifiés)"); + } else { + log_audit('warning', 'audit_requires_action', "Action requise sur $files_requiring_action fichier(s) sur $reviewed_files"); + } +} + +// --- Main --- +check_config_drift(); \ No newline at end of file diff --git a/servers/linux/monitoring/bin/monitor-update-config.sh b/servers/linux/monitoring/bin/monitoring-update-config.sh similarity index 100% rename from servers/linux/monitoring/bin/monitor-update-config.sh rename to servers/linux/monitoring/bin/monitoring-update-config.sh diff --git a/servers/linux/monitoring/bin/monitoring-update.php b/servers/linux/monitoring/bin/monitoring-update.php new file mode 100644 index 0000000..4302125 --- /dev/null +++ b/servers/linux/monitoring/bin/monitoring-update.php @@ -0,0 +1,219 @@ +#!/usr/bin/env php + ['timeout' => $UPDATE_TIMEOUT_TOTAL] + ])); + + if ($content === false) { + log_error("manifest_download_failed", "Impossible de télécharger le manifeste", ["url=$UPDATE_MANIFEST_URL"]); + return false; + } + + $lines = explode("\n", trim($content)); + $manifest_data = []; + + foreach ($lines as $line) { + if (empty(trim($line))) continue; + + $parts = preg_split('/\s+/', trim($line)); + if (count($parts) !== 3) continue; + + list($hash, $mode, $path) = $parts; + + // Validation stricte (Regex identique au Bash) + if (preg_match('/^[0-9a-f]{64}$/i', $hash) && + preg_match('/^(644|755)$/', $mode) && + preg_match('/^(bin|lib|conf)\/[A-Za-z0-9._\/-]+$/', $path) && + strpos($path, '..') === false) { + $manifest_data[] = ['hash' => strtolower($hash), 'mode' => $mode, 'path' => $path]; + } + } + + if (empty($manifest_data)) { + log_error("manifest_invalid", "Le manifeste distant est vide ou invalide"); + return false; + } + + log_info("manifest_downloaded", "Manifeste téléchargé", ["url=$UPDATE_MANIFEST_URL"]); + return $manifest_data; +} + +/** + * Mise à jour d'un fichier unique + */ +function update_one_file($expected_hash, $mode, $rel_path) { + global $MONITORING_BASE_DIR, $UPDATE_BASE_URL, $UPDATE_TMP_DIR, $UPDATE_TIMEOUT_TOTAL; + + $local_file = $MONITORING_BASE_DIR . '/' . $rel_path; + $remote_url = rtrim($UPDATE_BASE_URL, '/') . '/' . $rel_path; + $tmp_file = $UPDATE_TMP_DIR . '/' . basename($rel_path) . '.' . bin2hex(random_bytes(4)); + + $local_hash = file_exists($local_file) ? hash_file('sha256', $local_file) : null; + + if ($local_hash === $expected_hash) { + log_debug("update_not_needed", "Fichier déjà à jour", ["file=$rel_path"]); + return true; + } + + // Téléchargement via cURL pour gérer les timeouts proprement + $ch = curl_init($remote_url); + $fp = fopen($tmp_file, 'w'); + curl_setopt($ch, CURLOPT_FILE, $fp); + curl_setopt($ch, CURLOPT_TIMEOUT, $UPDATE_TIMEOUT_TOTAL); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + $success = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + fclose($fp); + + if (!$success || $http_code !== 200) { + log_error("update_download_failed", "Téléchargement impossible", ["file=$rel_path", "url=$remote_url"]); + @unlink($tmp_file); + return false; + } + + $downloaded_hash = hash_file('sha256', $tmp_file); + if ($downloaded_hash !== $expected_hash) { + log_error("update_hash_mismatch", "Hash téléchargé invalide", ["file=$rel_path", "expected=$expected_hash", "got=$downloaded_hash"]); + @unlink($tmp_file); + return false; + } + + // Installation + ensure_parent_dir($local_file); + chmod($tmp_file, ($mode == '755' ? 0755 : 0644)); + + if (rename($tmp_file, $local_file)) { + if (!$local_hash) { + log_notice("file_created", "Fichier créé", ["file=$rel_path", "mode=$mode"]); + } else { + log_notice("update_applied", "Mise à jour appliquée", ["file=$rel_path", "new_hash=$expected_hash"]); + } + return true; + } + + return false; +} + +/** + * Suppression des fichiers obsolètes + */ +function delete_extra_files($manifest_paths) { + global $UPDATE_ALLOW_DELETE, $MONITORING_BASE_DIR; + if (!$UPDATE_ALLOW_DELETE) return; + + $dirs = ['bin', 'lib', 'conf']; + foreach ($dirs as $dir) { + $full_dir = $MONITORING_BASE_DIR . '/' . $dir; + if (!is_dir($full_dir)) continue; + + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($full_dir)); + foreach ($iterator as $file) { + if ($file->isDir()) continue; + + $rel_path = str_replace($MONITORING_BASE_DIR . '/', '', $file->getPathname()); + + // Protection des .local.conf + if (str_ends_with($rel_path, '.local.conf') || str_ends_with($rel_path, '.local.php')) { + continue; + } + + if (!in_array($rel_path, $manifest_paths)) { + if (@unlink($file->getPathname())) { + log_notice("file_deleted", "Fichier obsolète supprimé", ["file=$rel_path"]); + } + } + } + } +} + +/** + * Lancement du script de synchronisation local + */ +function run_local_conf_sync() { + global $MONITORING_BASE_DIR; + $sync_script = $MONITORING_BASE_DIR . '/bin/monitoring-update-config.sh'; // On cherche la version PHP + + if (file_exists($sync_script)) { + log_info("local_conf_sync_start", "Synchronisation des configs locales"); + passthru("php " . escapeshellarg($sync_script), $return_var); + if ($return_var !== 0) { + log_warning("local_conf_sync_failed", "Échec de synchronisation"); + } + } +} + +// --- Exécution principale --- + +$manifest = fetch_manifest(); +if (!$manifest) exit(2); + +$total = count($manifest); +$updated = 0; +$failed = 0; +$remote_paths = []; + +foreach ($manifest as $item) { + $remote_paths[] = $item['path']; + if (update_one_file($item['hash'], $item['mode'], $item['path'])) { + $updated++; + } else { + $failed++; + } +} + +delete_extra_files($remote_paths); +run_local_conf_sync(); + +if ($failed > 0) { + log_warning("update_finished_with_errors", "Mise à jour terminée avec erreurs", ["total=$total", "failed=$failed"]); +} else { + log_info("update_finished", "Mise à jour terminée", ["total=$total", "checked=$updated"]); +} + +exit_with_status(); \ No newline at end of file diff --git a/servers/linux/monitoring/bin/monitoring-update.sh b/servers/linux/monitoring/bin/monitoring-update.sh index d665253..57a97f8 100755 --- a/servers/linux/monitoring/bin/monitoring-update.sh +++ b/servers/linux/monitoring/bin/monitoring-update.sh @@ -10,6 +10,8 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. +# +# Moteur de mise à jour des programmes et fichiers connexes set -u diff --git a/servers/linux/monitoring/bin/monitoring.php b/servers/linux/monitoring/bin/monitoring.php new file mode 100644 index 0000000..b55cdfa --- /dev/null +++ b/servers/linux/monitoring/bin/monitoring.php @@ -0,0 +1,227 @@ +#!/usr/bin/env php + $url, "error" => curl_error($ch)]); + return false; + } + curl_close($ch); + + $manifest_entries = []; + $lines = explode("\n", trim($content)); + + foreach ($lines as $line) { + $line = trim($line); + if (empty($line)) continue; + + // Validation format: hash(64) mode(3) path + if (preg_match('/^([0-9a-fA-F]{64})\s+(644|755)\s+((bin|lib|conf)\/[A-Za-z0-9._\/-]+)$/', $line, $matches)) { + $manifest_entries[] = [ + 'hash' => $matches[1], + 'mode' => $matches[2], + 'path' => $matches[3] + ]; + } + } + + if (empty($manifest_entries)) { + log_error("manifest_invalid", "Le manifeste distant est invalide ou vide", ["url" => $url]); + return false; + } + + log_info("manifest_downloaded", "Manifeste téléchargé", ["url" => $url]); + return $manifest_entries; +} + +/** + * Met à jour un fichier spécifique + */ +function update_one_file($entry) { + global $MONITORING_BASE_DIR, $UPDATE_BASE_URL, $UPDATE_TMP_DIR, $UPDATE_TIMEOUT_TOTAL; + + $rel_path = $entry['path']; + $target_file = $MONITORING_BASE_DIR . '/' . $rel_path; + $remote_url = rtrim($UPDATE_BASE_URL, '/') . '/' . $rel_path; + $expected_hash = strtolower($entry['hash']); + + // Calcul du hash local actuel + $local_hash = file_exists($target_file) ? hash_file('sha256', $target_file) : ""; + + if ($local_hash === $expected_hash) { + log_debug("update_not_needed", "Fichier déjà à jour", ["file" => $rel_path]); + return true; + } + + // Téléchargement + $tmp_file = $UPDATE_TMP_DIR . '/' . basename($rel_path) . '.' . uniqid(); + $ch = curl_init($remote_url); + $fp = fopen($tmp_file, 'wb'); + + curl_setopt($ch, CURLOPT_FILE, $fp); + curl_setopt($ch, CURLOPT_TIMEOUT, $UPDATE_TIMEOUT_TOTAL); + curl_setopt($ch, CURLOPT_FAILONERROR, true); + + $success = curl_exec($ch); + $error = curl_error($ch); + curl_close($ch); + fclose($fp); + + if (!$success) { + log_error("update_download_failed", "Téléchargement impossible", ["file" => $rel_path, "url" => $remote_url, "error" => $error]); + @unlink($tmp_file); + return false; + } + + // Vérification Hash + $downloaded_hash = hash_file('sha256', $tmp_file); + if ($downloaded_hash !== $expected_hash) { + log_error("update_hash_mismatch", "Hash téléchargé invalide", ["file" => $rel_path, "expected" => $expected_hash, "got" => $downloaded_hash]); + @unlink($tmp_file); + return false; + } + + // Installation + ensure_parent_dir($target_file); + chmod($tmp_file, octdec($entry['mode'])); + + if (!rename($tmp_file, $target_file)) { + fail_internal("Échec du déplacement de $tmp_file vers $target_file"); + } + + if ($local_hash === "") { + log_notice("file_created", "Fichier créé depuis le manifeste", ["file" => $rel_path, "mode" => $entry['mode']]); + } else { + log_notice("update_applied", "Mise à jour appliquée", ["file" => $rel_path, "old_hash" => $local_hash, "new_hash" => $expected_hash]); + } + + return true; +} + +/** + * Supprime les fichiers locaux absents du manifeste + */ +function delete_extra_files($remote_files) { + global $UPDATE_ALLOW_DELETE, $MONITORING_BASE_DIR; + if (!$UPDATE_ALLOW_DELETE) return; + + $directories = ['bin', 'lib', 'conf']; + foreach ($directories as $dir) { + $full_path = $MONITORING_BASE_DIR . '/' . $dir; + if (!is_dir($full_path)) continue; + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($full_path, RecursiveDirectoryIterator::SKIP_DOTS) + ); + + foreach ($iterator as $file) { + // On récupère le chemin relatif par rapport à la racine du monitoring + $rel_path = substr($file->getPathname(), strlen($MONITORING_BASE_DIR) + 1); + + // 1. Protection : Si c'est dans le manifeste distant, on ne touche à rien + if (in_array($rel_path, $remote_files)) { + continue; + } + + // 2. Protection générique : On n'efface JAMAIS les fichiers de configuration locale + // Cela couvre : *.local.conf.php, *.local.conf, et même *.local.php par sécurité + if (str_ends_with($rel_path, '.local.conf.php') || + str_ends_with($rel_path, '.local.conf') || + str_ends_with($rel_path, '.local.php')) { + + log_debug("delete_skipped", "Fichier local protégé (ignoré)", ["file" => $rel_path]); + continue; + } + + // 3. Suppression si le fichier est obsolète et non protégé + if (@unlink($file->getPathname())) { + log_notice("file_deleted", "Fichier obsolète supprimé", ["file" => $rel_path]); + } else { + log_error("delete_failed", "Impossible de supprimer le fichier local", ["file" => $rel_path]); + } + } + } +} + +// --- Main --- + +$manifest = fetch_manifest($UPDATE_MANIFEST_URL); +if ($manifest === false) exit(2); + +$total = count($manifest); +$updated = 0; +$failed = 0; +$remote_paths = []; + +foreach ($manifest as $entry) { + $remote_paths[] = $entry['path']; + if (update_one_file($entry)) { + $updated++; + } else { + $failed++; + } +} + +delete_extra_files($remote_paths); + +if ($failed > 0) { + log_warning("update_finished_with_errors", "Mise à jour terminée avec erreurs", ["total" => $total, "updated" => $updated, "failed" => $failed]); +} else { + log_info("update_finished", "Mise à jour terminée", ["total" => $total, "updated" => $updated]); +} + +exit_with_status(); \ No newline at end of file diff --git a/servers/linux/monitoring/bin/monitoring.sh b/servers/linux/monitoring/bin/monitoring.sh index e15f094..93a33e3 100755 --- a/servers/linux/monitoring/bin/monitoring.sh +++ b/servers/linux/monitoring/bin/monitoring.sh @@ -10,6 +10,8 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. +# +# lit le fichier log set -u diff --git a/servers/linux/monitoring/conf/alert-engine.conf.php b/servers/linux/monitoring/conf/alert-engine.conf.php new file mode 100644 index 0000000..34dcd51 --- /dev/null +++ b/servers/linux/monitoring/conf/alert-engine.conf.php @@ -0,0 +1,71 @@ + $monitoring_state_dir . '/alert-engine.offset', + 'ALERT_DEDUP_FILE' => $monitoring_state_dir . '/alert-engine.dedup', + + // --- Activation des canaux --- + 'ALERT_NTFY_ENABLED' => true, + 'ALERT_MAIL_ENABLED' => true, + + // --- Configuration Mail --- + 'ALERT_MAIL_BIN' => '/usr/sbin/sendmail', + 'ALERT_MAIL_SUBJECT_PREFIX' => '[monitoring]', + 'DEST' => 'admin@example.com', // N'oubliez pas de définir le destinataire + + // --- Configuration ntfy --- + 'NTFY_SERVER' => 'https://ntfy.sh', + 'NTFY_TOPIC' => 'TPOSOB84sBJ6HTZ7', + 'NTFY_TOKEN' => '', + 'NTFY_CLICK_URL' => '', + + // --- Déduplication --- + 'ALERT_DEDUP_WINDOW' => 3600, // en secondes + + // --- Événements à ignorer --- + 'ALERT_IGNORE_EVENTS' => [ + 'update_not_needed', + 'alert_sent_ntfy', + 'alert_sent_mail' + ], + + // --- Canaux par défaut selon le niveau --- + 'DEFAULT_CHANNELS' => [ + 'WARNING' => 'ntfy', + 'ERROR' => 'ntfy,mail', + 'CRITICAL' => 'ntfy,mail', + ], + + // --- Tags ntfy par niveau --- + 'NTFY_TAGS' => [ + 'WARNING' => 'warning', + 'ERROR' => 'warning,rotating_light', + 'CRITICAL' => 'skull,warning', + ], + + // --- Règles spécifiques par événement --- + // Si un événement est listé ici, il outrepasse les DEFAULT_CHANNELS + 'RULES' => [ + 'disk_usage_high' => 'ntfy', + 'disk_usage_critical' => 'ntfy,mail', + 'check_failed' => 'ntfy,mail', + 'internal_error' => 'ntfy,mail', + 'update_hash_unavailable' => 'ntfy', + 'update_download_failed' => 'ntfy,mail', + 'update_hash_mismatch' => 'ntfy,mail', + 'manifest_download_failed' => 'ntfy,mail', + 'manifest_invalid' => 'ntfy,mail', + 'update_finished_with_errors' => 'ntfy,mail', + ], +]; \ No newline at end of file diff --git a/servers/linux/monitoring/conf/monitoring.conf.php b/servers/linux/monitoring/conf/monitoring.conf.php new file mode 100644 index 0000000..d917710 --- /dev/null +++ b/servers/linux/monitoring/conf/monitoring.conf.php @@ -0,0 +1,54 @@ + $monitoring_base, + 'MONITORING_LOG_DIR' => $monitoring_log_dir, + 'MONITORING_STATE_DIR' => $monitoring_state_dir, + 'MONITORING_LOCK_DIR' => $monitoring_lock_dir, + 'LOG_FILE' => $monitoring_log_dir . '/events.jsonl', + + // --- Identification --- + 'HOSTNAME_FQDN' => $hostname_fqdn, + 'DEST' => 'root', + + // --- ntfy --- + 'NTFY_SERVER' => 'https://ntfy.sh', // Correction du nfy.sh en ntfy.sh + 'NTFY_TOPIC' => 'TPOSOB84sBJ6HTZ7', + 'NTFY_TOKEN' => '', + + // --- Mises à jour (Update) --- + 'UPDATE_ENABLED' => true, + 'UPDATE_BASE_URL' => 'https://git.abonnel.fr/cedricAbonnel/scripts-bash/raw/branch/main/servers/linux/monitoring', + 'UPDATE_MANIFEST_URL' => 'https://git.abonnel.fr/cedricAbonnel/scripts-bash/raw/branch/main/servers/linux/monitoring/manifest.txt', + 'UPDATE_TIMEOUT_CONNECT' => 3, + 'UPDATE_TIMEOUT_TOTAL' => 15, + 'UPDATE_TMP_DIR' => '/tmp/monitoring-update', + 'UPDATE_ALLOW_DELETE' => false, + + // --- Logs --- + 'LOG_LEVEL' => 'INFO', // DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL +]; \ No newline at end of file diff --git a/servers/linux/monitoring/lib/monitoring-lib.php b/servers/linux/monitoring/lib/monitoring-lib.php new file mode 100644 index 0000000..c619b38 --- /dev/null +++ b/servers/linux/monitoring/lib/monitoring-lib.php @@ -0,0 +1,161 @@ + '/var/log/monitoring/events.jsonl', + 'MONITORING_LOCK_DIR' => '/var/lock/monitoring', + 'LOG_LEVEL' => 'INFO' +]; + +if (file_exists($MONITORING_CONF_DIR . '/monitoring.conf.php')) { + $global_conf = include $MONITORING_CONF_DIR . '/monitoring.conf.php'; + if (is_array($global_conf)) { + $CONFIG = array_merge($CONFIG, $global_conf); + } +} + +// Variables d'exécution +$SCRIPT_NAME = basename($_SERVER['SCRIPT_FILENAME'] ?? $argv[0]); +$SCRIPT_PATH = realpath($_SERVER['SCRIPT_FILENAME'] ?? $argv[0]); + +// --- Fonctions de Log --- + +/** + * Log un événement au format JSONL + */ +function log_event(string $level, string $event, string $message, array $extra_kv = []) { + global $CONFIG, $SCRIPT_NAME; + + $ts = date('c'); // ISO-8601 + + // Détection Hostname + $host = getenv('HOSTNAME_FQDN') ?: (gethostname() ?: 'unknown'); + + $log_data = [ + "ts" => $ts, + "host" => $host, + "app" => $SCRIPT_NAME, + "level" => $level, + "event" => $event, + "message" => $message + ]; + + // Fusion des paires clé=valeur supplémentaires + foreach ($extra_kv as $kv) { + if (strpos($kv, '=') !== false) { + list($k, $v) = explode('=', $kv, 2); + $log_data[$k] = $v; + } + } + + $json_line = json_encode($log_data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + + $log_file = $CONFIG['LOG_FILE']; + ensure_parent_dir($log_file); + + file_put_contents($log_file, $json_line . "\n", FILE_APPEND | LOCK_EX); +} + +function set_status(int $new_status) { + global $CURRENT_STATUS; + if ($new_status > $CURRENT_STATUS) { + $CURRENT_STATUS = $new_status; + } +} + +// Helpers de logs +function log_debug($e, $m, $x = []) { log_event("DEBUG", $e, $m, $x); } +function log_info($e, $m, $x = []) { log_event("INFO", $e, $m, $x); } +function log_notice($e, $m, $x = []) { log_event("NOTICE", $e, $m, $x); } +function log_warning($e, $m, $x = []) { log_event("WARNING", $e, $m, $x); set_status(1); } +function log_error($e, $m, $x = []) { log_event("ERROR", $e, $m, $x); set_status(2); } +function log_critical($e, $m, $x = []) { log_event("CRITICAL", $e, $m, $x); set_status(2); } + +function fail_internal(string $msg) { + log_event("ERROR", "internal_error", $msg); + exit(3); // STATUS_INTERNAL +} + +function exit_with_status() { + global $CURRENT_STATUS; + exit($CURRENT_STATUS); +} + +// --- Utilitaires Système --- + +/** + * Vérifie la présence de commandes système + */ +function require_cmd(...$cmds) { + foreach ($cmds as $cmd) { + $output = []; + $res = 0; + exec("command -v " . escapeshellarg($cmd), $output, $res); + if ($res !== 0) { + fail_internal("Commande requise absente: $cmd"); + } + } +} + +/** + * Gestion du verrouillage (Lock) + */ +function lock_or_exit(?string $lock_name = null) { + global $CONFIG, $SCRIPT_NAME; + $name = $lock_name ?: $SCRIPT_NAME; + $lock_file = ($CONFIG['MONITORING_LOCK_DIR'] ?? '/var/lock/monitoring') . "/{$name}.lock"; + + ensure_parent_dir($lock_file); + $fp = fopen($lock_file, "w+"); + + if (!$fp || !flock($fp, LOCK_EX | LOCK_NB)) { + log_notice("already_running", "Une autre instance est déjà en cours", ["lock=$lock_file"]); + exit(0); + } + // On garde le descripteur ouvert pour maintenir le lock + return $fp; +} + +/** + * Évalue un niveau selon des seuils + */ +function threshold_level($value, $warning, $critical) { + if ($value >= $critical) return 'CRITICAL'; + if ($value >= $warning) return 'WARNING'; + return 'INFO'; +} + +function ensure_parent_dir(string $file) { + $dir = dirname($file); + if (!is_dir($dir)) { + if (!mkdir($dir, 0755, true)) { + // Ici on ne peut pas appeler log_event si c'est le répertoire de log qui échoue + error_log("Impossible de créer le répertoire : $dir"); + exit(3); + } + } +} + +function safe_mv(string $src, string $dst) { + if (!rename($src, $dst)) { + fail_internal("Échec du déplacement de $src vers $dst"); + } +} \ No newline at end of file