IvanCastl

Monitoreo foros

Aug 19th, 2026
614
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 27.99 KB | Cybersecurity | 0 0
  1. #!/usr/bin/env python3
  2. """
  3. Domain Watch β€” Monitor de dominios en tiempo real (CTI / domain monitoring)
  4. ============================================================================
  5.  
  6. Flujo simple
  7. ------------
  8. 1. Escribes los dominios base arriba (separados por comas: "breachforums, ejemplo").
  9. 2. BotΓ³n ESCANEAR: expande por TLD, sondea y pone en MONITOREO todo lo que
  10.   responda 200 OK. El monitoreo se repite solo cada INTERVALO segundos.
  11. 3. BotΓ³n DETENER: pausa el monitoreo (no borra nada).
  12. 4. BotΓ³n BORRAR: limpia todo el estado y empieza de cero.
  13.  
  14. Alertas (Telegram + interfaz)
  15. -----------------------------
  16. - πŸ”€ Un dominio monitoreado CAMBIA de DNS/IP.
  17. - 🚨 Su DNS cae en la lista de "DNS a espiar" (sinkhole / incautación) o el
  18.  contenido coincide con un patrΓ³n de takedown (FBI, Europol, "seized"...).
  19. - πŸ†• Aparece un dominio nuevo que antes no resolvΓ­a.
  20.  
  21. Uso
  22. ---
  23.    pip install flask requests
  24.    export TELEGRAM_TOKEN="123456:ABC..."
  25.    export TELEGRAM_CHAT_ID="-100..."
  26.    python3 domain_monitor.py
  27.    # abrir http://127.0.0.1:5000
  28. """
  29.  
  30. from __future__ import annotations
  31.  
  32. import json
  33. import logging
  34. import os
  35. import re
  36. import socket
  37. import threading
  38. import time
  39. from concurrent.futures import ThreadPoolExecutor, as_completed
  40. from dataclasses import dataclass, asdict, field
  41. from datetime import datetime, timezone
  42. from hashlib import sha256
  43. from pathlib import Path
  44. from typing import Optional
  45.  
  46. import requests
  47. from flask import Flask, jsonify, render_template_string, request
  48.  
  49. # verify=False en probe genera warnings ruidosos; los silenciamos.
  50. requests.packages.urllib3.disable_warnings()
  51.  
  52. # ══════════════════════════════════════════════════════════════
  53. # EDITABLE β€” ajusta a tu inteligencia
  54. # ══════════════════════════════════════════════════════════════
  55.  
  56. # TLDs con los que se expande cada dominio base. Lista completa (ccTLDs + com).
  57. # Algunos cΓ³digos (bl, eh, um, bv, bq, sj...) no tienen DNS activo y nunca
  58. # resolverΓ‘n: no estorban, se descartan solos.
  59. TLDS = [
  60.     "ac", "ad", "ae", "af", "ag", "ai", "al", "am", "ao", "aq", "ar", "as",
  61.     "at", "au", "aw", "ax", "az", "ba", "bb", "bd", "be", "bf", "bg", "bh",
  62.     "bi", "bj", "bm", "bn", "bo", "bq", "br", "bs", "bt", "bv", "bw", "by",
  63.     "bz", "ca", "cc", "cd", "cf", "cg", "ch", "ci", "ck", "cl", "cm", "cn",
  64.     "co", "cr", "cu", "cv", "cw", "cx", "cy", "cz", "de", "dj", "dk", "dm",
  65.     "do", "dz", "ec", "ee", "eg", "er", "es", "et", "eu", "fi", "fj", "fk",
  66.     "fm", "fo", "fr", "ga", "gb", "gd", "ge", "gf", "gg", "gh", "gi", "gl",
  67.     "gm", "gn", "gp", "gq", "gr", "gs", "gt", "gu", "gw", "gy", "hk", "hm",
  68.     "hn", "hr", "ht", "hu", "id", "ie", "il", "im", "in", "io", "iq", "ir",
  69.     "is", "it", "je", "jm", "jo", "jp", "ke", "kg", "kh", "ki", "km", "kn",
  70.     "kp", "kr", "kw", "ky", "kz", "la", "lb", "lc", "li", "lk", "lr", "ls",
  71.     "lt", "lu", "lv", "ly", "ma", "mc", "md", "me", "mf", "mg", "mh", "mk",
  72.     "ml", "mm", "mn", "mo", "mp", "mq", "mr", "ms", "mt", "mu", "mv", "mw",
  73.     "mx", "my", "mz", "na", "nc", "ne", "nf", "ng", "ni", "nl", "no", "np",
  74.     "nr", "nu", "nz", "om", "pa", "pe", "pf", "pg", "ph", "pk", "pl", "pm",
  75.     "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro", "rs", "ru", "rw",
  76.     "sa", "sb", "sc", "sd", "se", "sg", "sh", "si", "sj", "sk", "sl", "sm",
  77.     "sn", "so", "sr", "ss", "st", "su", "sv", "sx", "sy", "sz", "tc", "td",
  78.     "tf", "tg", "th", "tj", "tk", "tl", "tm", "tn", "to", "tr", "tt", "tv",
  79.     "tw", "tz", "ua", "ug", "uk", "us", "uy", "uz", "va", "vc", "ve", "vg",
  80.     "vi", "vn", "vu", "wf", "ws", "ye", "yt", "za", "zm", "bl", "eh", "um",
  81.     "com",
  82. ]
  83.  
  84. # DNS / IP "a espiar": si un dominio resuelve a una de estas IPs (o su prefijo),
  85. # se marca como POSIBLE INCAUTACIΓ“N y alerta a Telegram de inmediato.
  86. # Sustituye por los rangos de sinkhole / takedown reales de tu CTI.
  87. SEIZURE_IP_PREFIXES = [
  88.     "132.148.",
  89.     "184.168.",
  90.     "13.248.",
  91.     "76.223.",
  92. ]
  93.  
  94. # Palabras clave de pΓ‘ginas de takedown (2+ coincidencias = seizure).
  95. SEIZURE_KEYWORDS = [
  96.     "this domain has been seized", "this website has been seized",
  97.     "has been seized", "domain seizure", "seized by",
  98.     "operation ", "task force",
  99.     "federal bureau of investigation", "fbi",
  100.     "europol", "eurojust", "national crime agency", "nca",
  101.     "bundeskriminalamt", "politie", "department of justice",
  102.     "u.s. attorney", "law enforcement", "cyber division",
  103.     "incautad", "confiscad",
  104. ]
  105.  
  106. DEFAULT_INTERVAL = 300          # seg entre escaneos automΓ‘ticos
  107. CONNECT_TIMEOUT = 6             # seg para conectar
  108. READ_TIMEOUT = 10               # seg para leer respuesta
  109. RETRIES = 2                     # reintentos por dominio si falla la primera vez
  110. SCAN_THREADS = 20               # bajado de 30: menos saturaciΓ³n = menos timeouts falsos
  111. USER_AGENT = "Mozilla/5.0 (compatible; DomainWatch/1.0)"
  112. NOTIFY_ANY_DNS_CHANGE = True    # avisar a Telegram tambiΓ©n en cambios de DNS normales
  113.  
  114. TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN", "343123709:AAFtWMGNamKhY7ynNIH4uZk8TcBbyx897PC0")
  115. TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "2145147107")
  116. STATE_FILE = Path("watch_state.json")
  117.  
  118. # ══════════════════════════════════════════════════════════════
  119.  
  120. logging.basicConfig(
  121.     level=logging.INFO,
  122.     format="%(asctime)s | %(levelname)-7s | %(message)s",
  123.     datefmt="%H:%M:%S",
  124. )
  125. log = logging.getLogger("watch")
  126.  
  127.  
  128. def _now() -> str:
  129.     return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
  130.  
  131.  
  132. # ──────────────────────────────────────────────────────────────
  133. # Modelo
  134. # ──────────────────────────────────────────────────────────────
  135.  
  136. @dataclass
  137. class DomainState:
  138.     domain: str
  139.     ip: Optional[str] = None
  140.     status_code: Optional[int] = None
  141.     title: Optional[str] = None
  142.     body_hash: Optional[str] = None
  143.     seized: bool = False
  144.     ip_history: list = field(default_factory=list)   # [{ts, ip}]
  145.     first_seen: str = field(default_factory=_now)
  146.     last_checked: str = field(default_factory=_now)
  147.     last_change: Optional[str] = None
  148.     error: Optional[str] = None
  149.  
  150.     @property
  151.     def alive(self) -> bool:
  152.         return self.status_code == 200
  153.  
  154.  
  155. # ──────────────────────────────────────────────────────────────
  156. # Estado global
  157. # ──────────────────────────────────────────────────────────────
  158.  
  159. class Store:
  160.     def __init__(self) -> None:
  161.         self.lock = threading.RLock()
  162.         self.domains: dict[str, DomainState] = {}
  163.         self.base_domains: list[str] = []
  164.         self.interval: int = DEFAULT_INTERVAL
  165.         self.running: bool = False
  166.         self.last_scan: Optional[str] = None
  167.         self.events: list[dict] = []
  168.         self._load()
  169.  
  170.     def _load(self) -> None:
  171.         if not STATE_FILE.exists():
  172.             return
  173.         try:
  174.             data = json.loads(STATE_FILE.read_text(encoding="utf-8"))
  175.             self.base_domains = data.get("base_domains", [])
  176.             self.interval = data.get("interval", DEFAULT_INTERVAL)
  177.             for d in data.get("domains", []):
  178.                 self.domains[d["domain"]] = DomainState(**d)
  179.             self.events = data.get("events", [])[-200:]
  180.         except Exception as e:
  181.             log.warning("No se pudo cargar estado: %s", e)
  182.  
  183.     def save(self) -> None:
  184.         with self.lock:
  185.             data = {
  186.                 "base_domains": self.base_domains,
  187.                 "interval": self.interval,
  188.                 "domains": [asdict(d) for d in self.domains.values()],
  189.                 "events": self.events[-200:],
  190.             }
  191.         STATE_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False),
  192.                               encoding="utf-8")
  193.  
  194.     def reset(self) -> None:
  195.         with self.lock:
  196.             self.domains.clear()
  197.             self.events.clear()
  198.             self.last_scan = None
  199.             self.running = False
  200.         if STATE_FILE.exists():
  201.             STATE_FILE.unlink()
  202.         self.add_event("info", "Estado borrado. Listo para empezar de cero.")
  203.  
  204.     def add_event(self, level: str, msg: str) -> None:
  205.         with self.lock:
  206.             self.events.append({"ts": _now(), "level": level, "msg": msg})
  207.             self.events = self.events[-200:]
  208.         log.info("[%s] %s", level.upper(), msg)
  209.  
  210.  
  211. store = Store()
  212.  
  213.  
  214. # ──────────────────────────────────────────────────────────────
  215. # Telegram
  216. # ──────────────────────────────────────────────────────────────
  217.  
  218. def telegram_send(text: str) -> None:
  219.     if not (TELEGRAM_TOKEN and TELEGRAM_CHAT_ID):
  220.         log.warning("Telegram no configurado; alerta solo en interfaz.")
  221.         return
  222.     try:
  223.         requests.post(
  224.             f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage",
  225.             json={"chat_id": TELEGRAM_CHAT_ID, "text": text,
  226.                   "parse_mode": "HTML", "disable_web_page_preview": True},
  227.             timeout=10,
  228.         )
  229.     except requests.RequestException as e:
  230.         log.warning("Fallo Telegram: %s", e)
  231.  
  232.  
  233. # ──────────────────────────────────────────────────────────────
  234. # DetecciΓ³n
  235. # ──────────────────────────────────────────────────────────────
  236.  
  237. def resolve(domain: str) -> Optional[str]:
  238.     try:
  239.         return socket.gethostbyname(domain)
  240.     except socket.gaierror:
  241.         return None
  242.  
  243.  
  244. def looks_seized(ip: Optional[str], html: str) -> bool:
  245.     if ip and any(ip.startswith(p) for p in SEIZURE_IP_PREFIXES):
  246.         return True
  247.     low = html.lower()
  248.     return sum(1 for kw in SEIZURE_KEYWORDS if kw in low) >= 2
  249.  
  250.  
  251. TITLE_RE = re.compile(r"<title[^>]*>(.*?)</title>", re.I | re.S)
  252.  
  253.  
  254. def _fetch(url: str) -> Optional[requests.Response]:
  255.     """Un intento de GET. verify=False para no perder sitios con TLS roto
  256.    (comΓΊn en dominios reciΓ©n levantados o incautados)."""
  257.     return requests.get(
  258.         url, headers={"User-Agent": USER_AGENT},
  259.         timeout=(CONNECT_TIMEOUT, READ_TIMEOUT),
  260.         allow_redirects=True, verify=False,
  261.     )
  262.  
  263.  
  264. def probe(domain: str) -> DomainState:
  265.     ip = resolve(domain)
  266.     st = DomainState(domain=domain, ip=ip)
  267.     if ip is None:
  268.         return st
  269.  
  270.     r = None
  271.     last_err = None
  272.     # Reintenta HTTPS y, si falla, cae a HTTP. RETRIES intentos por esquema.
  273.     for scheme in ("https", "http"):
  274.         for attempt in range(RETRIES):
  275.             try:
  276.                 r = _fetch(f"{scheme}://{domain}")
  277.                 break
  278.             except requests.RequestException as e:
  279.                 last_err = type(e).__name__
  280.                 time.sleep(0.4 * (attempt + 1))   # pequeΓ±o backoff
  281.         if r is not None:
  282.             break
  283.  
  284.     if r is None:
  285.         st.error = last_err or "no_response"
  286.         return st
  287.  
  288.     st.status_code = r.status_code
  289.     body = r.text[:200_000]
  290.     st.body_hash = sha256(body.encode("utf-8", "ignore")).hexdigest()[:16]
  291.     m = TITLE_RE.search(body)
  292.     st.title = m.group(1).strip()[:200] if m else None
  293.     st.seized = looks_seized(ip, body)
  294.     return st
  295.  
  296.  
  297. # ──────────────────────────────────────────────────────────────
  298. # Diff + alertas
  299. # ──────────────────────────────────────────────────────────────
  300.  
  301. def diff_and_alert(old: Optional[DomainState], new: DomainState) -> DomainState:
  302.     # Dominio nuevo que aparece resolviendo
  303.     if old is None:
  304.         if new.ip:
  305.             new.ip_history = [{"ts": _now(), "ip": new.ip}]
  306.             store.add_event("new", f"πŸ†• Nuevo dominio activo: {new.domain} β†’ {new.ip}")
  307.             telegram_send(
  308.                 f"πŸ†• <b>Nuevo dominio activo</b>\n<code>{new.domain}</code>\n"
  309.                 f"IP: <code>{new.ip}</code>\nHTTP: {new.status_code}"
  310.             )
  311.         return new
  312.  
  313.     # conservar histΓ³rico
  314.     new.first_seen = old.first_seen
  315.     new.last_change = old.last_change
  316.     new.ip_history = list(old.ip_history)
  317.  
  318.     # cambio de IP / DNS
  319.     if old.ip and new.ip and old.ip != new.ip:
  320.         new.last_change = _now()
  321.         new.ip_history.append({"ts": _now(), "ip": new.ip})
  322.         if new.seized and not old.seized:
  323.             store.add_event("seizure",
  324.                 f"🚨 INCAUTACIΓ“N: {new.domain} {old.ip} β†’ {new.ip}")
  325.             telegram_send(
  326.                 f"🚨 <b>POSIBLE INCAUTACIΓ“N</b>\n<code>{new.domain}</code>\n"
  327.                 f"DNS: <code>{old.ip}</code> β†’ <code>{new.ip}</code>\n"
  328.                 f"TΓ­tulo: {new.title or 'β€”'}\nCoincide con patrΓ³n de takedown."
  329.             )
  330.         else:
  331.             store.add_event("dns",
  332.                 f"πŸ”€ Cambio DNS: {new.domain} {old.ip} β†’ {new.ip}")
  333.             if NOTIFY_ANY_DNS_CHANGE:
  334.                 telegram_send(
  335.                     f"πŸ”€ <b>Cambio de DNS</b>\n<code>{new.domain}</code>\n"
  336.                     f"<code>{old.ip}</code> β†’ <code>{new.ip}</code>"
  337.                 )
  338.     # se volviΓ³ seizure sin cambiar IP
  339.     elif new.seized and not old.seized:
  340.         new.last_change = _now()
  341.         store.add_event("seizure", f"🚨 INCAUTACIΓ“N: {new.domain}")
  342.         telegram_send(
  343.             f"🚨 <b>POSIBLE INCAUTACIΓ“N</b>\n<code>{new.domain}</code>\n"
  344.             f"El contenido coincide con un patrΓ³n de takedown."
  345.         )
  346.  
  347.     return new
  348.  
  349.  
  350. # ──────────────────────────────────────────────────────────────
  351. # Escaneo
  352. # ──────────────────────────────────────────────────────────────
  353.  
  354. def scan_once() -> None:
  355.     with store.lock:
  356.         bases = list(store.base_domains)
  357.     if not bases:
  358.         store.add_event("info", "No hay dominios base. Escribe alguno y vuelve a escanear.")
  359.         return
  360.  
  361.     targets = [f"{b}.{t}" for b in bases for t in TLDS]
  362.     # tambiΓ©n re-sondea los ya conocidos (por si cambian de DNS)
  363.     with store.lock:
  364.         targets += [d for d in store.domains if d not in targets]
  365.  
  366.     def run_batch(domains: list[str]) -> list[DomainState]:
  367.         out: list[DomainState] = []
  368.         with ThreadPoolExecutor(max_workers=SCAN_THREADS) as pool:
  369.             futures = {pool.submit(probe, d): d for d in domains}
  370.             for fut in as_completed(futures):
  371.                 try:
  372.                     out.append(fut.result())
  373.                 except Exception as e:
  374.                     log.warning("probe fallo: %s", e)
  375.         return out
  376.  
  377.     log.info("Escaneando %d dominios (1Βͺ pasada)...", len(targets))
  378.     results = run_batch(targets)
  379.  
  380.     # 2Βͺ pasada SOLO sobre los que resolvieron pero fallaron el HTTP
  381.     # (timeouts, conexiones caΓ­das). AsΓ­ no "aparecen" hasta el 2ΒΊ escaneo.
  382.     retry = [r.domain for r in results if r.ip and r.status_code is None]
  383.     if retry:
  384.         log.info("Reintentando %d que fallaron el HTTP...", len(retry))
  385.         redo = {r.domain: r for r in run_batch(retry)}
  386.         results = [redo.get(r.domain, r) for r in results]
  387.  
  388.     log.info("Escaneo terminado: %d activos (200).",
  389.              sum(1 for r in results if r.alive))
  390.     with store.lock:
  391.         for new in results:
  392.             old = store.domains.get(new.domain)
  393.             # Solo entran a monitoreo los que responden 200 (o los ya conocidos).
  394.             if old is None and not new.alive:
  395.                 continue
  396.             new.last_checked = _now()
  397.             store.domains[new.domain] = diff_and_alert(old, new)
  398.         store.last_scan = _now()
  399.     store.save()
  400.  
  401.  
  402. def scan_loop() -> None:
  403.     while True:
  404.         with store.lock:
  405.             running, interval = store.running, store.interval
  406.         if running:
  407.             try:
  408.                 scan_once()
  409.             except Exception as e:
  410.                 log.exception("Error en escaneo: %s", e)
  411.         time.sleep(max(5, interval) if running else 3)
  412.  
  413.  
  414. # ──────────────────────────────────────────────────────────────
  415. # Interfaz web
  416. # ──────────────────────────────────────────────────────────────
  417.  
  418. app = Flask(__name__)
  419.  
  420. PAGE = """<!doctype html>
  421. <html lang="es"><head>
  422. <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
  423. <title>Domain Watch</title>
  424. <style>
  425.  :root{--bg:#0d1117;--panel:#161b22;--border:#30363d;--fg:#e6edf3;
  426.        --muted:#8b949e;--green:#238636;--greenL:#2ea043;--red:#f85149;
  427.        --amber:#d29922;--blue:#388bfd;}
  428.  *{box-sizing:border-box}
  429.  body{margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  430.       background:var(--bg);color:var(--fg);font-size:14px}
  431.  header{background:var(--panel);border-bottom:1px solid var(--border);
  432.         padding:14px 20px;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
  433.  h1{font-size:16px;margin:0;letter-spacing:.5px}
  434.  .dot{width:9px;height:9px;border-radius:50%;display:inline-block}
  435.  .on{background:var(--greenL);box-shadow:0 0 8px var(--greenL)}
  436.  .off{background:var(--red)}
  437.  .statuspill{font-weight:700;font-size:13px;padding:5px 14px;border-radius:20px;letter-spacing:.5px}
  438.  .pill-on{background:rgba(46,160,67,.18);color:var(--greenL);border:1px solid var(--greenL)}
  439.  .pill-off{background:rgba(248,81,73,.15);color:var(--red);border:1px solid var(--red)}
  440.  .scanning{animation:pulse 1.2s infinite}
  441.  @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
  442.  main{padding:20px;max-width:1400px;margin:0 auto}
  443.  .bar{background:var(--panel);border:1px solid var(--border);border-radius:10px;
  444.       padding:16px;display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:20px}
  445.  .bar input{flex:1;min-width:240px;background:var(--bg);color:var(--fg);
  446.       border:1px solid var(--border);border-radius:6px;padding:10px;font-family:inherit}
  447.  .bar input.small{flex:0 0 130px;min-width:100px}
  448.  button{font-family:inherit;font-size:13px;border:0;border-radius:6px;
  449.         padding:10px 18px;cursor:pointer;color:#fff}
  450.  .scan{background:var(--green)} .scan:hover{background:var(--greenL)}
  451.  .stop{background:var(--amber)} .clear{background:var(--red)}
  452.  h2{font-size:13px;color:var(--muted);text-transform:uppercase;
  453.     letter-spacing:1px;margin:24px 0 12px}
  454.  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px}
  455.  .box{display:block;text-decoration:none;color:inherit;background:var(--panel);
  456.       border:1px solid var(--border);border-radius:8px;padding:12px;transition:.15s}
  457.  .box:hover{border-color:var(--blue)}
  458.  .box.seized{border-color:var(--red);box-shadow:0 0 0 1px var(--red)}
  459.  .box .d{font-weight:600;word-break:break-all}
  460.  .box .ip{color:var(--blue);font-size:12px;margin-top:4px}
  461.  .box .meta{color:var(--muted);font-size:11px;margin-top:6px;line-height:1.5}
  462.  .tag{display:inline-block;font-size:10px;padding:2px 7px;border-radius:20px;margin-top:8px;margin-right:4px}
  463.  .t200{background:rgba(46,160,67,.15);color:var(--greenL)}
  464.  .tseiz{background:rgba(248,81,73,.15);color:var(--red)}
  465.  .tchg{background:rgba(210,153,34,.15);color:var(--amber)}
  466.  .feed{background:var(--panel);border:1px solid var(--border);border-radius:8px;
  467.        padding:12px;max-height:320px;overflow:auto;font-size:12px;line-height:1.7}
  468.  .feed .seizure{color:var(--red)} .feed .new{color:var(--greenL)}
  469.  .feed .dns{color:var(--amber)} .feed .ts{color:var(--muted)}
  470.  .empty{color:var(--muted);padding:20px;text-align:center}
  471. </style></head>
  472. <body>
  473. <header>
  474.  <h1>DOMAIN WATCH</h1>
  475.  <span class="statuspill pill-off" id="statuspill">DETENIDO</span>
  476.  <span id="summary" style="color:var(--muted);font-size:12px"></span>
  477.  <span style="margin-left:auto;color:var(--muted);font-size:12px" id="lastscan"></span>
  478. </header>
  479. <main>
  480.  <div class="bar">
  481.    <input type="text" id="bases" placeholder="dominios base separados por comas: breachforums, ejemplo">
  482.    <input type="number" id="interval" class="small" min="5" value="300" title="Intervalo (seg)">
  483.    <button class="scan" onclick="scan()">β–Ά Escanear</button>
  484.    <button class="stop" onclick="stop()">⏸ Detener</button>
  485.    <button class="clear" onclick="clearAll()">πŸ—‘ Borrar</button>
  486.    <button style="background:var(--blue)" onclick="testTg()">✈ Probar Telegram</button>
  487.  </div>
  488.  <div id="hint" style="color:var(--muted);font-size:12px;margin:-10px 0 20px">
  489.    Escribe dominios base y pulsa <b>Escanear</b> para empezar. VerΓ‘s arriba si estΓ‘
  490.    <span style="color:var(--greenL)">MONITOREANDO</span> o
  491.    <span style="color:var(--red)">DETENIDO</span>. Cada tarjeta es un enlace: Γ‘brela para confirmar.
  492.  </div>
  493.  
  494.  <h2>Dominios en monitoreo (200 OK)</h2>
  495.  <div class="grid" id="cards"></div>
  496.  
  497.  <h2>Feed de alertas</h2>
  498.  <div class="feed" id="feed"></div>
  499. </main>
  500.  
  501. <script>
  502. const api=(p,o)=>fetch(p,o).then(r=>r.json());
  503. const esc=s=>(s||'').replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));
  504.  
  505. async function refresh(){
  506.  const s=await api('/api/state');
  507.  const pill=document.getElementById('statuspill');
  508.  if(s.running){ pill.textContent='● MONITOREANDO'; pill.className='statuspill pill-on'; }
  509.  else { pill.textContent='β–  DETENIDO'; pill.className='statuspill pill-off'; }
  510.  document.getElementById('lastscan').textContent=s.last_scan?('Último escaneo: '+s.last_scan):'Sin escaneos aΓΊn';
  511.  const alive=s.domains.filter(d=>d.status_code===200);
  512.  const seized=alive.filter(d=>d.seized).length;
  513.  document.getElementById('summary').textContent=
  514.    `${alive.length} en monitoreo Β· ${seized} incautados Β· cada ${s.interval}s`;
  515.  
  516.  const cards=document.getElementById('cards');
  517.  cards.innerHTML = alive.length ? alive.map(d=>`
  518.    <a class="box ${d.seized?'seized':''}" href="https://${esc(d.domain)}" target="_blank" rel="noopener">
  519.      <div class="d">${esc(d.domain)} <span style="color:var(--muted);font-weight:400">β†—</span></div>
  520.      <div class="ip">${esc(d.ip||'β€”')}</div>
  521.      <div class="meta">
  522.        HTTP ${d.status_code} Β· ${esc(d.title||'(sin tΓ­tulo)')}<br>
  523.        visto: ${esc(d.first_seen)}<br>
  524.        ${d.last_change?('ΓΊltimo cambio: '+esc(d.last_change)):''}
  525.        ${d.ip_history&&d.ip_history.length>1?('<br>IPs vistas: '+d.ip_history.length):''}
  526.      </div>
  527.      <span class="tag t200">200 OK</span>
  528.      ${d.last_change?'<span class="tag tchg">CAMBIΓ“</span>':''}
  529.      ${d.seized?'<span class="tag tseiz">INCAUTADO</span>':''}
  530.    </a>`).join('')
  531.    : '<div class="empty">Nada en monitoreo. Escribe dominios base y pulsa Escanear.</div>';
  532.  
  533.  document.getElementById('feed').innerHTML = s.events.slice().reverse().map(e=>
  534.    `<div class="${e.level}"><span class="ts">${esc(e.ts)}</span> β€” ${esc(e.msg)}</div>`
  535.  ).join('') || '<div class="empty">Sin eventos.</div>';
  536. }
  537.  
  538. async function scan(){
  539.  const bases=document.getElementById('bases').value;
  540.  const interval=document.getElementById('interval').value;
  541.  await api('/api/scan',{method:'POST',headers:{'Content-Type':'application/json'},
  542.    body:JSON.stringify({bases,interval})});
  543.  setTimeout(refresh,600);
  544. }
  545. async function stop(){ await api('/api/stop',{method:'POST'}); refresh(); }
  546. async function testTg(){
  547.  const r=await api('/api/testtg',{method:'POST'});
  548.  alert(r.ok ? 'βœ… Mensaje enviado a Telegram. Revisa tu chat.'
  549.             : '❌ No se pudo enviar. Revisa TOKEN/CHAT_ID (mira el feed de alertas).');
  550.  refresh();
  551. }
  552. async function clearAll(){
  553.  if(!confirm('ΒΏBorrar todo el estado y empezar de cero?'))return;
  554.  await api('/api/clear',{method:'POST'});
  555.  document.getElementById('bases').value='';
  556.  refresh();
  557. }
  558.  
  559. (async()=>{
  560.  const s=await api('/api/state');
  561.  document.getElementById('bases').value=s.base_domains.join(', ');
  562.  document.getElementById('interval').value=s.interval;
  563.  refresh();
  564. })();
  565. setInterval(refresh,4000);
  566. </script>
  567. </body></html>"""
  568.  
  569.  
  570. @app.route("/")
  571. def index():
  572.     return render_template_string(PAGE)
  573.  
  574.  
  575. @app.route("/api/state")
  576. def api_state():
  577.     with store.lock:
  578.         return jsonify({
  579.             "running": store.running,
  580.             "interval": store.interval,
  581.             "last_scan": store.last_scan,
  582.             "base_domains": store.base_domains,
  583.             "domains": [asdict(d) for d in store.domains.values()],
  584.             "events": store.events[-100:],
  585.         })
  586.  
  587.  
  588. @app.route("/api/scan", methods=["POST"])
  589. def api_scan():
  590.     data = request.get_json(force=True)
  591.     bases = [b.strip().lower() for b in str(data.get("bases", "")).split(",") if b.strip()]
  592.     try:
  593.         interval = max(5, int(data.get("interval", DEFAULT_INTERVAL)))
  594.     except (TypeError, ValueError):
  595.         interval = DEFAULT_INTERVAL
  596.     with store.lock:
  597.         store.base_domains = bases
  598.         store.interval = interval
  599.         store.running = True
  600.     store.add_event("info", f"Escaneo iniciado: {len(bases)} base(s), cada {interval}s")
  601.     threading.Thread(target=scan_once, daemon=True).start()
  602.     return jsonify({"ok": True})
  603.  
  604.  
  605. @app.route("/api/testtg", methods=["POST"])
  606. def api_testtg():
  607.     if not (TELEGRAM_TOKEN and TELEGRAM_CHAT_ID):
  608.         store.add_event("info", "⚠ Telegram NO configurado (falta TOKEN o CHAT_ID).")
  609.         return jsonify({"ok": False, "reason": "no_config"})
  610.     try:
  611.         r = requests.post(
  612.             f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage",
  613.             json={"chat_id": TELEGRAM_CHAT_ID,
  614.                   "text": "βœ… Domain Watch: prueba de conexiΓ³n OK."},
  615.             timeout=10,
  616.         )
  617.         ok = r.status_code == 200 and r.json().get("ok")
  618.         store.add_event("info",
  619.             "βœ… Telegram OK: mensaje enviado." if ok
  620.             else f"❌ Telegram respondiΓ³ error: {r.text[:120]}")
  621.         return jsonify({"ok": bool(ok)})
  622.     except requests.RequestException as e:
  623.         store.add_event("info", f"❌ Telegram fallΓ³: {e}")
  624.         return jsonify({"ok": False, "reason": str(e)})
  625.  
  626.  
  627. @app.route("/api/stop", methods=["POST"])
  628. def api_stop():
  629.     with store.lock:
  630.         store.running = False
  631.     store.add_event("info", "Monitoreo detenido.")
  632.     return jsonify({"ok": True})
  633.  
  634.  
  635. @app.route("/api/clear", methods=["POST"])
  636. def api_clear():
  637.     store.base_domains = []
  638.     store.reset()
  639.     return jsonify({"ok": True})
  640.  
  641.  
  642. def main() -> None:
  643.     threading.Thread(target=scan_loop, daemon=True).start()
  644.     log.info("Interfaz en http://127.0.0.1:5000")
  645.     if not (TELEGRAM_TOKEN and TELEGRAM_CHAT_ID):
  646.         log.warning("TELEGRAM_TOKEN / TELEGRAM_CHAT_ID no definidos: alertas solo en interfaz.")
  647.     app.run(host="0.0.0.0", port=5000, debug=False, use_reloader=False)
  648.  
  649.  
  650. if __name__ == "__main__":
  651.     main()
Tags: Foros
Advertisement
Add Comment
Please, Sign In to add comment