first commit
464
README.md
Normal file
|
@ -0,0 +1,464 @@
|
||||||
|
## Descripción
|
||||||
|
|
||||||
|
Este proyecto permite en tiempo real realizar correcciones (limpieza) en los atributos dentro de cada evento en MISP antes de ser publicado en la plataforma. Esto se lleva a cabo a través de la funcionalidad de Workflows que integra MISP.
|
||||||
|
|
||||||
|
Como resultado este proyecto implementa una API en un servidor para recibir los datos de cada evento para ser procesados.
|
||||||
|
|
||||||
|
(Se necesita el servicio gratuito de lookup de IP's, dominios y hashes que ofrece Kaspersky© Threat Intelligence Portal para la limpieza de falsos positivos)
|
||||||
|
|
||||||
|
|
||||||
|
## Características:
|
||||||
|
|
||||||
|
- Limpieza de falsos positivos de cada evento generado en MISP (IP, Domain, SHA256)
|
||||||
|
|
||||||
|
- Normalización de correlación y activación de tag IDS en cada atributo. Solo se activa tag IDS para el tipo de atributo que corresponde, como a su vez solo se activa la correlación para los tipos de atributos que deben ser correlacionados.
|
||||||
|
|
||||||
|
- Normalización de tags para eventos que contengan "tlp: white" ( Se agrega tag tlp:clear en cada caso)
|
||||||
|
|
||||||
|
- Utiliza dentro de Workflows, el trigger "event-publish"
|
||||||
|
|
||||||
|
## Requisitos
|
||||||
|
- Se requiere una cuenta en la plataforma Kaspersky© Threat Intelligence Portal. La cuenta es gratuita. ([https://opentip.kaspersky.com])
|
||||||
|
|
||||||
|
## Requisitos de Hardware recomendados
|
||||||
|
|
||||||
|
- 8/16 GB RAM
|
||||||
|
- 4 vCPU
|
||||||
|
- 100 GB almacenamiento
|
||||||
|
|
||||||
|
## Componentes
|
||||||
|
|
||||||
|
Este proyecto utiliza un BD local (SQLite en modo WALL) y FastAPI.
|
||||||
|
|
||||||
|
## Configuración inicial
|
||||||
|
|
||||||
|
1. En el archivo config.py se debe definir los datos de conexión a MISP, como también la API KEY asociada a Kaspersky TIP:
|
||||||
|
``` python
|
||||||
|
|
||||||
|
MISP_CONFIG = {
|
||||||
|
"misp_url":"URL_MISP",
|
||||||
|
"misp_authkey":"AUTHKEY_MISP"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Se si quiere agregar a exclusión de MISP cada atributo al deshabilitar correlación
|
||||||
|
NO_CORRELATIVOS_EXCLUSION = False
|
||||||
|
|
||||||
|
# Config Lookup KTIP
|
||||||
|
KTIP_CONFIG = {
|
||||||
|
"api_key":"APIKEY_KTIP",
|
||||||
|
"url_base": "https://opentip.kaspersky.com/api/v1/search/"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Rango de dias para verificar si IoC es falso positivo
|
||||||
|
RANGO_DIAS = 7
|
||||||
|
|
||||||
|
# Se debe establecer Manual
|
||||||
|
JWT_TOKEN_GEN = "BEARER_TOKEN"
|
||||||
|
|
||||||
|
# Máximo de atributos a procesar por evento (4000 es el recomendado)
|
||||||
|
MAX_ATTRS = 4000
|
||||||
|
|
||||||
|
# Falsos positivos comunes
|
||||||
|
# FP Comunes
|
||||||
|
FP_COMUNES = ['0.0.0.0','8.8.8.8','google.com','amazon.com','microsoft.com','cloudflare.com']
|
||||||
|
|
||||||
|
# Organizaciones que se puede omitir la revisión de eventos
|
||||||
|
ORG_OMITIR = []
|
||||||
|
|
||||||
|
NO_CORRELATIVOS = [
|
||||||
|
"comment”, # Comentarios descriptivos
|
||||||
|
"email-subject", # Asuntos de correos electrónicos
|
||||||
|
"email-dst", # Emails de destinatarios
|
||||||
|
"email-src", # Emails de remitentes
|
||||||
|
"hostname", # Nombres de host
|
||||||
|
"port", # Puertos
|
||||||
|
"link", # Enlaces
|
||||||
|
"phone-number", # Números de teléfono
|
||||||
|
"user-agent", # Agentes de usuario
|
||||||
|
"size-in-bytes", # Tamaños en bytes
|
||||||
|
"vulnerability", # Vulnerabilidades (CVE)
|
||||||
|
"whois-registrant-email", # Correos de registrantes WHOIS
|
||||||
|
"whois-registrant-name", # Nombres de registrantes WHOIS
|
||||||
|
"regkey", # Claves de registro de Windows
|
||||||
|
"regkey|value", # Claves de registro con valores
|
||||||
|
"text", # Texto libre
|
||||||
|
"datetime", # Fechas y horas
|
||||||
|
"campaign-name", # Nombres de campaña
|
||||||
|
"attachment", # Archivos adjuntos
|
||||||
|
"email" # Emails de remitentes
|
||||||
|
]
|
||||||
|
|
||||||
|
IDS_CORRELATIVOS = [
|
||||||
|
"ip-src", # IP de origen
|
||||||
|
"ip-dst", # IP de destino
|
||||||
|
"domain", # Dominio
|
||||||
|
"domain|ip", # Dominio con IP asociada
|
||||||
|
"url", # URL completas
|
||||||
|
"uri", # URI (fragmentos de rutas)
|
||||||
|
"http-method", # Métodos HTTP (GET, POST, etc.)
|
||||||
|
"email-attachment", # Nombres de archivos adjuntos en correos
|
||||||
|
"filename", # Nombres de archivo
|
||||||
|
"filename|md5", # Nombre de archivo con su hash MD5
|
||||||
|
"filename|sha1", # Nombre de archivo con su hash SHA1
|
||||||
|
"filename|sha256", # Nombre de archivo con su hash SHA256
|
||||||
|
"md5", # Hash MD5
|
||||||
|
"sha1", # Hash SHA1
|
||||||
|
"sha256", # Hash SHA256
|
||||||
|
"authentihash", # Hash Authentihash
|
||||||
|
"impfuzzy", # Hash ImpHash (ejecutable PE)
|
||||||
|
"tlsh", # Hash TLSH
|
||||||
|
"ssdeep", # Hash SSDEEP (fuzzy hash)
|
||||||
|
"mutex", # Nombres de mutex
|
||||||
|
"registry-key", # Claves de registro (si son relevantes)
|
||||||
|
"registry-key|value", # Claves de registro con valores
|
||||||
|
"ip-src|port", # IP de origen con puerto
|
||||||
|
"ip-dst|port", # IP de destino con puerto
|
||||||
|
"asn", # ASN (Autonomous System Number)
|
||||||
|
"cidr", # Rango CIDR (IPs)
|
||||||
|
"mac-address", # Dirección MAC
|
||||||
|
"x509-fingerprint-md5", # Huella de certificados X509 (MD5)
|
||||||
|
"x509-fingerprint-sha1", # Huella de certificados X509 (SHA1)
|
||||||
|
"x509-fingerprint-sha256", # Huella de certificados X509 (SHA256)
|
||||||
|
"ja3-fingerprint-md5", # Huella JA3 (TLS handshake)
|
||||||
|
"btc", # Dirección de Bitcoin
|
||||||
|
"iban", # Número de cuenta bancaria (IBAN)
|
||||||
|
"bank-account-nr", # Número de cuenta bancaria
|
||||||
|
"payment-card-number" # Número de tarjeta de pago
|
||||||
|
]
|
||||||
|
|
||||||
|
```
|
||||||
|
En config.py puedes realizar los ajustes:
|
||||||
|
|
||||||
|
- MISP_CONFIG: Configuración asociada a MISP
|
||||||
|
|
||||||
|
- NO_CORRELATIVOS_EXCLUSION: Si es True, cada atributo que sea corregida su correlación, ósea que se desactive, será agregado valor a la lista de exclusión de correlaciones.
|
||||||
|
|
||||||
|
- KTIP_CONFIG: Configuración de conexión a servicio de Lookup de Kaspersky. Se debe solo configurar "api_key".
|
||||||
|
|
||||||
|
- RANGO_DIAS: Establece según la fecha del atributo (IoC) cuantos días tomar como rango para verificar falso positivo
|
||||||
|
|
||||||
|
- JWT_TOKEN_GEN: Token personal generado.
|
||||||
|
|
||||||
|
- MAX_ATTRS: Cantidad de atributos a procesar por evento. Por defecto el máximo son 4000. No se recomienda aumentar este valor.
|
||||||
|
|
||||||
|
- FP_COMUNES: Falsos positivos conocidos.
|
||||||
|
|
||||||
|
- ORG_OMITIR: Organizaciones que se puede omitir la revisión de eventos.
|
||||||
|
|
||||||
|
- NO_CORRELATIVOS: Lista de tipos de atributos que no deben correlacionarse dentro de MISP.
|
||||||
|
|
||||||
|
- IDS_CORRELATIVOS: Lista de tipos de atributos que deberán tener el flag IDS activado dentro de MISP. Por defecto estos atributos son correlacionados dentro de MISP.
|
||||||
|
|
||||||
|
# Configuración Inicial
|
||||||
|
|
||||||
|
## Instalación en entorno virtual
|
||||||
|
|
||||||
|
1. Se crea entorno virtual de Python
|
||||||
|
``` shell
|
||||||
|
cd /home/user/misp-fixevent-webhook
|
||||||
|
|
||||||
|
python3 -m venv venv
|
||||||
|
|
||||||
|
source venv/bin/activate
|
||||||
|
```
|
||||||
|
2. Instalar bibliotecas de Python:
|
||||||
|
``` shell
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
3. Se debe crear archivo .sh o editar el archivo existente (start_api.sh) y anexarlo como servicio para que inicie con el sistema operativo:
|
||||||
|
|
||||||
|
``` shell
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Activate
|
||||||
|
source /home/user/misp-fixevent-webhook/venv/bin/activate
|
||||||
|
|
||||||
|
# Enter folder
|
||||||
|
cd /home/user/misp-fixevent-webhook/
|
||||||
|
|
||||||
|
# Actualizar bibliotecas si aplica
|
||||||
|
pip install --upgrade -r requirements.txt > /dev/null 2>&1
|
||||||
|
|
||||||
|
# Si vas a utilizar proxy reverso...
|
||||||
|
uvicorn main:app --host 0.0.0.0 --port 8000
|
||||||
|
|
||||||
|
# Si no vas a utilizar proxy reverso puedes utilizar certificados genéricos
|
||||||
|
uvicorn main:app --host 0.0.0.0 --port 8000 --ssl-keyfile key.pem --ssl-certfile cert.pem
|
||||||
|
|
||||||
|
```
|
||||||
|
Debe activar SSL en proyecto
|
||||||
|
|
||||||
|
## Agregar como servicio
|
||||||
|
|
||||||
|
1. Para agregar como servicio necesita crear dentro del directorio /etc/systemd/system (En caso de Ubuntu) un archivo .service(misp_webhooks.service). Podemos crearlo con nano (sudo) usando el siguiente formato:
|
||||||
|
|
||||||
|
``` shell
|
||||||
|
sudo nano /etc/systemd/system/misp_webhooks.service
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=MISP WebHooks Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/home/user/misp-fixevent-webhook/start_api.sh
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
2. Se deben entregar permisos de ejecución a archivo .sh:
|
||||||
|
```shell
|
||||||
|
chmod +x /home/user/misp-fixevent-webhook/start_api.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Se recargan servicios:
|
||||||
|
```shell
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Luego registramos este archivo para que se ejecute al iniciar el sistema:
|
||||||
|
``` shell
|
||||||
|
sudo systemctl enable misp_webhooks.sevice
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Ahora podemos iniciar, detener, reiniciar servicio...
|
||||||
|
``` shell
|
||||||
|
# Iniciar
|
||||||
|
sudo systemctl start misp_webhooks.sevice
|
||||||
|
|
||||||
|
# Detener
|
||||||
|
sudo systemctl stop misp_webhooks.sevice
|
||||||
|
|
||||||
|
# Reiniciar
|
||||||
|
sudo systemctl restart mmisp_webhooks.sevice
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Servicios (Endpoints) de API
|
||||||
|
|
||||||
|
- /webhoook/misp_event_fixer/
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Documentación
|
||||||
|
|
||||||
|
Debes agregar
|
||||||
|
Puedes acceder a la documentación:
|
||||||
|
|
||||||
|
SwaggerUI
|
||||||
|
```
|
||||||
|
http://URL_PROYECTO:8000/docs
|
||||||
|
```
|
||||||
|
|
||||||
|
Redocly
|
||||||
|
```
|
||||||
|
http://URL_PROYECTO:8000/redoc
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuración en Servidor MISP
|
||||||
|
|
||||||
|
Desde MISP necesitamos configurar el Workflow para utilizar nuestra API. Debemos verificar si tenemos "misp-modules" instalados. Estos módulos corren en el puerto 6666. Podemos verificar si el puerto esta activo:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
telnet localhost 6666
|
||||||
|
```
|
||||||
|
Si no hay respuesta, necesitamos instalar las siguientes librerías (Ubuntu):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo apt install git python3 python3-pip python3-dev libpq5 libjpeg-dev build-essential libssl-dev libffi-dev zlib1g-dev libmagic-dev libxml2-dev libxslt1-dev libpoppler-cpp-dev libzbar0 tesseract-ocr
|
||||||
|
```
|
||||||
|
Una vez instaladas, podemos instalar misp-modules en nuestro servidor MISP (como root).
|
||||||
|
Desde la versión 2.5, no se incluye misp-modules (Para más información: https://misp.github.io/misp-modules/install/)
|
||||||
|
|
||||||
|
1. Creamos directorio dentro de MISP:
|
||||||
|
``` shell
|
||||||
|
sudo su
|
||||||
|
cd /var/www/MISP
|
||||||
|
mkdir misp-modules
|
||||||
|
cd /misp-modules
|
||||||
|
```
|
||||||
|
2. Creamos entorno virtual y activamos:
|
||||||
|
``` shell
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Instalamos librerías:
|
||||||
|
``` shell
|
||||||
|
pip install \
|
||||||
|
misp-modules \
|
||||||
|
git+https://github.com/cartertemm/ODTReader.git \
|
||||||
|
git+https://github.com/abenassi/Google-Search-API \
|
||||||
|
git+https://github.com/SteveClement/trustar-python.git \
|
||||||
|
git+https://github.com/sebdraven/pydnstrails.git \
|
||||||
|
git+https://github.com/sebdraven/pyonyphe.git
|
||||||
|
|
||||||
|
```
|
||||||
|
4. Creamos un .sh (run.sh) para llamar como servicio cada vez que parta el equipo:
|
||||||
|
``` shell
|
||||||
|
sudo nano run.sh
|
||||||
|
```
|
||||||
|
5. Dentro del archivo definimos las rutas (según sea el caso):
|
||||||
|
```shell
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Activate
|
||||||
|
source /var/www/MISP/misp-modules/venv/bin/activate
|
||||||
|
|
||||||
|
# Enter folder
|
||||||
|
cd /var/www/MISP/misp-modules/venv/bin/
|
||||||
|
|
||||||
|
# Ejecutar misp-modules
|
||||||
|
misp-modules
|
||||||
|
|
||||||
|
```
|
||||||
|
6. Configuramos servicio:
|
||||||
|
```shell
|
||||||
|
sudo nano /etc/systemd/system/misp-modules.service
|
||||||
|
```
|
||||||
|
7. Dentro del archivo definimos las rutas (según sea el caso):
|
||||||
|
```shell
|
||||||
|
[Unit]
|
||||||
|
Description=MISP Modules Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/var/www/MISP/misp-modules/run.sh
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
7. Se deben entregar permisos de ejecución a archivo .sh:
|
||||||
|
```shell
|
||||||
|
chmod +x /var/www/MISP/misp-modules/run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
8. Se recargan servicios:
|
||||||
|
```shell
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
```
|
||||||
|
|
||||||
|
9. Luego registramos este archivo para que se ejecute al iniciar el sistema y arrancamos app:
|
||||||
|
``` shell
|
||||||
|
sudo systemctl enable misp-modules.sevice
|
||||||
|
sudo systemctl start misp-modules.sevice
|
||||||
|
```
|
||||||
|
|
||||||
|
10. Configuramos carpeta de MISP como owner original "www-data"
|
||||||
|
```shell
|
||||||
|
sudo chown -R www-data:www-data /var/www/MISP
|
||||||
|
```
|
||||||
|
|
||||||
|
## Activando / Configurando Workflow en MISP
|
||||||
|
|
||||||
|
1. Accedemos a MISP y nos dirigimos a "Administration"->"Server Settings & Maintenance"->"Plugin":
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
2. Dentro de Plugin activamos "Enrichment":
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. Luego "Action" y "Workflow":
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
4. Para poder llamar a nuestro webhook desde MISP necesitamos modificar una propiedad de MISP desde el MISP CLI desde terminal:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo -u www-data /var/www/MISP/app/Console/cake Admin setSetting Security.rest_client_enable_arbitrary_urls true
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Iniciamos sesión en nuestro MISP y nos dirigimos a "Administration"->"Worflows" de MISP:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
6. El webhook está diseñado para llamarse en el proceso previo a publicar un evento en MISP, este trigger se llama "event-publish" y debemos activarlo yendo a "List Triggers"->"Event Publish" y presionamos el botón de play:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
7. Luego en la misma ventana, vamos a la sección de "List Modules" y activamos el módulo de "Webhook":
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
8. Para agregar más control en el flujo (Para el caso de Eventos que no tengan atributos => 0) vamos a activar dentro del "List Modules" en "Logic" el módulo "IF::Count":
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
9. Finalmente desde la misma ventana, en "Blocking" activamos "Stop execution":
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Configuración Webhook
|
||||||
|
|
||||||
|
1. Ahora vamos a ir a la configuración de nuestro trigger "event-publish" para configurar los datos para enviar al webhook. Nos dirigimos al símbolo "</>":
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
2. Desde la lista de "Actions", Agregamos "Webhook" al plano de diseño arrastrando:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. Desde la lista de "Logic", Agregamos "IF :: Count" al plano de diseño arrastrando:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
4. Desde la lista de "Actions", Agregamos "Stop execution" al plano de diseño arrastrando:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
5. Conectamos "Event Publish" con "IF :: Count":
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
6. En el módulo IF :: Count configuramos "Data selector to count" con el valor "All Attributes", esto quiere decir que contara todos los atributos del evento:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
7. En el mismo módulo en "Condition" seteamos como "Equals to" y en "Value" queda en 0:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
8. Si el evento no tiene atributos, se detendrá la ejecución de "publish" en el evento. Debemos conectar en el resultado afirmativo del módulo IF :: Count el módulo "Stop execution":
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
9. En caso contrario, que Evento SI tenga atributos, lo conectamos al módulo "Webhook" para que procese los datos:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
10. Ahora necesitamos configurar los datos que tenemos que mandar al Webhook. Los datos son los siguientes:
|
||||||
|
|
||||||
|
- URL: https://IP_SERVIDOR_API:8000/webhook/misp_event_fixer/
|
||||||
|
- Payload: {
|
||||||
|
"event_id": "{{ Event.id }}",
|
||||||
|
"event_uuid": "{{ Event.uuid }}",
|
||||||
|
"event_attribute_count": "{{ Event.attribute_count }}"
|
||||||
|
}
|
||||||
|
- Header: Authorization: Bearer <MI_JWT_TOKEN>
|
||||||
|
|
||||||
|
11. Vamos a puntos arriba mano derecha y editamos el módulo para agregar esta información:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
12. Ingresamos la información mencionada en el punto 10 y para finalizar presionamos el botón "Close":
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
13. Guardamos el Workflow en MISP haciendo clic en "Save":
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Con estos pasos realizados, desde ahora en adelante dentro de MISP, cada evento generado por la organización local o de instancias remotas, será revisado y corregido por nuestro servicio web.
|
||||||
|
|
||||||
|
Cualquier duda o consulta envíanos un correo a: misp@anci.gob.cl
|
21
cert.pem
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDdTCCAl2gAwIBAgIUF4oJVu8qpUzGy1swWp4CcYANMwEwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwajELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM
|
||||||
|
DVNhbiBGcmFuY2lzY28xGjAYBgNVBAoMEVRlc3QgT3JnYW5pemF0aW9uMRIwEAYD
|
||||||
|
VQQDDAlsb2NhbGhvc3QwHhcNMjUwMTIyMTk1NjUxWhcNMjYwMTIyMTk1NjUxWjBq
|
||||||
|
MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2Fu
|
||||||
|
IEZyYW5jaXNjbzEaMBgGA1UECgwRVGVzdCBPcmdhbml6YXRpb24xEjAQBgNVBAMM
|
||||||
|
CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALagL4em
|
||||||
|
fQtGLW7iIVQzxELpG6s7CQsP/h7qDpNvQU7fbAJRaEAu3BR7RsihwKjYzQ37NcBy
|
||||||
|
BiY5gJQMCjje6k+P8J2K+ZH+N8yXaC+NxDA8wxcH/IXvOJufe7Qm41bWn7S5sYms
|
||||||
|
db2X8WLyOWvKdT21Ks9hA4ih+NsulZoRJ3zSRoYywKREgkUAdfqdf/JHy4j1JQiQ
|
||||||
|
h5JJ+u3cDtN/rkuuIRUEpCBbBNWYzDhNDHG9BGejYfTQIryzWQLoQJjWehZIWjwy
|
||||||
|
K9fGwNb3s9rUJit3GJoEQh5fDpQLdpOCbM5791g4nMv3ybNqLIhlAcaCrEJKUt8G
|
||||||
|
3s0EpAuAx/QRumcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B
|
||||||
|
AQsFAAOCAQEAP5IR3DoInqu0JRc81orJQfOJlW6pYIec4LaZIVC83DOtuVqtoLfA
|
||||||
|
fuAffxioanaYiNyt50ZCVypaSXfKSJc5aypw5wnv4Y4XE+bSDghmnfo8duwTnQMl
|
||||||
|
mwxFXrO3da8B8hIzlLrmv6xqU/GkJyp1SztkH7SJD/1R/XBsuqRYymOJZSUoIebM
|
||||||
|
FhG/4OYzgYgpQVOITpQ3yH5MNKorTFH/f2NiuaUZkNMNK1uXbZ4W2HvnkiOoDd5S
|
||||||
|
mUlc3meS/kpMb6lToO/9nmzaScJF11fr1OolqEwWNFQTq5nBQsXTNzT1kiwZzfOG
|
||||||
|
1liyNp4de3imIlN2yIMugarZ4eUoKQenEQ==
|
||||||
|
-----END CERTIFICATE-----
|
93
config.py
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
MISP_CONFIG = {
|
||||||
|
"misp_url":"URL_MISP",
|
||||||
|
"misp_authkey":"AUTHKEY"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Se si quiere agregar a exclusion de MISP cada atributo al deshabilitar correlacion
|
||||||
|
NO_CORRELATIVOS_EXCLUSION = False
|
||||||
|
|
||||||
|
|
||||||
|
# Config Lookup KTIP
|
||||||
|
KTIP_CONFIG = {
|
||||||
|
"api_key":"OPENTIP_APIKKEY",
|
||||||
|
"url_base": "https://opentip.kaspersky.com/api/v1/search/"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Rango de dias para verificar si IoC es falso positivo
|
||||||
|
RANGO_DIAS = 7
|
||||||
|
|
||||||
|
# Se debe establecer Manual
|
||||||
|
JWT_TOKEN_GEN = "JWT_TOKEN"
|
||||||
|
|
||||||
|
# Maximo de atributos a procesar por evento (4000 es el recomendado)
|
||||||
|
MAX_ATTRS = 4000
|
||||||
|
|
||||||
|
# Falsos positivos comunes
|
||||||
|
# FP Comunes
|
||||||
|
FP_COMUNES = ['0.0.0.0','8.8.8.8','google.com','amazon.com','microsoft.com','cloudflare.com']
|
||||||
|
|
||||||
|
# Organizaciones que se puede omitir la revisión de eventos
|
||||||
|
ORG_OMITIR = []
|
||||||
|
|
||||||
|
NO_CORRELATIVOS = [
|
||||||
|
"comment", # Comentarios descriptivos
|
||||||
|
"email-subject", # Asuntos de correos electrónicos
|
||||||
|
"email-dst", # Emails de destinatarios
|
||||||
|
"email-src", # Emails de remitentes
|
||||||
|
"hostname", # Nombres de host
|
||||||
|
"port", # Puertos
|
||||||
|
"link", # Enlaces
|
||||||
|
"phone-number", # Números de teléfono
|
||||||
|
"user-agent", # Agentes de usuario
|
||||||
|
"size-in-bytes", # Tamaños en bytes
|
||||||
|
"vulnerability", # Vulnerabilidades (CVE)
|
||||||
|
"whois-registrant-email", # Correos de registrantes WHOIS
|
||||||
|
"whois-registrant-name", # Nombres de registrantes WHOIS
|
||||||
|
"regkey", # Claves de registro de Windows
|
||||||
|
"regkey|value", # Claves de registro con valores
|
||||||
|
"text", # Texto libre
|
||||||
|
"datetime", # Fechas y horas
|
||||||
|
"campaign-name", # Nombres de campaña
|
||||||
|
"attachment", # Archivos adjuntos
|
||||||
|
"email" # Emails de remitentes
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
IDS_CORRELATIVOS = [
|
||||||
|
"ip-src", # IP de origen
|
||||||
|
"ip-dst", # IP de destino
|
||||||
|
"domain", # Dominio
|
||||||
|
"domain|ip", # Dominio con IP asociada
|
||||||
|
"url", # URL completas
|
||||||
|
"uri", # URI (fragmentos de rutas)
|
||||||
|
"http-method", # Métodos HTTP (GET, POST, etc.)
|
||||||
|
"email-attachment", # Nombres de archivos adjuntos en correos
|
||||||
|
"filename", # Nombres de archivo
|
||||||
|
"filename|md5", # Nombre de archivo con su hash MD5
|
||||||
|
"filename|sha1", # Nombre de archivo con su hash SHA1
|
||||||
|
"filename|sha256", # Nombre de archivo con su hash SHA256
|
||||||
|
"md5", # Hash MD5
|
||||||
|
"sha1", # Hash SHA1
|
||||||
|
"sha256", # Hash SHA256
|
||||||
|
"authentihash", # Hash Authentihash
|
||||||
|
"impfuzzy", # Hash ImpHash (ejecutable PE)
|
||||||
|
"tlsh", # Hash TLSH
|
||||||
|
"ssdeep", # Hash SSDEEP (fuzzy hash)
|
||||||
|
"mutex", # Nombres de mutex
|
||||||
|
"registry-key", # Claves de registro (si son relevantes)
|
||||||
|
"registry-key|value", # Claves de registro con valores
|
||||||
|
"ip-src|port", # IP de origen con puerto
|
||||||
|
"ip-dst|port", # IP de destino con puerto
|
||||||
|
"asn", # ASN (Autonomous System Number)
|
||||||
|
"cidr", # Rango CIDR (IPs)
|
||||||
|
"mac-address", # Dirección MAC
|
||||||
|
"x509-fingerprint-md5", # Huella de certificados X509 (MD5)
|
||||||
|
"x509-fingerprint-sha1", # Huella de certificados X509 (SHA1)
|
||||||
|
"x509-fingerprint-sha256", # Huella de certificados X509 (SHA256)
|
||||||
|
"ja3-fingerprint-md5", # Huella JA3 (TLS handshake)
|
||||||
|
"btc", # Dirección de Bitcoin
|
||||||
|
"iban", # Número de cuenta bancaria (IBAN)
|
||||||
|
"bank-account-nr", # Número de cuenta bancaria
|
||||||
|
"payment-card-number" # Número de tarjeta de pago
|
||||||
|
]
|
||||||
|
|
BIN
img_static/10_webhook_edit.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
img_static/11_ifcount_add.png
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
img_static/11_stop_execution_add.png
Normal file
After Width: | Height: | Size: 121 KiB |
BIN
img_static/11_webhook_add.png
Normal file
After Width: | Height: | Size: 111 KiB |
BIN
img_static/12_eventpublish_ifcount.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
img_static/13_ifcount.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
img_static/13_ifcount_2.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
img_static/13_ifcount_3.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
img_static/13_ifcount_webhook.png
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
img_static/14_webhook_config_1.png
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
img_static/14_webhook_config_2.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
img_static/14_webhook_edit.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
img_static/14_webhook_save.png
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
img_static/1_docs.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
img_static/2_plugin.png
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
img_static/3_enrich.png
Normal file
After Width: | Height: | Size: 119 KiB |
BIN
img_static/4_action.png
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
img_static/5_workflow.png
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
img_static/6_workflow_set.png
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
img_static/7_ev_publish.png
Normal file
After Width: | Height: | Size: 141 KiB |
BIN
img_static/8_list_modules.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
img_static/9_ifcount_play.png
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
img_static/9_stop_execution_play.png
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
img_static/9_webhook_play.png
Normal file
After Width: | Height: | Size: 150 KiB |
27
key.pem
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEowIBAAKCAQEAtqAvh6Z9C0YtbuIhVDPEQukbqzsJCw/+HuoOk29BTt9sAlFo
|
||||||
|
QC7cFHtGyKHAqNjNDfs1wHIGJjmAlAwKON7qT4/wnYr5kf43zJdoL43EMDzDFwf8
|
||||||
|
he84m597tCbjVtaftLmxiax1vZfxYvI5a8p1PbUqz2EDiKH42y6VmhEnfNJGhjLA
|
||||||
|
pESCRQB1+p1/8kfLiPUlCJCHkkn67dwO03+uS64hFQSkIFsE1ZjMOE0Mcb0EZ6Nh
|
||||||
|
9NAivLNZAuhAmNZ6FkhaPDIr18bA1vez2tQmK3cYmgRCHl8OlAt2k4Jsznv3WDic
|
||||||
|
y/fJs2osiGUBxoKsQkpS3wbezQSkC4DH9BG6ZwIDAQABAoIBAFgOhNxzenelLuL4
|
||||||
|
RfnDvC5HGABIRuP+ohll4gFU87iEIiA8AHhyH8wAZPD4jVzcrILBTfmtASoNL+Iy
|
||||||
|
q/sgAPq7/Nj52bx7R4xutN25DY/0vFyujSRHZJQlIhCLb7K/aeJKZ0Bq15rDWLDM
|
||||||
|
+sLuq/lFEY9Mx9dpwgRtQdU30EJuj81qgZnHpHoKgsiPZGLMrdJ9+rU7LvjkV9Wd
|
||||||
|
Pzj9t4gFn9FJsdcQ3zDKwkmrMK9n12iOH3Ip2nfH/XN5w/Cgy6kuqZx3xVL/L/+m
|
||||||
|
ZV6sS1FqJpE0KxNawStgZ55caS8HOJUgvE7SzYHcNYCCg9J24dTzcvaush9OzBzE
|
||||||
|
iQj1xwECgYEA7/Dxb9aiZ733/t0uqqmU5TtAjCqG55eEZPYDgWDZ6aQbbnwnqU7S
|
||||||
|
FV/L9ijNC3JvY+UFzwfwoFn4Sf5+5SV1QYYVeWcyrZwGbVGYmRge6rWwTuivRDLZ
|
||||||
|
xLIFaQQpVpmSmic0b8AWsm6pMyxF29Zfx7KGcSVsB+y8zP6bJ1CrYF8CgYEAwtk4
|
||||||
|
5fYhJUW2cP95GQ6//HmuRLfAfCve/EmYWfAlDRDZBJc7Dwh4z9G/+pPc25Svsb8a
|
||||||
|
dok/2C5YHbN4VUY7ym9SOUXBplQVVTcon5SMtbXk7LpRhbN6O/bACKT2QUcOHVF5
|
||||||
|
utrCqOyIh5RxCHlRQxaoNWCsAe0bB04XOZxHwvkCgYBFq/sPdI2X/iuC0Ar6918K
|
||||||
|
6RenG7osiWyiPGVsLglYtJRakqaZnQ+XsUdyZQqVJld99ESphy6yAS39nm6Ob0AL
|
||||||
|
FLorlHG8w6+CEs1ytvRTRwq4/wvVi8Z8PQ0hH1o5kUJmjLfHM0nj6gorl9F5MliB
|
||||||
|
ji9Hr4wdCPsRs2SuF9iLlQKBgQCOuhuJp59j9ArN/vUvu7Q6Ns/GmmsvCdvPJgGp
|
||||||
|
b9VUGtE9IaIrQuNsJ5Le9EzFs8Z3BytVRPg1XM1DBGHS5R2LDbxHI6fUNKdjwoHJ
|
||||||
|
U0E3IcRM+7YXn/6bygWkz2FrM6dNJo9qpjANGSZxWfTqZiN2ZzRT1TpqNsqjsTom
|
||||||
|
Ayo10QKBgHNp3VF//5lBH8DkKadnXrNT93yn6B81/sPC8mZZL2zlOVpY+P6l0eo5
|
||||||
|
onCCBZkK41AOCejyAO6YShX8h3PKxtYdKuI5YnKUkgi3p2nG1C0xRuQxGfV+8XzT
|
||||||
|
i6AxRpcn3T5PmiMf5xwxeFnb3BJpml5wgGMzmy9AVgNFl+pLc2Lv
|
||||||
|
-----END RSA PRIVATE KEY-----
|
508
main.py
Normal file
|
@ -0,0 +1,508 @@
|
||||||
|
from fastapi import FastAPI, Request, Body, Header, Depends
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
from typing import List, Annotated, Optional
|
||||||
|
from pymisp import PyMISP, PyMISPError
|
||||||
|
from sqlalchemy import create_engine, select, event
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
import urllib3
|
||||||
|
import requests
|
||||||
|
import config
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
from models import Base, ModificadosEv
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
import threading
|
||||||
|
|
||||||
|
urllib3.disable_warnings()
|
||||||
|
|
||||||
|
directorio_actual = os.getcwd()
|
||||||
|
|
||||||
|
tipos_ktip = {
|
||||||
|
"ip": ['ip-src','ip-dst'],
|
||||||
|
"domain": ['domain','hostname'],
|
||||||
|
"hash": ['sha256']
|
||||||
|
}
|
||||||
|
|
||||||
|
dir_logs = os.path.join(directorio_actual, 'logs')
|
||||||
|
os.makedirs(dir_logs, exist_ok=True)
|
||||||
|
|
||||||
|
rotating_handler = RotatingFileHandler(
|
||||||
|
os.path.join(dir_logs, "misp_webhooks.log"),
|
||||||
|
maxBytes=262144000,
|
||||||
|
backupCount=10
|
||||||
|
)
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
handlers=[rotating_handler],
|
||||||
|
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
|
||||||
|
ruta_base_datos = os.path.join(directorio_actual, "data", "procesados.db")
|
||||||
|
os.makedirs(os.path.dirname(ruta_base_datos), exist_ok=True)
|
||||||
|
|
||||||
|
sync_engine = create_engine(f'sqlite:///{ruta_base_datos}')
|
||||||
|
Base.metadata.create_all(bind=sync_engine)
|
||||||
|
|
||||||
|
async_engine = create_async_engine(f'sqlite+aiosqlite:///{ruta_base_datos}', echo=False, future=True)
|
||||||
|
|
||||||
|
def set_sqlite_pragma(dbapi_connection, connection_record):
|
||||||
|
cursor = dbapi_connection.cursor()
|
||||||
|
cursor.execute('PRAGMA journal_mode=WAL;')
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
event.listen(async_engine.sync_engine, 'connect', set_sqlite_pragma)
|
||||||
|
|
||||||
|
AsyncSessionLocal = sessionmaker(
|
||||||
|
bind=async_engine,
|
||||||
|
expire_on_commit=False,
|
||||||
|
class_=AsyncSession
|
||||||
|
)
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
version="1.0",
|
||||||
|
title="MISP-WEBHOOK",
|
||||||
|
description="Webhooks for MISP",
|
||||||
|
swagger_ui_parameters={"supportedSubmitMethods": []}
|
||||||
|
)
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
misp = PyMISP(config.MISP_CONFIG['misp_url'], config.MISP_CONFIG['misp_authkey'], False)
|
||||||
|
|
||||||
|
class InputModel(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
class EventData(InputModel):
|
||||||
|
event_id: str
|
||||||
|
event_uuid: str
|
||||||
|
event_attribute_count: str
|
||||||
|
|
||||||
|
class ResponseData(InputModel):
|
||||||
|
event_id: Optional[str] = None
|
||||||
|
status: Optional[str] = None
|
||||||
|
detail: Optional[str] = None
|
||||||
|
|
||||||
|
async def get_db():
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
yield session
|
||||||
|
|
||||||
|
disable_ids_data = Annotated[
|
||||||
|
EventData,
|
||||||
|
Body(
|
||||||
|
openapi_examples={
|
||||||
|
"fix_event": {
|
||||||
|
"summary": "Deshabilita correlación, limpia falsos positivos y corrige flag ids en tipos de atributos definidos.",
|
||||||
|
"description": "<p>Deshabilita correlación, limpia falsos positivos y corrige flag ids en tipos de atributos definidos.</p>",
|
||||||
|
"value": {
|
||||||
|
"event_id": "string",
|
||||||
|
"event_uuid": "string",
|
||||||
|
"event_attribute_count": "0"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
event_data_responses = {
|
||||||
|
200: {
|
||||||
|
"description": "Success",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"examples": {
|
||||||
|
"Procesado": {
|
||||||
|
"summary": "Respuesta Procesado",
|
||||||
|
"value": {
|
||||||
|
"event_id": "1231445",
|
||||||
|
"status": "Procesado"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Omitido": {
|
||||||
|
"summary": "Respuesta Omitido",
|
||||||
|
"value": {
|
||||||
|
"event_id": "1231445",
|
||||||
|
"status": "Omitido"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
401: {
|
||||||
|
"description": "Invalid Credentials",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"examples": {
|
||||||
|
"Formato de token inválido.": {
|
||||||
|
"summary": "Formato de token inválido.",
|
||||||
|
"value": {
|
||||||
|
"detail": "Formato de token inválido."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Valor de token inválido.": {
|
||||||
|
"summary": "Valor de token inválido.",
|
||||||
|
"value": {
|
||||||
|
"detail": "Valor de token inválido."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
"description": "Internal Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"examples": {
|
||||||
|
"Error al procesar datos": {
|
||||||
|
"summary": "Error al procesar datos",
|
||||||
|
"value": {
|
||||||
|
"detail": "Error al procesar datos: <detalle_error>"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"La respuesta de MISP no contiene 'Attribute'": {
|
||||||
|
"summary": "La respuesta de MISP no contiene 'Attribute'",
|
||||||
|
"value": {
|
||||||
|
"detail": "Error en la respuesta de MISP: 'Attribute' no encontrado."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def verificacion_ktip(ioc, tipo_ioc):
|
||||||
|
"""Verifica en KTIP si un IoC se considera 'Green' (limpio)."""
|
||||||
|
url = config.KTIP_CONFIG['url_base']
|
||||||
|
headers = {
|
||||||
|
'x-api-key': config.KTIP_CONFIG['api_key']
|
||||||
|
}
|
||||||
|
|
||||||
|
if str(ioc).lower() in config.FP_COMUNES or any(str(ioc).lower().endswith(item) for item in config.FP_COMUNES):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if tipo_ioc in tipos_ktip['ip']:
|
||||||
|
url = url + 'ip?request=' + ioc
|
||||||
|
elif tipo_ioc in tipos_ktip['domain']:
|
||||||
|
url = url + 'domain?request=' + ioc
|
||||||
|
elif tipo_ioc in tipos_ktip['hash']:
|
||||||
|
url = url + 'hash?request=' + ioc
|
||||||
|
else:
|
||||||
|
logging.warning(f"Tipo de IoC no reconocido para verificación KT: {tipo_ioc}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(url, headers=headers, verify=False)
|
||||||
|
if response.status_code == 200:
|
||||||
|
logging.info("Respondió KTIP con IoC :" + str(ioc))
|
||||||
|
data = response.json()
|
||||||
|
if data.get('Zone') == 'Green':
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except (Exception, requests.exceptions.HTTPError) as e:
|
||||||
|
logging.error(str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def eliminar_atributo_o_objeto(a, object_id, evento_id):
|
||||||
|
"""Elimina un atributo o un objeto completo de MISP si es un FP."""
|
||||||
|
IOC_TIPOS_OMITIR = [
|
||||||
|
'comment', 'text', 'other', 'datetime', 'attachment', 'port',
|
||||||
|
'size-in-bytes', 'counter', 'integer', 'cpe', 'float', 'hex',
|
||||||
|
'phone-number', 'boolean', 'anonymised', 'pgp-public-key', 'pgp-private-key'
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
logging.info("Falso Positivo encontrado : " + a['type'] + "->" + a['value'] + f" en evento #{evento_id}")
|
||||||
|
if object_id is None:
|
||||||
|
logging.info(f"Eliminando atributo UUID: {a['uuid']} / {a['value']} , evento #{evento_id}")
|
||||||
|
misp.direct_call("attributes/delete/" + a["uuid"], {"hard": "1"})
|
||||||
|
else:
|
||||||
|
objeto_existe = misp.direct_call("objects/view/" + object_id)
|
||||||
|
if 'Object' not in objeto_existe:
|
||||||
|
logging.warning(f"El objeto ID {object_id} no existe en MISP o ya fue eliminado. No se puede eliminar.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if a['type'] == 'sha256':
|
||||||
|
logging.info(f"Eliminando objeto UUID: {objeto_existe['uuid']} / {a['value']}, evento #{evento_id}")
|
||||||
|
misp.direct_call("objects/delete/" + objeto_existe['Object']['uuid'], {"hard": "1"})
|
||||||
|
else:
|
||||||
|
counter_validos = 0
|
||||||
|
for at in objeto_existe['Object']['Attribute']:
|
||||||
|
if at['type'] not in IOC_TIPOS_OMITIR:
|
||||||
|
counter_validos += 1
|
||||||
|
|
||||||
|
if counter_validos > 1:
|
||||||
|
logging.info(f"Eliminando atributo UUID: {a['uuid']} / {a['value']} en el objeto {objeto_existe['Object']['name']} , evento #{evento_id}")
|
||||||
|
misp.direct_call("attributes/delete/" + a["uuid"], {"hard": "1"})
|
||||||
|
else:
|
||||||
|
logging.info(f"Eliminando objeto UUID: {objeto_existe['Object']['uuid']} / {a['value']}, evento #{evento_id}")
|
||||||
|
misp.direct_call("objects/delete/" + objeto_existe['Object']["uuid"], {"hard": "1"})
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except (Exception, PyMISPError) as err:
|
||||||
|
logging.error(str(err))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def procesar_atributo(a, event_id, days_ago, now, lock_data, fp_temp, nfp_temp):
|
||||||
|
"""
|
||||||
|
Verifica si un atributo es un falso positivo (usando verificacion_ktip).
|
||||||
|
Si corresponde, lo elimina de MISP.
|
||||||
|
Retorna True si se eliminó, False si no se hace nada.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
attr_timestamp = int(a['timestamp'])
|
||||||
|
attr_datetime = datetime.fromtimestamp(attr_timestamp)
|
||||||
|
if not (days_ago.date() <= attr_datetime.date() <= now.date()):
|
||||||
|
return False
|
||||||
|
|
||||||
|
valor_lower = str(a['value']).strip().lower()
|
||||||
|
|
||||||
|
with lock_data:
|
||||||
|
if valor_lower in fp_temp:
|
||||||
|
if a['object_id'] == "0":
|
||||||
|
eliminar_atributo_o_objeto(a, None, event_id)
|
||||||
|
else:
|
||||||
|
eliminar_atributo_o_objeto(a, a['object_id'], event_id)
|
||||||
|
return True
|
||||||
|
elif valor_lower in nfp_temp:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if any(a['type'] in lista for lista in tipos_ktip.values()):
|
||||||
|
es_falso_positivo = verificacion_ktip(a['value'], a['type'])
|
||||||
|
if es_falso_positivo:
|
||||||
|
if a['object_id'] == "0":
|
||||||
|
eliminar_atributo_o_objeto(a, None, event_id)
|
||||||
|
else:
|
||||||
|
eliminar_atributo_o_objeto(a, a['object_id'], event_id)
|
||||||
|
fp_temp.add(valor_lower)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
nfp_temp.add(valor_lower)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error en procesar_atributo({a}): {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@app.post(
|
||||||
|
"/webhook/misp_event_fixer",
|
||||||
|
response_model=ResponseData,
|
||||||
|
responses=event_data_responses,
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
description="Deshabilita correlación, limpia falsos positivos, corrige flag IDS en tipos de atributos definidos y asigna tlp:clear si existe tlp:white en un evento",
|
||||||
|
summary="Deshabilita correlación, limpia falsos positivos, corrige flag IDS en tipos de atributos definidos y asigna tlp:clear si existe tlp:white en un evento"
|
||||||
|
)
|
||||||
|
async def webhook_misp_event_fixer(
|
||||||
|
request: Request,
|
||||||
|
event_data: disable_ids_data,
|
||||||
|
authorization: str = Header(...),
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
if not authorization.startswith("Bearer "):
|
||||||
|
logging.error("Formato de Token Inválido")
|
||||||
|
return JSONResponse(status_code=401, content={"detail": "Formato de token inválido."})
|
||||||
|
|
||||||
|
token = authorization.split("Bearer ")[1]
|
||||||
|
|
||||||
|
if token != config.JWT_TOKEN_GEN:
|
||||||
|
logging.error("Token Inválido")
|
||||||
|
return JSONResponse(status_code=401, content={"detail": "Valor de token inválido."})
|
||||||
|
|
||||||
|
stmt = select(ModificadosEv).where(
|
||||||
|
ModificadosEv.evento_uuid == event_data.event_uuid,
|
||||||
|
ModificadosEv.attribute_count >= int(event_data.event_attribute_count)
|
||||||
|
)
|
||||||
|
existe = (await db.scalars(stmt)).first()
|
||||||
|
if existe:
|
||||||
|
logging.info(f"Evento {event_data.event_uuid} ya procesado. Omitido.")
|
||||||
|
return {"event_id": event_data.event_id, "status": "Omitido"}
|
||||||
|
|
||||||
|
if 0 < int(event_data.event_attribute_count) <= config.MAX_ATTRS:
|
||||||
|
relative_path = 'events/view/' + str(event_data.event_id)
|
||||||
|
event_details = misp.direct_call(relative_path)
|
||||||
|
|
||||||
|
valores_minusculas = {item.lower() for item in config.ORG_OMITIR}
|
||||||
|
if str(event_details['Event']['Orgc']['name']).lower() in valores_minusculas:
|
||||||
|
logging.info(f"Evento {event_data.event_uuid} restringido para procesar. Omitido.")
|
||||||
|
return {"event_id": event_data.event_id, "status": "Omitido"}
|
||||||
|
|
||||||
|
logging.info(f"Procesando evento {event_data.event_uuid}, total atributos: {event_data.event_attribute_count}")
|
||||||
|
event_info = event_details["Event"]
|
||||||
|
tags = [tag["name"] for tag in event_info.get("Tag", [])]
|
||||||
|
|
||||||
|
if "tlp:white" in tags and "tlp:clear" not in tags:
|
||||||
|
try:
|
||||||
|
relative_path = "tags/attachTagToObject"
|
||||||
|
body = {
|
||||||
|
"uuid": event_data.event_uuid,
|
||||||
|
"tag": "tlp:clear",
|
||||||
|
"local": False
|
||||||
|
}
|
||||||
|
response = misp.direct_call(relative_path, body)
|
||||||
|
if "errors" in response:
|
||||||
|
logging.error(f"Error al agregar tlp:clear al evento {event_data.event_id}: {response['errors']}")
|
||||||
|
else:
|
||||||
|
logging.info(f"Se agregó tlp:clear al evento {event_data.event_id}.")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Excepción al agregar tlp:clear al evento {event_data.event_id}: {e}")
|
||||||
|
|
||||||
|
attrs_event = event_details['Event'].get('Attribute', [])
|
||||||
|
objs_event = event_details['Event'].get('Object', [])
|
||||||
|
attrs_objetos = []
|
||||||
|
for o in objs_event:
|
||||||
|
attrs_objetos.extend(o.get('Attribute', []))
|
||||||
|
all_attrs = attrs_event + attrs_objetos
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
days_ago = now - timedelta(days=config.RANGO_DIAS)
|
||||||
|
max_workers = getattr(config, 'NUM_WORKERS', 4)
|
||||||
|
total_eliminados = 0
|
||||||
|
|
||||||
|
# Aquí definimos los sets y el Lock para la ejecución actual
|
||||||
|
lock_data = threading.Lock()
|
||||||
|
fp_temp = set()
|
||||||
|
nfp_temp = set()
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||||
|
futures = []
|
||||||
|
for a in all_attrs:
|
||||||
|
f = executor.submit(
|
||||||
|
procesar_atributo,
|
||||||
|
a,
|
||||||
|
event_data.event_id,
|
||||||
|
days_ago,
|
||||||
|
now,
|
||||||
|
lock_data,
|
||||||
|
fp_temp,
|
||||||
|
nfp_temp
|
||||||
|
)
|
||||||
|
futures.append(f)
|
||||||
|
|
||||||
|
for future in as_completed(futures):
|
||||||
|
try:
|
||||||
|
resultado = future.result()
|
||||||
|
if resultado:
|
||||||
|
total_eliminados += 1
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error procesando atributo en hilo: {e}")
|
||||||
|
|
||||||
|
logging.info(f"Total de atributos eliminados: {total_eliminados}")
|
||||||
|
|
||||||
|
relative_path = 'events/view/' + str(event_data.event_id)
|
||||||
|
event_details = misp.direct_call(relative_path)
|
||||||
|
|
||||||
|
if int(event_details['Event']['attribute_count']) > 0:
|
||||||
|
relative_path_attr = 'attributes/restSearch'
|
||||||
|
body = {"eventid": event_data.event_id}
|
||||||
|
attr = misp.direct_call(relative_path_attr, body)
|
||||||
|
|
||||||
|
if "Attribute" not in attr:
|
||||||
|
logging.error("La respuesta de MISP no contiene 'Attribute'")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=500,
|
||||||
|
content={"detail": "Error en la respuesta de MISP: 'Attribute' no encontrado."}
|
||||||
|
)
|
||||||
|
|
||||||
|
for a in attr['Attribute']:
|
||||||
|
if a['type'] in config.NO_CORRELATIVOS:
|
||||||
|
if not a['disable_correlation']:
|
||||||
|
logging.info(f"Procesando atributo {a['id']}: {a['type']} -> {a['value']}")
|
||||||
|
relative_path_det = 'attributes/edit/' + a['id']
|
||||||
|
body_det = {
|
||||||
|
"to_ids": "0",
|
||||||
|
"disable_correlation": True
|
||||||
|
}
|
||||||
|
misp.direct_call(relative_path_det, body_det)
|
||||||
|
logging.info(f"Correlación deshabilitada para atributo: {a['id']}")
|
||||||
|
|
||||||
|
if config.NO_CORRELATIVOS_EXCLUSION:
|
||||||
|
relative_path_corr = 'correlation_exclusions/add'
|
||||||
|
body_corr = {
|
||||||
|
"value": a['value'],
|
||||||
|
"type": a['type'],
|
||||||
|
"enabled": True
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = misp.direct_call(relative_path_corr, body_corr)
|
||||||
|
if 'CorrelationExclusion' not in response:
|
||||||
|
err = response.get("errors", [])
|
||||||
|
if err and err[0] == 403:
|
||||||
|
logging.error(f"API Error: valor '{a['value']}' ya existe en exclusiones. Se omite.")
|
||||||
|
else:
|
||||||
|
logging.error(f"API Error: {response.get('errors')}")
|
||||||
|
else:
|
||||||
|
logging.info(f"Se agregó valor a excepciones: {a['value']}")
|
||||||
|
except PyMISPError as e:
|
||||||
|
logging.error(f"PyMISPError: {e}")
|
||||||
|
except UnicodeEncodeError as e:
|
||||||
|
logging.error(f"UnicodeEncodeError: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Excepción no manejada: {e}")
|
||||||
|
|
||||||
|
elif a['to_ids']:
|
||||||
|
relative_path_det = 'attributes/edit/' + a['id']
|
||||||
|
body_det = {"to_ids": "0"}
|
||||||
|
misp.direct_call(relative_path_det, body_det)
|
||||||
|
logging.info(f"Se deshabilita Flag IDS para atributo: {a['id']}")
|
||||||
|
|
||||||
|
elif a['type'] in config.IDS_CORRELATIVOS:
|
||||||
|
if not a['to_ids']:
|
||||||
|
logging.info(f"Procesando atributo {a['id']}: {a['type']} -> {a['value']}")
|
||||||
|
relative_temp = 'attributes/edit/' + a['id']
|
||||||
|
body_temp = {
|
||||||
|
"to_ids": "1",
|
||||||
|
"disable_correlation": False
|
||||||
|
}
|
||||||
|
misp.direct_call(relative_temp, body_temp)
|
||||||
|
logging.info(f"Flag IDS activado para atributo: {a['id']}")
|
||||||
|
elif a['disable_correlation']:
|
||||||
|
relative_path_det = 'attributes/edit/' + a['id']
|
||||||
|
body_det = {"disable_correlation": False}
|
||||||
|
misp.direct_call(relative_path_det, body_det)
|
||||||
|
logging.info(f"Correlación habilitada para atributo: {a['id']}")
|
||||||
|
else:
|
||||||
|
logging.info("Evento sin atributos.")
|
||||||
|
|
||||||
|
nuevo_registro = ModificadosEv(
|
||||||
|
evento_uuid=event_data.event_uuid,
|
||||||
|
publicado_fecha=datetime.now(),
|
||||||
|
attribute_count=int(event_details['Event']['attribute_count'])
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
db.add(nuevo_registro)
|
||||||
|
await db.commit()
|
||||||
|
logging.info(f"Registrado UUID de evento modificado: {event_data.event_uuid}")
|
||||||
|
except IntegrityError:
|
||||||
|
logging.warning(f"Registro duplicado: {event_data.event_uuid} ya existe con la misma fecha.")
|
||||||
|
await db.rollback()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error al guardar los datos en la base de datos: {e}")
|
||||||
|
await db.rollback()
|
||||||
|
else:
|
||||||
|
logging.info(f"Evento {event_data.event_id} supera el límite de atributos a procesar. Se omite.")
|
||||||
|
|
||||||
|
logging.info("Evento procesado")
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
logging.error(f"Excepción no manejada: {traceback.format_exc()}")
|
||||||
|
return JSONResponse(status_code=500, content={"detail": f"Error al procesar datos: {err}"})
|
||||||
|
|
||||||
|
return {"event_id": event_data.event_id, "status": "Procesado"}
|
28
models.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
from sqlalchemy import Column, Integer, String, UniqueConstraint, Date, DateTime
|
||||||
|
from sqlalchemy.orm import declarative_base
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
|
||||||
|
# Crear la base declarativa
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
# Se define modelo de eventos modificados al procesar
|
||||||
|
class ModificadosEv(Base):
|
||||||
|
__tablename__ = "modificados_ev"
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
evento_uuid = Column(String, nullable=False)
|
||||||
|
publicado_fecha = Column(DateTime, nullable=False)
|
||||||
|
attribute_count = Column(Integer, nullable=False)
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint('evento_uuid', 'attribute_count', name='ev_attr'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"evento_uuid": self.evento_uuid,
|
||||||
|
"publicado_fecha": self.publicado_fecha, # Se mantendrá como datetime
|
||||||
|
"attribute_count": self.attribute_count
|
||||||
|
|
||||||
|
}
|
5
requirements.txt
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
fastapi
|
||||||
|
pymisp
|
||||||
|
uvicorn
|
||||||
|
SQLAlchemy
|
||||||
|
aiosqlite
|
7
requirements_misp.txt
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# misp-modules
|
||||||
|
misp-modules
|
||||||
|
git+https://github.com/cartertemm/ODTReader.git
|
||||||
|
git+https://github.com/abenassi/Google-Search-API
|
||||||
|
git+https://github.com/SteveClement/trustar-python.git
|
||||||
|
git+https://github.com/sebdraven/pydnstrails.git
|
||||||
|
git+https://github.com/sebdraven/pyonyphe.git
|
17
start_api.sh
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Activate
|
||||||
|
source /home/user/misp-fixevent-webhook/venv/bin/activate
|
||||||
|
|
||||||
|
# Enter folder
|
||||||
|
cd /home/user/misp-fixevent-webhook/
|
||||||
|
|
||||||
|
# Actualizar librerias si aplica
|
||||||
|
pip install --upgrade -r requirements.txt > /dev/null 2>&1
|
||||||
|
|
||||||
|
# Llamar al script Python
|
||||||
|
uvicorn main:app --host 0.0.0.0 --port 8000
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|