Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- """
- CVE Telegram Bot - v7 MITRE (version LOCAL Windows)
- Fuente: CVEProject/cvelistV5 delta.json (GitHub)
- - Sin API key / sin rate limits
- - Polling cada 30 min
- - Sistema de aprobacion completo
- - Rutas RELATIVAS: los archivos se crean junto al script, sin importar
- desde donde lo ejecutes (usa la carpeta del .py).
- """
- import asyncio
- import json
- import os
- import requests
- from datetime import datetime
- from telegram import Bot, Update, InlineKeyboardButton, InlineKeyboardMarkup
- from telegram.ext import (Application, CommandHandler,
- CallbackQueryHandler, ContextTypes)
- from telegram.error import Forbidden, BadRequest, TimedOut, NetworkError
- import logging
- import sys
- # =============================================================================
- # CONFIGURACION
- # =============================================================================
- # Carpeta donde vive este .py -> todos los archivos de estado se guardan aqui.
- BASE_DIR = os.path.dirname(os.path.abspath(__file__))
- # Mejor NO dejar el token hardcodeado. Se lee de variable de entorno si existe;
- # si no, usa el valor de respaldo de abajo (cambialo por el tuyo).
- BOT_TOKEN = os.environ.get("CVE_BOT_TOKEN", "")
- ADMIN_CHAT_ID = int(os.environ.get("CVE_ADMIN_ID", ""))
- POLL_MINUTES = 30
- MAX_RETRIES = 3
- DELTA_URL = "https://raw.githubusercontent.com/CVEProject/cvelistV5/main/cves/delta.json"
- SEEN_FILE = os.path.join(BASE_DIR, "seen_cves.json")
- DELTA_FILE = os.path.join(BASE_DIR, "last_delta.json")
- APPROVED_FILE = os.path.join(BASE_DIR, "cve_approved_users.txt")
- PENDING_FILE = os.path.join(BASE_DIR, "cve_pending_users.txt")
- BANNED_FILE = os.path.join(BASE_DIR, "cve_banned_users.txt")
- # =============================================================================
- # LOGGING
- # =============================================================================
- def setup_logging():
- log_file = os.path.join(BASE_DIR, "cve_bot.log")
- logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s [%(levelname)s] %(message)s',
- handlers=[
- logging.FileHandler(log_file, encoding='utf-8'),
- logging.StreamHandler(sys.stdout)
- ]
- )
- logging.getLogger('httpx').setLevel(logging.WARNING)
- logging.getLogger('telegram').setLevel(logging.WARNING)
- logger = logging.getLogger(__name__)
- # =============================================================================
- # GESTION DE USUARIOS
- # =============================================================================
- def _load_ids(filepath) -> dict:
- result = {}
- if not os.path.exists(filepath):
- return result
- try:
- with open(filepath, 'r', encoding='utf-8') as f:
- for line in f:
- line = line.strip()
- if line:
- parts = line.split('|')
- if parts[0].lstrip('-').isdigit():
- result[int(parts[0])] = line
- except Exception as e:
- logger.error(f"Error leyendo {filepath}: {e}")
- return result
- def _save_ids(filepath, data: dict):
- try:
- with open(filepath, 'w', encoding='utf-8') as f:
- for line in data.values():
- f.write(line + '\n')
- except Exception as e:
- logger.error(f"Error escribiendo {filepath}: {e}")
- def load_approved() -> set:
- return set(_load_ids(APPROVED_FILE).keys())
- def load_pending() -> dict:
- return _load_ids(PENDING_FILE)
- def load_banned() -> set:
- return set(_load_ids(BANNED_FILE).keys())
- def add_pending(user_id: int, username: str, first_name: str) -> bool:
- pending = _load_ids(PENDING_FILE)
- approved = load_approved()
- banned = load_banned()
- if user_id in approved or user_id in banned or user_id in pending:
- return False
- ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- pending[user_id] = f"{user_id}|{username}|{first_name}|{ts}|PENDIENTE"
- _save_ids(PENDING_FILE, pending)
- logger.info(f"Pendiente CVE: {first_name} (@{username}) id={user_id}")
- return True
- def approve_user(user_id: int):
- pending = _load_ids(PENDING_FILE)
- if user_id in pending:
- line = pending.pop(user_id)
- _save_ids(PENDING_FILE, pending)
- approved = _load_ids(APPROVED_FILE)
- parts = line.split('|')
- parts[-1] = 'APROBADO'
- approved[user_id] = '|'.join(parts)
- _save_ids(APPROVED_FILE, approved)
- else:
- approved = _load_ids(APPROVED_FILE)
- if user_id not in approved:
- ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- approved[user_id] = f"{user_id}|desconocido|desconocido|{ts}|APROBADO"
- _save_ids(APPROVED_FILE, approved)
- logger.info(f"Aprobado CVE: id={user_id}")
- def ban_user(user_id: int, reason: str = "blocked"):
- for filepath in [APPROVED_FILE, PENDING_FILE]:
- data = _load_ids(filepath)
- if user_id in data:
- line = data.pop(user_id)
- _save_ids(filepath, data)
- banned = _load_ids(BANNED_FILE)
- ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- parts = line.split('|')
- uname = parts[1] if len(parts) > 1 else '?'
- name = parts[2] if len(parts) > 2 else '?'
- banned[user_id] = f"{user_id}|{uname}|{name}|{ts}|{reason}"
- _save_ids(BANNED_FILE, banned)
- logger.info(f"Baneado CVE: id={user_id} -- {reason}")
- return
- # =============================================================================
- # PERSISTENCIA CVEs
- # =============================================================================
- def load_seen() -> set:
- if os.path.exists(SEEN_FILE):
- try:
- with open(SEEN_FILE) as f:
- return set(json.load(f))
- except Exception:
- pass
- return set()
- def save_seen(seen: set):
- try:
- with open(SEEN_FILE, 'w') as f:
- json.dump(sorted(list(seen))[-5000:], f)
- except Exception as e:
- logger.error(f"Error guardando seen_cves: {e}")
- def load_last_fetch_time() -> str:
- if os.path.exists(DELTA_FILE):
- try:
- with open(DELTA_FILE) as f:
- return json.load(f).get("fetchTime", "")
- except Exception:
- pass
- return ""
- def save_last_fetch_time(fetch_time: str):
- try:
- with open(DELTA_FILE, 'w') as f:
- json.dump({"fetchTime": fetch_time}, f)
- except Exception as e:
- logger.error(f"Error guardando delta time: {e}")
- # =============================================================================
- # MITRE DELTA
- # =============================================================================
- def fetch_delta() -> dict:
- try:
- r = requests.get(DELTA_URL, timeout=15)
- r.raise_for_status()
- return r.json()
- except Exception as e:
- logger.error(f"MITRE delta error: {e}")
- return {}
- def fetch_cve_detail(github_link: str) -> dict:
- try:
- r = requests.get(github_link, timeout=10)
- r.raise_for_status()
- return r.json()
- except Exception:
- return {}
- def parse_mitre_cve(cve_id: str, github_link: str) -> dict:
- detail = fetch_cve_detail(github_link)
- if not detail:
- return {}
- meta = detail.get("cveMetadata", {})
- cna = detail.get("containers", {}).get("cna", {})
- desc = "Sin descripcion"
- for d in cna.get("descriptions", []):
- if d.get("lang") == "en":
- desc = d.get("value", desc)
- break
- published = meta.get("datePublished", "")[:10]
- affected = cna.get("affected", [])
- vendor = affected[0].get("vendor", "") if affected else ""
- product = affected[0].get("product", "") if affected else ""
- score = "N/A"
- severity = "N/A"
- try:
- metrics = cna.get("metrics", [])
- for version_key in ["cvssV3_1", "cvssV3_0", "cvssV4_0", "cvssV2_0"]:
- for m in metrics:
- cvss = m.get(version_key, {})
- if cvss.get("baseScore"):
- score = str(cvss["baseScore"])
- severity = cvss.get("baseSeverity", "N/A").upper()
- break
- if score != "N/A":
- break
- except Exception:
- pass
- return {
- "id": cve_id,
- "desc": desc,
- "severity": severity,
- "score": score,
- "published": published,
- "vendor": vendor,
- "product": product,
- }
- # =============================================================================
- # FORMATO MENSAJE
- # =============================================================================
- SEVERITY_EMOJI = {
- "CRITICAL": "\U0001F534",
- "HIGH": "\U0001F7E0",
- "MEDIUM": "\U0001F7E1",
- "LOW": "\U0001F7E2",
- "NONE": "\u26AA",
- "N/A": "\u26AA",
- }
- def format_cve(cve: dict) -> str:
- emoji = SEVERITY_EMOJI.get(cve["severity"], "\u26AA")
- desc = cve["desc"][:350] + ("..." if len(cve["desc"]) > 350 else "")
- desc = desc.replace("&", "&").replace("<", "<").replace(">", ">")
- vendor_product = ""
- v = cve.get("vendor", "").strip().lower()
- p = cve.get("product", "").strip().lower()
- if v and p and v != "n/a" and p != "n/a":
- vendor_product = f"\U0001F3E2 <b>{cve.get('vendor','')} {cve.get('product','')}</b>\n"
- nvd_link = f"\U0001F517 https://www.cve.org/CVERecord?id={cve['id']}\n"
- return (
- f"{emoji} <b>{cve['id']}</b>\n"
- f"{vendor_product}"
- f"\U0001F4C5 Publicado: <code>{cve['published']}</code>\n"
- f"\u26A0\uFE0F Severidad: <code>{cve['severity']}</code> | "
- f"Score: <code>{cve['score']}</code>\n"
- f"\U0001F4CB {desc}\n"
- f"{nvd_link}"
- f"<i>Fuente: MITRE CVE / NVD</i>"
- )
- # =============================================================================
- # ENVIO EN PARALELO
- # =============================================================================
- def clean(text) -> str:
- if not text:
- return ""
- return str(text).replace("&", "&").replace("<", "<").replace(">", ">")
- async def _send_one(bot: Bot, user_id: int, text: str) -> str:
- for attempt in range(MAX_RETRIES):
- try:
- await bot.send_message(
- chat_id=user_id,
- text=text,
- parse_mode='HTML',
- disable_web_page_preview=True
- )
- return 'ok'
- except (Forbidden, BadRequest) as e:
- logger.warning(f"Usuario {user_id} inaccesible: {e}")
- return 'banned'
- except (TimedOut, NetworkError) as e:
- logger.warning(f"Fallo temporal {user_id} intento {attempt+1}: {e}")
- if attempt < MAX_RETRIES - 1:
- await asyncio.sleep(2 ** attempt)
- except Exception as e:
- logger.error(f"Error inesperado {user_id}: {e}")
- return 'retry'
- return 'retry'
- async def broadcast(bot: Bot, text: str):
- approved = load_approved()
- approved.add(ADMIN_CHAT_ID)
- if not approved:
- return
- tasks = {
- uid: asyncio.create_task(_send_one(bot, uid, text))
- for uid in approved
- }
- results = await asyncio.gather(*tasks.values(), return_exceptions=True)
- result_map = dict(zip(tasks.keys(), results))
- ok = banned = failed = 0
- for uid, result in result_map.items():
- if result == 'ok':
- ok += 1
- elif result == 'banned':
- ban_user(uid, reason='bot_blocked_or_not_found')
- banned += 1
- else:
- failed += 1
- if banned or failed:
- logger.info(f"Broadcast CVE: ok={ok} baneados={banned} fallidos={failed}")
- # =============================================================================
- # MONITOR LOOP
- # =============================================================================
- async def monitor_loop(bot: Bot):
- logger.info(f"Monitor CVE iniciado -- polling cada {POLL_MINUTES} min (fuente: MITRE)")
- seen = load_seen()
- while True:
- try:
- logger.info(f"Consultando MITRE delta -- {datetime.now().strftime('%H:%M:%S')}")
- delta = fetch_delta()
- if not delta:
- logger.warning("No se pudo obtener delta.json")
- await asyncio.sleep(POLL_MINUTES * 60)
- continue
- fetch_time = delta.get("fetchTime", "")
- last_fetch = load_last_fetch_time()
- num_changes = delta.get("numberOfChanges", 0)
- logger.info(f"MITRE fetchTime={fetch_time} | cambios={num_changes} | ultimo={last_fetch}")
- if fetch_time == last_fetch:
- logger.info("Sin cambios desde la ultima consulta")
- await asyncio.sleep(POLL_MINUTES * 60)
- continue
- nuevos_list = [
- item for item in delta.get("new", [])
- if item.get("cveId", "").startswith("CVE-2026-")
- ]
- todos = nuevos_list
- logger.info(f"Nuevos 2026: {len(nuevos_list)} | Modificados: ignorados")
- nuevos_enviados = 0
- for item in todos:
- cve_id = item.get("cveId", "")
- github_link = item.get("githubLink", "")
- if not cve_id or cve_id in seen:
- continue
- seen.add(cve_id)
- parsed = parse_mitre_cve(cve_id, github_link)
- if not parsed:
- continue
- if parsed["desc"] in ("Sin descripcion", "") or not parsed["desc"]:
- logger.info(f"{cve_id} sin descripcion -- omitido")
- continue
- await broadcast(bot, format_cve(parsed))
- logger.info(f"OK {cve_id} [{parsed['severity']}] score={parsed['score']}")
- nuevos_enviados += 1
- await asyncio.sleep(1.5)
- logger.info(f"-> {nuevos_enviados} CVEs enviados")
- save_seen(seen)
- save_last_fetch_time(fetch_time)
- logger.info(f"Proxima revision en {POLL_MINUTES} min")
- await asyncio.sleep(POLL_MINUTES * 60)
- except Exception as e:
- logger.error(f"Error en monitor_loop: {e}")
- await asyncio.sleep(60)
- # =============================================================================
- # COMANDOS TELEGRAM
- # =============================================================================
- async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
- user = update.effective_user
- uid = user.id
- username = user.username or 'sin_username'
- name = user.first_name or 'sin_nombre'
- if uid in load_banned():
- await update.message.reply_text("Tu acceso ha sido revocado.")
- return
- if uid == ADMIN_CHAT_ID:
- approve_user(uid)
- await update.message.reply_text(
- f"<b>Bienvenido Admin {clean(name)}</b>\n\n"
- "Registrado y aprobado automaticamente.\n\n"
- "Comandos disponibles:\n"
- "/pendientes -- ver solicitudes pendientes\n"
- "/usuarios -- estadisticas de usuarios\n\n"
- "Criticos Altos Medios Bajos\n"
- "<b>CVE MOR Bot -- MITRE / NVD Monitor</b>",
- parse_mode='HTML'
- )
- logger.info(f"Admin registrado CVE bot: {name} id={uid}")
- return
- if uid in load_approved():
- await update.message.reply_text(
- "Ya estas suscrito. Recibiras alertas de nuevos CVEs automaticamente."
- )
- return
- es_nuevo = add_pending(uid, username, name)
- if es_nuevo:
- await update.message.reply_text(
- f"Hola <b>{clean(name)}</b>!\n\n"
- "Tu solicitud fue recibida. Recibiras notificaciones de CVEs una vez que el administrador la apruebe.\n\n"
- "<b>CVE MOR Bot -- MITRE / NVD Monitor</b>",
- parse_mode='HTML'
- )
- keyboard = InlineKeyboardMarkup([[
- InlineKeyboardButton("Aprobar", callback_data=f"approve:{uid}"),
- InlineKeyboardButton("Rechazar", callback_data=f"ban:{uid}"),
- ]])
- await context.bot.send_message(
- chat_id=ADMIN_CHAT_ID,
- text=(
- f"<b>Nueva solicitud -- CVE Bot</b>\n\n"
- f"Nombre: {clean(name)}\n"
- f"Usuario: @{clean(username)}\n"
- f"ID: <code>{uid}</code>"
- ),
- parse_mode='HTML',
- reply_markup=keyboard
- )
- else:
- await update.message.reply_text("Tu solicitud ya esta en revision.")
- async def cmd_usuarios(update: Update, context: ContextTypes.DEFAULT_TYPE):
- if update.effective_user.id != ADMIN_CHAT_ID:
- return
- await update.message.reply_text(
- f"<b>CVE Bot -- Usuarios</b>\n\n"
- f"Aprobados: <b>{len(load_approved())}</b>\n"
- f"Pendientes: <b>{len(load_pending())}</b>\n"
- f"Baneados: <b>{len(load_banned())}</b>",
- parse_mode='HTML'
- )
- async def cmd_pendientes(update: Update, context: ContextTypes.DEFAULT_TYPE):
- if update.effective_user.id != ADMIN_CHAT_ID:
- return
- pending = load_pending()
- if not pending:
- await update.message.reply_text("No hay solicitudes pendientes.")
- return
- for uid, line in pending.items():
- parts = line.split('|')
- name = parts[2] if len(parts) > 2 else '?'
- uname = parts[1] if len(parts) > 1 else '?'
- ts = parts[3] if len(parts) > 3 else '?'
- keyboard = InlineKeyboardMarkup([[
- InlineKeyboardButton("Aprobar", callback_data=f"approve:{uid}"),
- InlineKeyboardButton("Rechazar", callback_data=f"ban:{uid}"),
- ]])
- await update.message.reply_text(
- f"{clean(name)} (@{clean(uname)})\n"
- f"ID <code>{uid}</code>\n{ts}",
- parse_mode='HTML',
- reply_markup=keyboard
- )
- async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
- query = update.callback_query
- await query.answer()
- if update.effective_user.id != ADMIN_CHAT_ID:
- await query.edit_message_text("Sin permisos.")
- return
- action, uid_str = query.data.split(':')
- uid = int(uid_str)
- if action == 'approve':
- approve_user(uid)
- try:
- await context.bot.send_message(
- chat_id=uid,
- text=(
- "<b>Solicitud aprobada!</b>\n\n"
- "Ya recibiras alertas automaticas de nuevos CVEs.\n\n"
- "<b>CVE MOR Bot</b>"
- ),
- parse_mode='HTML'
- )
- except Exception:
- pass
- await query.edit_message_text(
- f"Usuario <code>{uid}</code> aprobado.",
- parse_mode='HTML'
- )
- elif action == 'ban':
- ban_user(uid, reason='rechazado_por_admin')
- await query.edit_message_text(
- f"Usuario <code>{uid}</code> rechazado.",
- parse_mode='HTML'
- )
- # =============================================================================
- # MAIN
- # =============================================================================
- async def main():
- setup_logging()
- logger.info("CVE Bot iniciando...")
- if BOT_TOKEN == "PON_AQUI_TU_TOKEN":
- logger.error("Falta el token. Define CVE_BOT_TOKEN o edita BOT_TOKEN en el archivo.")
- return
- app = Application.builder().token(BOT_TOKEN).build()
- app.add_handler(CommandHandler("start", cmd_start))
- app.add_handler(CommandHandler("usuarios", cmd_usuarios))
- app.add_handler(CommandHandler("pendientes", cmd_pendientes))
- app.add_handler(CallbackQueryHandler(callback_handler))
- await app.initialize()
- await app.start()
- await app.updater.start_polling(drop_pending_updates=True)
- logger.info("CVE Bot listo")
- monitor_task = asyncio.create_task(monitor_loop(app.bot))
- try:
- while True:
- await asyncio.sleep(3600)
- except (KeyboardInterrupt, asyncio.CancelledError):
- pass
- finally:
- monitor_task.cancel()
- try:
- await monitor_task
- except asyncio.CancelledError:
- pass
- await app.updater.stop()
- await app.stop()
- await app.shutdown()
- logger.info("CVE Bot detenido")
- if __name__ == "__main__":
- try:
- asyncio.run(main())
- except KeyboardInterrupt:
- print("\nDetenido por el usuario")
Advertisement
Add Comment
Please, Sign In to add comment