commit e9e3987226d2257188c2c9d60f116e2ab057c6bc Author: Felipe Luis Quezada Valenzuela Date: Thu Feb 13 16:41:25 2025 -0300 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..5e86248 --- /dev/null +++ b/README.md @@ -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/ + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/1_docs.png) + +## 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": + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/2_plugin.png) + +2. Dentro de Plugin activamos "Enrichment": + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/3_enrich.png) + +3. Luego "Action" y "Workflow": + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/4_action.png) + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/5_workflow.png) + +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: + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/6_workflow_set.png) + +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: + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/7_ev_publish.png) + +7. Luego en la misma ventana, vamos a la sección de "List Modules" y activamos el módulo de "Webhook": + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/8_list_modules.png) + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/9_webhook_play.png) + +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": + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/9_ifcount_play.png) + +9. Finalmente desde la misma ventana, en "Blocking" activamos "Stop execution": + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/9_stop_execution_play.png) + +## 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 "": + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/10_webhook_edit.png) + +2. Desde la lista de "Actions", Agregamos "Webhook" al plano de diseño arrastrando: + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/11_webhook_add.png) + +3. Desde la lista de "Logic", Agregamos "IF :: Count" al plano de diseño arrastrando: + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/11_ifcount_add.png) + +4. Desde la lista de "Actions", Agregamos "Stop execution" al plano de diseño arrastrando: + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/11_stop_execution_add.png) + +5. Conectamos "Event Publish" con "IF :: Count": + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/12_eventpublish_ifcount.png) + +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: + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/13_ifcount.png) + +7. En el mismo módulo en "Condition" seteamos como "Equals to" y en "Value" queda en 0: + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/13_ifcount_2.png) + +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": + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/13_ifcount_3.png) + +9. En caso contrario, que Evento SI tenga atributos, lo conectamos al módulo "Webhook" para que procese los datos: + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/13_ifcount_webhook.png) + +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 + +11. Vamos a puntos arriba mano derecha y editamos el módulo para agregar esta información: + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/14_webhook_edit.png) + +12. Ingresamos la información mencionada en el punto 10 y para finalizar presionamos el botón "Close": + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/14_webhook_config_1.png) + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/14_webhook_config_2.png) + +13. Guardamos el Workflow en MISP haciendo clic en "Save": + +![alt text](https://git.csirt.gob.cl/public/misp-fixevent-webhook/raw/branch/main/img_static/14_webhook_save.png) + +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 \ No newline at end of file diff --git a/cert.pem b/cert.pem new file mode 100644 index 0000000..bb77312 --- /dev/null +++ b/cert.pem @@ -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----- diff --git a/config.py b/config.py new file mode 100644 index 0000000..4558fef --- /dev/null +++ b/config.py @@ -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 +] + diff --git a/img_static/10_webhook_edit.png b/img_static/10_webhook_edit.png new file mode 100644 index 0000000..a41b480 Binary files /dev/null and b/img_static/10_webhook_edit.png differ diff --git a/img_static/11_ifcount_add.png b/img_static/11_ifcount_add.png new file mode 100644 index 0000000..4d07dee Binary files /dev/null and b/img_static/11_ifcount_add.png differ diff --git a/img_static/11_stop_execution_add.png b/img_static/11_stop_execution_add.png new file mode 100644 index 0000000..2ef0e27 Binary files /dev/null and b/img_static/11_stop_execution_add.png differ diff --git a/img_static/11_webhook_add.png b/img_static/11_webhook_add.png new file mode 100644 index 0000000..b53cb36 Binary files /dev/null and b/img_static/11_webhook_add.png differ diff --git a/img_static/12_eventpublish_ifcount.png b/img_static/12_eventpublish_ifcount.png new file mode 100644 index 0000000..e01935a Binary files /dev/null and b/img_static/12_eventpublish_ifcount.png differ diff --git a/img_static/13_ifcount.png b/img_static/13_ifcount.png new file mode 100644 index 0000000..646fc27 Binary files /dev/null and b/img_static/13_ifcount.png differ diff --git a/img_static/13_ifcount_2.png b/img_static/13_ifcount_2.png new file mode 100644 index 0000000..2d55210 Binary files /dev/null and b/img_static/13_ifcount_2.png differ diff --git a/img_static/13_ifcount_3.png b/img_static/13_ifcount_3.png new file mode 100644 index 0000000..20c90e1 Binary files /dev/null and b/img_static/13_ifcount_3.png differ diff --git a/img_static/13_ifcount_webhook.png b/img_static/13_ifcount_webhook.png new file mode 100644 index 0000000..96e1805 Binary files /dev/null and b/img_static/13_ifcount_webhook.png differ diff --git a/img_static/14_webhook_config_1.png b/img_static/14_webhook_config_1.png new file mode 100644 index 0000000..72d3e23 Binary files /dev/null and b/img_static/14_webhook_config_1.png differ diff --git a/img_static/14_webhook_config_2.png b/img_static/14_webhook_config_2.png new file mode 100644 index 0000000..028b2b9 Binary files /dev/null and b/img_static/14_webhook_config_2.png differ diff --git a/img_static/14_webhook_edit.png b/img_static/14_webhook_edit.png new file mode 100644 index 0000000..3a94369 Binary files /dev/null and b/img_static/14_webhook_edit.png differ diff --git a/img_static/14_webhook_save.png b/img_static/14_webhook_save.png new file mode 100644 index 0000000..9799498 Binary files /dev/null and b/img_static/14_webhook_save.png differ diff --git a/img_static/1_docs.png b/img_static/1_docs.png new file mode 100644 index 0000000..d9c95dc Binary files /dev/null and b/img_static/1_docs.png differ diff --git a/img_static/2_plugin.png b/img_static/2_plugin.png new file mode 100644 index 0000000..890c3ba Binary files /dev/null and b/img_static/2_plugin.png differ diff --git a/img_static/3_enrich.png b/img_static/3_enrich.png new file mode 100644 index 0000000..9bc82b9 Binary files /dev/null and b/img_static/3_enrich.png differ diff --git a/img_static/4_action.png b/img_static/4_action.png new file mode 100644 index 0000000..f6b32c4 Binary files /dev/null and b/img_static/4_action.png differ diff --git a/img_static/5_workflow.png b/img_static/5_workflow.png new file mode 100644 index 0000000..152a037 Binary files /dev/null and b/img_static/5_workflow.png differ diff --git a/img_static/6_workflow_set.png b/img_static/6_workflow_set.png new file mode 100644 index 0000000..3a8e0a8 Binary files /dev/null and b/img_static/6_workflow_set.png differ diff --git a/img_static/7_ev_publish.png b/img_static/7_ev_publish.png new file mode 100644 index 0000000..6e4b3f6 Binary files /dev/null and b/img_static/7_ev_publish.png differ diff --git a/img_static/8_list_modules.png b/img_static/8_list_modules.png new file mode 100644 index 0000000..fbb4ca1 Binary files /dev/null and b/img_static/8_list_modules.png differ diff --git a/img_static/9_ifcount_play.png b/img_static/9_ifcount_play.png new file mode 100644 index 0000000..df79492 Binary files /dev/null and b/img_static/9_ifcount_play.png differ diff --git a/img_static/9_stop_execution_play.png b/img_static/9_stop_execution_play.png new file mode 100644 index 0000000..ab73a2f Binary files /dev/null and b/img_static/9_stop_execution_play.png differ diff --git a/img_static/9_webhook_play.png b/img_static/9_webhook_play.png new file mode 100644 index 0000000..5b66f1a Binary files /dev/null and b/img_static/9_webhook_play.png differ diff --git a/key.pem b/key.pem new file mode 100644 index 0000000..bb84f35 --- /dev/null +++ b/key.pem @@ -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----- diff --git a/main.py b/main.py new file mode 100644 index 0000000..209a9b2 --- /dev/null +++ b/main.py @@ -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": "

Deshabilita correlación, limpia falsos positivos y corrige flag ids en tipos de atributos definidos.

", + "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: " + } + }, + "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"} diff --git a/models.py b/models.py new file mode 100644 index 0000000..4b86100 --- /dev/null +++ b/models.py @@ -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 + + } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..499c959 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi +pymisp +uvicorn +SQLAlchemy +aiosqlite \ No newline at end of file diff --git a/requirements_misp.txt b/requirements_misp.txt new file mode 100644 index 0000000..ac1c976 --- /dev/null +++ b/requirements_misp.txt @@ -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 diff --git a/start_api.sh b/start_api.sh new file mode 100644 index 0000000..a5b848d --- /dev/null +++ b/start_api.sh @@ -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 + + + +