""" Security module для PyGuardian Основная логика обнаружения угроз и скрытого реагирования на взломы """ import asyncio import logging import secrets import string import subprocess from datetime import datetime, timedelta from typing import Dict, List, Optional, Callable from cryptography.fernet import Fernet import base64 import json logger = logging.getLogger(__name__) class SecurityManager: """Менеджер безопасности - ключевой компонент системы""" def __init__(self, storage, firewall_manager, config: Dict): self.storage = storage self.firewall_manager = firewall_manager self.config = config # Параметры безопасности self.max_attempts = config.get('max_attempts', 5) self.time_window = config.get('time_window', 60) self.whitelist = config.get('whitelist', []) # Параметры для детекции взломов self.authorized_users = config.get('authorized_users', []) self.honeypot_users = config.get('honeypot_users', []) self.stealth_mode_duration = config.get('stealth_mode_duration', 300) # 5 минут # Шифрование для паролей self.encryption_key = self._get_or_create_key() self.cipher = Fernet(self.encryption_key) # Callbacks self.compromise_callback: Optional[Callable] = None self.ban_callback: Optional[Callable] = None def _get_or_create_key(self) -> bytes: """Получить или создать ключ шифрования""" key_file = "/var/lib/pyguardian/encryption.key" try: with open(key_file, 'rb') as f: return f.read() except FileNotFoundError: # Создаем новый ключ import os os.makedirs(os.path.dirname(key_file), exist_ok=True) key = Fernet.generate_key() with open(key_file, 'wb') as f: f.write(key) os.chmod(key_file, 0o600) # Только root может читать return key def set_callbacks(self, compromise_callback: Optional[Callable] = None, ban_callback: Optional[Callable] = None) -> None: """Установка callbacks для уведомлений""" self.compromise_callback = compromise_callback self.ban_callback = ban_callback async def analyze_login_event(self, event) -> None: """Анализ события входа в систему""" try: if event.is_success: await self._handle_successful_login(event) else: await self._handle_failed_login(event) except Exception as e: logger.error(f"Ошибка анализа события входа: {e}") async def _handle_failed_login(self, event) -> None: """Обработка неудачного входа""" # Проверяем белый список if await self.storage.is_whitelisted(event.ip_address, self.whitelist): return # Получаем количество попыток attempts = await self.storage.get_attack_count_for_ip(event.ip_address, self.time_window) # Проверяем honeypot пользователей if event.username in self.honeypot_users: logger.warning(f"Попытка входа под honeypot пользователем {event.username} от {event.ip_address}") # Мгновенный бан за попытку honeypot входа await self._execute_ban(event.ip_address, f"Попытка входа под honeypot пользователем {event.username}") return # Обычная логика брутфорса if attempts >= self.max_attempts: await self._execute_ban(event.ip_address, f"Брутфорс атака: {attempts} попыток за {self.time_window}с") async def _handle_successful_login(self, event) -> None: """КРИТИЧЕСКАЯ ФУНКЦИЯ: Обработка УСПЕШНОГО входа (потенциальный взлом)""" logger.info(f"Анализ успешного входа: {event.username}@{event.ip_address}") # Проверяем признаки взлома is_compromised = await self._detect_compromise(event) if is_compromised: logger.critical(f"🚨 ОБНАРУЖЕН ВЗЛОМ: {event.username}@{event.ip_address}") await self._handle_compromise(event) else: logger.info(f"✅ Легитимный вход: {event.username}@{event.ip_address}") # Записываем как успешный легитимный вход await self.storage.add_successful_login( event.ip_address, event.username, "legitimate_login" ) async def _detect_compromise(self, event) -> bool: """Детекция признаков взлома""" suspicious_indicators = [] # 1. IP был замечен в брутфорс атаках recent_attempts = await self.storage.get_attack_count_for_ip(event.ip_address, 3600) # За час if recent_attempts > 0: suspicious_indicators.append(f"Предыдущие атаки: {recent_attempts}") # 2. IP не в белом списке if not await self.storage.is_whitelisted(event.ip_address, self.whitelist): suspicious_indicators.append("IP не в белом списке") # 3. Пользователь не должен входить извне if event.username not in self.authorized_users: suspicious_indicators.append(f"Неавторизованный пользователь: {event.username}") # 4. Honeypot пользователь (это точно взлом!) if event.username in self.honeypot_users: suspicious_indicators.append(f"HONEYPOT пользователь: {event.username}") return True # Безусловно взлом # 5. Проверяем паттерны времени (например, вход ночью) current_hour = datetime.now().hour if current_hour < 6 or current_hour > 23: # Подозрительное время suspicious_indicators.append(f"Подозрительное время: {current_hour}:xx") # 6. Проверяем предыдущие взломы с этого IP details = await self.storage.get_ip_details(event.ip_address) if details.get('previous_compromises', 0) > 0: suspicious_indicators.append("Предыдущие взломы с этого IP") logger.info(f"Индикаторы подозрительности для {event.ip_address}: {suspicious_indicators}") # Считаем взломом если есть >= 2 индикаторов return len(suspicious_indicators) >= 2 async def _handle_compromise(self, event) -> None: """🔥 СКРЫТОЕ РЕАГИРОВАНИЕ НА ВЗЛОМ""" logger.critical(f"Инициация скрытой реакции на взлом {event.username}@{event.ip_address}") compromise_info = { 'ip': event.ip_address, 'username': event.username, 'timestamp': event.timestamp, 'detection_time': datetime.now(), 'session_active': True } try: # 1. МГНОВЕННО блокируем IP (скрытно - новые подключения невозможны) await self._stealth_block_ip(event.ip_address) # 2. АВТОМАТИЧЕСКИ меняем пароль пользователя new_password = await self._change_user_password(event.username) compromise_info['new_password'] = new_password # 3. Записываем инцидент в базу await self._record_compromise(compromise_info) # 4. Получаем информацию об активной сессии session_info = await self._get_active_sessions(event.username) compromise_info['sessions'] = session_info # 5. Уведомляем администратора через Telegram if self.compromise_callback: await self.compromise_callback(compromise_info) logger.info("✅ Скрытая реакция на взлом выполнена успешно") except Exception as e: logger.error(f"❌ Ошибка в скрытой реакции на взлом: {e}") # Даже при ошибке пытаемся уведомить if self.compromise_callback: compromise_info['error'] = str(e) await self.compromise_callback(compromise_info) async def _stealth_block_ip(self, ip: str) -> None: """Скрытная блокировка IP (новые соединения)""" try: # Блокируем через firewall success = await self.firewall_manager.ban_ip(ip) if success: # Записываем в базу как компромисс-бан await self.storage.ban_ip( ip, "🚨 АВТОМАТИЧЕСКИЙ БАН - ОБНАРУЖЕН ВЗЛОМ", 86400, # 24 часа manual=False, attempts_count=999 # Специальный маркер взлома ) logger.info(f"🔒 IP {ip} скрытно заблокирован (взлом)") else: logger.error(f"❌ Не удалось заблокировать IP {ip}") except Exception as e: logger.error(f"Ошибка скрытной блокировки IP {ip}: {e}") async def _change_user_password(self, username: str) -> str: """Автоматическая смена пароля пользователя""" try: # Генерируем криптостойкий пароль new_password = self.generate_secure_password() # Меняем пароль через chpasswd process = await asyncio.create_subprocess_exec( 'chpasswd', stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) # Отправляем username:password в chpasswd password_input = f"{username}:{new_password}\n" stdout, stderr = await process.communicate(password_input.encode()) if process.returncode == 0: # Сохраняем зашифрованный пароль encrypted_password = self.cipher.encrypt(new_password.encode()).decode() await self._store_password(username, encrypted_password) logger.info(f"🔑 Пароль пользователя {username} автоматически изменен") return new_password else: error = stderr.decode() if stderr else "Unknown error" logger.error(f"❌ Ошибка смены пароля для {username}: {error}") return None except Exception as e: logger.error(f"Ошибка смены пароля для {username}: {e}") return None def generate_secure_password(self, length: int = 16) -> str: """Генерация криптостойкого пароля""" # Используем все безопасные символы alphabet = string.ascii_letters + string.digits + "!@#$%^&*" # Обеспечиваем наличие разных типов символов password = [ secrets.choice(string.ascii_lowercase), secrets.choice(string.ascii_uppercase), secrets.choice(string.digits), secrets.choice("!@#$%^&*") ] # Добавляем оставшиеся символы for _ in range(length - 4): password.append(secrets.choice(alphabet)) # Перемешиваем secrets.SystemRandom().shuffle(password) return ''.join(password) async def _store_password(self, username: str, encrypted_password: str) -> None: """Сохранение зашифрованного пароля""" try: passwords_file = "/var/lib/pyguardian/passwords.json" # Загружаем существующие пароли try: with open(passwords_file, 'r') as f: passwords = json.load(f) except FileNotFoundError: passwords = {} # Добавляем новый пароль с timestamp passwords[username] = { 'password': encrypted_password, 'changed_at': datetime.now().isoformat(), 'reason': 'compromise_detection' } # Сохраняем import os os.makedirs(os.path.dirname(passwords_file), exist_ok=True) with open(passwords_file, 'w') as f: json.dump(passwords, f, indent=2) os.chmod(passwords_file, 0o600) except Exception as e: logger.error(f"Ошибка сохранения пароля для {username}: {e}") async def get_stored_password(self, username: str) -> Optional[str]: """Получение сохраненного пароля""" try: passwords_file = "/var/lib/pyguardian/passwords.json" with open(passwords_file, 'r') as f: passwords = json.load(f) if username in passwords: encrypted = passwords[username]['password'].encode() return self.cipher.decrypt(encrypted).decode() return None except Exception as e: logger.error(f"Ошибка получения пароля для {username}: {e}") return None async def _get_active_sessions(self, username: str = None) -> List[Dict]: """Получение информации об активных SSH сессиях""" try: sessions = [] # Используем who для получения активных сессий process = await asyncio.create_subprocess_exec( 'who', '-u', stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await process.communicate() if process.returncode == 0: lines = stdout.decode().strip().split('\n') for line in lines: if line.strip(): parts = line.split() if len(parts) >= 7: session_user = parts[0] tty = parts[1] login_time = ' '.join(parts[2:6]) pid = parts[6] # Фильтруем по пользователю если указан if username is None or session_user == username: sessions.append({ 'username': session_user, 'tty': tty, 'login_time': login_time, 'pid': pid.strip('()'), 'type': 'ssh' if 'pts' in tty else 'console' }) return sessions except Exception as e: logger.error(f"Ошибка получения активных сессий: {e}") return [] async def terminate_user_sessions(self, username: str) -> int: """Завершение всех сессий пользователя""" try: # Получаем активные сессии sessions = await self._get_active_sessions(username) terminated = 0 for session in sessions: pid = session.get('pid') if pid and pid.isdigit(): try: # Завершаем процесс process = await asyncio.create_subprocess_exec( 'kill', '-KILL', pid, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) await process.communicate() if process.returncode == 0: terminated += 1 logger.info(f"🔪 Завершена сессия {session['tty']} (PID {pid}) пользователя {username}") except Exception as e: logger.error(f"Ошибка завершения сессии PID {pid}: {e}") logger.info(f"Завершено {terminated} сессий пользователя {username}") return terminated except Exception as e: logger.error(f"Ошибка завершения сессий пользователя {username}: {e}") return 0 async def _record_compromise(self, compromise_info: Dict) -> None: """Запись информации о взломе в базу""" try: # Расширяем таблицу компромиссов в storage await self.storage.record_compromise(compromise_info) except Exception as e: logger.error(f"Ошибка записи компромисса: {e}") async def _execute_ban(self, ip: str, reason: str) -> None: """Выполнение бана IP""" try: # Записываем в базу данных success = await self.storage.ban_ip( ip, reason, self.config.get('unban_time', 3600), manual=False, attempts_count=0 ) if success: # Блокируем через firewall firewall_success = await self.firewall_manager.ban_ip(ip) if firewall_success: logger.warning(f"IP {ip} забанен: {reason}") # Уведомление через callback if self.ban_callback: ban_info = { 'ip': ip, 'reason': reason, 'attempts': 0, 'auto': True } await self.ban_callback(ban_info) else: logger.error(f"Не удалось заблокировать IP {ip} через firewall") else: logger.error(f"Не удалось записать бан IP {ip} в базу данных") except Exception as e: logger.error(f"Ошибка выполнения бана IP {ip}: {e}") async def manual_password_change(self, username: str, new_password: str = None) -> str: """Ручная смена пароля через Telegram""" if new_password is None: new_password = self.generate_secure_password() try: # Меняем пароль process = await asyncio.create_subprocess_exec( 'chpasswd', stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) password_input = f"{username}:{new_password}\n" stdout, stderr = await process.communicate(password_input.encode()) if process.returncode == 0: # Сохраняем зашифрованный пароль encrypted_password = self.cipher.encrypt(new_password.encode()).decode() await self._store_password(username, encrypted_password) logger.info(f"🔑 Пароль пользователя {username} изменен вручную") return new_password else: error = stderr.decode() if stderr else "Unknown error" logger.error(f"❌ Ошибка ручной смены пароля для {username}: {error}") return None except Exception as e: logger.error(f"Ошибка ручной смены пароля для {username}: {e}") return None class HoneypotManager: """Менеджер honeypot пользователей и ловушек""" def __init__(self, config: Dict): self.honeypot_users = config.get('honeypot_users', []) self.fake_services = config.get('fake_services', {}) async def setup_honeypots(self) -> None: """Настройка honeypot пользователей""" try: for user in self.honeypot_users: await self._create_honeypot_user(user) except Exception as e: logger.error(f"Ошибка настройки honeypot: {e}") async def _create_honeypot_user(self, username: str) -> None: """Создание honeypot пользователя""" try: # Проверяем, существует ли пользователь process = await asyncio.create_subprocess_exec( 'id', username, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) await process.communicate() if process.returncode != 0: # Пользователь не существует, создаем process = await asyncio.create_subprocess_exec( 'useradd', '-m', '-s', '/bin/bash', username, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) await process.communicate() if process.returncode == 0: # Устанавливаем слабый пароль для honeypot weak_password = username # Очень слабый пароль process = await asyncio.create_subprocess_exec( 'chpasswd', stdin=asyncio.subprocess.PIPE ) password_input = f"{username}:{weak_password}\n" await process.communicate(password_input.encode()) logger.info(f"🍯 Honeypot пользователь {username} создан") else: logger.error(f"Ошибка создания honeypot пользователя {username}") except Exception as e: logger.error(f"Ошибка создания honeypot пользователя {username}: {e}")