first commit

This commit is contained in:
Felipe Luis Quezada Valenzuela 2025-02-13 16:41:25 -03:00
commit e9e3987226
33 changed files with 1170 additions and 0 deletions

464
README.md Normal file
View 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/
![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 <MI_JWT_TOKEN>
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

21
cert.pem Normal file
View 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
View 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
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
img_static/13_ifcount.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
img_static/13_ifcount_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
img_static/13_ifcount_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
img_static/1_docs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
img_static/2_plugin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
img_static/3_enrich.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

BIN
img_static/4_action.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
img_static/5_workflow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
img_static/7_ev_publish.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

27
key.pem Normal file
View 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
View 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
View 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
View file

@ -0,0 +1,5 @@
fastapi
pymisp
uvicorn
SQLAlchemy
aiosqlite

7
requirements_misp.txt Normal file
View 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
View 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