IvanCastl

Monitor_CVE_Bot

Jul 9th, 2026 (edited)
528
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 24.94 KB | Cybersecurity | 0 0
  1. """
  2. CVE Bot ESCANER - Telegram (version LOCAL Windows)
  3. --------------------------------------------------
  4. Mandas una URL al bot y te responde con los componentes que detecta
  5. Wappalyzer y sus CVEs, usando el MISMO motor del scanner:
  6.  - Indice completo local de MITRE (cvelistV5) + actualizacion por delta.
  7.  - Matching por producto normalizado + rango de version (con soporte para
  8.    rangos embebidos como string: "< 1.13.2", ">= 1.12.0, < 3.5.0", etc.).
  9. Mantiene el sistema interactivo de aprobacion de usuarios del bot original.
  10.  
  11. Uso:
  12.  /start          -> registrarse (el admin aprueba)
  13.  <pega una URL>  -> el bot la escanea y responde
  14.  /usuarios       -> (admin) estadisticas
  15.  /pendientes     -> (admin) solicitudes por aprobar
  16.  
  17. Rutas RELATIVAS: los archivos de estado y el cache MITRE se crean junto al .py.
  18. """
  19.  
  20. import asyncio
  21. import io
  22. import json
  23. import os
  24. import re
  25. import sys
  26. import zipfile
  27. import logging
  28. from datetime import datetime
  29. from concurrent.futures import ThreadPoolExecutor
  30.  
  31. import requests
  32. from wappalyzer import analyze
  33. from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
  34. from telegram.ext import (Application, CommandHandler, MessageHandler,
  35.                           CallbackQueryHandler, ContextTypes, filters)
  36.  
  37. # =============================================================================
  38. # CONFIGURACION
  39. # =============================================================================
  40. BASE_DIR       = os.path.dirname(os.path.abspath(__file__))
  41.  
  42. BOT_TOKEN      = os.environ.get("CVE_BOT_TOKEN", "AQUITUTOKEN")
  43. ADMIN_CHAT_ID  = int(os.environ.get("CVE_ADMIN_ID", "TUID"))
  44.  
  45. # -- MITRE / scanner -----------------------------------------------------------
  46. CACHE_DIR      = os.path.join(BASE_DIR, 'mitre_cache')
  47. INDEX_FILE     = os.path.join(CACHE_DIR, 'product_index.json')
  48. META_FILE      = os.path.join(CACHE_DIR, 'meta.json')
  49. CVE_ZIP_URL    = "https://github.com/CVEProject/cvelistV5/archive/refs/heads/main.zip"
  50. DELTA_URL      = "https://raw.githubusercontent.com/CVEProject/cvelistV5/main/cves/delta.json"
  51. RAW_BASE       = "https://raw.githubusercontent.com/CVEProject/cvelistV5/main/cves/"
  52. TIMEOUT_URL    = 30
  53. MAX_MSG        = 3800   # limite seguro por mensaje de Telegram (max real 4096)
  54.  
  55. # -- archivos de usuarios ------------------------------------------------------
  56. APPROVED_FILE  = os.path.join(BASE_DIR, "cve_approved_users.txt")
  57. PENDING_FILE   = os.path.join(BASE_DIR, "cve_pending_users.txt")
  58. BANNED_FILE    = os.path.join(BASE_DIR, "cve_banned_users.txt")
  59.  
  60. SEVERITY_EMOJI = {
  61.     "CRITICAL": "\U0001F534", "HIGH": "\U0001F7E0", "MEDIUM": "\U0001F7E1",
  62.     "LOW": "\U0001F7E2", "NONE": "⚪", "LIMPIO": "✅", "N/A": "⚪",
  63. }
  64.  
  65. # indice MITRE en memoria (se carga en main)
  66. INDEX = {}
  67. _scan_pool = ThreadPoolExecutor(max_workers=4)
  68.  
  69. # =============================================================================
  70. # LOGGING
  71. # =============================================================================
  72. def setup_logging():
  73.     log_file = os.path.join(BASE_DIR, "cve_bot_scan.log")
  74.     logging.basicConfig(
  75.         level=logging.INFO,
  76.         format='%(asctime)s [%(levelname)s] %(message)s',
  77.         handlers=[logging.FileHandler(log_file, encoding='utf-8'),
  78.                   logging.StreamHandler(sys.stdout)]
  79.     )
  80.     logging.getLogger('httpx').setLevel(logging.WARNING)
  81.     logging.getLogger('telegram').setLevel(logging.WARNING)
  82.  
  83. logger = logging.getLogger(__name__)
  84.  
  85. # =============================================================================
  86. # GESTION DE USUARIOS
  87. # =============================================================================
  88. def _load_ids(filepath) -> dict:
  89.     result = {}
  90.     if not os.path.exists(filepath):
  91.         return result
  92.     try:
  93.         with open(filepath, 'r', encoding='utf-8') as f:
  94.             for line in f:
  95.                 line = line.strip()
  96.                 if line:
  97.                     parts = line.split('|')
  98.                     if parts[0].lstrip('-').isdigit():
  99.                         result[int(parts[0])] = line
  100.     except Exception as e:
  101.         logger.error(f"Error leyendo {filepath}: {e}")
  102.     return result
  103.  
  104. def _save_ids(filepath, data: dict):
  105.     try:
  106.         with open(filepath, 'w', encoding='utf-8') as f:
  107.             for line in data.values():
  108.                 f.write(line + '\n')
  109.     except Exception as e:
  110.         logger.error(f"Error escribiendo {filepath}: {e}")
  111.  
  112. def load_approved() -> set:
  113.     return set(_load_ids(APPROVED_FILE).keys())
  114.  
  115. def load_pending() -> dict:
  116.     return _load_ids(PENDING_FILE)
  117.  
  118. def load_banned() -> set:
  119.     return set(_load_ids(BANNED_FILE).keys())
  120.  
  121. def add_pending(user_id: int, username: str, first_name: str) -> bool:
  122.     pending  = _load_ids(PENDING_FILE)
  123.     approved = load_approved()
  124.     banned   = load_banned()
  125.     if user_id in approved or user_id in banned or user_id in pending:
  126.         return False
  127.     ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  128.     pending[user_id] = f"{user_id}|{username}|{first_name}|{ts}|PENDIENTE"
  129.     _save_ids(PENDING_FILE, pending)
  130.     logger.info(f"Pendiente: {first_name} (@{username}) id={user_id}")
  131.     return True
  132.  
  133. def approve_user(user_id: int):
  134.     pending = _load_ids(PENDING_FILE)
  135.     if user_id in pending:
  136.         line = pending.pop(user_id)
  137.         _save_ids(PENDING_FILE, pending)
  138.         approved = _load_ids(APPROVED_FILE)
  139.         parts = line.split('|'); parts[-1] = 'APROBADO'
  140.         approved[user_id] = '|'.join(parts)
  141.         _save_ids(APPROVED_FILE, approved)
  142.     else:
  143.         approved = _load_ids(APPROVED_FILE)
  144.         if user_id not in approved:
  145.             ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  146.             approved[user_id] = f"{user_id}|desconocido|desconocido|{ts}|APROBADO"
  147.             _save_ids(APPROVED_FILE, approved)
  148.     logger.info(f"Aprobado: id={user_id}")
  149.  
  150. def ban_user(user_id: int, reason: str = "blocked"):
  151.     for filepath in [APPROVED_FILE, PENDING_FILE]:
  152.         data = _load_ids(filepath)
  153.         if user_id in data:
  154.             line = data.pop(user_id)
  155.             _save_ids(filepath, data)
  156.             banned = _load_ids(BANNED_FILE)
  157.             ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  158.             parts = line.split('|')
  159.             uname = parts[1] if len(parts) > 1 else '?'
  160.             name  = parts[2] if len(parts) > 2 else '?'
  161.             banned[user_id] = f"{user_id}|{uname}|{name}|{ts}|{reason}"
  162.             _save_ids(BANNED_FILE, banned)
  163.             logger.info(f"Baneado: id={user_id} -- {reason}")
  164.             return
  165.  
  166. def clean(text) -> str:
  167.     if not text:
  168.         return ""
  169.     return str(text).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
  170.  
  171. # =============================================================================
  172. # MOTOR MITRE (mismo del scanner)
  173. # =============================================================================
  174. def norm(s):
  175.     return re.sub(r'[^a-z0-9]', '', (s or '').lower())
  176.  
  177. def parse_version(v):
  178.     if not v:
  179.         return None
  180.     nums = re.findall(r'\d+', str(v))
  181.     return tuple(int(x) for x in nums[:4]) if nums else None
  182.  
  183. def vcmp(a, b):
  184.     if a is None or b is None:
  185.         return 0
  186.     la = list(a) + [0] * (4 - len(a))
  187.     lb = list(b) + [0] * (4 - len(b))
  188.     return (la > lb) - (la < lb)
  189.  
  190. def parse_cve(data):
  191.     if not isinstance(data, dict):
  192.         return None, []
  193.     cna = data.get('containers', {}).get('cna', {})
  194.     if not isinstance(cna, dict):
  195.         return None, []
  196.     cve_id = data.get('cveMetadata', {}).get('cveId')
  197.     if not cve_id:
  198.         return None, []
  199.  
  200.     score, severity = None, 'NONE'
  201.     metrics = cna.get('metrics', [])
  202.     if isinstance(metrics, list):
  203.         for m in metrics:
  204.             if not isinstance(m, dict):
  205.                 continue
  206.             for k in ('cvssV3_1', 'cvssV3_0', 'cvssV4_0', 'cvssV2_0'):
  207.                 if k in m and isinstance(m[k], dict):
  208.                     score = m[k].get('baseScore')
  209.                     severity = (m[k].get('baseSeverity') or 'NONE').upper()
  210.                     break
  211.             if score is not None:
  212.                 break
  213.  
  214.     entradas = []
  215.     for aff in cna.get('affected', []) or []:
  216.         if not isinstance(aff, dict):
  217.             continue
  218.         pkey = norm(aff.get('product', ''))
  219.         if not pkey or pkey in ('na',):
  220.             continue
  221.         versions = []
  222.         for v in aff.get('versions', []) or []:
  223.             if isinstance(v, dict):
  224.                 versions.append({'version': v.get('version'),
  225.                                  'lessThan': v.get('lessThan'),
  226.                                  'lessThanOrEqual': v.get('lessThanOrEqual')})
  227.         entradas.append({'pkey': pkey, 'cve': cve_id, 'versions': versions,
  228.                          'score': score,
  229.                          'severity': severity if score is not None else 'NONE'})
  230.     return cve_id, entradas
  231.  
  232. def build_full_index():
  233.     logger.info("[MITRE] Descargando cvelistV5 completo (solo la primera vez)...")
  234.     r = requests.get(CVE_ZIP_URL, stream=True, timeout=600)
  235.     r.raise_for_status()
  236.     buf = io.BytesIO(r.content)
  237.     index = {}
  238.     with zipfile.ZipFile(buf) as z:
  239.         names = [n for n in z.namelist() if n.endswith('.json') and '/cves/' in n]
  240.         logger.info(f"[MITRE] Procesando {len(names)} CVEs...")
  241.         for i, name in enumerate(names):
  242.             if i % 20000 == 0:
  243.                 logger.info(f"[MITRE]   {i}/{len(names)}")
  244.             try:
  245.                 data = json.loads(z.read(name))
  246.             except Exception:
  247.                 continue
  248.             _, entradas = parse_cve(data)
  249.             for e in entradas:
  250.                 pkey = e.pop('pkey')
  251.                 index.setdefault(pkey, []).append(e)
  252.     return index
  253.  
  254. def merge_cve(index, cve_id, entradas):
  255.     for pkey in list(index.keys()):
  256.         index[pkey] = [e for e in index[pkey] if e['cve'] != cve_id]
  257.         if not index[pkey]:
  258.             del index[pkey]
  259.     for e in entradas:
  260.         pkey = e.pop('pkey')
  261.         index.setdefault(pkey, []).append(e)
  262.  
  263. def actualizar_delta(index):
  264.     logger.info("[MITRE] Buscando CVEs nuevos/modificados (delta)...")
  265.     try:
  266.         r = requests.get(DELTA_URL, timeout=60,
  267.                          headers={'User-Agent': 'CiberAudit-Bot/1.0'})
  268.         r.raise_for_status()
  269.         delta = r.json()
  270.     except Exception as e:
  271.         logger.warning(f"[MITRE] No se pudo leer delta ({str(e)[:60]}). Uso indice actual.")
  272.         return index, 0
  273.  
  274.     pendientes = []
  275.     for clave in ('new', 'updated'):
  276.         for item in delta.get(clave, []):
  277.             if isinstance(item, dict) and item.get('cveId'):
  278.                 pendientes.append((item.get('cveId'), item.get('githubLink')))
  279.     if not pendientes:
  280.         logger.info("[MITRE] Indice ya al dia.")
  281.         return index, 0
  282.  
  283.     logger.info(f"[MITRE] {len(pendientes)} CVEs a actualizar...")
  284.     sesion = requests.Session()
  285.     sesion.headers.update({'User-Agent': 'CiberAudit-Bot/1.0'})
  286.  
  287.     def descargar_uno(cid, link):
  288.         url = None
  289.         if link and link.startswith('http'):
  290.             url = link.replace('https://github.com/CVEProject/cvelistV5/blob/',
  291.                                'https://raw.githubusercontent.com/CVEProject/cvelistV5/')
  292.         else:
  293.             m = re.match(r'CVE-(\d{4})-(\d+)', cid)
  294.             if m:
  295.                 anio, num = m.group(1), m.group(2)
  296.                 rango = num[:-3] + 'xxx' if len(num) > 3 else '0xxx'
  297.                 url = f"{RAW_BASE}{anio}/{rango}/{cid}.json"
  298.         if not url:
  299.             return None
  300.         try:
  301.             rr = sesion.get(url, timeout=30)
  302.             if rr.status_code == 200:
  303.                 return rr.json()
  304.         except Exception:
  305.             return None
  306.         return None
  307.  
  308.     resultados = {}
  309.     with ThreadPoolExecutor(max_workers=8) as ex:
  310.         futs = {ex.submit(descargar_uno, cid, link): cid for cid, link in pendientes}
  311.         for fut in futs:
  312.             cid = futs[fut]
  313.             try:
  314.                 resultados[cid] = fut.result()
  315.             except Exception:
  316.                 resultados[cid] = None
  317.  
  318.     n = 0
  319.     for cid, data in resultados.items():
  320.         if data is None:
  321.             continue
  322.         cve_id, entradas = parse_cve(data)
  323.         if cve_id:
  324.             merge_cve(index, cve_id, entradas)
  325.             n += 1
  326.     logger.info(f"[MITRE] {n} CVEs actualizados desde el delta.")
  327.     return index, n
  328.  
  329. def cargar_indice():
  330.     os.makedirs(CACHE_DIR, exist_ok=True)
  331.     if os.path.exists(INDEX_FILE):
  332.         logger.info(f"[MITRE] Cargando indice local: {INDEX_FILE}")
  333.         with open(INDEX_FILE, encoding='utf-8') as f:
  334.             index = json.load(f)
  335.         index, n = actualizar_delta(index)
  336.         if n > 0:
  337.             with open(INDEX_FILE, 'w', encoding='utf-8') as f:
  338.                 json.dump(index, f)
  339.     else:
  340.         index = build_full_index()
  341.         with open(INDEX_FILE, 'w', encoding='utf-8') as f:
  342.             json.dump(index, f)
  343.         logger.info(f"[MITRE] Indice construido: {len(index)} productos")
  344.     with open(META_FILE, 'w', encoding='utf-8') as f:
  345.         json.dump({'ultima_actualizacion': datetime.now().isoformat(),
  346.                    'productos': len(index)}, f)
  347.     return index
  348.  
  349. # -- matching por version (con soporte de rangos en string) --------------------
  350. def _test_constraint(t, raw):
  351.     raw = (raw or '').strip()
  352.     if not raw or raw == '0':
  353.         return False
  354.     if ' - ' in raw and not re.search(r'[<>=]', raw):
  355.         lo, hi = raw.split(' - ', 1)
  356.         lo, hi = parse_version(lo), parse_version(hi)
  357.         return lo is not None and hi is not None and vcmp(t, lo) >= 0 and vcmp(t, hi) <= 0
  358.     if re.search(r'[<>=]', raw):
  359.         oks = []
  360.         for tok in raw.split(','):
  361.             tok = tok.strip()
  362.             if not tok:
  363.                 continue
  364.             m = re.match(r'(>=|<=|>|<|==|=)?\s*(.+)', tok)
  365.             if not m:
  366.                 continue
  367.             op = m.group(1) or '='
  368.             ver = parse_version(m.group(2))
  369.             if ver is None:
  370.                 continue
  371.             c = vcmp(t, ver)
  372.             oks.append(
  373.                 (op in ('=', '==') and c == 0) or
  374.                 (op == '>'  and c > 0)  or (op == '>=' and c >= 0) or
  375.                 (op == '<'  and c < 0)  or (op == '<=' and c <= 0)
  376.             )
  377.         return bool(oks) and all(oks)
  378.     base = parse_version(raw)
  379.     return base is not None and vcmp(t, base) == 0
  380.  
  381. def version_afectada(target, ventry):
  382.     t = parse_version(target)
  383.     if t is None:
  384.         return False
  385.     lt  = parse_version(ventry.get('lessThan'))
  386.     lte = parse_version(ventry.get('lessThanOrEqual'))
  387.     raw = (ventry.get('version') or '').strip()
  388.     if lt is not None or lte is not None:
  389.         base = parse_version(raw) if raw and raw != '0' else None
  390.         low_ok = base is None or vcmp(t, base) >= 0
  391.         if lt is not None:
  392.             return low_ok and vcmp(t, lt) < 0
  393.         return low_ok and vcmp(t, lte) <= 0
  394.     return _test_constraint(t, raw)
  395.  
  396. def match_cves(index, component, version):
  397.     ckey = norm(component)
  398.     if not ckey or not version:
  399.         return []
  400.     seen, out = set(), []
  401.     for e in index.get(ckey, []):
  402.         if any(version_afectada(version, v) for v in e['versions']) and e['cve'] not in seen:
  403.             seen.add(e['cve'])
  404.             out.append({'id': e['cve'], 'score': e['score'], 'severity': e['severity']})
  405.     return out
  406.  
  407. def severity_label(cves):
  408.     order = {'CRITICAL': 4, 'HIGH': 3, 'MEDIUM': 2, 'LOW': 1, 'NONE': 0}
  409.     if not cves:
  410.         return 'LIMPIO'
  411.     return max(cves, key=lambda c: order.get(c['severity'], 0))['severity']
  412.  
  413. # =============================================================================
  414. # ESCANEO (sincrono, corre en un hilo aparte)
  415. # =============================================================================
  416. def scan_sync(url):
  417.     if not url.startswith('http'):
  418.         url = 'https://' + url
  419.     results = analyze(url=url, scan_type='full', timeout=TIMEOUT_URL)
  420.     techs = results.get(url, {})
  421.     if not techs:
  422.         return {'url': url, 'ok': False, 'msg': 'Sin tecnologias detectadas'}
  423.  
  424.     componentes, grupos, todos = [], [], []
  425.     for name, data in sorted(techs.items()):
  426.         ver = data.get('version')
  427.         componentes.append(f"{name}{(' ' + ver) if ver else ''}")
  428.         if not ver:
  429.             continue
  430.         cves = match_cves(INDEX, name, ver)
  431.         if cves:
  432.             grupos.append((f"{name} {ver}", cves))
  433.             todos.extend(cves)
  434.  
  435.     return {'url': url, 'ok': True, 'componentes': componentes,
  436.             'grupos': grupos, 'sev_max': severity_label(todos),
  437.             'total': len({c['id'] for c in todos})}
  438.  
  439. def formatear_resultado(res):
  440.     """Devuelve una lista de mensajes HTML (troceada si es larga)."""
  441.     if not res['ok']:
  442.         return [f"⚠️ <b>{clean(res['url'])}</b>\n{clean(res['msg'])}"]
  443.  
  444.     emoji = SEVERITY_EMOJI.get(res['sev_max'], "⚪")
  445.     head = (f"\U0001F50D <b>Resultado</b>\n{clean(res['url'])}\n\n"
  446.             f"{emoji} Severidad max: <b>{res['sev_max']}</b>  |  "
  447.             f"CVEs unicos: <b>{res['total']}</b>\n"
  448.             f"\U0001F9E9 Componentes: <code>{clean(', '.join(res['componentes']))}</code>\n")
  449.  
  450.     if not res['grupos']:
  451.         return [head + "\n✅ Sin CVEs conocidos para las versiones detectadas."]
  452.  
  453.     bloques = []
  454.     for comp, cves in res['grupos']:
  455.         e = SEVERITY_EMOJI.get(severity_label(cves), "⚪")
  456.         lineas = [f"\n{e} <b>{clean(comp)}</b>"]
  457.         for c in cves:
  458.             s = c['score'] if c['score'] is not None else 'N/A'
  459.             lineas.append(f"   • <code>{c['id']}</code> [{c['severity']}] score {s}")
  460.         bloques.append("\n".join(lineas))
  461.  
  462.     # trocear para no pasar el limite de Telegram
  463.     mensajes, actual = [], head
  464.     for b in bloques:
  465.         if len(actual) + len(b) > MAX_MSG:
  466.             mensajes.append(actual)
  467.             actual = ""
  468.         actual += b
  469.     if actual.strip():
  470.         mensajes.append(actual)
  471.     return mensajes
  472.  
  473. # =============================================================================
  474. # HANDLERS TELEGRAM
  475. # =============================================================================
  476. async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
  477.     user = update.effective_user
  478.     uid, username, name = user.id, (user.username or 'sin_username'), (user.first_name or 'sin_nombre')
  479.  
  480.     if uid in load_banned():
  481.         await update.message.reply_text("Tu acceso ha sido revocado.")
  482.         return
  483.  
  484.     if uid == ADMIN_CHAT_ID:
  485.         approve_user(uid)
  486.         await update.message.reply_text(
  487.             f"<b>Bienvenido Admin {clean(name)}</b>\n\n"
  488.             "Manda una URL y la escaneo (Wappalyzer + MITRE).\n\n"
  489.             "/pendientes -- solicitudes\n/usuarios -- estadisticas",
  490.             parse_mode='HTML')
  491.         return
  492.  
  493.     if uid in load_approved():
  494.         await update.message.reply_text(
  495.             "Ya estas aprobado. Mandame una URL (ej: https://ejemplo.com) y la escaneo.")
  496.         return
  497.  
  498.     if add_pending(uid, username, name):
  499.         await update.message.reply_text(
  500.             f"Hola <b>{clean(name)}</b>! Tu solicitud fue recibida. "
  501.             "El administrador debe aprobarte para poder escanear.", parse_mode='HTML')
  502.         keyboard = InlineKeyboardMarkup([[
  503.             InlineKeyboardButton("Aprobar", callback_data=f"approve:{uid}"),
  504.             InlineKeyboardButton("Rechazar", callback_data=f"ban:{uid}")]])
  505.         await context.bot.send_message(
  506.             chat_id=ADMIN_CHAT_ID,
  507.             text=(f"<b>Nueva solicitud</b>\nNombre: {clean(name)}\n"
  508.                   f"Usuario: @{clean(username)}\nID: <code>{uid}</code>"),
  509.             parse_mode='HTML', reply_markup=keyboard)
  510.     else:
  511.         await update.message.reply_text("Tu solicitud ya esta en revision.")
  512.  
  513. async def handle_url(update: Update, context: ContextTypes.DEFAULT_TYPE):
  514.     user = update.effective_user
  515.     uid = user.id
  516.     if uid in load_banned():
  517.         return
  518.     if uid != ADMIN_CHAT_ID and uid not in load_approved():
  519.         await update.message.reply_text("Necesitas aprobacion. Usa /start para solicitarla.")
  520.         return
  521.  
  522.     texto = (update.message.text or "").strip()
  523.     m = re.search(r'(https?://\S+|[a-z0-9.-]+\.[a-z]{2,}\S*)', texto, re.I)
  524.     if not m:
  525.         await update.message.reply_text("Mandame una URL valida, ej: https://ejemplo.com")
  526.         return
  527.     url = m.group(1)
  528.  
  529.     aviso = await update.message.reply_text(f"\U0001F50E Escaneando {clean(url)} ... (puede tardar unos segundos)")
  530.     loop = asyncio.get_running_loop()
  531.     try:
  532.         res = await loop.run_in_executor(_scan_pool, scan_sync, url)
  533.     except Exception as e:
  534.         logger.error(f"Error escaneando {url}: {e}")
  535.         await aviso.edit_text(f"❌ Error al escanear: {clean(str(e)[:150])}")
  536.         return
  537.  
  538.     mensajes = formatear_resultado(res)
  539.     try:
  540.         await aviso.edit_text(mensajes[0], parse_mode='HTML', disable_web_page_preview=True)
  541.     except Exception:
  542.         await aviso.edit_text(mensajes[0])
  543.     for extra in mensajes[1:]:
  544.         await update.message.reply_text(extra, parse_mode='HTML', disable_web_page_preview=True)
  545.     logger.info(f"Escaneo {url} por id={uid}: {res.get('total', 0)} CVEs [{res.get('sev_max')}]")
  546.  
  547. async def cmd_usuarios(update: Update, context: ContextTypes.DEFAULT_TYPE):
  548.     if update.effective_user.id != ADMIN_CHAT_ID:
  549.         return
  550.     await update.message.reply_text(
  551.         f"<b>Usuarios</b>\nAprobados: <b>{len(load_approved())}</b>\n"
  552.         f"Pendientes: <b>{len(load_pending())}</b>\nBaneados: <b>{len(load_banned())}</b>",
  553.         parse_mode='HTML')
  554.  
  555. async def cmd_pendientes(update: Update, context: ContextTypes.DEFAULT_TYPE):
  556.     if update.effective_user.id != ADMIN_CHAT_ID:
  557.         return
  558.     pending = load_pending()
  559.     if not pending:
  560.         await update.message.reply_text("No hay solicitudes pendientes.")
  561.         return
  562.     for uid, line in pending.items():
  563.         parts = line.split('|')
  564.         name  = parts[2] if len(parts) > 2 else '?'
  565.         uname = parts[1] if len(parts) > 1 else '?'
  566.         ts    = parts[3] if len(parts) > 3 else '?'
  567.         keyboard = InlineKeyboardMarkup([[
  568.             InlineKeyboardButton("Aprobar", callback_data=f"approve:{uid}"),
  569.             InlineKeyboardButton("Rechazar", callback_data=f"ban:{uid}")]])
  570.         await update.message.reply_text(
  571.             f"{clean(name)} (@{clean(uname)})\nID <code>{uid}</code>\n{ts}",
  572.             parse_mode='HTML', reply_markup=keyboard)
  573.  
  574. async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
  575.     query = update.callback_query
  576.     await query.answer()
  577.     if update.effective_user.id != ADMIN_CHAT_ID:
  578.         await query.edit_message_text("Sin permisos.")
  579.         return
  580.     action, uid_str = query.data.split(':')
  581.     uid = int(uid_str)
  582.     if action == 'approve':
  583.         approve_user(uid)
  584.         try:
  585.             await context.bot.send_message(
  586.                 chat_id=uid,
  587.                 text="<b>Solicitud aprobada!</b>\nYa puedes mandarme URLs para escanear.",
  588.                 parse_mode='HTML')
  589.         except Exception:
  590.             pass
  591.         await query.edit_message_text(f"Usuario <code>{uid}</code> aprobado.", parse_mode='HTML')
  592.     elif action == 'ban':
  593.         ban_user(uid, reason='rechazado_por_admin')
  594.         await query.edit_message_text(f"Usuario <code>{uid}</code> rechazado.", parse_mode='HTML')
  595.  
  596. # =============================================================================
  597. # MAIN
  598. # =============================================================================
  599. async def main():
  600.     setup_logging()
  601.     logger.info("CVE Bot Escaner iniciando...")
  602.  
  603.     if BOT_TOKEN == "PON_AQUI_TU_TOKEN":
  604.         logger.error("Falta el token. Define CVE_BOT_TOKEN o edita BOT_TOKEN.")
  605.         return
  606.  
  607.     global INDEX
  608.     logger.info("Cargando indice MITRE (puede tardar la primera vez)...")
  609.     INDEX = cargar_indice()
  610.     logger.info(f"Indice listo: {len(INDEX)} productos.")
  611.  
  612.     app = Application.builder().token(BOT_TOKEN).build()
  613.     app.add_handler(CommandHandler("start", cmd_start))
  614.     app.add_handler(CommandHandler("usuarios", cmd_usuarios))
  615.     app.add_handler(CommandHandler("pendientes", cmd_pendientes))
  616.     app.add_handler(CallbackQueryHandler(callback_handler))
  617.     app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_url))
  618.  
  619.     await app.initialize()
  620.     await app.start()
  621.     await app.updater.start_polling(drop_pending_updates=True)
  622.     logger.info("CVE Bot Escaner listo. Mandale una URL.")
  623.  
  624.     try:
  625.         while True:
  626.             await asyncio.sleep(3600)
  627.     except (KeyboardInterrupt, asyncio.CancelledError):
  628.         pass
  629.     finally:
  630.         await app.updater.stop()
  631.         await app.stop()
  632.         await app.shutdown()
  633.         logger.info("CVE Bot Escaner detenido")
  634.  
  635. if __name__ == "__main__":
  636.     try:
  637.         asyncio.run(main())
  638.     except KeyboardInterrupt:
  639.         print("\nDetenido por el usuario")
Tags: python
Advertisement
Add Comment
Please, Sign In to add comment