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
|
||||
|
||||
|
||||
|
||||
|