En esta sección explicaré cómo podemos realizar un scraping a gran escala de las publicaciones recopiladas. El trabajo preliminar ya está hecho, los detalles de las publicaciones se han extraído, pero su contenido no, por lo que en la fase final extraemos el contenido y lo traducimos si es necesario.

Esta es la fase más interesante porque estamos llegando al final del curso. Identificar las ventas de IAB era nuestro objetivo inicial, que es para lo que estás aquí, pero esta sección puede ser la más complicada de todas, al menos a primera vista.

Es compleja porque estamos creando una solución para el escaneo de datos a gran escala, lo que obviamente va a ser complicado, con muchas partes móviles, pero es la realidad de la monitorización de amenazas a gran escala.

Los temas de esta sección incluyen lo siguiente:

  1. Componentes del rastreador de datos
  2. Modelos de bases de datos
  3. Plantillas para gestionar y mostrar los escaneos
  4. Plantilla para mostrar el resultado de cada escaneo
  5. Rutas de backend
  6. Pruebas

Componentes del rastreador de datos

Nuestro rastreador de datos consta de varios componentes, incluidos módulos diseñados para tareas como rastrear detalles de publicaciones, traducir contenido cuando es necesario y clasificar datos.

Los componentes principales se encuentran en app/scrapers/post_scraper.py.

  1. scrape_post_details:

    • Finalidad: extrae detalles (título, marca de tiempo, autor, contenido) de una URL de publicación específica utilizando técnicas de extracción web.
    • Parámetros clave:
      • post_link: URL de la publicación que se va a extraer.
      • session_cookie: cookie de autenticación para acceder a la publicación.
      • tor_proxy: Dirección proxy opcional para el enrutamiento Tor.
      • user_agent: Cadena de agente de usuario para los encabezados de solicitud.
      • timeout: Duración del tiempo de espera de la solicitud (por defecto: 30 segundos).
    • Devuelve: Cadena JSON que contiene los detalles de la publicación extraída o información sobre el error si la solicitud falla.
  2. translate_string:

    • Propósito: Detecta el idioma de una cadena de entrada y la traduce al inglés (o al idioma de destino especificado) utilizando la API de DeepL si aún no está en inglés.
    • Parámetros clave:
      • input_string: Texto a analizar y posiblemente traducir.
      • auth_key: Clave de autenticación de la API de DeepL.
      • target_lang: Idioma de destino para la traducción (por defecto: EN-US).
    • Devuelve: Cadena JSON con el texto original, el idioma detectado y el texto traducido (si procede) o los detalles del error.
  3. iab_classify:

    • Finalidad: Clasifica una publicación utilizando el modelo Claude de Anthropic para determinar si se refiere a la venta de acceso inicial, artículos no relacionados o advertencias/quejas.
    • Parámetros clave:
      • api_key: Clave API de Anthropic para la autenticación.
      • model_name: Nombre del modelo Claude que se va a utilizar (por ejemplo, «claude-3-5-sonnet-20241022»).
      • prompt: Texto que contiene la publicación que se va a clasificar.
      • max_tokens: Número máximo de tokens de salida (por defecto: 100).
    • Devuelve: Cadena JSON con el resultado de la clasificación, las puntuaciones o la información de error si la clasificación falla.

Función scrape_post_details

El foro Tornet requiere una sesión iniciada para leer las publicaciones, lo cual es habitual en la mayoría de los foros. Para solucionar esto, he desarrollado una función que toma un enlace a una publicación y recupera sus datos.

Este enfoque es lógico, ya que la tabla marketplace_posts almacena todos los detalles y enlaces de las publicaciones. Al cargar estos datos, podemos pasar cada enlace de publicación a una función como scrape_post_details para extraer la información necesaria.

Función translate_string

En la función translate_string, utilizamos DeepL para la traducción de datos. Sin embargo, en app/routes/posts.py, primero empleamos la biblioteca langdetect para identificar el idioma de una publicación. Si la detección del idioma falla o el idioma detectado no es el inglés, pasamos la publicación a la función translate_string.

Una ventaja clave es que si se proporciona una cadena que contiene saltos de línea:

Venta de acceso a Horizon Logistics\nIngresos: 1200 millones de dólares\nAcceso: RDP con DA\nPrecio: 0,8 BTC\nDM para más detalles

La función los conserva en la salida traducida:

Sale of access to Horizon Logistics\nRevenue: $1.2 billion\nAccess: RDP with DA\nPrice: 0.8 BTC\nDM for more details

Función iab_classify

En iab_classify, nuestra temperatura está establecida por defecto en 0,1, pero puedes cambiarla si lo deseas.

En las interacciones con IA o LLM, la temperatura es un hiperparámetro que controla la aleatoriedad o la creatividad de la salida del modelo:

  • Propósito: ajusta la distribución de probabilidad de las posibles salidas del modelo (por ejemplo, palabras o tokens) durante la generación.
  • Cómo funciona:
  • Temperatura baja (por ejemplo, 0,1): hace que el modelo sea más determinista, favoreciendo los resultados de alta probabilidad. Da lugar a respuestas más centradas, predecibles y conservadoras.
  • Temperatura alta (por ejemplo, 1,0 o superior): aumenta la aleatoriedad, dando más posibilidades a los resultados con menor probabilidad. Da lugar a respuestas más creativas, diversas o inesperadas.
  • Ejemplo en código: en la función iab_classify proporcionada, se utiliza temperature=0.1 para que los resultados de clasificación del modelo Claude sean más coherentes y menos aleatorios.
  • Rango: Normalmente entre 0 y 1, aunque algunos modelos permiten valores más altos para una aleatoriedad extrema.

Modelos de base de datos

Para extraer publicaciones, el tamaño de los lotes de publicaciones y almacenar datos, necesitamos dos tablas. Así es como se ven tus modelos:

class PostDetailScan(Base):
    __tablename__ = "post_detail_scans"

    id = Column(Integer, primary_key=True, index=True)
    scan_name = Column(String, nullable=False, unique=True)
    source_scan_name = Column(String, ForeignKey("marketplace_post_scans.scan_name"), nullable=False)
    start_date = Column(DateTime(timezone=True), default=datetime.utcnow)
    completion_date = Column(DateTime(timezone=True), nullable=True)
    status = Column(Enum(ScanStatus), default=ScanStatus.STOPPED, nullable=False)
    batch_size = Column(Integer, nullable=False)
    site_url = Column(String, nullable=False)
    timestamp = Column(DateTime(timezone=True), default=datetime.utcnow)


class MarketplacePostDetails(Base):
    __tablename__ = "marketplace_post_details"

    id = Column(Integer, primary_key=True, index=True)
    scan_id = Column(Integer, ForeignKey("post_detail_scans.id"), nullable=False)
    batch_name = Column(String, nullable=False)
    title = Column(String, nullable=False)
    content = Column(Text, nullable=False)
    timestamp = Column(String, nullable=False)
    author = Column(String, nullable=False)
    link = Column(String, nullable=False)
    original_language = Column(String, nullable=True)
    original_text = Column(Text, nullable=True)
    translated_language = Column(String, nullable=True)
    translated_text = Column(Text, nullable=True)
    is_translated = Column(Boolean, default=False)
    sentiment = Column(String, nullable=True)
    positive_score = Column(Float, nullable=True)
    negative_score = Column(Float, nullable=True)
    neutral_score = Column(Float, nullable=True)
    timestamp_added = Column(DateTime(timezone=True), default=datetime.utcnow)
    __table_args__ = (UniqueConstraint('scan_id', 'timestamp', 'batch_name', name='uix_scan_timestamp_batch'),)

post_detail_scans

La tabla post_detail_scans se utiliza para crear escaneos que recuperan datos de la tabla marketplace_post_scans. También almacena el tamaño del lote, ya que muchos sitios imponen límites de velocidad, como restricciones en el número de publicaciones que se pueden leer en 24 horas. Para gestionar esto, dividimos las publicaciones en lotes de 10 o 20 y asignamos estos lotes a bots configurados con el propósito scrape_post.

La tabla marketplace_post_scans almacena los metadatos de las publicaciones, incluyendo el título, el enlace, la marca de tiempo y el autor, pero excluye el contenido detallado.

marketplace_post_details

Esta tabla almacena los resultados de cada escaneo. Iniciamos los escaneos en «post_detail_scans», los cargamos en el rastreador y comenzamos a recopilar datos. Una vez recopilados, los datos se guardan en la tabla «marketplace_post_details».

Realizamos un seguimiento de todos los detalles, incluyendo el texto original, el idioma original, el texto traducido, el sentimiento, las puntuaciones de confianza, el autor, la marca de tiempo y mucho más.


Plantillas para gestionar y mostrar los escaneos

Necesitamos dos plantillas: una para gestionar los escaneos y otra para mostrar los resultados de cada escaneo. La plantilla para gestionar los escaneos es posts_scans.html. Aquí tienes una vista previa de su interfaz: Plantilla de escaneos de publicaciones

El código correspondiente se encuentra en app/templates/posts_scans.html.

  1. Creación e inicio del escaneo de detalles de la publicación:

    • Propósito: Crea e inicia un nuevo escaneo de detalles de la publicación.
    • Interacción con el backend:
      • El botón «Nuevo escaneo de detalles de la publicación» abre un modal (newScanModal) con campos para el nombre del escaneo, el escaneo de la publicación de origen (menú desplegable de escaneos completados), el tamaño del lote y la URL del sitio.
      • El envío del formulario activa una solicitud AJAX POST a /api/posts-scanner/create (gestionada por posts_api_router) con los datos del formulario, seguida de una solicitud POST a /api/posts-scanner/{id}/start para iniciar el escaneo.
      • El backend crea un registro «PostDetailScan», lo vincula a un «MarketplacePostScan» y comienza el rastreo. Los datos del formulario se almacenan en «sessionStorage» para su reutilización en «startScan()». Si se realiza correctamente, se muestra una alerta de éxito, se cierra el modal y «refreshScans()» actualiza la tabla. Los errores activan una alerta con el mensaje de error.
  2. Listado y actualización del escaneo de detalles de publicaciones:

    • Propósito: muestra y actualiza una tabla con los escaneos de detalles de publicaciones.
    • Interacción con el backend:
      • La función refreshScans(), llamada al cargar la página y al pulsar el botón «Refresh Scans», envía una solicitud AJAX GET a /api/posts-scanner/list (gestionada por posts_api_router).
      • El backend devuelve una lista de registros PostDetailScan (ID, nombre del escaneo, nombre del escaneo de origen, fechas de inicio/finalización, publicaciones extraídas, estado). La tabla se rellena con insignias de estado (por ejemplo, badge-success para completadas). Si no hay escaneos, se muestra el mensaje «No hay escaneos disponibles». Los errores activan una alerta.
  3. Iniciar un escaneo detallado de una publicación:

    • Objetivo: Inicia un escaneo detallado de una publicación existente.
    • Interacción con el backend:
      • El botón «Iniciar» de cada fila de la tabla (desactivado para los escaneos en ejecución) llama a startScan(scanId), enviando una solicitud AJAX POST a /api/posts-scanner/{scanId}/start (gestionada por posts_api_router) con batch_size y site_url de sessionStorage.
      • El backend inicia el escaneo y actualiza el estado de PostDetailScan. Si se realiza correctamente, se muestra una alerta de éxito y refreshScans() actualiza la tabla. Los errores activan una alerta.
  4. Visualización de los resultados del escaneo:

    • Finalidad: Redirige a una página de resultados para un escaneo específico.
    • Interacción con el backend:
      • El botón «Ver» de cada fila de la tabla llama a viewResults(scanId, scanName), redirigiendo a /posts-scan-result/{scanId}?name={scanName} (gestionado por main.py::posts_scan_result).
      • El backend renderiza una plantilla con los detalles del escaneo, recuperando los registros MarketplacePostDetails asociados. Aquí no se produce ninguna llamada AJAX directa, pero la redirección depende de los datos del backend.
  5. Eliminar un escaneo de detalles de una publicación:

    • Propósito: Elimina un escaneo de detalles de una publicación.
    • Interacción con el backend:
      • El botón «Eliminar» de cada fila de la tabla llama a deleteScan(scanId) tras la confirmación del usuario, enviando una solicitud AJAX DELETE a /api/posts-scanner/{scanId} (gestionado por posts_api_router).
      • El backend elimina el registro PostDetailScan. Si se realiza correctamente, se muestra una alerta de éxito y refreshScans() actualiza la tabla. Los errores activan una alerta.
  6. Rellenar el menú desplegable de escaneos de publicaciones de origen:

    • Objetivo: Rellena el menú desplegable de escaneos de origen con los escaneos de publicaciones completados.
    • Interacción del backend:
      • Al cargar la página, una solicitud AJAX GET a /api/posts-scanner/completed-post-scans (gestionada por posts_api_router) obtiene una lista de nombres MarketplacePostScan completados.
      • El backend devuelve los nombres de los escaneos, que se añaden como opciones al menú desplegable del nuevo modal de escaneo. Los errores se registran en la consola.

Plantilla para mostrar el resultado de cada escaneo

Al mostrar el resultado de cada escaneo del mercado, utilizamos modales para mostrarlos. Pero aquí, debido a que hay mucha información, búsquedas y filtros, necesitamos una plantilla diferente solo para mostrar los resultados.

Así es como utilizo la búsqueda y la clasificación de sentimientos para filtrar los resultados positivos que hablan de IAB y tienen la palabra clave «shell» en el título:

Plantilla de resultados del escaneo de publicaciones

Como analista, algo así te resulta extremadamente útil porque puedes filtrar los resultados, ver solo lo que consideras importante para tu investigación y descargarlo en formato JSON.

La plantilla utilizada para mostrar el resultado de cada escaneo se encuentra en: app/templates/posts_scan_result.html.

  1. Carga de los resultados del escaneo:

    • Propósito: muestra los resultados de un escaneo detallado de una publicación específica en una tabla.
    • Interacción con el backend:
      • Al cargar la página, la función loadResults() extrae el scanId de la URL y envía una solicitud AJAX GET a /api/posts-scanner/{scanId}/results (gestionada por posts_api_router).
      • El backend consulta la tabla MarketplacePostDetails para obtener los resultados del escaneo (ID, título, marca de tiempo, autor, nombre del lote, puntuaciones de sentimiento, idioma, texto traducido) y los devuelve como JSON.
      • La tabla se rellena con filas, cada una de las cuales muestra una casilla de verificación, el nombre del lote, el título truncado, la marca de tiempo, el autor, las puntuaciones de sentimiento (positivo, negativo, neutro), el sentimiento dominante (calculado en el lado del cliente como la puntuación más alta), el idioma y el estado de la traducción. Los errores activan una alerta.
  2. Búsqueda y filtrado por sentimiento:

    • Objetivo: Filtra la tabla de resultados por título de la publicación y sentimiento.
    • Interacción con el backend:
      • La función filterTable(), activada por la tecla #searchInput y el cambio en #sentimentFilter, filtra las filas de la tabla en el lado del cliente en función del término de búsqueda (título) y el sentimiento seleccionado (all, positive, negative, neutral).
      • No se realizan llamadas directas al backend; el filtrado utiliza el atributo data-sentiment establecido durante loadResults(). Las filas se muestran u ocultan en función de las coincidencias, lo que garantiza actualizaciones dinámicas sin solicitudes adicionales.
  3. Visualización de los detalles de la publicación:

    • Finalidad: muestra información detallada de una publicación seleccionada en una ventana modal.
    • Interacción con el backend:
      • El botón «Ver» de cada fila de la tabla rellena el «viewModal» con los datos almacenados en los atributos «data-*» del botón (título, marca de tiempo, autor, lote, puntuaciones de sentimiento, sentimiento, idioma, estado de la traducción, enlace, contenido original/traducido) de la respuesta inicial «loadResults()».
      • No se requiere ninguna llamada adicional al backend; el modal muestra campos y áreas de texto de solo lectura. El botón «Cerrar» oculta el modal sin interacción con el backend.
  4. Descarga de los resultados seleccionados:

    • Finalidad: Exporta los resultados de las publicaciones seleccionadas como un archivo JSON.
    • Interacción con el backend:
      • El botón «Descargar seleccionados» (#downloadSelected) recopila los ID de las filas marcadas (filtradas por visibilidad) y envía una solicitud AJAX POST a /api/posts-scanner/{scanId}/download (gestionada por posts_api_router) con la matriz post_ids.
      • El backend recupera los registros MarketplacePostDetails correspondientes y los devuelve como JSON. El cliente crea un archivo JSON descargable (scan_{scanId}_results.json) utilizando un Blob. Si no se selecciona ninguna fila o se produce un error, se muestra una alerta.

Rutas del backend

El código del backend se encuentra en app/routes/posts.py. A continuación se explican las funciones clave.

get_post_scans:

  • Propósito: recupera todos los escaneos de detalles de publicaciones de la base de datos, incluidos detalles como el ID del escaneo, el nombre, el nombre del escaneo de origen, las fechas de inicio y finalización, el estado y el recuento de publicaciones extraídas.
  • Características principales:
    • Consulta PostDetailScan y se une a MarketplacePostDetails para contar las publicaciones extraídas.
    • Devuelve una respuesta JSON con los detalles del escaneo.
    • Gestiona los errores con un código de estado 500 si la consulta falla.

get_completed_post_scans:

  • Finalidad: Obtiene los nombres de los escaneos MarketplacePostScan completados para utilizarlos en un menú desplegable.
  • Características principales:
    • Filtra los escaneos con estado COMPLETED y una fecha de finalización que no sea nula.
    • Devuelve una lista JSON de nombres de escaneos.
    • Genera un error 500 si la consulta falla.

create_post_scan:

  • Finalidad: Crea un nuevo escaneo de detalles de publicación basado en una configuración proporcionada.
  • Características principales:
    • Valida que el nombre del escaneo sea único y que el escaneo de origen esté completado.
    • Crea un registro PostDetailScan con estado STOPPED y almacena el tamaño del lote y la URL del sitio.
    • Devuelve una respuesta JSON con el ID del escaneo y un mensaje de éxito.
    • Gestiona los nombres de escaneo duplicados (400) o los escaneos de origen que faltan (404).

start_post_scan:

  • Propósito: Inicia un escaneo de detalles de publicaciones procesando las publicaciones de un escaneo de origen en lotes utilizando varios bots.
  • Características principales:
    • Verifica que el escaneo existe, no se está ejecutando y tiene las API necesarias (traducción e IAB) y los bots activos.
    • Divide las publicaciones en lotes y las asigna a los bots para su raspado, traducción y clasificación simultáneos.
    • Utiliza scrape_post_details, translate_string e iab_classify para procesar las publicaciones.
    • Guarda los resultados en MarketplacePostDetails y actualiza el estado del escaneo a RUNNING o COMPLETED/STOPPED según el éxito.
    • Gestiona los errores con los códigos de estado HTTP adecuados (404, 400, 500).

delete_post_scan:

  • Finalidad: Elimina un escaneo de detalles de una publicación específica de la base de datos.
  • Características principales:
    • Comprueba que el escaneo existe antes de eliminarlo.
    • Elimina el registro PostDetailScan y confirma el cambio.
    • Devuelve un mensaje de éxito en JSON o un error 404 si no se encuentra el escaneo.
    • Gestiona los errores inesperados con un código de estado 500.

get_scan_results:

  • Finalidad: Recupera los resultados detallados de un escaneo de detalles de una publicación específica.
  • Características principales:
    • Consulta MarketplacePostDetails para un ID de escaneo determinado.
    • Devuelve una respuesta JSON con detalles como el título, el contenido, el autor, la marca de tiempo, los datos de traducción y las puntuaciones de clasificación.
    • Genera un error 500 si la consulta falla.

download_post_results:

  • Finalidad: Descarga los detalles de una publicación específica para un ID de escaneo determinado en función de los ID de publicación proporcionados.
  • Características principales:
    • Verifica que el escaneo existe y que los ID de publicación solicitados son válidos.
    • Devuelve una respuesta JSON con los detalles de la publicación seleccionada, incluyendo el título, la marca de tiempo, el autor, las puntuaciones de sentimiento y los datos de traducción.
    • Genera un error 404 si no se encuentra el escaneo o las publicaciones, o un error 500 por otros problemas.

Utilizamos la función detect de la biblioteca langdetect dentro de scrape_post_batches. Si lo deseas, puedes modificar la función «translate_string» en «post_scraper.py» para incorporar también «langdetect». Aunque es una opción, prefiero el enfoque actual por su eficiencia.


Pruebas

Para probar esta funcionalidad, configura los siguientes componentes:

  1. Extrae los detalles de las publicaciones del mercado utilizando la página «/marketplace-scan».
  2. Configure una API de IA para eludir el CAPTCHA.
  3. Cree al menos dos perfiles de bot con el propósito establecido en «scrape_post» e inicie sesión para obtener las cookies de sesión.
  4. Configure la API de DeepL para la traducción.
  5. Configure la API de IAB para identificar los brokers de acceso inicial.

Para la API de IAB, necesitará el siguiente prompt:

Does this post discuss selling initial access to a company (e.g., RDP, VPN, admin access), selling unrelated items (e.g., accounts, tools), or warnings/complaints? Classify it as:
- Positive Posts: direct sale of unauthorized access to a company, this usually include the target's name.
- Neutral Posts: general offers for tools, exploits or malware without naming a specific target.
- Negative Posts: off-topic or unrelated services such as hosting, spam tools or generic VPS sales.

The content must be specifically about selling access to a company or business whose name is mentioned in the post. 

Return **only** a JSON object with:
- `classification`: "Positive", "Neutral", or "Negative".
- `scores`: Probabilities for `positive`, `neutral`, `negative` (summing to 1).

Wrap the JSON in ```json
{
  ...
}
``` to ensure proper formatting. Do not include any reasoning or extra text.

Post:
```markdown
TARGET-POST-PLACEHOLDER
``` 

Puedes modificar el mensaje y probarlo por tu cuenta.