import os import re import socket import logging import asyncio import httpx from typing import Optional logger = logging.getLogger("CyberAgentCommands") # --- ВСПОМОГАТЕЛЬНЫЕ УТИЛИТЫ --- def _extract_target(text: str) -> str: """ Универсальный безопасный извлекатель доменов/URL из текста. Находит первый похожий на домен или URL элемент, игнорируя команды и лишний текст. """ # Удаляем телеграм-команды вроде /ip, /headers, /mx_audit clean_text = re.sub(sub_pattern := r"/[a-zA-Z0-9_]+", "", text).strip() # Ищем паттерны URL или доменных имен domain_match = re.search(r'(https?://[^\s]+|[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})', clean_text) if domain_match: return domain_match.group(1).strip().strip('"').strip("'") # Фолбэк на старое поведение, если регулярка не сработала return text.split()[-1] if text.split() else "" # --- 1. ИНТЕГРАЦИЯ С РЕПОЗИТОРИЕМ --- def create_or_update_file(path: str, content: str, commit_message: str) -> str: """Создает или изменяет файл в репозитории GitHub через ядро.""" try: # Динамический импорт для исключения циклических зависимостей при инициализации from api.index import github success = github.commit_file(path, content, commit_message) return f"✅ Файл {path} успешно сохранен в репозитории GitHub!" if success else f"❌ Ошибка записи файла {path}." except Exception as e: logger.error(f"Ошибка интеграции с GitHub при записи {path}: {e}") return f"❌ Ошибка интеграции с GitHub: {str(e)}" # --- 2. OSINT: АНАЛИЗ HTTP-ЗАГОЛОВКОВ --- async def check_web_headers(url: str) -> str: """OSINT-инструмент: собирает и анализирует заголовки безопасности веб-сервера.""" target_url = _extract_target(url) if not target_url: return "❌ Не удалось распознать URL для анализа заголовков." if not target_url.startswith("http://") and not target_url.startswith("https://"): target_url = "https://" + target_url async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client: try: res = await client.get(target_url, headers={"User-Agent": "Mozilla/5.0 CyberAgent/1.0", "Accept": "*/*"}) headers = res.headers csp = headers.get('Content-Security-Policy') hsts = headers.get('Strict-Transport-Security') xfo = headers.get('X-Frame-Options') xcto = headers.get('X-Content-Type-Options') analysis = ( f"📊 *Анализ заголовков безопасности для {target_url}:*\n\n" f"• *Server:* `{headers.get('Server', 'Не указан / Скрыт')}`\n" f"• *X-Powered-By:* `{headers.get('X-Powered-By', 'Не указан / Скрыт')}`\n" f"• *Content-Security-Policy:* {'✅ Настроен' if csp else '❌ ОТСУТСТВУЕТ (Риск XSS/Инъекций)'}\n" f"• *Strict-Transport-Security (HSTS):* {'✅ Настроен' if hsts else '⚠️ Отсутствует (Риск MitM-атак)'}\n" f"• *X-Frame-Options:* {'✅ Настроен' if xfo else '⚠️ Отсутствует (Риск Clickjacking)'}\n" f"• *X-Content-Type-Options:* {'✅ Настроен' if xcto else '⚠️ Отсутствует (Риск MIME-sniffing)'}\n" ) return analysis except httpx.TimeoutException: return f"❌ Превышено время ожидания (Timeout) при подключении к {target_url}." except httpx.ConnectError: return f"❌ Ошибка соединения. Не удалось разрешить имя хоста или установить связь с {target_url}." except Exception as e: return f"❌ Ошибка сканирования заголовков для {target_url}: {str(e)}" # --- 3. OSINT: DNS RESOLVER --- async def resolve_host_ip(domain: str) -> str: """OSINT-инструмент: асинхронно преобразует доменное имя в IP-адрес.""" target = _extract_target(domain) clean_domain = target.replace("https://", "").replace("http://", "").split("/")[0].split(":")[0] if not clean_domain: return "❌ Не удалось извлечь корректное доменное имя для DNS Lookup." try: # Выполняем блокирующий системный вызов DNS в отдельном пуле потоков ip_addr = await asyncio.to_thread(socket.gethostbyname, clean_domain) return f"🌐 *Домен:* `{clean_domain}`\n📍 *IP-адрес:* `{ip_addr}`" except socket.gaierror: return f"❌ Ошибка разрешений DNS: Хост `{clean_domain}` не найден в глобальной сети." except Exception as e: return f"❌ Ошибка разрешения DNS для `{clean_domain}`: {str(e)}" # --- 4. OSINT: КОМПЛЕКСНЫЙ АУДИТ ПОЧТЫ (MX, SPF, DMARC) --- async def check_mx_audit(domain: str) -> str: """ Инструмент комплексного аудита почтовой безопасности домена. Работает строго через HTTPS API DoH (DNS-over-HTTPS) Google. """ target = _extract_target(domain) clean_domain = target.replace("https://", "").replace("http://", "").split("/")[0].split(":")[0] if not clean_domain: return "❌ Не удалось распознать целевой домен для почтового аудита." dns_api_url = "https://dns.google/resolve" mx_servers = [] spf_records = [] dmarc_records = [] async with httpx.AsyncClient(timeout=10.0) as client: # 1. Запрос MX-записей try: res = await client.get(dns_api_url, params={"name": clean_domain, "type": "MX"}) if res.status_code == 200 and "Answer" in res.json(): mx_servers = [record["data"] for record in res.json()["Answer"] if "data" in record] except Exception as e: logger.error(f"Ошибка запроса MX для {clean_domain}: {e}") # 2. Запрос SPF (TXT на основном домене) try: res = await client.get(dns_api_url, params={"name": clean_domain, "type": "TXT"}) if res.status_code == 200 and "Answer" in res.json(): for record in res.json()["Answer"]: if "data" in record: # Удаляем внешние экранирующие кавычки от DNS ответа data_clean = record["data"].strip().strip('"').strip('\\"') if data_clean.startswith("v=spf1"): spf_records.append(data_clean) except Exception as e: logger.error(f"Ошибка SPF запроса для {clean_domain}: {e}") # 3. Запрос DMARC (TXT на поддомене _dmarc) try: res = await client.get(dns_api_url, params={"name": f"_dmarc.{clean_domain}", "type": "TXT"}) if res.status_code == 200 and "Answer" in res.json(): for record in res.json()["Answer"]: if "data" in record: data_clean = record["data"].strip().strip('"').strip('\\"') if data_clean.upper().startswith("V=DMARC1"): dmarc_records.append(data_clean) except Exception as e: logger.error(f"Ошибка DMARC запроса для _dmarc.{clean_domain}: {e}") # --- Аналитический блок вердиктов Web3CyberServices --- mx_status = f"✅ Настроены ({len(mx_servers)} серверов)" if mx_servers else "❌ ОТСУТСТВУЮТ (Почта на домен не будет доставлена)" spf_status = "❌ ОТСУТСТВУЕТ (Защита от спуфинга отключена)" spf_verdict = "• 🚨 *Критическая уязвимость:* Отсутствие записи SPF позволяет злоумышленникам отправлять фишинг с вашего домена." anti_spoofing_safe = False if spf_records: spf_str = spf_records[0] if "-all" in spf_str: spf_status = f"✅ Настроен строго (`{spf_str}`)" spf_verdict = "• *SPF Конфигурация:* Настроено жесткое отклонение (`-all`). Неавторизованные узлы блокируются." anti_spoofing_safe = True elif "~all" in spf_str: spf_status = f"⚠️ Настроен мягко (`{spf_str}`)" spf_verdict = "• ⚠️ *Уязвимая конфигурация:* Использование мягкого флага `~all` вместо `-all`. Почтовые провайдеры могут пропустить спуфинговые письма в спам." else: spf_status = f"⚠️ Настроен с флагами (`{spf_str}`)" spf_verdict = "• *SPF Конфигурация:* Проверьте валидность флагов политики прохода писем." dmarc_status = "❌ ОТСУТСТВУЕТ (Риск подделки заголовков)" dmarc_verdict = "• 🚨 *Критическая уязвимость:* Без DMARC невозможно контролировать подделку видимого поля 'From' в письмах." dmarc_safe = False if dmarc_records: dmarc_str = dmarc_records[0] dmarc_status = f"✅ Настроен (`{dmarc_str}`)" dmarc_str_lower = dmarc_str.lower() if "p=reject" in dmarc_str_lower: dmarc_verdict = "• *DMARC Политика:* Максимальная защита (`p=reject`). Поддельные письма уничтожаются на стороне получателя." dmarc_safe = True elif "p=quarantine" in dmarc_str_lower: dmarc_verdict = "• *DMARC Политика:* Средняя защита (`p=quarantine`). Письма-клоны принудительно перемещаются в спам." dmarc_safe = True else: dmarc_verdict = "• ⚠️ *DMARC Мониторинг:* Установлен флаг `p=none`. Защита работает в режиме сбора статистики, реальной блокировки спуфинга не происходит." final_verdict = "🛡️ Домен надежно защищен от спуфинга и подделки почты." if (anti_spoofing_safe and dmarc_safe) else "⚠️ ВНИМАНИЕ: Обнаружены бреши! Домен уязвим для Email-спуфинга." analysis = ( f"✉️ *Аудит почтовой безопасности домена `{clean_domain}`:*\n\n" f"• *Записи MX:* {mx_status}\n" f"• *Политика SPF:* {spf_status}\n" f"• *Политика DMARC:* {dmarc_status}\n\n" f"📋 *Аналитический вердикт Web3CyberServices:*\n" f"{spf_verdict}\n" f"{dmarc_verdict}\n\n" f"🔒 *Итог:* `{final_verdict}`" ) return analysis # --- 5. ФОЛЛБЭК: ЛОКАЛЬНЫЙ ТЕКСТОВЫЙ ПРОЦЕССОР --- def get_fallback_response(user_text: str) -> str: """Локальный аварийный текстовый обработчик на случай сбоя всех ИИ контуров.""" return ( f"🤖 *Локальный режим Cyber-Agent.*\n\n" f"⚠️ Все внешние ИИ-интерфейсы сейчас недоступны.\n" f"Доступные офлайн-команды:\n" f"• `/mx_audit google.com` — аудит почтовой безопасности домена\n" f"• `/ip google.com` — узнать IP-адрес узла\n" f"• `/headers github.com` — анализ HTTP-заголовков безопасности" ) # --- 6. РЕЗЕРВНЫЙ КОНТУР ИИ (HUGGING FACE) --- async def call_huggingface_fallback(user_text: str) -> str: """Резервный контур генерации: Обращение к бесплатным эндпоинтам HF.""" hf_token = os.environ.get("HF_TOKEN") if not hf_token: return get_fallback_response(user_text) url = "https://api-inference.huggingface.co/v1/chat/completions" headers = {"Authorization": f"Bearer {hf_token}", "Content-Type": "application/json"} hf_models = [ "meta-llama/Llama-3.3-70B-Instruct", "meta-llama/Meta-Llama-3-70B-Instruct", "Qwen/Qwen2.5-72B-Instruct-AWQ" ] async with httpx.AsyncClient(timeout=15.0) as client: for model in hf_models: payload = { "model": model, "messages": [ { "role": "system", "content": "Ты — ИБ-ассистент школы Web3CyberServices. Отвечаешь строго по делу, экспертно и лаконично. К пользователю ОБЯЗАТЕЛЬНО всегда обращайся только по титулу 'Gossipin'." }, {"role": "user", "content": user_text} ], "max_tokens": 1000, "temperature": 0.3 } try: res = await client.post(url, headers=headers, json=payload) if res.status_code == 200: data = res.json() return f"🤗 *[HuggingFace / {model.split('/')[-1]}]:*\n\n{data['choices'][0]['message']['content']}" except Exception as e: logger.warning(f"Ошибка вызова HF модели {model}: {e}") continue return get_fallback_response(user_text) # --- 7. ЦЕНТРАЛЬНЫЙ ДИСПЕТЧЕР БАЗОВЫХ КОМАНД --- async def handle_telegram_message(text: str, chat_id: int, history: Optional[str] = None) -> str: """ Основной неблокирующий диспетчер команд и ИИ-генерации. Сюда управление попадает только если кастомные плагины не перехватили сообщение. """ clean_msg = text.strip() upper_msg = clean_msg.upper() # Системные команды Telegram if clean_msg.startswith("/start") or clean_msg.startswith("/help"): return ( f"Приветствую вас, Gospodin! Я — автономный ИБ-агент ядра Web3CyberServices.\n\n" f"Инструменты на борту:\n" f"• `/mx_audit <домен>` — Проверка SPF/DMARC/MX записей\n" f"• `/ip <домен>` — Быстрый DNS Lookup\n" f"• `/headers ` — Анализ заголовков безопасности веб-сервера\n\n" f"Также вы можете ставить задачи на авто-эволюцию кода через префикс `!`" ) # Маршрутизация OSINT-команд (с использованием улучшенного парсинга) if clean_msg.startswith("/ip") or any(k in upper_msg for k in ["УЗНАТЬ IP", "RESOLVE IP"]): return await resolve_host_ip(clean_msg) if clean_msg.startswith("/headers") or any(k in upper_msg for k in ["ЗАГОЛОВКИ", "CHECK HEADERS"]): return await check_web_headers(clean_msg) if clean_msg.startswith("/mx_audit") or "AUDIT" in upper_msg or "ПОЧТ" in upper_msg: return await check_mx_audit(clean_msg) # Главный контур ИИ (если текстовое сообщение не является явной OSINT командой) groq_api_key = os.environ.get("GROQ_API_KEY", "") keys_pool = [k.strip() for k in groq_api_key.split(",") if k.strip()] if keys_pool: async with httpx.AsyncClient(timeout=25.0) as client: for key in keys_pool: try: messages = [ { "role": "system", "content": "Ты — ведущий искусственный интеллект ИБ-агента школы Web3CyberServices. Твоя речь должна быть точной, уверенной и лаконичной. ОБЯЗАТЕЛЬНО обращайся к пользователю только по титулу 'Gospodin'." } ] if history: messages.append({"role": "system", "content": f"Предыдущий контекст беседы:\n{history}"}) messages.append({"role": "user", "content": clean_msg}) res = await client.post( "https://api.groq.com/openai/v1/chat/completions", headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"}, json={ "model": "llama-3.3-70b-versatile", "messages": messages, "temperature": 0.4 } ) if res.status_code == 200: return res.json()["choices"][0]["message"]["content"].strip() except Exception as e: logger.warning(f"Ошибка вызова Groq внутри handle_telegram_message: {e}") continue # Если Groq-пул недоступен или исчерпан, уходим на резервный контур HuggingFace return await call_huggingface_fallback(clean_msg)