Spaces:
Sleeping
Sleeping
| 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 <url>` — Анализ заголовков безопасности веб-сервера\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) | |