IvanCastl

Monitor CVE

Jul 8th, 2026
638
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 20.67 KB | Cybersecurity | 0 0
  1. """
  2. CVE Telegram Bot - v7 MITRE (version LOCAL Windows)
  3. Fuente: CVEProject/cvelistV5 delta.json (GitHub)
  4. - Sin API key / sin rate limits
  5. - Polling cada 30 min
  6. - Sistema de aprobacion completo
  7. - Rutas RELATIVAS: los archivos se crean junto al script, sin importar
  8.  desde donde lo ejecutes (usa la carpeta del .py).
  9. """
  10.  
  11. import asyncio
  12. import json
  13. import os
  14. import requests
  15. from datetime import datetime
  16. from telegram import Bot, Update, InlineKeyboardButton, InlineKeyboardMarkup
  17. from telegram.ext import (Application, CommandHandler,
  18.                            CallbackQueryHandler, ContextTypes)
  19. from telegram.error import Forbidden, BadRequest, TimedOut, NetworkError
  20. import logging
  21. import sys
  22.  
  23. # =============================================================================
  24. # CONFIGURACION
  25. # =============================================================================
  26. # Carpeta donde vive este .py -> todos los archivos de estado se guardan aqui.
  27. BASE_DIR       = os.path.dirname(os.path.abspath(__file__))
  28.  
  29. # Mejor NO dejar el token hardcodeado. Se lee de variable de entorno si existe;
  30. # si no, usa el valor de respaldo de abajo (cambialo por el tuyo).
  31. BOT_TOKEN      = os.environ.get("CVE_BOT_TOKEN", "")
  32. ADMIN_CHAT_ID  = int(os.environ.get("CVE_ADMIN_ID", ""))
  33.  
  34. POLL_MINUTES   = 30
  35. MAX_RETRIES    = 3
  36.  
  37. DELTA_URL      = "https://raw.githubusercontent.com/CVEProject/cvelistV5/main/cves/delta.json"
  38.  
  39. SEEN_FILE      = os.path.join(BASE_DIR, "seen_cves.json")
  40. DELTA_FILE     = os.path.join(BASE_DIR, "last_delta.json")
  41. APPROVED_FILE  = os.path.join(BASE_DIR, "cve_approved_users.txt")
  42. PENDING_FILE   = os.path.join(BASE_DIR, "cve_pending_users.txt")
  43. BANNED_FILE    = os.path.join(BASE_DIR, "cve_banned_users.txt")
  44.  
  45. # =============================================================================
  46. # LOGGING
  47. # =============================================================================
  48. def setup_logging():
  49.     log_file = os.path.join(BASE_DIR, "cve_bot.log")
  50.     logging.basicConfig(
  51.         level=logging.INFO,
  52.         format='%(asctime)s [%(levelname)s] %(message)s',
  53.         handlers=[
  54.             logging.FileHandler(log_file, encoding='utf-8'),
  55.             logging.StreamHandler(sys.stdout)
  56.         ]
  57.     )
  58.     logging.getLogger('httpx').setLevel(logging.WARNING)
  59.     logging.getLogger('telegram').setLevel(logging.WARNING)
  60.  
  61. logger = logging.getLogger(__name__)
  62.  
  63. # =============================================================================
  64. # GESTION DE USUARIOS
  65. # =============================================================================
  66. def _load_ids(filepath) -> dict:
  67.     result = {}
  68.     if not os.path.exists(filepath):
  69.         return result
  70.     try:
  71.         with open(filepath, 'r', encoding='utf-8') as f:
  72.             for line in f:
  73.                 line = line.strip()
  74.                 if line:
  75.                     parts = line.split('|')
  76.                     if parts[0].lstrip('-').isdigit():
  77.                         result[int(parts[0])] = line
  78.     except Exception as e:
  79.         logger.error(f"Error leyendo {filepath}: {e}")
  80.     return result
  81.  
  82. def _save_ids(filepath, data: dict):
  83.     try:
  84.         with open(filepath, 'w', encoding='utf-8') as f:
  85.             for line in data.values():
  86.                 f.write(line + '\n')
  87.     except Exception as e:
  88.         logger.error(f"Error escribiendo {filepath}: {e}")
  89.  
  90. def load_approved() -> set:
  91.     return set(_load_ids(APPROVED_FILE).keys())
  92.  
  93. def load_pending() -> dict:
  94.     return _load_ids(PENDING_FILE)
  95.  
  96. def load_banned() -> set:
  97.     return set(_load_ids(BANNED_FILE).keys())
  98.  
  99. def add_pending(user_id: int, username: str, first_name: str) -> bool:
  100.     pending  = _load_ids(PENDING_FILE)
  101.     approved = load_approved()
  102.     banned   = load_banned()
  103.     if user_id in approved or user_id in banned or user_id in pending:
  104.         return False
  105.     ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  106.     pending[user_id] = f"{user_id}|{username}|{first_name}|{ts}|PENDIENTE"
  107.     _save_ids(PENDING_FILE, pending)
  108.     logger.info(f"Pendiente CVE: {first_name} (@{username}) id={user_id}")
  109.     return True
  110.  
  111. def approve_user(user_id: int):
  112.     pending = _load_ids(PENDING_FILE)
  113.     if user_id in pending:
  114.         line = pending.pop(user_id)
  115.         _save_ids(PENDING_FILE, pending)
  116.         approved = _load_ids(APPROVED_FILE)
  117.         parts = line.split('|')
  118.         parts[-1] = 'APROBADO'
  119.         approved[user_id] = '|'.join(parts)
  120.         _save_ids(APPROVED_FILE, approved)
  121.     else:
  122.         approved = _load_ids(APPROVED_FILE)
  123.         if user_id not in approved:
  124.             ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  125.             approved[user_id] = f"{user_id}|desconocido|desconocido|{ts}|APROBADO"
  126.             _save_ids(APPROVED_FILE, approved)
  127.     logger.info(f"Aprobado CVE: id={user_id}")
  128.  
  129. def ban_user(user_id: int, reason: str = "blocked"):
  130.     for filepath in [APPROVED_FILE, PENDING_FILE]:
  131.         data = _load_ids(filepath)
  132.         if user_id in data:
  133.             line = data.pop(user_id)
  134.             _save_ids(filepath, data)
  135.             banned = _load_ids(BANNED_FILE)
  136.             ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  137.             parts = line.split('|')
  138.             uname = parts[1] if len(parts) > 1 else '?'
  139.             name  = parts[2] if len(parts) > 2 else '?'
  140.             banned[user_id] = f"{user_id}|{uname}|{name}|{ts}|{reason}"
  141.             _save_ids(BANNED_FILE, banned)
  142.             logger.info(f"Baneado CVE: id={user_id} -- {reason}")
  143.             return
  144.  
  145. # =============================================================================
  146. # PERSISTENCIA CVEs
  147. # =============================================================================
  148. def load_seen() -> set:
  149.     if os.path.exists(SEEN_FILE):
  150.         try:
  151.             with open(SEEN_FILE) as f:
  152.                 return set(json.load(f))
  153.         except Exception:
  154.             pass
  155.     return set()
  156.  
  157. def save_seen(seen: set):
  158.     try:
  159.         with open(SEEN_FILE, 'w') as f:
  160.             json.dump(sorted(list(seen))[-5000:], f)
  161.     except Exception as e:
  162.         logger.error(f"Error guardando seen_cves: {e}")
  163.  
  164. def load_last_fetch_time() -> str:
  165.     if os.path.exists(DELTA_FILE):
  166.         try:
  167.             with open(DELTA_FILE) as f:
  168.                 return json.load(f).get("fetchTime", "")
  169.         except Exception:
  170.             pass
  171.     return ""
  172.  
  173. def save_last_fetch_time(fetch_time: str):
  174.     try:
  175.         with open(DELTA_FILE, 'w') as f:
  176.             json.dump({"fetchTime": fetch_time}, f)
  177.     except Exception as e:
  178.         logger.error(f"Error guardando delta time: {e}")
  179.  
  180. # =============================================================================
  181. # MITRE DELTA
  182. # =============================================================================
  183. def fetch_delta() -> dict:
  184.     try:
  185.         r = requests.get(DELTA_URL, timeout=15)
  186.         r.raise_for_status()
  187.         return r.json()
  188.     except Exception as e:
  189.         logger.error(f"MITRE delta error: {e}")
  190.         return {}
  191.  
  192. def fetch_cve_detail(github_link: str) -> dict:
  193.     try:
  194.         r = requests.get(github_link, timeout=10)
  195.         r.raise_for_status()
  196.         return r.json()
  197.     except Exception:
  198.         return {}
  199.  
  200. def parse_mitre_cve(cve_id: str, github_link: str) -> dict:
  201.     detail = fetch_cve_detail(github_link)
  202.     if not detail:
  203.         return {}
  204.  
  205.     meta = detail.get("cveMetadata", {})
  206.     cna  = detail.get("containers", {}).get("cna", {})
  207.  
  208.     desc = "Sin descripcion"
  209.     for d in cna.get("descriptions", []):
  210.         if d.get("lang") == "en":
  211.             desc = d.get("value", desc)
  212.             break
  213.  
  214.     published = meta.get("datePublished", "")[:10]
  215.  
  216.     affected = cna.get("affected", [])
  217.     vendor   = affected[0].get("vendor", "") if affected else ""
  218.     product  = affected[0].get("product", "") if affected else ""
  219.  
  220.     score    = "N/A"
  221.     severity = "N/A"
  222.     try:
  223.         metrics = cna.get("metrics", [])
  224.         for version_key in ["cvssV3_1", "cvssV3_0", "cvssV4_0", "cvssV2_0"]:
  225.             for m in metrics:
  226.                 cvss = m.get(version_key, {})
  227.                 if cvss.get("baseScore"):
  228.                     score    = str(cvss["baseScore"])
  229.                     severity = cvss.get("baseSeverity", "N/A").upper()
  230.                     break
  231.             if score != "N/A":
  232.                 break
  233.     except Exception:
  234.         pass
  235.  
  236.     return {
  237.         "id":        cve_id,
  238.         "desc":      desc,
  239.         "severity":  severity,
  240.         "score":     score,
  241.         "published": published,
  242.         "vendor":    vendor,
  243.         "product":   product,
  244.     }
  245.  
  246. # =============================================================================
  247. # FORMATO MENSAJE
  248. # =============================================================================
  249. SEVERITY_EMOJI = {
  250.     "CRITICAL": "\U0001F534",
  251.     "HIGH":     "\U0001F7E0",
  252.     "MEDIUM":   "\U0001F7E1",
  253.     "LOW":      "\U0001F7E2",
  254.     "NONE":     "\u26AA",
  255.     "N/A":      "\u26AA",
  256. }
  257.  
  258. def format_cve(cve: dict) -> str:
  259.     emoji = SEVERITY_EMOJI.get(cve["severity"], "\u26AA")
  260.     desc  = cve["desc"][:350] + ("..." if len(cve["desc"]) > 350 else "")
  261.     desc  = desc.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
  262.     vendor_product = ""
  263.     v = cve.get("vendor", "").strip().lower()
  264.     p = cve.get("product", "").strip().lower()
  265.     if v and p and v != "n/a" and p != "n/a":
  266.         vendor_product = f"\U0001F3E2 <b>{cve.get('vendor','')} {cve.get('product','')}</b>\n"
  267.     nvd_link = f"\U0001F517 https://www.cve.org/CVERecord?id={cve['id']}\n"
  268.     return (
  269.         f"{emoji} <b>{cve['id']}</b>\n"
  270.         f"{vendor_product}"
  271.         f"\U0001F4C5 Publicado: <code>{cve['published']}</code>\n"
  272.         f"\u26A0\uFE0F Severidad: <code>{cve['severity']}</code>  |  "
  273.         f"Score: <code>{cve['score']}</code>\n"
  274.         f"\U0001F4CB {desc}\n"
  275.         f"{nvd_link}"
  276.         f"<i>Fuente: MITRE CVE / NVD</i>"
  277.     )
  278.  
  279. # =============================================================================
  280. # ENVIO EN PARALELO
  281. # =============================================================================
  282. def clean(text) -> str:
  283.     if not text:
  284.         return ""
  285.     return str(text).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
  286.  
  287. async def _send_one(bot: Bot, user_id: int, text: str) -> str:
  288.     for attempt in range(MAX_RETRIES):
  289.         try:
  290.             await bot.send_message(
  291.                 chat_id=user_id,
  292.                 text=text,
  293.                 parse_mode='HTML',
  294.                 disable_web_page_preview=True
  295.             )
  296.             return 'ok'
  297.         except (Forbidden, BadRequest) as e:
  298.             logger.warning(f"Usuario {user_id} inaccesible: {e}")
  299.             return 'banned'
  300.         except (TimedOut, NetworkError) as e:
  301.             logger.warning(f"Fallo temporal {user_id} intento {attempt+1}: {e}")
  302.             if attempt < MAX_RETRIES - 1:
  303.                 await asyncio.sleep(2 ** attempt)
  304.         except Exception as e:
  305.             logger.error(f"Error inesperado {user_id}: {e}")
  306.             return 'retry'
  307.     return 'retry'
  308.  
  309. async def broadcast(bot: Bot, text: str):
  310.     approved = load_approved()
  311.     approved.add(ADMIN_CHAT_ID)
  312.     if not approved:
  313.         return
  314.     tasks = {
  315.         uid: asyncio.create_task(_send_one(bot, uid, text))
  316.         for uid in approved
  317.     }
  318.     results = await asyncio.gather(*tasks.values(), return_exceptions=True)
  319.     result_map = dict(zip(tasks.keys(), results))
  320.     ok = banned = failed = 0
  321.     for uid, result in result_map.items():
  322.         if result == 'ok':
  323.             ok += 1
  324.         elif result == 'banned':
  325.             ban_user(uid, reason='bot_blocked_or_not_found')
  326.             banned += 1
  327.         else:
  328.             failed += 1
  329.     if banned or failed:
  330.         logger.info(f"Broadcast CVE: ok={ok} baneados={banned} fallidos={failed}")
  331.  
  332. # =============================================================================
  333. # MONITOR LOOP
  334. # =============================================================================
  335. async def monitor_loop(bot: Bot):
  336.     logger.info(f"Monitor CVE iniciado -- polling cada {POLL_MINUTES} min (fuente: MITRE)")
  337.     seen = load_seen()
  338.  
  339.     while True:
  340.         try:
  341.             logger.info(f"Consultando MITRE delta -- {datetime.now().strftime('%H:%M:%S')}")
  342.  
  343.             delta = fetch_delta()
  344.             if not delta:
  345.                 logger.warning("No se pudo obtener delta.json")
  346.                 await asyncio.sleep(POLL_MINUTES * 60)
  347.                 continue
  348.  
  349.             fetch_time    = delta.get("fetchTime", "")
  350.             last_fetch    = load_last_fetch_time()
  351.             num_changes   = delta.get("numberOfChanges", 0)
  352.  
  353.             logger.info(f"MITRE fetchTime={fetch_time} | cambios={num_changes} | ultimo={last_fetch}")
  354.  
  355.             if fetch_time == last_fetch:
  356.                 logger.info("Sin cambios desde la ultima consulta")
  357.                 await asyncio.sleep(POLL_MINUTES * 60)
  358.                 continue
  359.  
  360.             nuevos_list = [
  361.                 item for item in delta.get("new", [])
  362.                 if item.get("cveId", "").startswith("CVE-2026-")
  363.             ]
  364.             todos = nuevos_list
  365.  
  366.             logger.info(f"Nuevos 2026: {len(nuevos_list)} | Modificados: ignorados")
  367.  
  368.             nuevos_enviados = 0
  369.             for item in todos:
  370.                 cve_id      = item.get("cveId", "")
  371.                 github_link = item.get("githubLink", "")
  372.  
  373.                 if not cve_id or cve_id in seen:
  374.                     continue
  375.  
  376.                 seen.add(cve_id)
  377.                 parsed = parse_mitre_cve(cve_id, github_link)
  378.  
  379.                 if not parsed:
  380.                     continue
  381.  
  382.                 if parsed["desc"] in ("Sin descripcion", "") or not parsed["desc"]:
  383.                     logger.info(f"{cve_id} sin descripcion -- omitido")
  384.                     continue
  385.  
  386.                 await broadcast(bot, format_cve(parsed))
  387.                 logger.info(f"OK {cve_id} [{parsed['severity']}] score={parsed['score']}")
  388.                 nuevos_enviados += 1
  389.                 await asyncio.sleep(1.5)
  390.  
  391.             logger.info(f"-> {nuevos_enviados} CVEs enviados")
  392.             save_seen(seen)
  393.             save_last_fetch_time(fetch_time)
  394.  
  395.             logger.info(f"Proxima revision en {POLL_MINUTES} min")
  396.             await asyncio.sleep(POLL_MINUTES * 60)
  397.  
  398.         except Exception as e:
  399.             logger.error(f"Error en monitor_loop: {e}")
  400.             await asyncio.sleep(60)
  401.  
  402. # =============================================================================
  403. # COMANDOS TELEGRAM
  404. # =============================================================================
  405. async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
  406.     user     = update.effective_user
  407.     uid      = user.id
  408.     username = user.username or 'sin_username'
  409.     name     = user.first_name or 'sin_nombre'
  410.  
  411.     if uid in load_banned():
  412.         await update.message.reply_text("Tu acceso ha sido revocado.")
  413.         return
  414.  
  415.     if uid == ADMIN_CHAT_ID:
  416.         approve_user(uid)
  417.         await update.message.reply_text(
  418.             f"<b>Bienvenido Admin {clean(name)}</b>\n\n"
  419.             "Registrado y aprobado automaticamente.\n\n"
  420.             "Comandos disponibles:\n"
  421.             "/pendientes -- ver solicitudes pendientes\n"
  422.             "/usuarios -- estadisticas de usuarios\n\n"
  423.             "Criticos  Altos  Medios  Bajos\n"
  424.             "<b>CVE MOR Bot -- MITRE / NVD Monitor</b>",
  425.             parse_mode='HTML'
  426.         )
  427.         logger.info(f"Admin registrado CVE bot: {name} id={uid}")
  428.         return
  429.  
  430.     if uid in load_approved():
  431.         await update.message.reply_text(
  432.             "Ya estas suscrito. Recibiras alertas de nuevos CVEs automaticamente."
  433.         )
  434.         return
  435.  
  436.     es_nuevo = add_pending(uid, username, name)
  437.     if es_nuevo:
  438.         await update.message.reply_text(
  439.             f"Hola <b>{clean(name)}</b>!\n\n"
  440.             "Tu solicitud fue recibida. Recibiras notificaciones de CVEs una vez que el administrador la apruebe.\n\n"
  441.             "<b>CVE MOR Bot -- MITRE / NVD Monitor</b>",
  442.             parse_mode='HTML'
  443.         )
  444.         keyboard = InlineKeyboardMarkup([[
  445.             InlineKeyboardButton("Aprobar", callback_data=f"approve:{uid}"),
  446.             InlineKeyboardButton("Rechazar", callback_data=f"ban:{uid}"),
  447.         ]])
  448.         await context.bot.send_message(
  449.             chat_id=ADMIN_CHAT_ID,
  450.             text=(
  451.                 f"<b>Nueva solicitud -- CVE Bot</b>\n\n"
  452.                 f"Nombre: {clean(name)}\n"
  453.                 f"Usuario: @{clean(username)}\n"
  454.                 f"ID: <code>{uid}</code>"
  455.             ),
  456.             parse_mode='HTML',
  457.             reply_markup=keyboard
  458.         )
  459.     else:
  460.         await update.message.reply_text("Tu solicitud ya esta en revision.")
  461.  
  462. async def cmd_usuarios(update: Update, context: ContextTypes.DEFAULT_TYPE):
  463.     if update.effective_user.id != ADMIN_CHAT_ID:
  464.         return
  465.     await update.message.reply_text(
  466.         f"<b>CVE Bot -- Usuarios</b>\n\n"
  467.         f"Aprobados:  <b>{len(load_approved())}</b>\n"
  468.         f"Pendientes: <b>{len(load_pending())}</b>\n"
  469.         f"Baneados:   <b>{len(load_banned())}</b>",
  470.         parse_mode='HTML'
  471.     )
  472.  
  473. async def cmd_pendientes(update: Update, context: ContextTypes.DEFAULT_TYPE):
  474.     if update.effective_user.id != ADMIN_CHAT_ID:
  475.         return
  476.     pending = load_pending()
  477.     if not pending:
  478.         await update.message.reply_text("No hay solicitudes pendientes.")
  479.         return
  480.     for uid, line in pending.items():
  481.         parts = line.split('|')
  482.         name  = parts[2] if len(parts) > 2 else '?'
  483.         uname = parts[1] if len(parts) > 1 else '?'
  484.         ts    = parts[3] if len(parts) > 3 else '?'
  485.         keyboard = InlineKeyboardMarkup([[
  486.             InlineKeyboardButton("Aprobar", callback_data=f"approve:{uid}"),
  487.             InlineKeyboardButton("Rechazar", callback_data=f"ban:{uid}"),
  488.         ]])
  489.         await update.message.reply_text(
  490.             f"{clean(name)} (@{clean(uname)})\n"
  491.             f"ID <code>{uid}</code>\n{ts}",
  492.             parse_mode='HTML',
  493.             reply_markup=keyboard
  494.         )
  495.  
  496. async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
  497.     query = update.callback_query
  498.     await query.answer()
  499.  
  500.     if update.effective_user.id != ADMIN_CHAT_ID:
  501.         await query.edit_message_text("Sin permisos.")
  502.         return
  503.  
  504.     action, uid_str = query.data.split(':')
  505.     uid = int(uid_str)
  506.  
  507.     if action == 'approve':
  508.         approve_user(uid)
  509.         try:
  510.             await context.bot.send_message(
  511.                 chat_id=uid,
  512.                 text=(
  513.                     "<b>Solicitud aprobada!</b>\n\n"
  514.                     "Ya recibiras alertas automaticas de nuevos CVEs.\n\n"
  515.                     "<b>CVE MOR Bot</b>"
  516.                 ),
  517.                 parse_mode='HTML'
  518.             )
  519.         except Exception:
  520.             pass
  521.         await query.edit_message_text(
  522.             f"Usuario <code>{uid}</code> aprobado.",
  523.             parse_mode='HTML'
  524.         )
  525.     elif action == 'ban':
  526.         ban_user(uid, reason='rechazado_por_admin')
  527.         await query.edit_message_text(
  528.             f"Usuario <code>{uid}</code> rechazado.",
  529.             parse_mode='HTML'
  530.         )
  531.  
  532. # =============================================================================
  533. # MAIN
  534. # =============================================================================
  535. async def main():
  536.     setup_logging()
  537.     logger.info("CVE Bot iniciando...")
  538.  
  539.     if BOT_TOKEN == "PON_AQUI_TU_TOKEN":
  540.         logger.error("Falta el token. Define CVE_BOT_TOKEN o edita BOT_TOKEN en el archivo.")
  541.         return
  542.  
  543.     app = Application.builder().token(BOT_TOKEN).build()
  544.     app.add_handler(CommandHandler("start",      cmd_start))
  545.     app.add_handler(CommandHandler("usuarios",   cmd_usuarios))
  546.     app.add_handler(CommandHandler("pendientes", cmd_pendientes))
  547.     app.add_handler(CallbackQueryHandler(callback_handler))
  548.  
  549.     await app.initialize()
  550.     await app.start()
  551.     await app.updater.start_polling(drop_pending_updates=True)
  552.  
  553.     logger.info("CVE Bot listo")
  554.  
  555.     monitor_task = asyncio.create_task(monitor_loop(app.bot))
  556.  
  557.     try:
  558.         while True:
  559.             await asyncio.sleep(3600)
  560.     except (KeyboardInterrupt, asyncio.CancelledError):
  561.         pass
  562.     finally:
  563.         monitor_task.cancel()
  564.         try:
  565.             await monitor_task
  566.         except asyncio.CancelledError:
  567.             pass
  568.         await app.updater.stop()
  569.         await app.stop()
  570.         await app.shutdown()
  571.         logger.info("CVE Bot detenido")
  572.  
  573. if __name__ == "__main__":
  574.     try:
  575.         asyncio.run(main())
  576.     except KeyboardInterrupt:
  577.         print("\nDetenido por el usuario")
Advertisement
Add Comment
Please, Sign In to add comment