Проверка на новые файлы с уведомлением в телегу
Здравствуйте.
Прочитал новый топик о новом нашествии вирусов, которые меняют файлы и что-то там «майнят» создавая нагрузку.
Решил поделиться своим простым скриптом, который контролирует появление новых файлов их изменения, и если таковые будут выявлены, вышлет уведомление в телеграм. Нужно поместить данный скрипт в папку в корне сайта.
Например, detect/file_change_detector.php
Я например, запускаю раз в 6 часов по крону.
P.S.
Пишите, если решение будет востребовано могу создать компонент для MODX
Update:
Компонент создан для MODX2 и MODX3. Загрузил в репозиторий, жду модерации.
Прочитал новый топик о новом нашествии вирусов, которые меняют файлы и что-то там «майнят» создавая нагрузку.
Решил поделиться своим простым скриптом, который контролирует появление новых файлов их изменения, и если таковые будут выявлены, вышлет уведомление в телеграм. Нужно поместить данный скрипт в папку в корне сайта.
Например, detect/file_change_detector.php
Я например, запускаю раз в 6 часов по крону.
P.S.
Пишите, если решение будет востребовано могу создать компонент для MODX
Update:
Компонент создан для MODX2 и MODX3. Загрузил в репозиторий, жду модерации.
<?php
/**
* Скрипт для мониторинга изменений файлов в директории и отправки уведомлений в Telegram.
* Версия для PHP 7.4+
* 1.0.1
* Принцип работы:
* 1. Сканирует указанную директорию рекурсивно.
* 2. Вычисляет MD5-хеши для каждого найденного файла.
* 3. Сравнивает текущие хеши с хешами, сохраненными при предыдущем запуске (из файла .filehashes).
* 4. Определяет добавленные, удаленные и измененные файлы.
* 5. Если обнаружены изменения, отправляет уведомление в Telegram.
* 6. Сохраняет текущее состояние хешей для следующего запуска.
* 7. Ведет лог своей работы в текстовый файл.
*/
// === НАСТРОЙКИ ===
define('MONITOR_DIR', '/home/best/web/bestkaminy.ru/public_html');
define('HASH_FILE', __DIR__ . '/.filehashes.json');
define('LOG_FILE', __DIR__ . '/file_monitor.log');
define('TELEGRAM_BOT_TOKEN', '');
define('TELEGRAM_CHAT_ID', '');
$excludePaths = [
'core/cache/',
'core/export/',
'core/logs/',
//'core/packages/',
'assets/cache/',
//'assets/components/',
'.git/',
'1detect/file_change_detector.php', // Имя вашего скрипта
'1detect/.filehashes.json',
'1detect/file_monitor.log',
];
define('MAX_FILES_IN_MESSAGE', 50);
date_default_timezone_set('Europe/Moscow');
// --- Функции ---
function logMessage(string $message): void
{
$timestamp = date('Y-m-d H:i:s');
$logEntry = "[{$timestamp}] {$message}" . PHP_EOL;
file_put_contents(LOG_FILE, $logEntry, FILE_APPEND);
}
function sanitizeForTelegramCodeSpan(string $text): string
{
$text = str_replace('`', "'", $text);
return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
}
function sendTelegramNotification(string $message): bool
{
if (TELEGRAM_BOT_TOKEN === 'YOUR_TELEGRAM_BOT_TOKEN' || TELEGRAM_CHAT_ID === 'YOUR_TELEGRAM_CHAT_ID') {
logMessage("Ошибка: Не настроены TELEGRAM_BOT_TOKEN или TELEGRAM_CHAT_ID в коде.");
return false;
}
$url = 'https://api.telegram.org/bot' . TELEGRAM_BOT_TOKEN . '/sendMessage';
$data = [
'chat_id' => TELEGRAM_CHAT_ID,
'text' => $message,
'parse_mode' => 'Markdown',
'disable_web_page_preview' => true,
];
$options = [
'http' => [
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'POST',
'content' => http_build_query($data),
'ignore_errors' => true,
'timeout' => 10,
],
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
];
$context = stream_context_create($options);
$result = @file_get_contents($url, false, $context);
if ($result === false) {
$error = error_get_last();
logMessage("Ошибка отправки в Telegram: Не удалось выполнить запрос. " . ($error['message'] ?? 'Нет деталей'));
return false;
}
$response = json_decode($result, true);
if (!$response || !$response['ok']) {
$errorCode = $response['error_code'] ?? 'N/A';
$description = $response['description'] ?? 'No description';
$problematicPart = mb_substr($message, 0, 200);
logMessage("Ошибка отправки в Telegram: API вернуло ошибку {$errorCode} - {$description}. Начало сообщения: {$problematicPart}");
return false;
}
return true;
}
function getFilesInDirectory(string $dir, array $excludePaths, string $baseDir): array
{
$files = [];
$baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$dir = rtrim($dir, DIRECTORY_SEPARATOR);
if (!is_dir($dir)) {
logMessage("Ошибка: Директория '{$dir}' не найдена или не является директорией.");
return [];
}
try {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS | RecursiveDirectoryIterator::FOLLOW_SYMLINKS),
RecursiveIteratorIterator::SELF_FIRST
);
$iterator->setMaxDepth(-1);
foreach ($iterator as $fileInfo) {
$realPath = $fileInfo->getRealPath();
if ($realPath === false) {
logMessage("Предупреждение: Не удалось получить реальный путь для: " . $fileInfo->getPathname());
continue;
}
$relativePath = str_replace($baseDir, '', $realPath);
$normalizedRelativePath = str_replace(DIRECTORY_SEPARATOR, '/', $relativePath);
$isExcluded = false;
foreach ($excludePaths as $exclude) {
if (strpos($normalizedRelativePath, rtrim($exclude, '/')) === 0) {
if (substr($exclude, -1) === '/') {
if (strpos($normalizedRelativePath . '/', $exclude) === 0) {
$isExcluded = true;
break;
}
} else {
if ($normalizedRelativePath === $exclude) {
$isExcluded = true;
break;
}
}
}
}
if ($isExcluded) continue;
if ($fileInfo->isFile() && $fileInfo->isReadable()) {
$hash = @md5_file($realPath);
if ($hash === false) {
logMessage("Предупреждение: Не удалось вычислить хеш для файла: {$relativePath}");
continue;
}
$files[$relativePath] = $hash;
}
}
} catch (Exception $e) {
logMessage("Ошибка при сканировании директории '{$dir}': " . $e->getMessage());
}
return $files;
}
function loadHashes(string $filepath): array
{
if (!file_exists($filepath) || !is_readable($filepath)) return [];
$content = @file_get_contents($filepath);
if ($content === false) {
logMessage("Ошибка: Не удалось прочитать файл хешей '{$filepath}'.");
return [];
}
$hashes = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
logMessage("Ошибка: Не удалось декодировать JSON из файла хешей '{$filepath}'. Ошибка: " . json_last_error_msg());
if (!empty(trim($content))) {
$backupFile = $filepath . '.bak.' . date('YmdHis');
@copy($filepath, $backupFile);
logMessage("Создана резервная копия поврежденного файла хешей: {$backupFile}");
}
return [];
}
return is_array($hashes) ? $hashes : [];
}
function saveHashes(string $filepath, array $hashes): bool
{
$jsonContent = json_encode($hashes, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
if (json_last_error() !== JSON_ERROR_NONE) {
logMessage("Ошибка: Не удалось закодировать хеши в JSON. Ошибка: " . json_last_error_msg());
return false;
}
if (@file_put_contents($filepath, $jsonContent) === false) {
logMessage("Ошибка: Не удалось записать хеши в файл '{$filepath}'. Проверьте права на запись.");
return false;
}
return true;
}
// --- Основная логика ---
logMessage("Запуск скрипта мониторинга.");
if (!is_dir(MONITOR_DIR)) {
$errorMsg = "Критическая ошибка: Директория для мониторинга '" . MONITOR_DIR . "' не существует или недоступна.";
logMessage($errorMsg);
sendTelegramNotification("⚠️ Ошибка конфигурации мониторинга файлов:\nДиректория `" . sanitizeForTelegramCodeSpan(MONITOR_DIR) . "` не найдена.");
exit(1);
}
$oldHashes = loadHashes(HASH_FILE);
$isFirstRun = empty($oldHashes);
if ($isFirstRun) {
logMessage("Файл хешей '" . HASH_FILE . "' не найден или пуст. Считаем это первым запуском.");
} else {
logMessage("Загружено " . count($oldHashes) . " хешей из файла '" . HASH_FILE . "'.");
}
logMessage("Начинаю сканирование директории '" . MONITOR_DIR . "'.");
$currentHashes = getFilesInDirectory(MONITOR_DIR, $excludePaths, MONITOR_DIR);
logMessage("Сканирование завершено. Найдено " . count($currentHashes) . " файлов для проверки.");
$addedFiles = [];
$deletedFiles = [];
$modifiedFiles = [];
foreach ($currentHashes as $file => $hash) {
if (!isset($oldHashes[$file])) {
$addedFiles[] = $file;
} elseif ($oldHashes[$file] !== $hash) {
$modifiedFiles[] = $file;
}
}
foreach ($oldHashes as $file => $hash) {
if (!isset($currentHashes[$file])) {
$deletedFiles[] = $file;
}
}
$totalChanges = count($addedFiles) + count($deletedFiles) + count($modifiedFiles);
if ($totalChanges > 0) {
logMessage("Обнаружены изменения: Добавлено: " . count($addedFiles) . ", Удалено: " . count($deletedFiles) . ", Изменено: " . count($modifiedFiles));
if ($isFirstRun) {
logMessage("Первый запуск: инициализация списка файлов. Уведомление в Telegram не отправлено.");
} else {
$message = "⚠️ *Обнаружены изменения в файлах сайта!*\n\n";
$message .= "Проверена директория: `" . sanitizeForTelegramCodeSpan(MONITOR_DIR) . "`\n";
$message .= "Время проверки: " . date('Y-m-d H:i:s') . "\n\n";
$filesListed = 0;
if (!empty($modifiedFiles)) {
$message .= "*Измененные файлы (" . count($modifiedFiles) . " шт.):*\n";
foreach ($modifiedFiles as $file) {
if ($filesListed >= MAX_FILES_IN_MESSAGE) {
$message .= "- ...и еще " . (count($modifiedFiles) - $filesListed) . " файлов\n";
break;
}
$message .= "- `" . sanitizeForTelegramCodeSpan($file) . "`\n";
$filesListed++;
}
$message .= "\n";
}
$filesListed = 0;
if (!empty($addedFiles)) {
$message .= "*Добавленные файлы (" . count($addedFiles) . " шт.):*\n";
foreach ($addedFiles as $file) {
if ($filesListed >= MAX_FILES_IN_MESSAGE) {
$message .= "- ...и еще " . (count($addedFiles) - $filesListed) . " файлов\n";
break;
}
$message .= "- `" . sanitizeForTelegramCodeSpan($file) . "`\n";
$filesListed++;
}
$message .= "\n";
}
$filesListed = 0;
if (!empty($deletedFiles)) {
$message .= "*Удаленные файлы (" . count($deletedFiles) . " шт.):*\n";
foreach ($deletedFiles as $file) {
if ($filesListed >= MAX_FILES_IN_MESSAGE) {
$message .= "- ...и еще " . (count($deletedFiles) - $filesListed) . " файлов\n";
break;
}
$message .= "- `" . sanitizeForTelegramCodeSpan($file) . "`\n";
$filesListed++;
}
$message .= "\n";
}
$maxLen = 4000;
if (mb_strlen($message, 'UTF-8') > $maxLen) {
logMessage("Сообщение слишком длинное (" . mb_strlen($message, 'UTF-8') . " символов), будет разбито на части.");
$partsSentSuccessfully = 0;
$lines = explode("\n", $message);
$currentPart = "";
foreach ($lines as $line) {
if (mb_strlen($currentPart . $line . "\n", 'UTF-8') > $maxLen) {
if (!empty(trim($currentPart))) {
if (sendTelegramNotification(trim($currentPart))) {
$partsSentSuccessfully++;
} else {
logMessage("Не удалось отправить часть уведомления в Telegram (после разбивки по строкам).");
}
usleep(500000);
}
$currentPart = $line . "\n";
} else {
$currentPart .= $line . "\n";
}
}
if (!empty(trim($currentPart))) {
if (sendTelegramNotification(trim($currentPart))) {
$partsSentSuccessfully++;
} else {
logMessage("Не удалось отправить последнюю часть уведомления в Telegram.");
}
}
if($partsSentSuccessfully > 0) logMessage("Уведомление успешно отправлено в Telegram ($partsSentSuccessfully частей).");
} else {
if (sendTelegramNotification($message)) {
logMessage("Уведомление успешно отправлено в Telegram (целиком).");
} else {
logMessage("Не удалось отправить уведомление в Telegram (целиком).");
}
}
}
logMessage("Сохраняю актуальное состояние хешей в файл '" . HASH_FILE . "'.");
if (!saveHashes(HASH_FILE, $currentHashes)) {
logMessage("Критическая ошибка: Не удалось сохранить файл хешей. Изменения при следующем запуске могут быть определены некорректно.");
sendTelegramNotification("⚠️ Ошибка мониторинга файлов:\nНе удалось сохранить файл хешей `" . sanitizeForTelegramCodeSpan(HASH_FILE) . "`. Проверьте права на запись.");
}
} else {
logMessage("Изменений в файлах не обнаружено.");
if ($oldHashes !== $currentHashes) {
logMessage("Сохраняю актуальное состояние хешей (несмотря на отсутствие изменений, предыдущее сохранение могло быть неполным или файл хешей был поврежден).");
if (!saveHashes(HASH_FILE, $currentHashes)) {
logMessage("Критическая ошибка: Не удалось сохранить файл хешей при отсутствии изменений. Проверьте права на запись для файла: " . HASH_FILE);
}
}
}
logMessage("Завершение работы скрипта.");
exit(0);
Комментарии: 9
Хорошее решения для того что бы не отслеживать сайт самому, да ещё и уведомляет в телеграм, интересно на сколько сильно загружает во время обработки, а так супер, если будет приложение, я думаю будет пользоваться спросом
Круто! Считай готовый «антивирус». Конечно голосуем за пакет! (и поддержку sсheduler для периодического запуска :))
А вы какой планировщик имеете ввиду? (Scheduler — modstore.pro/packages/utilities/scheduler) он?
Но там ведь нет возможности указать периодичность запусков заданий. Только можно указать точное время запуска.
При такой структуре Scheduler'а, для достижения периодичности выполнения нужно будет реализовать логику самоперепланирования внутри скриптов, а первый запуск придется сделать вручную. Это как-то не очень удобно не находите? и не очень надежно. Если вы знаете в каком компоненте есть реализация нормальной работы с этим планировщиком напишите. я гляну, вдруг что-то не так понимаю.
Но там ведь нет возможности указать периодичность запусков заданий. Только можно указать точное время запуска.
При такой структуре Scheduler'а, для достижения периодичности выполнения нужно будет реализовать логику самоперепланирования внутри скриптов, а первый запуск придется сделать вручную. Это как-то не очень удобно не находите? и не очень надежно. Если вы знаете в каком компоненте есть реализация нормальной работы с этим планировщиком напишите. я гляну, вдруг что-то не так понимаю.
Насколько я вижу из кода, скрипт в первый раз сохраняет хэши файлов, а потом их проверяет. Но если сайт уже заражён — то это никак не поможет.
Подкидываю альтернативную идею, если интересно — проверять версию MODX (или брать из настроек), скачивать соответствующий дистрибутив, и проверять хэши файлов сайта по файлам дистрибутива.
То есть, берём оригинальные файлы index.php в connectors, manager и корне, а так же файлы из core — и проверяем, чтобы все они присутствовали на сайте с оригинальным хэшем.
Если все основные файлы не изменены, то сайт не заражён и должен работать корректно.
Правда, есть еще возможность заражения только файлов дополнений, без ядра. Наверное, можно и их сверять с дистрибутивами из репозитория по той же логике — скачать нужную версию и сравнить хэши…
Кстати, вот вам еще идея — создать онлайн базу для проверки хэшей файлов MODX и дополнений через API. Чтобы простые GET запросы, типа /api/hash/modx/2.8.1/core/model/modx.class.php возвращали sha1 хэш запрошенного файла или 404.
Конечно, это не спасёт от уже залитых шеллов и вредоносов, но они не будут запускаться через сайт. А если запустятся и что-то изменят, то следующая проверка это покажет. И если раз за разом файлы будут меняться — то можно уже более внимательно искать, что там такое у вас залито.
Подкидываю альтернативную идею, если интересно — проверять версию MODX (или брать из настроек), скачивать соответствующий дистрибутив, и проверять хэши файлов сайта по файлам дистрибутива.
То есть, берём оригинальные файлы index.php в connectors, manager и корне, а так же файлы из core — и проверяем, чтобы все они присутствовали на сайте с оригинальным хэшем.
Если все основные файлы не изменены, то сайт не заражён и должен работать корректно.
Правда, есть еще возможность заражения только файлов дополнений, без ядра. Наверное, можно и их сверять с дистрибутивами из репозитория по той же логике — скачать нужную версию и сравнить хэши…
Кстати, вот вам еще идея — создать онлайн базу для проверки хэшей файлов MODX и дополнений через API. Чтобы простые GET запросы, типа /api/hash/modx/2.8.1/core/model/modx.class.php возвращали sha1 хэш запрошенного файла или 404.
Конечно, это не спасёт от уже залитых шеллов и вредоносов, но они не будут запускаться через сайт. А если запустятся и что-то изменят, то следующая проверка это покажет. И если раз за разом файлы будут меняться — то можно уже более внимательно искать, что там такое у вас залито.
создать онлайн базу для проверки хэшей файлов MODX и дополнений через APIИнтересная идея, я бы занялся на досуге. Ты мог бы в общих чертах описать для каких файлов делать хэш у ядра и у компонентов?
Да для всех, какие есть в дистрибутиве — лишнего-то там быть ничего не должно, по идее.
В идеале вообще автоматизировать и качать всё новое бесплатное из репозитория MODX / modstore, распаковывать и забивать в БД:
— название: MODX или дополнение
— версия дистрибутива
— путь к файлу
— sha1 хэш файла
В идеале вообще автоматизировать и качать всё новое бесплатное из репозитория MODX / modstore, распаковывать и забивать в БД:
— название: MODX или дополнение
— версия дистрибутива
— путь к файлу
— sha1 хэш файла
А файлы ядра тоже все хэшировать?
Да, конечно. Разве что кроме архива core.transport.zip.
Смысл же в том, чтобы сравнить свои файлы с эталонными на предмет изменений. Файлы ядра меняться не должны.
Смысл же в том, чтобы сравнить свои файлы с эталонными на предмет изменений. Файлы ядра меняться не должны.
Компонент создан для MODX2 и MODX3. Загрузил в репозиторий, жду модерации.
Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.