IvanCastl

Monitoreo Ramsonware

Jul 21st, 2026
371
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 12.10 KB | Cybersecurity | 0 0
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. ransomware_bot.py
  5. Bot de Telegram para consultar victimas de ransomware desde la API publica de
  6. ransomware.live (v2). Interfaz con botones inline: elegir pais o ver lo mas
  7. reciente a nivel global.
  8.  
  9. Requisitos:
  10.    pip install python-telegram-bot requests
  11.  
  12. Configuracion (variables de entorno - NUNCA hardcodear el token):
  13.    Opcion A (recomendada): archivo .env junto al script con la linea:
  14.        TELEGRAM_BOT_TOKEN=123456:ABC-tu-token-de-BotFather
  15.    Opcion B: variable de entorno:
  16.        export TELEGRAM_BOT_TOKEN="123456:ABC-tu-token-de-BotFather"
  17.  
  18. Ejecutar:
  19.    python3 ransomware_bot.py
  20.  
  21. Comandos:
  22.    /start   -> mensaje de bienvenida + menu principal
  23.    /pais MX -> consulta directa por codigo ISO-2
  24.    /reciente-> victimas globales mas recientes
  25.    /help    -> ayuda
  26.  
  27. Nota: la API gratuita es solo para uso personal. Para uso institucional revisar T&C.
  28. """
  29.  
  30. import html
  31. import logging
  32. import os
  33. from collections import Counter
  34. from datetime import datetime, timezone
  35.  
  36. import requests
  37. from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
  38. from telegram.constants import ParseMode
  39. from telegram.ext import (
  40.     Application,
  41.     CallbackQueryHandler,
  42.     CommandHandler,
  43.     ContextTypes,
  44.     MessageHandler,
  45.     filters,
  46. )
  47.  
  48. # ----------------------------- Config -----------------------------
  49.  
  50. API_BASE = "https://api.ransomware.live/v2"
  51. HEADERS = {"User-Agent": "ivancastl-cti/1.0"}
  52. TIMEOUT = 30
  53. MAX_LISTADO = 15  # cuantas victimas listar en el mensaje (Telegram limita a 4096 chars)
  54.  
  55. # Paises que apareceran como botones en el menu.
  56. PAISES = {
  57.     "MX": "๐Ÿ‡ฒ๐Ÿ‡ฝ Mexico",
  58.     "US": "๐Ÿ‡บ๐Ÿ‡ธ EE.UU.",
  59.     "BR": "๐Ÿ‡ง๐Ÿ‡ท Brasil",
  60.     "AR": "๐Ÿ‡ฆ๐Ÿ‡ท Argentina",
  61.     "CO": "๐Ÿ‡จ๐Ÿ‡ด Colombia",
  62.     "CL": "๐Ÿ‡จ๐Ÿ‡ฑ Chile",
  63.     "PE": "๐Ÿ‡ต๐Ÿ‡ช Peru",
  64.     "ES": "๐Ÿ‡ช๐Ÿ‡ธ Espana",
  65.     "IT": "๐Ÿ‡ฎ๐Ÿ‡น Italia",
  66.     "DE": "๐Ÿ‡ฉ๐Ÿ‡ช Alemania",
  67.     "GB": "๐Ÿ‡ฌ๐Ÿ‡ง Reino Unido",
  68.     "FR": "๐Ÿ‡ซ๐Ÿ‡ท Francia",
  69.     "CA": "๐Ÿ‡จ๐Ÿ‡ฆ Canada",
  70.     "JP": "๐Ÿ‡ฏ๐Ÿ‡ต Japon",
  71. }
  72.  
  73. logging.basicConfig(
  74.     format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
  75.     level=logging.INFO,
  76. )
  77. logger = logging.getLogger(__name__)
  78.  
  79.  
  80. # ----------------------- Utilidades de API ------------------------
  81.  
  82. def _get(endpoint: str):
  83.     """GET a la API. Devuelve lista/dict o None si falla."""
  84.     url = f"{API_BASE}/{endpoint}"
  85.     try:
  86.         r = requests.get(url, headers=HEADERS, timeout=TIMEOUT)
  87.         r.raise_for_status()
  88.         return r.json()
  89.     except requests.exceptions.RequestException as e:
  90.         logger.error("Error consultando %s: %s", url, e)
  91.         return None
  92.     except ValueError:
  93.         logger.error("Respuesta no-JSON de %s", url)
  94.         return None
  95.  
  96.  
  97. def victimas_por_pais(country: str):
  98.     return _get(f"countryvictims/{country}")
  99.  
  100.  
  101. def victimas_recientes():
  102.     return _get("recentvictims")
  103.  
  104.  
  105. def _parse_fecha(valor):
  106.     if not valor:
  107.         return None
  108.     try:
  109.         return datetime.fromisoformat(str(valor).replace("Z", "+00:00"))
  110.     except (ValueError, AttributeError):
  111.         return None
  112.  
  113.  
  114. def _campo(v: dict, *claves):
  115.     """Devuelve el primer campo no vacio entre varias claves posibles.
  116.    (recentvictims usa 'group'/'domain'; countryvictims usa 'group_name'/'website')."""
  117.     for k in claves:
  118.         val = v.get(k)
  119.         if val:
  120.             return val
  121.     return None
  122.  
  123.  
  124. # --------------------- Formateo de reportes -----------------------
  125.  
  126. def formatear_reporte(titulo: str, victimas: list) -> str:
  127.     """Construye el texto HTML del reporte para Telegram."""
  128.     if victimas is None:
  129.         return "โš ๏ธ <b>Error</b> al consultar la API. Intenta de nuevo en unos minutos."
  130.     if not victimas:
  131.         return f"<b>{html.escape(titulo)}</b>\n\nNo se encontraron victimas."
  132.  
  133.     # Orden por fecha de descubrimiento, mas reciente primero
  134.     victimas = sorted(
  135.         victimas,
  136.         key=lambda v: _parse_fecha(v.get("discovered")) or datetime.min.replace(tzinfo=timezone.utc),
  137.         reverse=True,
  138.     )
  139.  
  140.     grupos = Counter(_campo(v, "group", "group_name") or "desconocido" for v in victimas)
  141.     sectores = Counter(v.get("activity") or "No especificado" for v in victimas)
  142.  
  143.     lineas = []
  144.     lineas.append(f"<b>{html.escape(titulo)}</b>")
  145.     lineas.append(f"Total: <b>{len(victimas)}</b> victimas")
  146.     lineas.append(f"<i>{datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}</i>")
  147.  
  148.     top_g = ", ".join(f"{g} ({n})" for g, n in grupos.most_common(5))
  149.     top_s = ", ".join(f"{s} ({n})" for s, n in sectores.most_common(5))
  150.     lineas.append(f"\n๐Ÿฆน <b>Top grupos:</b> {html.escape(top_g)}")
  151.     lineas.append(f"๐Ÿญ <b>Top sectores:</b> {html.escape(top_s)}")
  152.  
  153.     lineas.append(f"\n๐Ÿ“‹ <b>Ultimas {min(len(victimas), MAX_LISTADO)}:</b>")
  154.     for v in victimas[:MAX_LISTADO]:
  155.         f = _parse_fecha(v.get("discovered"))
  156.         fecha = f.strftime("%Y-%m-%d") if f else "N/A"
  157.         nombre = _campo(v, "post_title", "domain", "website") or "N/A"
  158.         grupo = _campo(v, "group", "group_name") or "N/A"
  159.         pais = v.get("country") or ""
  160.         lineas.append(
  161.             f"\nโ€ข <code>{fecha}</code> {html.escape(str(nombre))}"
  162.             f"\n   โ†ณ {html.escape(str(grupo))}"
  163.             + (f" ยท {html.escape(str(pais))}" if pais else "")
  164.         )
  165.  
  166.     if len(victimas) > MAX_LISTADO:
  167.         lineas.append(f"\n<i>...y {len(victimas) - MAX_LISTADO} mas.</i>")
  168.  
  169.     return "\n".join(lineas)
  170.  
  171.  
  172. # ------------------------- Teclados -------------------------------
  173.  
  174. def menu_principal() -> InlineKeyboardMarkup:
  175.     """Menu con paises (2 por fila) + boton de reciente global."""
  176.     botones = []
  177.     fila = []
  178.     for code, label in PAISES.items():
  179.         fila.append(InlineKeyboardButton(label, callback_data=f"pais:{code}"))
  180.         if len(fila) == 2:
  181.             botones.append(fila)
  182.             fila = []
  183.     if fila:
  184.         botones.append(fila)
  185.     botones.append([InlineKeyboardButton("๐ŸŒŽ Mas recientes (global)", callback_data="reciente")])
  186.     botones.append([InlineKeyboardButton("โ„น๏ธ Ayuda", callback_data="ayuda")])
  187.     return InlineKeyboardMarkup(botones)
  188.  
  189.  
  190. def boton_volver() -> InlineKeyboardMarkup:
  191.     return InlineKeyboardMarkup(
  192.         [[InlineKeyboardButton("โฌ…๏ธ Volver al menu", callback_data="menu")]]
  193.     )
  194.  
  195.  
  196. # ------------------------- Handlers -------------------------------
  197.  
  198. TEXTO_BIENVENIDA = (
  199.     "๐Ÿ‘‹ <b>Bienvenido al Ransomware Watch Bot</b>\n\n"
  200.     "Consulto en tiempo real la base de <b>ransomware.live</b> para mostrarte "
  201.     "victimas de ransomware publicadas en sitios de filtracion (leak sites).\n\n"
  202.     "Puedo darte:\n"
  203.     "โ€ข ๐Ÿ“Š Un reporte por <b>pais</b> (grupos, sectores y victimas)\n"
  204.     "โ€ข ๐ŸŒŽ Las victimas <b>mas recientes</b> a nivel global\n\n"
  205.     "Elige una opcion ๐Ÿ‘‡"
  206. )
  207.  
  208. TEXTO_AYUDA = (
  209.     "<b>โ„น๏ธ Ayuda</b>\n\n"
  210.     "<b>Botones:</b> toca un pais o 'Mas recientes'.\n\n"
  211.     "<b>Comandos:</b>\n"
  212.     "โ€ข <code>/start</code> โ€” menu principal\n"
  213.     "โ€ข <code>/pais MX</code> โ€” consulta por codigo ISO-2 (ej: MX, US, IT, JP)\n"
  214.     "โ€ข <code>/reciente</code> โ€” victimas globales recientes\n"
  215.     "โ€ข <code>/help</code> โ€” esta ayuda\n\n"
  216.     "<i>Fuente: ransomware.live ยท API gratuita solo para uso personal.</i>"
  217. )
  218.  
  219.  
  220. async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
  221.     await update.message.reply_text(
  222.         TEXTO_BIENVENIDA, parse_mode=ParseMode.HTML, reply_markup=menu_principal()
  223.     )
  224.  
  225.  
  226. async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
  227.     await update.message.reply_text(
  228.         TEXTO_AYUDA, parse_mode=ParseMode.HTML, reply_markup=boton_volver()
  229.     )
  230.  
  231.  
  232. async def cmd_pais(update: Update, context: ContextTypes.DEFAULT_TYPE):
  233.     """Comando /pais MX"""
  234.     if not context.args:
  235.         await update.message.reply_text(
  236.             "Uso: <code>/pais MX</code> (codigo ISO-2)",
  237.             parse_mode=ParseMode.HTML,
  238.         )
  239.         return
  240.     code = context.args[0].upper()[:2]
  241.     await _responder_pais(update.message, code)
  242.  
  243.  
  244. async def cmd_reciente(update: Update, context: ContextTypes.DEFAULT_TYPE):
  245.     await _responder_reciente(update.message)
  246.  
  247.  
  248. async def _responder_pais(message, code: str):
  249.     aviso = await message.reply_text(f"๐Ÿ”Ž Consultando <b>{code}</b>...", parse_mode=ParseMode.HTML)
  250.     data = victimas_por_pais(code)
  251.     nombre = PAISES.get(code, code)
  252.     texto = formatear_reporte(f"Ransomware โ€” {nombre}", data)
  253.     await aviso.edit_text(texto, parse_mode=ParseMode.HTML, reply_markup=boton_volver(),
  254.                           disable_web_page_preview=True)
  255.  
  256.  
  257. async def _responder_reciente(message):
  258.     aviso = await message.reply_text("๐Ÿ”Ž Trayendo lo mas reciente...", parse_mode=ParseMode.HTML)
  259.     data = victimas_recientes()
  260.     texto = formatear_reporte("Ransomware โ€” Mas recientes (global)", data)
  261.     await aviso.edit_text(texto, parse_mode=ParseMode.HTML, reply_markup=boton_volver(),
  262.                           disable_web_page_preview=True)
  263.  
  264.  
  265. async def on_button(update: Update, context: ContextTypes.DEFAULT_TYPE):
  266.     """Maneja los clics de botones inline."""
  267.     query = update.callback_query
  268.     await query.answer()
  269.     data = query.data
  270.  
  271.     if data == "menu":
  272.         await query.edit_message_text(
  273.             TEXTO_BIENVENIDA, parse_mode=ParseMode.HTML, reply_markup=menu_principal()
  274.         )
  275.         return
  276.  
  277.     if data == "ayuda":
  278.         await query.edit_message_text(
  279.             TEXTO_AYUDA, parse_mode=ParseMode.HTML, reply_markup=boton_volver()
  280.         )
  281.         return
  282.  
  283.     if data == "reciente":
  284.         await query.edit_message_text("๐Ÿ”Ž Trayendo lo mas reciente...", parse_mode=ParseMode.HTML)
  285.         resultado = victimas_recientes()
  286.         texto = formatear_reporte("Ransomware โ€” Mas recientes (global)", resultado)
  287.         await query.edit_message_text(texto, parse_mode=ParseMode.HTML,
  288.                                       reply_markup=boton_volver(), disable_web_page_preview=True)
  289.         return
  290.  
  291.     if data.startswith("pais:"):
  292.         code = data.split(":", 1)[1]
  293.         nombre = PAISES.get(code, code)
  294.         await query.edit_message_text(f"๐Ÿ”Ž Consultando <b>{nombre}</b>...", parse_mode=ParseMode.HTML)
  295.         resultado = victimas_por_pais(code)
  296.         texto = formatear_reporte(f"Ransomware โ€” {nombre}", resultado)
  297.         await query.edit_message_text(texto, parse_mode=ParseMode.HTML,
  298.                                       reply_markup=boton_volver(), disable_web_page_preview=True)
  299.         return
  300.  
  301.  
  302. async def on_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
  303.     """Si el usuario escribe un codigo de pais suelto (ej: 'MX'), lo interpretamos."""
  304.     txt = (update.message.text or "").strip().upper()
  305.     if len(txt) == 2 and txt.isalpha():
  306.         await _responder_pais(update.message, txt)
  307.     else:
  308.         await update.message.reply_text(
  309.             "No entendi. Usa /start para ver el menu.",
  310.             reply_markup=menu_principal(),
  311.         )
  312.  
  313.  
  314. # ----------------------------- Main -------------------------------
  315.  
  316. # ================== TOKEN ==================
  317. # Pega aqui tu token de @BotFather (reemplaza el texto entre comillas).
  318. TELEGRAM_BOT_TOKEN = "TU TOKEN AQUI"
  319. # ==========================================
  320.  
  321.  
  322. def main():
  323.     token = TELEGRAM_BOT_TOKEN
  324.     if not token or token == "PEGA-AQUI-TU-TOKEN":
  325.         raise SystemExit(
  326.             "Falta el token. Edita la variable TELEGRAM_BOT_TOKEN en este archivo "
  327.             "y pega el token que te dio @BotFather."
  328.         )
  329.  
  330.     app = Application.builder().token(token).build()
  331.  
  332.     app.add_handler(CommandHandler("start", cmd_start))
  333.     app.add_handler(CommandHandler("help", cmd_help))
  334.     app.add_handler(CommandHandler("pais", cmd_pais))
  335.     app.add_handler(CommandHandler("reciente", cmd_reciente))
  336.     app.add_handler(CallbackQueryHandler(on_button))
  337.     app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, on_text))
  338.  
  339.     logger.info("Bot iniciado. Esperando mensajes...")
  340.     app.run_polling(allowed_updates=Update.ALL_TYPES)
  341.  
  342.  
  343. if __name__ == "__main__":
  344.     main()
Tags: python
Advertisement
Add Comment
Please, Sign In to add comment