#content
This commit is contained in:
parent
97a0a07fa7
commit
575ea7dfb8
7 changed files with 911 additions and 0 deletions
71
add_user.py
Normal file
71
add_user.py
Normal 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
36
config.py
Normal 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
485
defs.py
Normal 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
238
main.py
Normal 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
34
models.py
Normal 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
7
requirements.txt
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
pymisp
|
||||||
|
urllib3
|
||||||
|
psutil
|
||||||
|
fastapi
|
||||||
|
SQLAlchemy
|
||||||
|
uvicorn
|
||||||
|
aiosqlite
|
40
run_daily.py
Normal file
40
run_daily.py
Normal 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)
|
Loading…
Add table
Reference in a new issue