Web3CyberAgent / api /commands.py
web3cyberservices
Update commands.py
1090a87 unverified
Raw
History Blame Contribute Delete
19.1 kB
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)