This commit is contained in:
Felipe Luis Quezada Valenzuela 2024-11-06 14:53:19 -03:00
parent 97a0a07fa7
commit 575ea7dfb8
7 changed files with 911 additions and 0 deletions

71
add_user.py Normal file
View file

@ -0,0 +1,71 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.exc import ProgrammingError
from models import Usuario, Base
import logging
from logging.handlers import RotatingFileHandler
import os
import sys
# Directorio actual
dir_actual = os.getcwd()
# Directorio para Logs
dir_logs = dir_actual+'/logs'
# Directorio para data BD
dir_data = dir_actual+'/data'
# Se crea directorio logs y data por si no existe...
os.makedirs(dir_logs, exist_ok=True)
os.makedirs(dir_data, exist_ok=True)
rotating_handler = RotatingFileHandler(os.path.join(dir_logs,"users.log"), maxBytes=262144000, backupCount=10)
logging.basicConfig(level=logging.INFO, handlers=[rotating_handler],
format='%(asctime)s - %(levelname)s - %(message)s')
# Definir la ruta personalizada para el archivo de la base de datos
ruta_base_datos = os.path.join(dir_data, "registros.db")
# Crear una conexión a la base de datos SQLite
engine = create_engine(f'sqlite:///{ruta_base_datos}')
# Crear la tabla en la base de datos si no existe
Base.metadata.create_all(engine)
# Crear una sesión
Session = sessionmaker(bind=engine)
session = Session()
def crear_usuario(user_sync, org):
# Verificar si el usuario ya existe
usuario_existente = session.query(Usuario).filter_by(usuario_sync=user_sync).first()
if usuario_existente:
logging.error(f"El usuario '{user_sync}' ya existe en la base de datos.")
print(f"El usuario '{user_sync}' ya existe en la base de datos.")
return
try:
# Crear un nuevo usuario
nuevo_usuario = Usuario(usuario_sync=str(user_sync).lower(), organizacion=org)
session.add(nuevo_usuario)
session.commit()
except Exception:
pass
finally:
session.close()
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Uso: python add_user.py <user_sync> <organizacion>")
sys.exit(1)
user_par = sys.argv[1]
org_par = sys.argv[2]
try:
crear_usuario(user_par, org_par)
logging.info(f"Se crea usuario '{user_par}' en la base de datos.")
print(f"Se crea usuario '{user_par}' en la base de datos.")
except ProgrammingError as e:
logging.error(str(e))
print(str(e))

36
config.py Normal file
View file

@ -0,0 +1,36 @@
# Config MISP
MISP_CONFIG = {
"URL": "<URL_MISP>",
"AUTHKEY": "<AUTHKEY_MISP>"
}
CONFIG_WL = {
"filtros_buscar": ["osint", "google", "1000","microsoft","amazon","cloudflare"],
"max_reg": 4000
}
KTIP_CONFIG = {
"api_key": "<APIKEY>",
"url_base": "https://opentip.kaspersky.com/api/v1/search/"
}
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'
]

485
defs.py Normal file
View file

@ -0,0 +1,485 @@
import os
import logging
from logging.handlers import RotatingFileHandler
from datetime import datetime
import psutil
import multiprocessing
import ipaddress
import json
from concurrent.futures import ThreadPoolExecutor, as_completed
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from pymisp import PyMISP
import config
import urllib3
import requests
from models import Base, Registro, Usuario
from datetime import datetime, timedelta
import threading
urllib3.disable_warnings()
class MISPProcessorTop:
def __init__(self):
# Directorio actual
self.dir_actual = os.getcwd()
# Directorio para Logs
self.dir_logs = self.dir_actual + '/logs'
# Directorio para data en JSON / BD
self.dir_data = self.dir_actual + '/data'
# Cuentas
self.creators_accounts = {}
# Se crea directorio logs y data por si no existe...
os.makedirs(self.dir_logs, exist_ok=True)
os.makedirs(self.dir_data, exist_ok=True)
# Logging...
rotating_handler = RotatingFileHandler(
os.path.join(self.dir_logs, "misp_ioc_top_app_" + datetime.now().strftime("%Y%m%d") + ".log"),
maxBytes=262144000,
backupCount=10
)
logging.basicConfig(
level=logging.INFO,
handlers=[rotating_handler],
format='%(asctime)s - %(levelname)s - %(message)s'
)
# Configuración MISP
misp_url = config.MISP_CONFIG['URL']
misp_key = config.MISP_CONFIG['AUTHKEY']
self.misp = PyMISP(misp_url, misp_key, False)
# Lista que almacena IoC procesados corresponden FP (FALSOPOSITIVO)
self.procesados_ioc_fp = set()
# Lista que almacena IoC procesados corresponden VP (VERDADEROPOSITIVO)
self.procesados_ioc_vp = set()
def calcular_numero_de_workers(self):
# Obtener el número de CPUs
num_cpus = multiprocessing.cpu_count()
# Obtener la memoria disponible en GB
mem = psutil.virtual_memory()
available_memory_gb = mem.available / (1024 ** 3)
# Ajustar el número de workers en función de CPUs y memoria
workers_por_memoria = int(available_memory_gb // 1)
num_workers = min(num_cpus, workers_por_memoria)
return max(1, num_workers) # Asegurarse de que haya al menos un worker
def calcular_fecha_7_dias_antes(self, fecha_str):
# Convertir la cadena de texto en un objeto de fecha
fecha = datetime.strptime(fecha_str, "%Y-%m-%d")
# Calcular la fecha 7 días antes
fecha_7_dias_antes = fecha - timedelta(days=7)
# Devolver la fecha en formato YYYY-MM-DD
return fecha_7_dias_antes.strftime("%Y-%m-%d")
def eliminar_atributo_o_objeto(self, a, o, evento_id):
try:
logging.info("Falso Positivo encontrado : " + a['type'] + "->" + a['value'] + " en evento #" + evento_id)
if o is None:
# entonces se procesa atributo independiente de objeto
logging.info(f"Eliminando atributo UUID: {a['uuid']} / {a['value']} , evento #{evento_id}")
self.misp.delete_attribute(a['uuid'], True)
else:
# Verificación de existencia del objeto en MISP
objeto_existe = self.misp.get_object(o['uuid'], pythonify=True)
if not objeto_existe:
logging.warning(f"El objeto UUID {o['uuid']} no existe en MISP o ya fue eliminado. No se puede eliminar.")
return False # Para que no se contemple en la cuenta de FP...
# Se verifica para los casos de objetos que son con mas hash que sha256
if a['type'] == 'sha256':
logging.info(f"Eliminando objeto UUID: {o['uuid']} / {a['value']}, evento #{evento_id}")
self.misp.delete_object(o['uuid'], True)
else:
counter_validos = 0
# Se recorre para ver tipos
for at in o['Attribute']:
if at['type'] not in config.IOC_TIPOS_OMITIR:
counter_validos = counter_validos + 1
# Si es mayor a 1, no se puede borrar objeto completo
if counter_validos > 1:
logging.info(f"Eliminando atributo UUID: {a['uuid']} / {a['value']} en el objeto {o['name']} , evento #{evento_id}")
self.misp.delete_attribute(a['uuid'], True)
else:
logging.info(f"Eliminando objeto UUID: {o['uuid']} / {a['value']}, evento #{evento_id}")
self.misp.delete_object(o['uuid'], True)
return False
except Exception as err:
logging.error(str(err))
return True
def verificar_atributo_en_warninglist(self, a, lista):
"""
Verifica si un atributo coincide con alguna entrada en las WarningLists,
considerando también el tipo de atributo según 'WarninglistType'.
"""
for l in lista:
for entry in l['Warninglist']['WarninglistEntry']:
# Verificar si el tipo del atributo está soportado por la entrada de la WarningList
if a['type'] in l['Warninglist']['WarninglistType']:
if 'ip-' in a['type'] and 'port' not in a['type']:
if self._comparar_ip(entry['value'], a['value'], a['type']):
return False
elif any(item in a['type'] for item in ['domain', 'hostname']):
if self._comparar_dominio(entry['value'], a['value'], a['type']):
return False
return True
def procesar_atributo(self, a, o, e, lista):
if a['value'] in self.procesados_ioc_fp:
return self.eliminar_atributo_o_objeto(a, o, e['Event']['id'])
elif a['value'] in self.procesados_ioc_vp:
return True # No se realiza ningún proceso de eliminación
if self.verificar_atributo_en_warninglist(a, lista):
# Se verifica atributo que coincide con Warninglist...
if not self.verificar_y_procesar_atributo(a, o, e, lista, ['ip-', 'domain', 'hostname','sha256']):
return False
else:
return self.eliminar_atributo_o_objeto(a, o, e['Event']['id'])
return True
def verificar_y_procesar_atributo(self, a, o, e, lista, tipos):
"""
Verifica y procesa un atributo basado en su tipo.
"""
if any(item in a['type'] for item in tipos):
if '|ip' not in a['type'] and 'port' not in a['type']:
# Solo llamar a verificacion_ktip si el valor no está en las listas de FP o VP
if a['value'] not in self.procesados_ioc_fp and a['value'] not in self.procesados_ioc_vp:
if self.verificacion_ktip(a['value'], a['type']):
self.procesados_ioc_fp.add(a['value'])
return self.eliminar_atributo_o_objeto(a, o, e['Event']['id'])
else:
self.procesados_ioc_vp.add(a['value'])
return True
def _comparar_ip(self, valor_warning, valor_ip, tipo_ioc):
"""
Compara una IP con una entrada en la WarningList.
"""
try:
if '/' in valor_warning:
ip_obj = ipaddress.ip_address(valor_ip)
cidr_obj = ipaddress.ip_network(valor_warning)
if ip_obj in cidr_obj:
# Solo llamar a verificacion_ktip si el valor no está en las listas de FP o VP
if valor_ip not in self.procesados_ioc_fp and valor_ip not in self.procesados_ioc_vp:
if not self.verificacion_ktip(valor_ip, tipo_ioc):
self.procesados_ioc_vp.add(valor_ip)
return True
elif valor_ip in valor_warning:
if valor_ip not in self.procesados_ioc_fp and valor_ip not in self.procesados_ioc_vp:
if not self.verificacion_ktip(valor_ip, tipo_ioc):
self.procesados_ioc_vp.add(valor_ip)
return True
except Exception as e:
logging.error(str(e))
return False
def _comparar_dominio(self, valor_warning, valor_dominio, tipo_ioc):
"""
Compara un dominio con una entrada en la WarningList.
"""
if valor_dominio.strip() == valor_warning:
if valor_dominio not in self.procesados_ioc_fp and valor_dominio not in self.procesados_ioc_vp:
if not self.verificacion_ktip(valor_dominio, tipo_ioc):
self.procesados_ioc_vp.add(valor_dominio)
return True
return False
def verificacion_ktip(self, ioc, tipo_ioc):
# URL Base
url = config.KTIP_CONFIG['url_base']
# Encabezados de la solicitud, incluyendo la API key para autenticación
headers = {
'x-api-key': config.KTIP_CONFIG['api_key']
}
# Mapeo según tipo
tipos_ktip = {
"ip": ['ip-src','ip-dst'],
"domain":['domain','hostname'],
"hash": ['sha256']
}
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:
# Realiza la solicitud GET a la API
response = requests.get(url, headers=headers, verify=False)
# Verifica si la solicitud fue exitosa
if response.status_code == 200:
data = response.json()
# Ahora verificar si IoC está limpio
if data['Zone'] == 'Green':
return True
# por defecto
return False
except (Exception, requests.exceptions.HTTPError) as e:
logging.error(str(e))
return False
def procesar_evento(self, e, lista, prom):
# Se saca publish_timestamp para que recorra desde 7 dias atras en cada de eventos antiguos
date_back = datetime.fromtimestamp(int(e['Event']['publish_timestamp'])).date()
date_back = self.calcular_fecha_7_dias_antes(date_back.strftime('%Y-%m-%d'))
date_back = datetime.strptime(date_back,'%Y-%m-%d').date()
resultados = {}
ioc_valido = True
if str(e['Event']['event_creator_email']).lower() in self.creators_accounts:
if 0 < int(e['Event']['attribute_count']) <= prom:
logging.info(f"Procesando Evento #{e['Event']['id']} con fecha {e['Event']['date']}")
for a in e['Event']['Attribute']:
if datetime.fromtimestamp(int(a['timestamp'])).date() >= date_back:
if a['type'] not in config.IOC_TIPOS_OMITIR:
if not self.procesar_atributo(a, None, e, lista):
ioc_valido = False
else:
# IoC a contar
resultados[e['Event']['event_creator_email']] = resultados.get(e['Event']['event_creator_email'], 0) + 1
if 'Object' in e['Event']:
for o in e['Event']['Object']:
for a in o['Attribute']:
if datetime.fromtimestamp(int(a['timestamp'])).date() >= date_back:
if a['type'] not in config.IOC_TIPOS_OMITIR:
if not self.procesar_atributo(a, o, e, lista):
ioc_valido = False
else:
# IoC a contar
resultados[e['Event']['event_creator_email']] = resultados.get(e['Event']['event_creator_email'], 0) + 1
if not ioc_valido:
try:
# Se consulta evento y se verifica cantidad de atributos
evento = self.misp.get_event(int(e['Event']['id']))
if evento:
if int(evento['Event']['attribute_count']) > 0:
self.misp.publish(int(e['Event']['id']))
logging.info("Publicando cambios de evento #" + e['Event']['id'])
else:
# Se elimina evento con cero atributo...
self.misp.delete_event(int(e['Event']['id']))
logging.info("Eliminando evento #" +e['Event']['id']+" por carencia de atributos")
except Exception as err:
logging.error(str(err))
return resultados
def calcula_calidad_iocs(self, desde: str, hasta: str, a_por_evento=None):
try:
# Para salida
output = []
self.creators_accounts = self.obtener_cuentas_sync()
if self.creators_accounts:
puntos = {k: 0 for k in self.creators_accounts.keys()}
# Para filtrar versión de Warninglist donde buscar, se toma desde fecha "desde"
actual = datetime.now().year
version = str(actual)
# Fechas para sacar la mayor...
fechas = []
# Filtros para seleccionar Warninglist
filtros = config.CONFIG_WL['filtros_buscar']
# Para acumular warninglist
lista = []
logging.info("Limpiador de IoC App v1.0 comenzando...")
# Actualizar Warninglist por si acaso...
self.misp.update_warninglists()
# Warninglist completas
wl = self.misp.warninglists()
for l in wl:
if str(l['Warninglist']['version']).startswith(str(actual)):
fechas.append(l['Warninglist']['version'])
if fechas:
# Saca la versión más alta...
version = max(fechas)
for l in wl:
if str(l['Warninglist']['version']) == version:
if any(filtro in str(l['Warninglist']['name']).lower() for filtro in filtros):
if int(str(l['Warninglist']['warninglist_entry_count'])) <= config.CONFIG_WL['max_reg']:
lista.append(self.misp.get_warninglist(int(l['Warninglist']['id'])))
if lista:
# Promedio por defecto
prom = 0
logging.info("Warninglist de MISP Cargadas : " + str(len(lista)))
# Rango completo de fechas....
logging.info("Buscando IoC Desde :" + desde + " Hasta :" + hasta)
eventos_tmp = self.misp.search(publish_timestamp=desde)
#eventos = self.misp.search(date_from=desde, date_to=hasta, published=True)
# Si existen eventos, se realiza proceso...
if eventos_tmp:
eventos = []
# Se seleccionan eventos para establecer limite de fechas
for e in eventos_tmp:
if datetime.fromtimestamp(int(e['Event']['publish_timestamp'])).date() <= datetime.strptime(hasta, '%Y-%m-%d').date():
eventos.append(e)
# Atributos por evento es None, se calcula promedio...
if a_por_evento is None:
cont_atrr = 0
cont_eventos = 0
for e in eventos:
if str(e['Event']['event_creator_email']).lower() in self.creators_accounts:
cont_atrr += int(e['Event']['attribute_count'])
cont_eventos += 1
logging.info("Promedio de atributos a procesar listos.")
prom = int(cont_atrr / cont_eventos) if cont_eventos > 0 else 0
else:
prom = int(a_por_evento)
logging.info("Eventos por procesar :" + str(len(eventos)))
logging.info("Máximo de atributos a procesar por evento :" + str(prom))
num_workers = 4
logging.info(f"Usando {num_workers} workers")
with ThreadPoolExecutor(max_workers=num_workers) as executor:
futures = [executor.submit(self.procesar_evento, e, lista, prom) for e in eventos]
for future in as_completed(futures):
result = future.result()
for email, puntos_obtenidos in result.items():
puntos[email] += puntos_obtenidos
puntos_ordenados = {k: v for k, v in sorted(puntos.items(), key=lambda item: item[1], reverse=True)}
for k in puntos_ordenados.keys():
temp = {}
temp['comunidad'] = self.creators_accounts[k]
temp['cantidad_ioc'] = puntos_ordenados[k]
output.append(temp)
logging.info("Total Falsos positivos detectados y eliminados : " + str(len(self.procesados_ioc_fp)))
logging.info("Proceso de conteo de IoC finalizado...")
else:
logging.error("No existe eventos para rango de fechas. Se detiene proceso")
else:
logging.error("No se encuentran Warninglist actualizadas para comparar. Se detiene proceso")
else:
logging.error("No se encuentran cuentas asociadas a MISP. Se detiene proceso")
return output
except Exception as err:
logging.error(str(err))
def guarda_ioc_json(self, data: list, filename: str):
try:
salida = os.path.join(self.dir_data, filename)
with open(salida, "w") as archivo_json:
json.dump(data, archivo_json, indent=4)
logging.info("Data volcada a ruta :" + salida)
except Exception as e:
logging.info("Error al escribir JSON :" + str(e))
def guardar_bd(self, data: list, fecha: str):
try:
ruta_base_datos = os.path.join(self.dir_data, "registros.db")
engine = create_engine(f'sqlite:///{ruta_base_datos}')
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
# Fecha corte
fecha_corte = fecha.split("-")
ano_t = int(fecha_corte[0])
mes_t = int(fecha_corte[1])
dia_t = int(fecha_corte[2])
for item in data:
organizacion = str(item['comunidad']).upper()
cantidad_ioc = item['cantidad_ioc']
nuevo_registro = Registro(
organizacion=organizacion,
ano=ano_t,
mes=mes_t,
dia=dia_t,
fecha_creado = datetime.strptime(fecha, '%Y-%m-%d').date(),
cantidad_ioc=cantidad_ioc
)
session.add(nuevo_registro)
try:
session.commit()
logging.info("Datos guardados exitosamente en la base de datos.")
except Exception as e:
logging.error(f"Error al guardar los datos en la base de datos: {e}")
session.rollback()
finally:
session.close()
except Exception as err:
logging.error(f"Error al guardar en la base de datos: {err}")
def obtener_cuentas_sync(self):
cuentas = {}
try:
ruta_base_datos = os.path.join(self.dir_data, "registros.db")
engine = create_engine(f'sqlite:///{ruta_base_datos}')
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
query_cuentas = session.query(Usuario).all()
for item in query_cuentas:
cuentas[item.usuario_sync] = item.organizacion
return cuentas
except Exception as err:
logging.error(f"Error al obtener datos: {err}")
return None
finally:
session.close()

238
main.py Normal file
View file

@ -0,0 +1,238 @@
from fastapi import FastAPI, HTTPException, Depends, Query
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import create_engine, func, desc
from sqlalchemy.orm import sessionmaker
from sqlalchemy.sql import label
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.future import select
from pydantic import BaseModel
from typing import Optional
import os
import logging
from logging.handlers import RotatingFileHandler
from models import Base, Registro
from datetime import datetime, date
# Configurar el nivel de logging para SQLAlchemy
logging.getLogger('sqlalchemy').setLevel(logging.WARNING)
# Directorio actual
directorio_actual = os.getcwd()
# Crear directorio de logs si no existe
dir_logs = os.path.join(directorio_actual, 'logs')
os.makedirs(dir_logs, exist_ok=True)
rotating_handler = RotatingFileHandler(os.path.join(dir_logs, "server_errors.log"), maxBytes=262144000, backupCount=10)
logging.basicConfig(level=logging.INFO, handlers=[rotating_handler],
format='%(asctime)s - %(levelname)s - %(message)s')
# Configurar la ruta de la base de datos
ruta_base_datos = os.path.join(directorio_actual, "data", "registros.db")
os.makedirs(os.path.dirname(ruta_base_datos), exist_ok=True)
# Crear un motor síncrono para la creación de tablas
sync_engine = create_engine(f'sqlite:///{ruta_base_datos}')
Base.metadata.create_all(bind=sync_engine)
# Crear un motor asíncrono para las operaciones de la base de datos
async_engine = create_async_engine(f'sqlite+aiosqlite:///{ruta_base_datos}', echo=False, future=True)
# Función síncrona para ejecutar PRAGMAs al establecer una conexión
def set_sqlite_pragma(dbapi_connection, connection_record):
cursor = dbapi_connection.cursor()
cursor.execute('PRAGMA journal_mode=WAL;')
cursor.close()
# Escucha el evento 'connect' para aplicar el PRAGMA cada vez que se conecta a la base de datos
from sqlalchemy import event
event.listen(async_engine.sync_engine, 'connect', set_sqlite_pragma)
# Crear una sesión asíncrona
AsyncSessionLocal = sessionmaker(
bind=async_engine,
expire_on_commit=False,
class_=AsyncSession
)
# FastAPI
app = FastAPI(version="1.0.0", title="MISP Top Contrib",
description="<p>Esta API fue desarrollada para entregar información de la cantidad de contribuciones (IoC) de calidad entregadas por cada comunidad conectada a la plataforma MISP de CSIRT de Gobierno.</p>")
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Modelo Pydantic para la salida de datos (sin el campo id)
class RegistroOutput(BaseModel):
organizacion: str
dia: Optional[int] = None
mes: Optional[int] = None
ano: Optional[int] = None
fecha_creado: Optional[date] = None
cantidad_ioc: int
# Modelo Pydantic para la cantidad de IoC por organización
class IoCCountOutput(BaseModel):
organizacion: str
mes: Optional[int] = None # Hacemos opcional el campo 'mes'
ano: Optional[int] = None # Hacemos opcional el campo 'ano'
cantidad_total_ioc: int
# Dependencia para obtener la sesión de base de datos
async def get_db():
async with AsyncSessionLocal() as session:
yield session
# Método GET para obtener todos los registros
@app.get(
"/api/stats/", response_model=list[RegistroOutput], response_model_exclude_none=True,
description="Obtiene el listado completo de organizaciones junto a la cantidad de IoC recopilados por período.",
summary="Cantidad de contribuciones (IoC) de cada organización.",
responses={
404: {
"description": "No se encontraron registros.",
"content": {
"application/json": {
"example": {"detail": "No se encontraron registros"}
}
}
}
}
)
async def leer_registros(
start_date: str = Query(None, description="Fecha de inicio en formato YYYY-MM-DD"),
end_date: str = Query(None, description="Fecha de fin en formato YYYY-MM-DD"),
organizacion: str = Query(None, description="Organizacion a filtrar"),
db: AsyncSession = Depends(get_db)
):
try:
# Construir la consulta base
query = select(Registro.organizacion, Registro.fecha_creado, Registro.cantidad_ioc).filter(Registro.cantidad_ioc > 0)
# Si se proporcionan fechas de inicio y fin, aplicar el filtro de rango de fechas
if start_date:
start_datetime = datetime.strptime(start_date, "%Y-%m-%d").date()
query = query.filter(Registro.fecha_creado >= start_datetime)
if end_date:
end_datetime = datetime.strptime(end_date, "%Y-%m-%d").date()
query = query.filter(Registro.fecha_creado <= end_datetime)
if organizacion:
query = query.filter(Registro.organizacion == organizacion.upper())
# Ordenar los resultados
query = query.order_by(desc(Registro.fecha_creado))
# Ejecutar la consulta
result = await db.execute(query)
registros = result.fetchall()
registros = [{"organizacion": r[0], "fecha_creado": r[1], "cantidad_ioc": r[2]} for r in registros]
if not registros:
raise HTTPException(status_code=404, detail="No se encontraron registros")
return registros
except HTTPException as http_err:
raise http_err
except Exception as e:
logging.error(f"Error al obtener registros: {e}")
raise HTTPException(status_code=500, detail="Error interno del servidor")
# Método GET para obtener la cantidad de IoC por organización, filtrado opcionalmente por año
@app.get(
"/api/stats/cantidad_por_organizacion/", response_model=list[IoCCountOutput], response_model_exclude_none=True,
description="Obtiene el listado completo de organizaciones junto a la cantidad de IoC recopilados por año específico.",
summary="Cantidad total de contribuciones (IoC) de cada organización (Año).",
responses={
404: {
"description": "No se encontraron registros.",
"content": {
"application/json": {
"example": {"detail": "No se encontraron registros."}
}
}
}
}
)
async def cantidad_ioc_por_organizacion(ano: int = Query(None, description="Año para filtrar los resultados"),
db: AsyncSession = Depends(get_db)):
try:
query = select(
Registro.organizacion,
label("cantidad_total_ioc", func.sum(Registro.cantidad_ioc))
).filter(Registro.cantidad_ioc > 0)
if ano is not None:
query = query.filter(Registro.ano == ano)
query = query.group_by(Registro.organizacion).order_by(
desc(label("cantidad_total_ioc", func.sum(Registro.cantidad_ioc))))
result = await db.execute(query)
resultados = result.fetchall()
if not resultados:
raise HTTPException(status_code=404, detail="No se encontraron registros.")
return [IoCCountOutput(organizacion=r.organizacion, cantidad_total_ioc=r.cantidad_total_ioc) for r in
resultados]
except HTTPException as http_err:
raise http_err
except Exception as e:
logging.error(f"Error al obtener cantidad de IoC por organización: {e}")
raise HTTPException(status_code=500, detail="Error interno del servidor")
# Método GET para obtener registros filtrados por año y mes
@app.get(
"/api/stats/por_periodo/",
response_model=list[IoCCountOutput], response_model_exclude_none=True,
description="Obtiene cantidad de IoC recopilados de cada organización por un período específico.",
summary="Cantidad de contribuciones (IoC) de cada organización de un período específico.",
responses={
404: {
"description": "No se encontraron registros para este período.",
"content": {
"application/json": {
"example": {"detail": "No se encontraron registros para este período"}
}
}
}
}
)
async def registros_por_fecha(ano: int, mes: int, db: AsyncSession = Depends(get_db)):
try:
query = select(
Registro.organizacion,
Registro.mes,
Registro.ano,
label("cantidad_total_ioc", func.sum(Registro.cantidad_ioc))
).filter(Registro.cantidad_ioc > 0)
if ano is not None:
query = query.filter(Registro.ano == ano)
if mes is not None:
query = query.filter(Registro.mes == mes)
query = query.group_by(Registro.organizacion, Registro.ano, Registro.mes).order_by(
desc(label("cantidad_total_ioc", func.sum(Registro.cantidad_ioc))))
result = await db.execute(query)
registros = result.fetchall()
if not registros:
raise HTTPException(status_code=404, detail="No se encontraron registros para este periodo")
return registros
except HTTPException as http_err:
raise http_err
except Exception as e:
logging.error(f"Error al obtener registros por fecha: {e}")
raise HTTPException(status_code=500, detail="Error interno del servidor")

34
models.py Normal file
View file

@ -0,0 +1,34 @@
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()
# Definir el modelo de la tabla
class Registro(Base):
__tablename__ = 'registros'
id = Column(Integer, primary_key=True, autoincrement=True)
organizacion = Column(String, nullable=False)
ano = Column(Integer, nullable=False)
mes = Column(Integer, nullable=False)
dia = Column(Integer, nullable=False)
fecha_creado = Column(Date, nullable=True)
cantidad_ioc = Column(Integer, nullable=False)
# Definir una clave única compuesta
#__table_args__ = (UniqueConstraint('organizacion', 'ano', 'mes', name='_org_ano_mes_uc'),)
# Se define modelo de usuario
class Usuario(Base):
__tablename__ = 'usuarios'
id = Column(Integer, primary_key=True, autoincrement=True)
usuario_sync = Column(String, unique=True, nullable=False)
organizacion = Column(String, nullable=False)
creado = Column(DateTime, default=datetime.now(timezone.utc))

7
requirements.txt Normal file
View file

@ -0,0 +1,7 @@
pymisp
urllib3
psutil
fastapi
SQLAlchemy
uvicorn
aiosqlite

40
run_daily.py Normal file
View file

@ -0,0 +1,40 @@
from defs import MISPProcessorTop
import calendar
from datetime import datetime, timedelta
max_ioc = 5000
# Obtener la fecha y hora actuales
hoy = datetime.now()
# Verificar si hoy es el primer día del mes
if hoy.day == 1:
# Si es el primer día del mes, calcular el último día del mes anterior
mes_anterior = hoy.month - 1 if hoy.month > 1 else 12
año_anterior = hoy.year if hoy.month > 1 else hoy.year - 1
ultimo_dia_mes_anterior = calendar.monthrange(año_anterior, mes_anterior)[1]
fecha_anterior = datetime(año_anterior, mes_anterior, ultimo_dia_mes_anterior)
else:
# Si no es el primer día del mes, restar un día a la fecha actual
fecha_anterior = hoy - timedelta(days=1)
fecha_anterior = fecha_anterior.strftime('%Y-%m-%d')
obj = MISPProcessorTop()
# Se llama a calcular fecha
#iocs = obj.calcula_calidad_iocs(desde_fecha, hasta_fecha, 1000)
iocs = obj.calcula_calidad_iocs(fecha_anterior, fecha_anterior, max_ioc)
if iocs:
# Solo para efectos de backup fisico por dia...
#obj.guarda_ioc_json(iocs, fecha_anterior.strftime('%Y%m%d')"_"+".json")
# Se imprime estructura
print(iocs)
# Guarda en BD (SQLite)
obj.guardar_bd(iocs,fecha_anterior)