diff --git a/README_1.md b/README_1.md new file mode 100644 index 0000000..df92152 --- /dev/null +++ b/README_1.md @@ -0,0 +1,65 @@ +# IRC-DEFENDER +Defender est un service IRC basé sur la sécurité des réseaux IRC ( UnrealIRCD ) +Il permet d'ajouter une sécurité supplémentaire pour vérifier les users connectés au réseau +en demandant aux user un code de validation. + +Pré-requis : + + - Python version >= 3.10 + - Pip de python installé sur la machine + - Python librairies psutil & sqlalchemy & requests + - IRC Serveur Version >= UnrealIRCd-6.1.2.2 + +Lancement de Defender : + + - Installer les librairies python : psutil & sqlalchemy & requests + - pip3 install psutil sqlalchemy requests ou pip install psutil sqlalchemy requests + - Ne pas lancer Defender en tant que root + - Créer plutot un service qui lancera Defender en tant qu'utilisateur non root + - Un fichier PID sera crée + +# TO DO LIST + + - Optimiser le systeme de réputation: + - lorsque les users ce connectent, Ils entrent dans un salon puis une fraction de seconde le service les bans + - Déplacer les deux variables RESTART et INIT de la configuration a Irc + + +# VERSION 1 + [28.12.2023] + - Changement de méthode pour récuperer la version actuelle de python + - Ajout de la réponse a une PING de la part d'un utilisateur + - Installation automatique des packages sqlalchemy, requests et psutil + +# BUG FIX + + [29.12.2023] + - Correction des messages de receptions trop longs > 4070 caractéres; + - la méthode boucle et incrémente la réponse tant que le nombre de caractére reçu est supérieur a 4072 + - Rajout du protocol MTAGS a la connexion du service + - Impact majeur dans la lecture des messages reçu du serveur ( PRIVMSG, SLOGS, UID, QUIT, NICK, PONG, SJOIN) + +# ALREADY IMPLEMENTED + + - Connexion en tant que service + - Gestion des messages reçus/envoyés par le serveur + - Gestion des caractéres spéciaux + - Gestion des logs (salon, fichiers et console) + - Mode debug : gestion des logs coté console + - Création du systeme de gestion de commandes + - Defender reconnait les commandes qui commence par le suffix définit dans la configuration + - Defender reconnait aussi reconnaitre les commandes qui viennent de /msg Defender [commande] + - Identifications + - Systéme d'identification [OK] + - Systéme de changement d'information [OK] + - Suppression d'un admin + - Systéme de groupe d'accés [OK] + + Reputation security + - Activation ou désaction du systéme --> OK | .reputation ON/off + - Le user sera en mesure de changer la limite de la réputation --> OK | .reputation set limit 120 + - Defender devra envoyer l'utilisateur dans un salon définit dans la configuration --> OK + - Defender bannira l'utilisateur de la totalité des salons, il le bannira aussi lorsqu'il souhaitera accéder a de nouveau salon --> OK + - Defender devra envoyer un message du type "Merci de taper cette comande /msg {nomdudefender} code {un code générer aléatoirement} --> OK + - Defender devra reconnaitre le code --> OK + - Defender devra liberer l'utilisateur et l'envoyer vers un salon définit dans la configuration --> OK diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/base.py b/core/base.py new file mode 100644 index 0000000..0fc08ad --- /dev/null +++ b/core/base.py @@ -0,0 +1,413 @@ +import time, threading, os, sys, random, socket, hashlib +from platform import python_version +from datetime import datetime +from sqlalchemy import create_engine, Engine, Connection, CursorResult +from sqlalchemy.sql import text +from core.configuration import Config + +class Base: + + CORE_DB_PATH = 'core' + os.sep + 'db' + os.sep # Le dossier bases de données core + MODS_DB_PATH = 'mods' + os.sep + 'db' + os.sep # Le dossier bases de données des modules + PYTHON_MIN_VERSION = '3.10' # Version min de python + DB_SCHEMA:list[str] = { + 'admins': 'sys_admins', + 'commandes': 'sys_commandes', + 'logs': 'sys_logs', + 'modules': 'sys_modules' + } + + def __init__(self, Config: Config) -> None: + + self.Config = Config # Assigner l'objet de configuration + + # Tester si la version de python est correcte + if not self.isRightPythonVersion(): + sys.exit(1) + + self.running_timers:list[threading.Timer] = [] # Liste des timers en cours + self.running_threads:list[threading.Thread] = [] # Liste des threads en cours + self.running_sockets: list[socket.socket] = [] # Les sockets ouvert + self.periodic_func:dict[object] = {} # Liste des fonctions en attentes + + self.lock = threading.RLock() # Création du lock + + self.engine, self.cursor = self.db_init() # Initialisation de la connexion a la base de données + self.__create_db() # Initialisation de la base de données + + self.db_create_first_admin() + + def isRightPythonVersion(self) -> bool: + """Test si la version de python est autorisée ou non + + Returns: + bool: True si la version de python est autorisé sinon False + """ + python_required_version = self.PYTHON_MIN_VERSION.split('.') + python_current_version = python_version().split('.') + + if int(python_current_version[0]) < int(python_required_version[0]): + print(f"## Your python version must be greather than or equal to {self.PYTHON_MIN_VERSION} ##") + return False + elif int(python_current_version[1]) < int(python_required_version[1]): + print(f"### Your python version must be greather than or equal to {self.PYTHON_MIN_VERSION} ###") + return False + + # print(f"===> Version of python : {python_version()} ==> OK") + + return True + + def get_unixtime(self)->int: + """ + Cette fonction retourne un UNIXTIME de type 12365456 + Return: Current time in seconds since the Epoch (int) + """ + unixtime = int( time.time() ) + return unixtime + + def get_datetime(self)->str: + """ + Retourne une date au format string (24-12-2023 20:50:59) + """ + currentdate = datetime.now().strftime('%d-%m-%Y %H:%M:%S') + return currentdate + + def create_log(self, log_message: str) -> None: + """Enregiste les logs + + Args: + string (str): Le message a enregistrer + + Returns: + None: Aucun retour + """ + sql_insert = f"INSERT INTO {self.DB_SCHEMA['logs']} (datetime, server_msg) VALUES (:datetime, :server_msg)" + mes_donnees = {'datetime': str(self.get_datetime()),'server_msg': f'{log_message}'} + self.db_execute_query(sql_insert, mes_donnees) + + return None + + def log_cmd(self, user_cmd:str, cmd:str) -> None: + """Enregistre les commandes envoyées par les utilisateurs + + Args: + cmd (str): la commande a enregistrer + """ + insert_cmd_query = f"INSERT INTO {self.DB_SCHEMA['commandes']} (datetime, user, commande) VALUES (:datetime, :user, :commande)" + mes_donnees = {'datetime': self.get_datetime(), 'user': user_cmd, 'commande': cmd} + self.db_execute_query(insert_cmd_query, mes_donnees) + + return False + + def db_isModuleExist(self, module_name:str) -> bool: + """Teste si un module existe déja dans la base de données + + Args: + module_name (str): le non du module a chercher dans la base de données + + Returns: + bool: True si le module existe déja dans la base de données sinon False + """ + query = f"SELECT id FROM {self.DB_SCHEMA['modules']} WHERE module = :module" + mes_donnes = {'module': module_name} + results = self.db_execute_query(query, mes_donnes) + + if results.fetchall(): + return True + else: + return False + + def db_record_module(self, user_cmd:str, module_name:str) -> None: + """Enregistre les modules dans la base de données + + Args: + cmd (str): le module a enregistrer + """ + + if not self.db_isModuleExist(module_name): + self.__debug(f"Le module {module_name} n'existe pas alors ont le créer") + insert_cmd_query = f"INSERT INTO {self.DB_SCHEMA['modules']} (datetime, user, module) VALUES (:datetime, :user, :module)" + mes_donnees = {'datetime': self.get_datetime(), 'user': user_cmd, 'module': module_name} + self.db_execute_query(insert_cmd_query, mes_donnees) + # self.db_close_session(self.session) + else: + self.__debug(f"Le module {module_name} existe déja dans la base de données") + + return False + + def db_delete_module(self, module_name:str) -> None: + """Supprime les modules de la base de données + + Args: + cmd (str): le module a enregistrer + """ + insert_cmd_query = f"DELETE FROM {self.DB_SCHEMA['modules']} WHERE module = :module" + mes_donnees = {'module': module_name} + self.db_execute_query(insert_cmd_query, mes_donnees) + + return False + + def db_create_first_admin(self) -> None: + + user = self.db_execute_query(f"SELECT id FROM {self.DB_SCHEMA['admins']}") + if not user.fetchall(): + admin = self.Config.OWNER + password = self.crypt_password(self.Config.PASSWORD) + + mes_donnees = {'createdOn': self.get_datetime(), 'user': admin, 'password': password, 'hostname': '*', 'vhost': '*', 'level': 5} + self.db_execute_query(f""" + INSERT INTO {self.DB_SCHEMA['admins']} + (createdOn, user, password, hostname, vhost, level) + VALUES + (:createdOn, :user, :password, :hostname, :vhost, :level)""" + , mes_donnees) + + pass + + def create_timer(self, time_to_wait: float, func: object, func_args: tuple = ()) -> None: + + try: + t = threading.Timer(interval=time_to_wait, function=func, args=func_args) + t.setName(func.__name__) + t.start() + + self.running_timers.append(t) + + self.__debug(f"Timer ID : {str(t.ident)} | Running Threads : {len(threading.enumerate())}") + + except AssertionError as ae: + self.__debug(f'Assertion Error -> {ae}') + + def create_thread(self, func:object, func_args: tuple = ()) -> None: + try: + func_name = func.__name__ + # if func_name in self.running_threads: + # print(f"HeartBeat is running") + # return None + + th = threading.Thread(target=func, args=func_args, name=str(func_name), daemon=True) + th.start() + + self.running_threads.append(th) + self.__debug(f"Thread ID : {str(th.ident)} | Thread name : {th.getName()} | Running Threads : {len(threading.enumerate())}") + + except AssertionError as ae: + self.__debug(f'Assertion Error -> {ae}') + + def garbage_collector_timer(self) -> None: + """Methode qui supprime les timers qui ont finis leurs job + """ + try: + self.__debug(f"=======> Checking for Timers to stop") + # print(f"{self.running_timers}") + for timer in self.running_timers: + if not timer.is_alive(): + timer.cancel() + self.running_timers.remove(timer) + self.__debug(f"Timer {str(timer)} removed") + else: + self.__debug(f"===> Timer {str(timer)} Still running ...") + + except AssertionError as ae: + print(f'Assertion Error -> {ae}') + + def garbage_collector_thread(self) -> None: + """Methode qui supprime les threads qui ont finis leurs job + """ + try: + self.__debug(f"=======> Checking for Threads to stop") + for thread in self.running_threads: + if thread.getName() != 'heartbeat': + if not thread.is_alive(): + self.running_threads.remove(thread) + self.__debug(f"Thread {str(thread.getName())} {str(thread.native_id)} removed") + + # print(threading.enumerate()) + except AssertionError as ae: + self.__debug(f'Assertion Error -> {ae}') + + def garbage_collector_sockets(self) -> None: + + self.__debug(f"=======> Checking for Sockets to stop") + for soc in self.running_sockets: + while soc.fileno() != -1: + self.__debug(soc.fileno()) + soc.close() + + soc.close() + self.running_sockets.remove(soc) + self.__debug(f"Socket ==> closed {str(soc.fileno())}") + + def shutdown(self) -> None: + """Methode qui va préparer l'arrêt complêt du service + """ + # Nettoyage des timers + print(f"=======> Checking for Timers to stop") + for timer in self.running_timers: + while timer.is_alive(): + print(f"> waiting for {timer.getName()} to close") + timer.cancel() + time.sleep(0.2) + self.running_timers.remove(timer) + print(f"> Cancelling {timer.getName()} {timer.native_id}") + + print(f"=======> Checking for Threads to stop") + for thread in self.running_threads: + if thread.getName() == 'heartbeat' and thread.is_alive(): + self.execute_periodic_action() + print(f"> Running the last periodic action") + self.running_threads.remove(thread) + print(f"> Cancelling {thread.getName()} {thread.native_id}") + + print(f"=======> Checking for Sockets to stop") + for soc in self.running_sockets: + soc.close() + while soc.fileno() != -1: + soc.close() + + self.running_sockets.remove(soc) + print(f"> Socket ==> closed {str(soc.fileno())}") + + pass + + def db_init(self) -> tuple[Engine, Connection]: + + db_directory = self.Config.DEFENDER_DB_PATH + full_path_db = self.Config.DEFENDER_DB_PATH + self.Config.DEFENDER_DB_NAME + + if not os.path.exists(db_directory): + os.makedirs(db_directory) + + engine = create_engine(f'sqlite:///{full_path_db}.db', echo=False) + cursor = engine.connect() + + return engine, cursor + + def __create_db(self) -> None: + + table_logs = f'''CREATE TABLE IF NOT EXISTS {self.DB_SCHEMA['logs']} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + datetime TEXT, + server_msg TEXT + ) + ''' + + table_cmds = f'''CREATE TABLE IF NOT EXISTS {self.DB_SCHEMA['commandes']} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + datetime TEXT, + user TEXT, + commande TEXT + ) + ''' + + table_modules = f'''CREATE TABLE IF NOT EXISTS {self.DB_SCHEMA['modules']} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + datetime TEXT, + user TEXT, + module TEXT + ) + ''' + + table_admins = f'''CREATE TABLE IF NOT EXISTS {self.DB_SCHEMA['admins']} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + createdOn TEXT, + user TEXT, + hostname TEXT, + vhost TEXT, + password TEXT, + level INTEGER + ) + ''' + + self.db_execute_query(table_logs) + self.db_execute_query(table_cmds) + self.db_execute_query(table_modules) + self.db_execute_query(table_admins) + + return None + + def db_execute_query(self, query:str, params:dict = {}) -> CursorResult: + + with self.lock: + insert_query = text(query) + if not params: + response = self.cursor.execute(insert_query) + else: + response = self.cursor.execute(insert_query, params) + + self.cursor.commit() + + return response + + def db_close(self) -> None: + + try: + self.cursor.close() + except AttributeError as ae: + self.__debug(f"Attribute Error : {ae}") + + def crypt_password(self, password:str) -> str: + """Retourne un mot de passe chiffré en MD5 + + Args: + password (str): Le password en clair + + Returns: + str: Le password en MD5 + """ + md5_password = hashlib.md5(password.encode()).hexdigest() + + return md5_password + + def int_if_possible(self, value): + """Convertit la valeur reçue en entier, si possible. + Sinon elle retourne la valeur initiale. + + Args: + value (any): la valeur à convertir + + Returns: + any: Retour un entier, si possible. Sinon la valeur initiale. + """ + try: + response = int(value) + return response + except ValueError: + return value + except TypeError: + return value + + def get_random(self, lenght:int) -> str: + """ + Retourn une chaîne aléatoire en fonction de la longueur spécifiée. + """ + caracteres = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + randomize = ''.join(random.choice(caracteres) for _ in range(lenght)) + + return randomize + + def execute_periodic_action(self) -> None: + + if not self.periodic_func: + # Run Garbage Collector Timer + self.garbage_collector_timer() + self.garbage_collector_thread() + self.garbage_collector_sockets() + return None + + for key, value in self.periodic_func.items(): + obj = value['object'] + method_name = value['method_name'] + param = value['param'] + f = getattr(obj, method_name, None) + f(*param) + + # Vider le dictionnaire de fonction + self.periodic_func.clear() + + def __debug(self, debug_msg:str) -> None: + + if self.Config.DEBUG == 1: + print(f"[{self.get_datetime()}] - {debug_msg}") + + return None \ No newline at end of file diff --git a/core/exemple_configuration.py b/core/exemple_configuration.py new file mode 100644 index 0000000..7e929e5 --- /dev/null +++ b/core/exemple_configuration.py @@ -0,0 +1,53 @@ +import os +########################################## +# CONFIGURATION FILE : # +# Rename file to : configuration.py # +########################################## + +class Config: + + DEFENDER_VERSION = '1.1.0' # MAJOR.MINOR.BATCH + DEFENDER_DB_PATH = 'db' + os.sep # Séparateur en fonction de l'OS + DEFENDER_DB_NAME = 'defender' # Le nom de la base de données principale + SERVICE_NAME = 'defender' # Le nom du service + + SERVEUR_IP = '8.8.8.8' # IP ou host du serveur à rejoindre + SERVEUR_HOSTNAME = 'your hostname' # Le hostname du serveur IRC + SERVEUR_LINK = 'your link' # Host attendu par votre IRCd (ex. dans votre link block pour Unrealircd) + SERVEUR_PORT = 6666 # Port du link + SERVEUR_PASSWORD = 'your link password' # Mot de passe du link (Privilégiez argon2 sur Unrealircd) + SERVEUR_ID = '002' # SID (identification) du bot en tant que Services + + SERVICE_NICKNAME = 'BotName' # Nick du bot sur IRC + SERVICE_REALNAME = 'BotRealname' # Realname du bot + SERVICE_USERNAME = 'BotIdent' # Ident du bot + SERVICE_HOST = 'your service host' # Host du bot + SERVICE_INFO = 'Network IRC Service' # swhois du bot + SERVICE_CHANLOG = '#services' # Salon des logs et autres messages issus du bot + SERVICE_SMODES = '+ioqBS' # Mode du service + SERVICE_CMODES = 'ntsO' # Mode du salon (#ChanLog) que le bot appliquera à son entrée + SERVICE_UMODES = 'o' # Mode que le bot pourra se donner à sa connexion au salon chanlog + SERVICE_PREFIX = '.' # Prefix pour envoyer les commandes au bot + SERVICE_ID = SERVEUR_ID + 'AAAAAB' # L'identifiant du service + + OWNER = 'admin' # Identifiant du compte admin + PASSWORD = 'password' # Mot de passe du compte admin + + SALON_JAIL = '#JAIL' # Salon pot de miel + SALON_JAIL_MODES = 'sS' # Mode du salon pot de miel + SALON_LIBERER = '#welcome' # Le salon ou sera envoyé l'utilisateur clean + + PORTS_TO_SCAN = [3028, 8080, 1080, 1085, 4145, 9050] # Liste des ports a scanné pour une detection de proxy + + DEBUG = 0 # Afficher l'ensemble des messages du serveurs dans la console + + CONFIG_COLOR = { + 'blanche': '\x0300', # Couleur blanche + 'noire': '\x0301', # Couleur noire + 'bleue': '\x0302', # Couleur Bleue + 'verte': '\x0303', # Couleur Verte + 'rouge': '\x0304', # Couleur rouge + 'jaune': '\x0306', # Couleur jaune + 'gras': '\x02', # Gras + 'nogc': '\x02\x03' # Retirer gras et couleur + } diff --git a/core/installation.py b/core/installation.py new file mode 100644 index 0000000..22d2851 --- /dev/null +++ b/core/installation.py @@ -0,0 +1,66 @@ +from importlib.util import find_spec +from subprocess import check_call, run +from platform import python_version +from sys import exit + +class Install: + + def __init__(self) -> None: + self.PYTHON_MIN_VERSION = '3.10' + self.module_to_install = ['sqlalchemy','psutil','requests'] + + if not self.checkPythonVersion(): + # Tester si c'est la bonne version de python + exit("Python Version Error") + else: + # Sinon tester les dependances python et les installer avec pip + self.checkDependencies() + + return None + + def checkPythonVersion(self) -> bool: + """Test si la version de python est autorisée ou non + + Returns: + bool: True si la version de python est autorisé sinon False + """ + python_required_version = self.PYTHON_MIN_VERSION.split('.') + python_current_version = python_version().split('.') + + if int(python_current_version[0]) < int(python_required_version[0]): + print(f"## Your python version must be greather than or equal to {self.PYTHON_MIN_VERSION} ##") + return False + elif int(python_current_version[1]) < int(python_required_version[1]): + print(f"### Your python version must be greather than or equal to {self.PYTHON_MIN_VERSION} ###") + return False + + print(f"===> Version of python : {python_version()} ==> OK") + + return True + + def checkDependencies(self) -> None: + """Check python dependencies + """ + do_install = False + + for module in self.module_to_install: + if find_spec(module) is None: + do_install = True + + if not do_install: + return None + + print("===> Verifier si pip est a jour") + run(['python', '-m', 'pip', 'install', '--upgrade', 'pip']) + + if find_spec('greenlet') is None: + check_call(['pip','install', '--only-binary', ':all:', 'greenlet']) + print('====> Module Greenlet installé') + + for module in self.module_to_install: + if find_spec(module) is None: + print("### Trying to install missing python packages ###") + check_call(['pip','install', module]) + print(f"====> Module {module} installé") + else: + print(f"==> {module} already installed") diff --git a/core/irc.py b/core/irc.py new file mode 100644 index 0000000..9bb11aa --- /dev/null +++ b/core/irc.py @@ -0,0 +1,1145 @@ +import ssl, re, importlib, sys, time, threading, socket +from datetime import datetime, timedelta + +from core.configuration import Config +from core.base import Base + +class Irc: + + def __init__(self) -> 'Irc': + + self.defender_connexion_datetime = datetime.now() # Date et heure de la premiere connexion de Defender + self.db_uid = {} # Definir la variable qui contiendra la liste des utilisateurs connectés au réseau + self.db_admin = {} # Definir la variable qui contiendra la liste des administrateurs + self.db_chan = [] # Definir la variable qui contiendra la liste des salons + self.loaded_classes:dict[str, 'Irc'] = {} # Definir la variable qui contiendra la liste modules chargés + self.beat = 30 # Lancer toutes les 30 secondes des actions de nettoyages + self.hb_active = True # Heartbeat active + self.HSID = '' # ID du serveur qui accueil le service ( Host Serveur Id ) + + self.INIT = 1 # Variable d'intialisation | 1 -> indique si le programme est en cours d'initialisation + self.RESTART = 0 # Variable pour le redemarrage du bot | 0 -> indique que le programme n'es pas en cours de redemarrage + self.CHARSET = ['utf-8', 'iso-8859-1'] # Charset utiliser pour décoder/encoder les messages + + self.Config = Config() + + # Liste des commandes internes du bot + self.commands_level = { + 0: ['help', 'auth', 'copyright'], + 1: ['load','reload','unload', 'deauth', 'uptime'], + 2: ['show_sessions','show_modules', 'show_timers', 'show_threads'], + 3: ['quit', 'restart','addaccess','editaccess', 'delaccess'] + } + + # l'ensemble des commandes. + self.commands = [] + for level, commands in self.commands_level.items(): + for command in self.commands_level[level]: + self.commands.append(command) + + self.Base = Base(self.Config) + self.Base.create_thread(self.heartbeat, (self.beat, )) + + ############################################## + # CONNEXION IRC # + ############################################## + def init_irc(self, ircInstance:'Irc') -> None: + try: + self.__create_socket() + self.__connect_to_irc(ircInstance) + except AssertionError as ae: + self.debug(f'Assertion error : {ae}') + + def __create_socket(self) -> None: + + self.IrcSocket: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + connexion_information = (self.Config.SERVEUR_IP, self.Config.SERVEUR_PORT) + self.IrcSocket.connect(connexion_information) + + # Créer un object ssl + ssl_context = self.__ssl_context() + ssl_connexion = ssl_context.wrap_socket(self.IrcSocket, server_hostname=self.Config.SERVEUR_HOSTNAME) + self.IrcSocket = ssl_connexion + + return None + + def __ssl_context(self) -> ssl.SSLContext: + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + + return ctx + + def __connect_to_irc(self, ircInstance: 'Irc') -> None: + try: + self.ircObject = ircInstance # créer une copie de l'instance Irc + self.__link(self.IrcSocket) # établir la connexion au serveur IRC + self.signal = True # Une variable pour initier la boucle infinie + self.load_existing_modules() # Charger les modules existant dans la base de données + + while self.signal: + if self.RESTART == 1: + self.IrcSocket.shutdown(socket.SHUT_RDWR) + self.IrcSocket.close() + + while self.IrcSocket.fileno() != -1: + time.sleep(0.5) + self.debug("--> En attente de la fermeture du socket ...") + + self.__create_socket() + self.__link(self.IrcSocket) + self.load_existing_modules() + self.RESTART = 0 + # 4072 max what the socket can grab + buffer_size = self.IrcSocket.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF) + # data = self.IrcSocket.recv(buffer_size).splitlines(True) + + data_in_bytes = self.IrcSocket.recv(buffer_size) + count_bytes = len(data_in_bytes) + + while count_bytes > 4070: + # If the received message is > 4070 then loop and add the value to the variable + new_data = self.IrcSocket.recv(buffer_size) + data_in_bytes += new_data + count_bytes = len(new_data) + # print("========================================================") + + data = data_in_bytes.splitlines() + + # print(f"{str(buffer_size)} - {str(len(data_in_bytes))}") + + if not data: + break + + self.send_response(data) + + self.IrcSocket.shutdown(socket.SHUT_RDWR) + self.IrcSocket.close() + + except AssertionError as ae: + self.debug(f'Assertion error : {ae}') + except ValueError as ve: + self.debug(f'Value Error : {ve}') + except OSError as oe: + self.debug(f"OS Error : {oe}") + + def __link(self, writer:socket.socket) -> None: + """Créer le link et envoyer les informations nécessaires pour la + connexion au serveur. + + Args: + writer (StreamWriter): permet l'envoi des informations au serveur. + """ + + nickname = self.Config.SERVICE_NICKNAME + username = self.Config.SERVICE_USERNAME + realname = self.Config.SERVICE_REALNAME + chan = self.Config.SERVICE_CHANLOG + info = self.Config.SERVICE_INFO + smodes = self.Config.SERVICE_SMODES + cmodes = self.Config.SERVICE_CMODES + umodes = self.Config.SERVICE_UMODES + host = self.Config.SERVICE_HOST + service_name = self.Config.SERVICE_NAME + + password = self.Config.SERVEUR_PASSWORD + link = self.Config.SERVEUR_LINK + sid = self.Config.SERVEUR_ID + service_id = self.Config.SERVICE_ID + + version = self.Config.DEFENDER_VERSION + unixtime = self.Base.get_unixtime() + + # Envoyer un message d'identification + # strtobytes = bytes(":" + sid + " PASS :" + password + "\r\n", 'utf-8') + # self.IrcSocket.send(strtobytes) + writer.send(f":{sid} PASS :{password}\r\n".encode('utf-8')) + writer.send(f":{sid} PROTOCTL NICKv2 VHP UMODE2 NICKIP SJOIN SJOIN2 SJ3 NOQUIT TKLEXT MLOCK SID MTAGS\r\n".encode('utf-8')) + writer.send(f":{sid} PROTOCTL EAUTH={link},,,{service_name}-v{version}\r\n".encode('utf-8')) + writer.send(f":{sid} PROTOCTL SID={sid}\r\n".encode('utf-8')) + writer.send(f":{sid} SERVER {link} 1 :{info}\r\n".encode('utf-8')) + writer.send(f":{sid} {nickname} :Reserved for services\r\n".encode('utf-8')) + writer.send(f":{sid} UID {nickname} 1 {unixtime} {username} {host} {service_id} * {smodes} * * * :{realname}\r\n".encode('utf-8')) + writer.send(f":{sid} SJOIN {unixtime} {chan} + :{service_id}\r\n".encode('utf-8')) + writer.send(f":{sid} MODE {chan} +{cmodes}\r\n".encode('utf-8')) + writer.send(f":{service_id} SAMODE {chan} +{umodes} {nickname}\r\n".encode('utf-8')) + + # writer.write(f"USER {nickname} {username} {username} {nickname} {username} :{username}\r\n".encode('utf-8')) + # writer.write(f"USER {username} {username} {username} :{username}\r\n".encode('utf-8')) + # writer.write(f"NICK {nickname}\r\n".encode('utf-8')) + + def send2socket(self, send_message:str)->None: + """Envoit les commandes à envoyer au serveur. + + Args: + string (Str): contient la commande à envoyer au serveur. + """ + try: + self.IrcSocket.send(f"{send_message}\r\n".encode(self.CHARSET[0])) + + except UnicodeDecodeError: + self.debug('Write Decode impossible try iso-8859-1') + self.IrcSocket.send(f"{send_message}\r\n".encode(self.CHARSET[0],'replace')) + except UnicodeEncodeError: + self.debug('Write Encode impossible ... try iso-8859-1') + self.IrcSocket.send(f"{send_message}\r\n".encode(self.CHARSET[0],'replace')) + except AssertionError as ae: + self.debug(f"Assertion error : {ae}") + except OSError as oe: + self.debug(f"OS Error : {oe}") + + def send_response(self, responses:list[bytes]) -> None: + try: + # print(data) + for data in responses: + response = data.decode(self.CHARSET[0]).split() + self.cmd(response) + except UnicodeEncodeError: + for data in responses: + response = data.decode(self.CHARSET[1],'replace').split() + self.cmd(response) + except UnicodeDecodeError: + for data in responses: + response = data.decode(self.CHARSET[1],'replace').split() + self.cmd(response) + except AssertionError as ae: + self.debug(f"Assertion error : {ae}") + ############################################## + # FIN CONNEXION IRC # + ############################################## + def load_existing_modules(self) -> None: + """Charge les modules qui existe déja dans la base de données + + Returns: + None: Aucun retour requis, elle charge puis c'est tout + """ + result = self.Base.db_execute_query(f"SELECT module FROM {self.Base.DB_SCHEMA['modules']}") + for r in result.fetchall(): + self.load_module('sys', r[0], True) + + return None + + def get_defender_uptime(self)->str: + """Savoir depuis quand Defender est connecté + + Returns: + str: L'écart entre la date du jour et celle de la connexion de Defender + """ + current_datetime = datetime.now() + diff_date = current_datetime - self.defender_connexion_datetime + uptime = timedelta(days=diff_date.days, seconds=diff_date.seconds) + + return uptime + + def heartbeat(self, beat:float) -> None: + """Execute certaines commandes de nettoyage toutes les x secondes + x étant définit a l'initialisation de cette class (self.beat) + + Args: + beat (float): Nombre de secondes entre chaque exécution + """ + while self.hb_active: + time.sleep(beat) + service_id = self.Config.SERVICE_ID + hsid = self.HSID + # self.send2socket(f':{service_id} PING :{hsid}') + self.Base.execute_periodic_action() + + def create_ping_timer(self, time_to_wait:float, class_name:str, method_name: str, method_args: list=[]) -> None: + # 1. Timer créer + # 1.1 Créer la fonction a executer + # 1.2 Envoyer le ping une fois le timer terminer + # 2. Executer la fonction + try: + if not class_name in self.loaded_classes: + self.debug(f"La class [{class_name} n'existe pas !!]") + return False + + class_instance = self.loaded_classes[class_name] + + t = threading.Timer(interval=time_to_wait, function=self.__create_tasks, args=(class_instance, method_name, method_args)) + t.start() + + self.Base.running_timers.append(t) + + self.debug(f"Timer ID : {str(t.ident)} | Running Threads : {len(threading.enumerate())}") + + except AssertionError as ae: + self.debug(f'Assertion Error -> {ae}') + except TypeError as te: + self.debug(f"Type error -> {te}") + + def __create_tasks(self, obj: object, method_name: str, param:list) -> None: + """#### Ajouter les méthodes a éxecuter dans un dictionnaire + + Args: + obj (object): Une instance de la classe qui va etre executer + method_name (str): Le nom de la méthode a executer + param (list): les parametres a faire passer + + Returns: + None: aucun retour attendu + """ + self.Base.periodic_func[len(self.Base.periodic_func) + 1] = { + 'object': obj, + 'method_name': method_name, + 'param': param + } + + self.debug(f'Function to execute : {str(self.Base.periodic_func)}') + self.send_ping_to_sereur() + return None + + def send_ping_to_sereur(self) -> None: + """### Envoyer un PING au serveur + """ + service_id = self.Config.SERVICE_ID + hsid = self.HSID + self.send2socket(f':{service_id} PING :{hsid}') + + return None + + def load_module(self, fromuser:str, module_name:str, init:bool = False) -> bool: + + try: + # module_name : mod_voice + module_name = module_name.lower() + class_name = module_name.split('_')[1].capitalize() # ==> Voice + + # print(self.loaded_classes) + + # Si le module est déja chargé + if 'mods.' + module_name in sys.modules: + self.debug("Module déja chargé ...") + self.debug('module name = ' + module_name) + if class_name in self.loaded_classes: + # Si le module existe dans la variable globale retourne False + self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :Le module {module_name} est déja chargé ! si vous souhaiter le recharge tapez {self.Config.SERVICE_PREFIX}reload {module_name}") + return False + + the_module = sys.modules['mods.' + module_name] + importlib.reload(the_module) + my_class = getattr(the_module, class_name, None) + new_instance = my_class(self.ircObject) + self.loaded_classes[class_name] = new_instance + + # Créer le module dans la base de données + if not init: + self.Base.db_record_module(fromuser, module_name) + + self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :Module {module_name} chargé") + return False + + # Charger le module + loaded_module = importlib.import_module(f"mods.{module_name}") + + my_class = getattr(loaded_module, class_name, None) # Récuperer le nom de classe + create_instance_of_the_class = my_class(self.ircObject) # Créer une nouvelle instance de la classe + self.loaded_classes[class_name] = create_instance_of_the_class # Charger la nouvelle class dans la variable globale + + # Enregistrer le module dans la base de données + if not init: + self.Base.db_record_module(fromuser, module_name) + self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :Module {module_name} chargé") + + self.debug(self.loaded_classes) + return True + + except ModuleNotFoundError as moduleNotFound: + self.debug(f"MODULE_NOT_FOUND: {moduleNotFound}") + self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :[ {self.Config.CONFIG_COLOR['rouge']}MODULE_NOT_FOUND{self.Config.CONFIG_COLOR['noire']} ]: {moduleNotFound}") + + def insert_db_uid(self, uid:str, nickname:str, username:str, hostname:str, umodes:str, vhost:str, isWebirc: bool) -> None: + + if uid in self.db_uid: + return None + + self.db_uid[uid] = { + 'nickname': nickname, + 'username': username, + 'hostname': hostname, + 'umodes': umodes, + 'vhost': vhost, + 'isWebirc': isWebirc, + 'datetime': datetime.now() + } + + self.db_uid[nickname] = { + 'uid': uid, + 'username': username, + 'hostname': hostname, + 'umodes': umodes, + 'vhost': vhost, + 'isWebirc': isWebirc, + 'datetime': datetime.now() + } + + return None + + def update_db_uid(self, uid:str, newnickname:str) -> None: + + # Récupérer l'ancien nickname + oldnickname = self.db_uid[uid]['nickname'] + + # Enregistrement du nouveau nickname + self.db_uid[newnickname] = { + 'uid': uid, + 'username': self.db_uid[uid]['username'], + 'hostname': self.db_uid[uid]['hostname'], + 'umodes': self.db_uid[uid]['umodes'], + 'vhost': self.db_uid[uid]['vhost'] + } + + # Modification du nickname dans la ligne UID + self.db_uid[uid]['nickname'] = newnickname + + # Supprimer l'ancien nickname + if oldnickname in self.db_uid: + del self.db_uid[oldnickname] + else: + self.debug(f"L'ancien nickname {oldnickname} n'existe pas dans UID_DB") + response = False + + self.debug(f"{oldnickname} changed to {newnickname}") + + return None + + def delete_db_uid(self, uid:str) -> None: + + uid_reel = self.get_uid(uid) + nickname = self.get_nickname(uid_reel) + + if uid_reel in self.db_uid: + del self.db_uid[uid] + + if nickname in self.db_uid: + del self.db_uid[nickname] + + return None + + def insert_db_admin(self, uid:str, level:int) -> None: + + if not uid in self.db_uid: + return None + + nickname = self.db_uid[uid]['nickname'] + username = self.db_uid[uid]['username'] + hostname = self.db_uid[uid]['hostname'] + umodes = self.db_uid[uid]['umodes'] + vhost = self.db_uid[uid]['vhost'] + level = int(level) + + + + self.db_admin[uid] = { + 'nickname': nickname, + 'username': username, + 'hostname': hostname, + 'umodes': umodes, + 'vhost': vhost, + 'datetime': self.Base.get_datetime(), + 'level': level + } + + self.db_admin[nickname] = { + 'uid': uid, + 'username': username, + 'hostname': hostname, + 'umodes': umodes, + 'vhost': vhost, + 'datetime': self.Base.get_datetime(), + 'level': level + } + + return None + + def delete_db_admin(self, uid:str) -> None: + + if not uid in self.db_admin: + return None + + nickname_admin = self.db_admin[uid]['nickname'] + + if uid in self.db_admin: + del self.db_admin[uid] + + if nickname_admin in self.db_admin: + del self.db_admin[nickname_admin] + + return None + + def insert_db_chan(self, channel:str) -> bool: + """Ajouter l'ensemble des salons dans la variable {CHAN_DB} + + Args: + channel (str): le salon à insérer dans {CHAN_DB} + + Returns: + bool: True si insertion OK / False si insertion KO + """ + if channel in self.db_chan: + return False + + response = True + # Ajouter un nouveau salon + self.db_chan.append(channel) + + # Supprimer les doublons de la liste + self.db_chan = list(set(self.db_chan)) + + self.debug(f"Le salon {channel} a été ajouté à la liste CHAN_DB") + + return response + + def create_defender_user(self, nickname:str, level: int, password:str) -> str: + + nickname = self.get_nickname(nickname) + response = '' + + if level > 4: + response = "Impossible d'ajouter un niveau > 4" + self.debug(response) + return response + + # Verification si le user existe dans notre UID_DB + if not nickname in self.db_uid: + response = f"{nickname} n'est pas connecté, impossible de l'enregistrer pour le moment" + self.debug(response) + return response + + hostname = self.db_uid[nickname]['hostname'] + vhost = self.db_uid[nickname]['vhost'] + spassword = self.Base.crypt_password(password) + + mes_donnees = {'admin': nickname} + query_search_user = f"SELECT id FROM {self.Base.DB_SCHEMA['admins']} WHERE user=:admin" + r = self.Base.db_execute_query(query_search_user, mes_donnees) + exist_user = r.fetchone() + + # On verifie si le user exist dans la base + if not exist_user: + mes_donnees = {'datetime': self.Base.get_datetime(), 'user': nickname, 'password': spassword, 'hostname': hostname, 'vhost': vhost, 'level': level} + self.Base.db_execute_query(f'''INSERT INTO {self.Base.DB_SCHEMA['admins']} + (createdOn, user, password, hostname, vhost, level) VALUES + (:datetime, :user, :password, :hostname, :vhost, :level) + ''', mes_donnees) + response = f"{nickname} ajouté en tant qu'administrateur de niveau {level}" + self.send2socket(f':{self.Config.SERVICE_NICKNAME} NOTICE {nickname} : {response}') + self.debug(response) + return response + else: + response = f'{nickname} Existe déjà dans les users enregistrés' + self.send2socket(f':{self.Config.SERVICE_NICKNAME} NOTICE {nickname} : {response}') + self.debug(response) + return response + + def get_uid(self, uidornickname:str) -> str | None: + + uid_recherche = uidornickname + response = None + for uid, value in self.db_uid.items(): + if uid == uid_recherche: + if 'nickname' in value: + response = uid + if 'uid' in value: + response = value['uid'] + + return response + + def get_nickname(self, uidornickname:str) -> str | None: + + nickname_recherche = uidornickname + + response = None + for nickname, value in self.db_uid.items(): + if nickname == nickname_recherche: + if 'nickname' in value: + response = value['nickname'] + if 'uid' in value: + response = nickname + + return response + + def is_cmd_allowed(self,nickname:str, cmd:str) -> bool: + + # Vérifier si le user est identifié et si il a les droits + is_command_allowed = False + uid = self.get_uid(nickname) + + if uid in self.db_admin: + admin_level = self.db_admin[uid]['level'] + + for ref_level, ref_commands in self.commands_level.items(): + # print(f"LevelNo: {ref_level} - {ref_commands} - {admin_level}") + if ref_level <= int(admin_level): + # print(f"LevelNo: {ref_level} - {ref_commands}") + if cmd in ref_commands: + is_command_allowed = True + else: + for ref_level, ref_commands in self.commands_level.items(): + if ref_level == 0: + # print(f"LevelNo: {ref_level} - {ref_commands}") + if cmd in ref_commands: + is_command_allowed = True + + return is_command_allowed + + def debug(self, debug_msg:str) -> None: + + if self.Config.DEBUG == 1: + if type(debug_msg) == list: + if debug_msg[0] != 'PING': + print(f"[{self.Base.get_datetime()}] - {debug_msg}") + else: + print(f"[{self.Base.get_datetime()}] - {debug_msg}") + + return None + + def logs(self, log_msg:str) -> None: + + mes_donnees = {'datetime': self.Base.get_datetime(), 'server_msg': log_msg} + self.Base.db_execute_query('INSERT INTO sys_logs (datetime, server_msg) VALUES (:datetime, :server_msg)', mes_donnees) + + return None + + def cmd(self, data:list) -> None: + try: + cmd = data + + if len(cmd) == 0 or len(cmd) == 1: + return False + + self.debug(cmd) + + match cmd[0]: + + case 'PING': + # Sending PONG response to the serveur + pong = str(cmd[1]).replace(':','') + self.send2socket(f"PONG :{pong}") + return None + + case 'PROTOCTL': + #['PROTOCTL', 'CHANMODES=beI,fkL,lFH,cdimnprstzCDGKMNOPQRSTVZ', 'USERMODES=diopqrstwxzBDGHIRSTWZ', 'BOOTED=1702138935', + # 'PREFIX=(qaohv)~&@%+', 'SID=001', 'MLOCK', 'TS=1703793941', 'EXTSWHOIS'] + + # GET SERVER ID HOST + if len(cmd) > 5: + if '=' in cmd[5]: + serveur_hosting_id = str(cmd[5]).split('=') + self.HSID = serveur_hosting_id[1] + return False + + case _: + pass + + if len(cmd) < 2: + return False + + match cmd[1]: + + case 'SLOG': + # self.Base.scan_ports(cmd[7]) + # if self.Config.ABUSEIPDB == 1: + # self.Base.create_thread(self.abuseipdb_scan, (cmd[7], )) + pass + + case 'REPUTATION': + # :001 REPUTATION 91.168.141.239 118 + try: + # if self.Config.ABUSEIPDB == 1: + # self.Base.create_thread(self.abuseipdb_scan, (cmd[2], )) + pass + # Possibilité de déclancher les bans a ce niveau. + except IndexError: + self.debug(f'cmd reputation: index error') + + case '320': + #:irc.deb.biz.st 320 PyDefender IRCParis07 :is in security-groups: known-users,webirc-users,tls-and-known-users,tls-users + pass + + case '318': + #:irc.deb.biz.st 318 PyDefender IRCParis93 :End of /WHOIS list. + pass + + case 'MD': + # [':001', 'MD', 'client', '001CG0TG7', 'webirc', ':2'] + pass + + case 'EOS': + + hsid = str(cmd[0]).replace(':','') + if hsid == self.HSID: + if self.INIT == 1: + self.send2socket(f"MODE {self.Config.SERVICE_NICKNAME} +B") + self.send2socket(f"JOIN {self.Config.SERVICE_CHANLOG}") + print(f"################### DEFENDER ###################") + print(f"# SERVICE CONNECTE ") + print(f"# SERVEUR : {self.Config.SERVEUR_IP} ") + print(f"# PORT : {self.Config.SERVEUR_PORT} ") + print(f"# NICKNAME : {self.Config.SERVICE_NICKNAME} ") + print(f"# CHANNEL : {self.Config.SERVICE_CHANLOG} ") + print(f"# VERSION : {self.Config.DEFENDER_VERSION} ") + print(f"################################################") + + # Initialisation terminé aprés le premier PING + self.INIT = 0 + # self.send2socket(f':{self.Config.SERVICE_ID} PING :{hsid}') + # print(self.db_uid) + + case _: + pass + + if len(cmd) < 3: + return False + + match cmd[2]: + + case 'QUIT': + # :001N1WD7L QUIT :Quit: free_znc_1 + cmd.pop(0) + uid_who_quit = str(cmd[0]).replace(':', '') + self.delete_db_uid(uid_who_quit) + + case 'PONG': + # ['@msgid=aTNJhp17kcPboF5diQqkUL;time=2023-12-28T20:35:58.411Z', ':irc.deb.biz.st', 'PONG', 'irc.deb.biz.st', ':Dev-PyDefender'] + self.Base.execute_periodic_action() + + case 'NICK': + # ['@unrealircd.org/geoip=FR;unrealircd.org/', ':001OOU2H3', 'NICK', 'WebIrc', '1703795844'] + # Changement de nickname + + # Supprimer la premiere valeur de la liste + cmd.pop(0) + uid = str(cmd[0]).replace(':','') + newnickname = cmd[2] + + self.update_db_uid(uid, newnickname) + + case 'SJOIN': + # ['@msgid=ictnEBhHmTUHzkEeVZl6rR;time=2023-12-28T20:03:18.482Z', ':001', 'SJOIN', '1702139101', '#stats', '+nst', ':@001SB890A', '@00BAAAAAI'] + cmd.pop(0) + channel = cmd[3] + self.insert_db_chan(channel) + + case 'UID': + + if 'webirc' in cmd[0]: + isWebirc = True + else: + isWebirc = False + + uid = str(cmd[8]) + nickname = str(cmd[3]) + username = str(cmd[6]) + hostname = str(cmd[7]) + umodes = str(cmd[10]) + vhost = str(cmd[11]) + + self.insert_db_uid(uid, nickname, username, hostname, umodes, vhost, isWebirc) + + case 'PRIVMSG': + try: + # Supprimer la premiere valeur + cmd.pop(0) + + get_uid_or_nickname = str(cmd[0].replace(':','')) + # user_trigger = get_user.split('!')[0] + user_trigger = self.get_nickname(get_uid_or_nickname) + dnickname = self.Config.SERVICE_NICKNAME + + pattern = fr'(:\{self.Config.SERVICE_PREFIX})(.*)$' + hcmds = re.search(pattern, ' '.join(cmd)) # va matcher avec tout les caractéres aprés le . + + if hcmds: # Commande qui commencent par le point + liste_des_commandes = list(hcmds.groups()) + convert_to_string = ' '.join(liste_des_commandes) + arg = convert_to_string.split() + arg.remove(f':{self.Config.SERVICE_PREFIX}') + if not arg[0].lower() in self.commands: + self.debug(f"This command {arg[0]} is not available") + return False + + cmd_to_send = convert_to_string.replace(':','') + self.Base.log_cmd(user_trigger, cmd_to_send) + + self._hcmds(user_trigger, arg) + + if cmd[2] == self.Config.SERVICE_ID: + pattern = fr'^:.*?:(.*)$' + + hcmds = re.search(pattern, ' '.join(cmd)) + + if hcmds: # par /msg defender [commande] + liste_des_commandes = list(hcmds.groups()) + convert_to_string = ' '.join(liste_des_commandes) + arg = convert_to_string.split() + + # Réponse a un CTCP VERSION + if arg[0] == '\x01VERSION\x01': + self.send2socket(f':{dnickname} NOTICE {user_trigger} :\x01VERSION Service {self.Config.SERVICE_NICKNAME} V{self.Config.DEFENDER_VERSION}\x01') + return False + + # Réponse a un TIME + if arg[0] == '\x01TIME\x01': + current_datetime = self.Base.get_datetime() + self.send2socket(f':{dnickname} NOTICE {user_trigger} :\x01TIME {current_datetime}\x01') + return False + + # Réponse a un PING + if arg[0] == '\x01PING': + recieved_unixtime = int(arg[1].replace('\x01','')) + current_unixtime = self.Base.get_unixtime() + ping_response = current_unixtime - recieved_unixtime + self.send2socket(f':{dnickname} NOTICE {user_trigger} :\x01PING {str(ping_response)} secs\x01') + return False + + if not arg[0].lower() in self.commands: + self.debug(f"This command {arg[0]} is not available") + return False + + cmd_to_send = convert_to_string.replace(':','') + self.Base.log_cmd(self.get_nickname(user_trigger), cmd_to_send) + + self._hcmds(user_trigger, arg) + + except IndexError: + self.debug(f'cmd --> PRIVMSG --> List index out of range') + + case _: + pass + + # Envoyer la commande aux classes dynamiquement chargées + for classe_name, classe_object in self.loaded_classes.items(): + classe_object.cmd(cmd) + + except IndexError as ie: + self.debug(f"IRC CMD -> IndexError : {ie} - {cmd} - length {str(len(cmd))}") + + def _hcmds(self, user: str, cmd:list) -> None: + + fromuser = self.get_nickname(user) # Nickname qui a lancé la commande + uid = self.get_uid(fromuser) # Récuperer le uid de l'utilisateur + + # Defender information + dnickname = self.Config.SERVICE_NICKNAME # Defender nickname + dchanlog = self.Config.SERVICE_CHANLOG # Defender chan log + + if len(cmd) > 0: + command = str(cmd[0]).lower() + else: + return False + + is_command_allowed = self.is_cmd_allowed(fromuser, command) + if not is_command_allowed: + command = 'notallowed' + + # Envoyer la commande aux classes dynamiquement chargées + if command != 'notallowed': + for classe_name, classe_object in self.loaded_classes.items(): + classe_object._hcmds(user, cmd) + + match command: + + case 'notallowed': + try: + current_command = cmd[0] + self.send2socket(f':{dnickname} PRIVMSG {dchanlog} :[ {self.Config.CONFIG_COLOR["rouge"]}{current_command}{self.Config.CONFIG_COLOR["noire"]} ] - Accès Refusé à {self.get_nickname(fromuser)}') + self.send2socket(f':{dnickname} NOTICE {fromuser} : Accès Refusé') + except IndexError: + self.debug(f'_hcmd notallowed : Index Error') + + case 'deauth': + + current_command = cmd[0] + uid_to_deauth = self.get_uid(fromuser) + self.delete_db_admin(uid_to_deauth) + self.send2socket(f":{dnickname} PRIVMSG {dchanlog} :[ {self.Config.CONFIG_COLOR['rouge']}{current_command}{self.Config.CONFIG_COLOR['noire']} ] - {self.get_nickname(fromuser)} est désormais déconnecter de {dnickname}") + + case 'auth': + # ['auth', 'adator', 'password'] + current_command = cmd[0] + user_to_log = self.get_nickname(cmd[1]) + password = cmd[2] + + if not user_to_log is None: + mes_donnees = {'user': user_to_log, 'password': self.Base.crypt_password(password)} + query = f"SELECT id, level FROM {self.Base.DB_SCHEMA['admins']} WHERE user = :user AND password = :password" + result = self.Base.db_execute_query(query, mes_donnees) + user_from_db = result.fetchone() + + if not user_from_db is None: + uid_user = self.get_uid(user_to_log) + self.insert_db_admin(uid_user, user_from_db[1]) + self.send2socket(f":{dnickname} PRIVMSG {dchanlog} :[ {self.Config.CONFIG_COLOR['verte']}{current_command}{self.Config.CONFIG_COLOR['noire']} ] - {self.get_nickname(fromuser)} est désormais connecté a {dnickname}") + else: + self.send2socket(f":{dnickname} PRIVMSG {dchanlog} :[ {self.Config.CONFIG_COLOR['rouge']}{current_command}{self.Config.CONFIG_COLOR['noire']} ] - {self.get_nickname(fromuser)} a tapé un mauvais mot de pass") + self.send2socket(f":{self.Config.SERVICE_NICKNAME} NOTICE {fromuser} :Mot de passe incorrecte") + + else: + self.send2socket(f":{self.Config.SERVICE_NICKNAME} NOTICE {fromuser} :L'utilisateur {user_to_log} n'existe pas") + + case 'addaccess': + try: + # .addaccess adator 5 password + newnickname = cmd[1] + newlevel = self.Base.int_if_possible(cmd[2]) + password = cmd[3] + + response = self.create_defender_user(newnickname, newlevel, password) + self.send2socket(f':{dnickname} NOTICE {fromuser} : {response}') + self.debug(response) + + except IndexError as ie: + self.debug(f'_hcmd addaccess: {ie}') + self.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} addaccess [nickname] [level] [password]') + except TypeError as te: + self.debug(f'_hcmd addaccess: out of index : {te}') + self.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} addaccess [nickname] [level] [password]') + + case 'editaccess': + # .editaccess [USER] [PASSWORD] [LEVEL] + try: + user_to_edit = cmd[1] + user_new_level = int(cmd[3]) + user_password = self.Base.crypt_password(cmd[2]) + + if len(cmd) < 4 or len(cmd) > 4: + self.send2socket(f':{dnickname} NOTICE {fromuser} : .editaccess [USER] [NEWPASSWORD] [NEWLEVEL]') + return None + + current_user = self.get_nickname(fromuser) + current_uid = self.get_uid(fromuser) + current_user_level = self.db_admin[current_uid]['level'] + + if user_new_level > 5: + self.send2socket(f':{dnickname} NOTICE {fromuser} : Maximum authorized level is 5') + return None + + # Rechercher le user dans la base de données. + mes_donnees = {'user': user_to_edit} + query = f"SELECT user, level FROM {self.Base.DB_SCHEMA['admins']} WHERE user = :user" + result = self.Base.db_execute_query(query, mes_donnees) + + isUserExist = result.fetchone() + if not isUserExist is None: + + if current_user_level < int(isUserExist[1]): + self.send2socket(f':{dnickname} NOTICE {fromuser} : You are not allowed to edit this access') + return None + + if current_user_level == int(isUserExist[1]) and current_user != user_to_edit: + self.send2socket(f":{dnickname} NOTICE {fromuser} : You can't edit access of a user with same level") + return None + + # Le user existe dans la base de données + data_to_update = {'user': user_to_edit, 'password': user_password, 'level': user_new_level} + sql_update = f"UPDATE {self.Base.DB_SCHEMA['admins']} SET level = :level, password = :password WHERE user = :user" + exec_query = self.Base.db_execute_query(sql_update, data_to_update) + if exec_query.rowcount > 0: + self.send2socket(f':{dnickname} NOTICE {fromuser} : User {user_to_edit} has been modified with level {str(user_new_level)}') + else: + self.send2socket(f":{dnickname} NOTICE {fromuser} : Impossible de modifier l'utilisateur {str(user_new_level)}") + + except TypeError as te: + self.debug(f"Type error : {te}") + except ValueError as ve: + self.debug(f"Value Error : {ve}") + self.send2socket(f':{dnickname} NOTICE {fromuser} : .editaccess [USER] [NEWPASSWORD] [NEWLEVEL]') + + case 'delaccess': + # .delaccess [USER] [CONFIRMUSER] + user_to_del = cmd[1] + user_confirmation = cmd[2] + + if user_to_del != user_confirmation: + self.send2socket(f':{dnickname} NOTICE {fromuser} : Les user ne sont pas les mêmes, tu dois confirmer le user que tu veux supprimer') + return None + + print(len(cmd)) + if len(cmd) < 3: + self.send2socket(f':{dnickname} NOTICE {fromuser} : .delaccess [USER] [CONFIRMUSER]') + return None + + current_user = self.get_nickname(fromuser) + current_uid = self.get_uid(fromuser) + current_user_level = self.db_admin[current_uid]['level'] + + # Rechercher le user dans la base de données. + mes_donnees = {'user': user_to_del} + query = f"SELECT user, level FROM {self.Base.DB_SCHEMA['admins']} WHERE user = :user" + result = self.Base.db_execute_query(query, mes_donnees) + info_user = result.fetchone() + + if not info_user is None: + level_user_to_del = info_user[1] + if current_user_level <= level_user_to_del: + self.send2socket(f':{dnickname} NOTICE {fromuser} : You are not allowed to delete this access') + return None + + data_to_delete = {'user': user_to_del} + sql_delete = f"DELETE FROM {self.Base.DB_SCHEMA['admins']} WHERE user = :user" + exec_query = self.Base.db_execute_query(sql_delete, data_to_delete) + if exec_query.rowcount > 0: + self.send2socket(f':{dnickname} NOTICE {fromuser} : User {user_to_del} has been deleted !') + else: + self.send2socket(f":{dnickname} NOTICE {fromuser} : Impossible de supprimer l'utilisateur.") + + case 'help': + + help = '' + count_level_definition = 0 + if uid in self.db_admin: + user_level = self.db_admin[uid]['level'] + else: + user_level = 0 + + self.send2socket(f':{dnickname} NOTICE {fromuser} : **************** LIST DES COMMANDES *****************') + self.send2socket(f':{dnickname} NOTICE {fromuser} : ') + for levDef in self.commands_level: + + if int(user_level) >= int(count_level_definition): + + self.send2socket(f':{dnickname} NOTICE {fromuser} : **************** {self.Config.CONFIG_COLOR["noire"]}[ {self.Config.CONFIG_COLOR["verte"]}LEVEL {str(levDef)} {self.Config.CONFIG_COLOR["noire"]}] ****************') + count_commands = 0 + help = '' + for comm in self.commands_level[count_level_definition]: + + help += f"{comm.upper()}" + if int(count_commands) < len(self.commands_level[count_level_definition])-1: + help += ' | ' + count_commands += 1 + + self.send2socket(f':{dnickname} NOTICE {fromuser} : {help}') + + count_level_definition += 1 + + self.send2socket(f':{dnickname} NOTICE {fromuser} : **************** FIN DES COMMANDES *****************') + + case 'load': + + self.load_module(fromuser, str(cmd[1])) + + case 'unload': + # unload mod_dktmb + module_name = str(cmd[1]).lower() # Le nom du module. exemple: mod_defender + class_name = module_name.split('_')[1].capitalize() # Nom de la class. exemple: Defender + + if class_name in self.loaded_classes: + + for level, command in self.loaded_classes[class_name].commands_level.items(): + # Supprimer la commande de la variable commands + for c in self.loaded_classes[class_name].commands_level[level]: + self.commands.remove(c) + self.commands_level[level].remove(c) + + del self.loaded_classes[class_name] + + # Supprimer le module de la base de données + self.Base.db_delete_module(module_name) + + self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :Module {module_name} supprimé") + + case 'reload': + # reload mod_dktmb + module_name = str(cmd[1]).lower() # ==> mod_defender + class_name = module_name.split('_')[1].capitalize() # ==> Defender + + if 'mods.' + module_name in sys.modules: + self.debug('Module Already Loaded ... reload the module ...') + the_module = sys.modules['mods.' + module_name] + importlib.reload(the_module) + + # Supprimer la class déja instancier + if class_name in self.loaded_classes: + # Supprimer les commandes déclarer dans la classe + for level, command in self.loaded_classes[class_name].commands_level.items(): + # Supprimer la commande de la variable commands + for c in self.loaded_classes[class_name].commands_level[level]: + self.commands.remove(c) + self.commands_level[level].remove(c) + + del self.loaded_classes[class_name] + + my_class = getattr(the_module, class_name, None) + new_instance = my_class(self.ircObject) + self.loaded_classes[class_name] = new_instance + + self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :Module {module_name} rechargé") + return False + else: + self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :Module {module_name} n'est pas chargé !") + + case 'quit': + try: + reason = [] + for i in range(1, len(cmd)): + reason.append(cmd[i]) + final_reason = ' '.join(reason) + + self.hb_active = False + self.Base.shutdown() + self.Base.execute_periodic_action() + + self.send2socket(f':{dnickname} NOTICE {fromuser} : Arrêt du service {dnickname}') + self.send2socket(f':{self.Config.SERVEUR_LINK} SQUIT {self.Config.SERVEUR_LINK} :{final_reason}') + self.debug(f'Arrêt du server {dnickname}') + self.RESTART = 0 + self.signal = False + + except IndexError: + self.debug('_hcmd die: out of index') + + self.send2socket(f"QUIT Good bye") + + case 'restart': + reason = [] + for i in range(1, len(cmd)): + reason.append(cmd[i]) + final_reason = ' '.join(reason) + + self.db_uid.clear() #Vider UID_DB + self.db_chan = [] #Vider les salons + + self.send2socket(f':{dnickname} NOTICE {fromuser} : Redémarrage du service {dnickname}') + self.send2socket(f':{self.Config.SERVEUR_LINK} SQUIT {self.Config.SERVEUR_LINK} :{final_reason}') + self.debug(f'Redémarrage du server {dnickname}') + self.loaded_classes.clear() + self.RESTART = 1 # Set restart status to 1 saying that the service will restart + self.INIT = 1 # set init to 1 saying that the service will be re initiated + + case 'show_modules': + + self.debug(self.loaded_classes) + + results = self.Base.db_execute_query(f'SELECT module FROM {self.Base.DB_SCHEMA["modules"]}') + results = results.fetchall() + + if len(results) == 0: + self.send2socket(f":{dnickname} PRIVMSG {dchanlog} :Aucun module chargé") + return False + + for r in results: + self.send2socket(f":{dnickname} PRIVMSG {dchanlog} :Le module {r[0]} chargé") + self.debug(r[0]) + + case 'show_timers': + + if self.Base.running_timers: + self.send2socket(f":{dnickname} PRIVMSG {dchanlog} :{self.Base.running_timers}") + self.debug(self.Base.running_timers) + else: + self.send2socket(f":{dnickname} PRIVMSG {dchanlog} :Aucun timers en cours d'execution") + + case 'show_threads': + self.send2socket(f":{dnickname} PRIVMSG {dchanlog} :{self.Base.running_threads}") + + case 'uptime': + uptime = self.get_defender_uptime() + self.send2socket(f':{dnickname} NOTICE {fromuser} : {uptime}') + + case 'copyright': + self.send2socket(f':{dnickname} NOTICE {fromuser} : # Defender V.{self.Config.DEFENDER_VERSION} Developped by adator® and dktmb® #') + + case _: + pass diff --git a/main.py b/main.py new file mode 100644 index 0000000..6e5de0c --- /dev/null +++ b/main.py @@ -0,0 +1,26 @@ +from core import installation + +############################################# +# @Version : 1 # +# Requierements : # +# Python3.10 or higher # +# SQLAlchemy, requests, psutil # +# UnrealIRCD 6.2.2 or higher # +############################################# + +######################### +# LANCEMENT DE DEFENDER # +######################### + +try: + + installation.Install() + + from core.irc import Irc + ircInstance = Irc() + ircInstance.init_irc(ircInstance) + +except AssertionError as ae: + print(f'Assertion Error -> {ae}') +except KeyboardInterrupt as k: + ircInstance.Base.execute_periodic_action() \ No newline at end of file diff --git a/mods/__init__.py b/mods/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mods/mod_defender.py b/mods/mod_defender.py new file mode 100644 index 0000000..66b0620 --- /dev/null +++ b/mods/mod_defender.py @@ -0,0 +1,1245 @@ +from datetime import datetime +import re, socket, psutil, requests, json +from core.irc import Irc + +# Le module crée devra réspecter quelques conditions +# 1. Le nom de la classe devra toujours s'appeler comme le module. Exemple => nom de class Defender | nom du module mod_defender +# 2. la methode __init__ devra toujours avoir les parametres suivant (self, irc:object) +# 1 . Créer la variable Irc dans le module +# 2 . Récuperer la configuration dans une variable +# 3 . Définir et enregistrer les nouvelles commandes +# 4 . Créer vos tables, en utilisant toujours le nom des votre classe en minuscule ==> defender_votre-table +# 3. une methode _hcmds(self, user:str, cmd: list) devra toujours etre crée. + +class Defender(): + + def __init__(self, ircInstance:Irc) -> None: + + self.Irc = ircInstance # Ajouter l'object mod_irc a la classe ( Obligatoire ) + self.Config = ircInstance.Config # Ajouter la configuration a la classe ( Obligatoire ) + self.Base = ircInstance.Base # Ajouter l'objet Base au module ( Obligatoire ) + + self.Irc.debug(f'Module {self.__class__.__name__} loaded ...') + + # Créer les nouvelles commandes du module + self.commands_level = { + 0: ['code'], + 1: ['join','part', 'info'], + 2: ['q', 'dq', 'o', 'do', 'h', 'dh', 'v', 'dv', 'b', 'ub','k', 'kb'], + 3: ['reputation','proxy_scan', 'status', 'timer','show_reputation', 'show_users'] + } + self.__set_commands(self.commands_level) # Enrigstrer les nouvelles commandes dans le code + + self.__create_tables() # Créer les tables necessaire a votre module (ce n'es pas obligatoire) + + self.init_defender() # Créer une methode init ( ce n'es pas obligatoire ) + + def __set_commands(self, commands:dict) -> None: + """### Rajoute les commandes du module au programme principal + + Args: + commands (list): Liste des commandes du module + + Returns: + None: Aucun retour attendu + """ + for level, com in commands.items(): + for c in commands[level]: + self.Irc.commands_level[level].append(c) + self.Irc.commands.append(c) + + return None + + def __create_tables(self) -> None: + """Methode qui va créer la base de donnée si elle n'existe pas. + Une Session unique pour cette classe sera crée, qui sera utilisé dans cette classe / module + Args: + database_name (str): Nom de la base de données ( pas d'espace dans le nom ) + + Returns: + None: Aucun retour n'es attendu + """ + + table_channel = '''CREATE TABLE IF NOT EXISTS def_channels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + datetime TEXT, + channel TEXT + ) + ''' + + table_config = '''CREATE TABLE IF NOT EXISTS def_config ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + datetime TEXT, + parameter TEXT, + value TEXT + ) + ''' + + table_trusted = '''CREATE TABLE IF NOT EXISTS def_trusted ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + datetime TEXT, + user TEXT, + host TEXT, + vhost TEXT + ) + ''' + + self.Base.db_execute_query(table_channel) + self.Base.db_execute_query(table_config) + self.Base.db_execute_query(table_trusted) + return None + + def init_defender(self) -> bool: + + self.db_reputation = {} # Definir la variable qui contiendra la liste des user concerné par la réputation + self.flood_system = {} # Variable qui va contenir les users + self.reputation_first_connexion = {'ip': '', 'score': -1} # Contient les premieres informations de connexion + self.abuseipdb_key = '13c34603fee4d2941a2c443cc5c77fd750757ca2a2c1b304bd0f418aff80c24be12651d1a3cfe674' # Laisser vide si aucune clé + + # Rejoindre les salons + self.join_saved_channels() + + # Variable qui va contenir les options de configuration du module Defender + self.defConfig = { + 'reputation': 0, + 'reputation_timer': 0, + 'reputation_seuil': 600, + 'reputation_ban_all_chan': 0, + 'local_scan': 0, + 'psutil_scan': 0, + 'abuseipdb_scan': 0, + 'flood': 0, + 'flood_message': 5, + 'flood_time': 1, + 'flood_timer': 20 + } + + # Syncrhoniser la variable defConfig avec la configuration de la base de données. + self.sync_db_configuration() + + return True + + def sync_db_configuration(self) -> None: + + query = "SELECT parameter, value FROM def_config" + response = self.Base.db_execute_query(query) + + result = response.fetchall() + + # Si le resultat ne contient aucune valeur + if not result: + # Base de données vide Inserer la premiere configuration + for param, value in self.defConfig.items(): + mes_donnees = {'datetime': self.Base.get_datetime(), 'parameter': param, 'value': value} + insert = self.Base.db_execute_query('INSERT INTO def_config (datetime, parameter, value) VALUES (:datetime, :parameter, :value)', mes_donnees) + insert_rows = insert.rowcount + if insert_rows > 0: + self.Irc.debug(f'Row affected into def_config : {insert_rows}') + + # Inserer une nouvelle configuration + for param, value in self.defConfig.items(): + mes_donnees = {'parameter': param} + search_param_query = "SELECT parameter, value FROM def_config WHERE parameter = :parameter" + result = self.Base.db_execute_query(search_param_query, mes_donnees) + isParamExist = result.fetchone() + + if isParamExist is None: + mes_donnees = {'datetime': self.Base.get_datetime(), 'parameter': param, 'value': value} + insert = self.Base.db_execute_query('INSERT INTO def_config (datetime, parameter, value) VALUES (:datetime, :parameter, :value)', mes_donnees) + insert_rows = insert.rowcount + if insert_rows > 0: + self.Irc.debug(f'DB_Def_config - new param included : {insert_rows}') + + # Supprimer un parameter si il n'existe plus dans la variable global + query = "SELECT parameter FROM def_config" + response = self.Base.db_execute_query(query) + dbresult = response.fetchall() + + for dbparam in dbresult: + if not dbparam[0] in self.defConfig: + mes_donnees = {'parameter': dbparam[0]} + delete = self.Base.db_execute_query('DELETE FROM def_config WHERE parameter = :parameter', mes_donnees) + row_affected = delete.rowcount + if row_affected > 0: + self.Irc.debug(f'DB_Def_config - param [{dbparam[0]}] has been deleted') + + # Synchroniser la base de données avec la variable global + query = "SELECT parameter, value FROM def_config" + response = self.Base.db_execute_query(query) + result = response.fetchall() + + for param, value in result: + self.defConfig[param] = self.Base.int_if_possible(value) + + self.Irc.debug(self.defConfig) + return None + + def update_db_configuration(self, param:str, value:str) -> None: + + if not param in self.defConfig: + self.Irc.debug(f"Le parametre {param} n'existe pas dans la variable global") + return None + + mes_donnees = {'parameter': param} + search_param_query = "SELECT parameter, value FROM def_config WHERE parameter = :parameter" + result = self.Base.db_execute_query(search_param_query, mes_donnees) + isParamExist = result.fetchone() + + if not isParamExist is None: + mes_donnees = {'datetime': self.Base.get_datetime(), 'parameter': param, 'value': value} + update = self.Base.db_execute_query('UPDATE def_config SET datetime = :datetime, value = :value WHERE parameter = :parameter', mes_donnees) + updated_rows = update.rowcount + if updated_rows > 0: + self.defConfig[param] = self.Base.int_if_possible(value) + self.Irc.debug(f'DB_Def_config - new param updated : {param} {value}') + + self.Irc.debug(self.defConfig) + + def add_defender_channel(self, channel:str) -> bool: + """Cette fonction ajoute les salons de join de Defender + + Args: + channel (str): le salon à enregistrer. + """ + mes_donnees = {'channel': channel} + response = self.Base.db_execute_query("SELECT id FROM def_channels WHERE channel = :channel", mes_donnees) + + isChannelExist = response.fetchone() + + if isChannelExist is None: + mes_donnees = {'datetime': self.Base.get_datetime(), 'channel': channel} + insert = self.Base.db_execute_query(f"INSERT INTO def_channels (datetime, channel) VALUES (:datetime, :channel)", mes_donnees) + return True + else: + return False + + def delete_defender_channel(self, channel:str) -> bool: + """Cette fonction supprime les salons de join de Defender + + Args: + channel (str): le salon à enregistrer. + """ + mes_donnes = {'channel': channel} + response = self.Base.db_execute_query("DELETE FROM def_channels WHERE channel = :channel", mes_donnes) + + affected_row = response.rowcount + + if affected_row > 0: + return True + else: + return False + + def insert_db_reputation(self, uid:str, ip:str, nickname:str, username:str, hostname:str, umodes:str, vhost:str, score:int, isWebirc:bool) -> None: + + currentDateTime = self.Base.get_datetime() + secret_code = self.Base.get_random(8) + # Vérifier si le uid existe déja + if uid in self.db_reputation: + return None + + self.db_reputation[uid] = { + 'nickname': nickname, + 'username': username, + 'hostname': hostname, + 'umodes': umodes, + 'vhost': vhost, + 'ip': ip, + 'score': score, + 'isWebirc': isWebirc, + 'secret_code': secret_code, + 'connected_datetime': currentDateTime, + 'updated_datetime': currentDateTime + } + + return None + + def update_db_reputation(self, uidornickname:str, newnickname:str) -> None: + + uid = self.Irc.get_uid(uidornickname) + currentDateTime = self.Base.get_datetime() + secret_code = self.Base.get_random(8) + + if not uid in self.Irc.db_uid: + self.Irc.debug(f'Etrange UID {uid}') + return None + + if uid in self.db_reputation: + self.db_reputation[uid]['nickname'] = newnickname + self.db_reputation[uid]['updated_datetime'] = currentDateTime + self.db_reputation[uid]['secret_code'] = secret_code + else: + self.Irc.debug(f"L'UID {uid} n'existe pas dans REPUTATION_DB") + + return None + + def delete_db_reputation(self, uid:str) -> None: + """Cette fonction va supprimer le UID du dictionnaire self.db_reputation + + Args: + uid (str): le uid ou le nickname du user + """ + + # Si le UID existe dans le dictionnaire alors le supprimer + if uid in self.db_reputation: + # Si le nickname existe dans le dictionnaire alors le supprimer + del self.db_reputation[uid] + self.Irc.debug(f"Le UID {uid} a été supprimé du REPUTATION_DB") + + def insert_db_trusted(self, uid: str, nickname:str) -> None: + + uid = self.Irc.get_uid(uid) + nickname = self.Irc.get_nickname(nickname) + + query = "SELECT id FROM def_trusted WHERE user = ?" + exec_query = self.Base.db_execute_query(query, {"user": nickname}) + response = exec_query.fetchone() + + if not response is None: + q_insert = "INSERT INTO def_trusted (datetime, user, host, vhost) VALUES (?, ?, ?, ?)" + mes_donnees = {'datetime': self.Base.get_datetime(), 'user': nickname, 'host': '*', 'vhost': '*'} + exec_query = self.Base.db_execute_query(q_insert, mes_donnees) + pass + + + def join_saved_channels(self) -> None: + + result = self.Base.db_execute_query("SELECT id, channel FROM def_channels") + channels = result.fetchall() + jail_chan = self.Config.SALON_JAIL + jail_chan_mode = self.Config.SALON_JAIL_MODES + service_id = self.Config.SERVICE_ID + dumodes = self.Config.SERVICE_UMODES + dnickname = self.Config.SERVICE_NICKNAME + + unixtime = self.Base.get_unixtime() + + for channel in channels: + id, chan = channel + self.Irc.send2socket(f":{self.Config.SERVEUR_ID} SJOIN {unixtime} {chan} + :{self.Config.SERVICE_ID}") + if chan == jail_chan: + self.Irc.send2socket(f":{service_id} SAMODE {jail_chan} +{dumodes} {dnickname}") + self.Irc.send2socket(f":{service_id} MODE {jail_chan} +{jail_chan_mode}") + + return None + + def get_user_uptime_in_minutes(self, uidornickname:str) -> float: + """Retourne depuis quand l'utilisateur est connecté (en secondes ). + + Args: + uid (str): le uid ou le nickname de l'utilisateur + + Returns: + int: Temps de connexion de l'utilisateur en secondes + """ + + get_uid = self.Irc.get_uid(uidornickname) + + if not get_uid in self.Irc.db_uid: + return 0 + + # Convertir la date enregistrée dans UID_DB en un objet {datetime} + connected_time_string = self.Irc.db_uid[get_uid]['datetime'] + if type(connected_time_string) == datetime: + connected_time = connected_time_string + else: + connected_time = datetime.strptime(connected_time_string, "%Y-%m-%d %H:%M:%S.%f") + + # Quelle heure est-il ? + current_datetime = datetime.now() + + uptime = current_datetime - connected_time + convert_to_minutes = uptime.seconds / 60 + uptime_minutes = round(number=convert_to_minutes, ndigits=2) + + return uptime_minutes + + def system_reputation(self, uid:str)->None: + # Reputation security + # - Activation ou désactivation du système --> OK + # - Le user sera en mesure de changer la limite de la réputation --> OK + # - Defender devra envoyer l'utilisateur sur un salon défini dans la configuration, {jail_chan} + # - Defender devra bloquer cet utilisateur sur le salon qui sera en mode (+m) + # - Defender devra envoyer un message du type "Merci de taper cette comande /msg {nomdudefender} {un code généré aléatoirement} + # - Defender devra reconnaître le code + # - Defender devra libérer l'utilisateur et l'envoyer vers un salon défini dans la configuration {welcome_chan} + # - Defender devra intégrer une liste d'IDs (pseudo/host) exemptés de 'Reputation security' malgré un score de rép. faible et un pseudo non enregistré. + try: + + if not uid in self.db_reputation: + return False + + code = self.db_reputation[uid]['secret_code'] + salon_logs = self.Config.SERVICE_CHANLOG + salon_jail = self.Config.SALON_JAIL + jailed_nickname = self.db_reputation[uid]['nickname'] + jailed_score = self.db_reputation[uid]['score'] + + color_red = self.Config.CONFIG_COLOR['rouge'] + color_black = self.Config.CONFIG_COLOR['noire'] + color_bold = self.Config.CONFIG_COLOR['gras'] + service_id = self.Config.SERVICE_ID + service_prefix = self.Config.SERVICE_PREFIX + reputation_ban_all_chan = self.Base.int_if_possible(self.defConfig['reputation_ban_all_chan']) + + if not self.db_reputation[uid]['isWebirc']: + # Si le user ne vient pas de webIrc + + self.Irc.send2socket(f":{service_id} SAJOIN {jailed_nickname} {salon_jail}") + self.Irc.send2socket(f":{service_id} PRIVMSG {salon_logs} :[{color_red} REPUTATION {color_black}] : Connexion de {jailed_nickname} ({jailed_score}) ==> {salon_jail}") + self.Irc.send2socket(f":{service_id} NOTICE {jailed_nickname} :[{color_red} {jailed_nickname} {color_black}] : Merci de tapez la commande suivante {color_bold}{service_prefix}code {code}{color_bold}") + if reputation_ban_all_chan == 1: + for chan in self.Irc.db_chan: + if chan != salon_jail: + self.Irc.send2socket(f":{service_id} MODE {chan} +b {jailed_nickname}!*@*") + self.Irc.send2socket(f":{service_id} KICK {chan} {jailed_nickname}") + + self.Irc.debug(f"system_reputation : {jailed_nickname} à été capturé par le système de réputation") + # self.Irc.create_ping_timer(int(self.defConfig['reputation_timer']) * 60, 'Defender', 'system_reputation_timer') + self.Base.create_timer(int(self.defConfig['reputation_timer']) * 60, self.system_reputation_timer) + else: + self.Irc.debug(f"system_reputation : {jailed_nickname} à été supprimé du système de réputation car connecté via WebIrc ou il est dans la 'Trusted list'") + self.delete_db_reputation(uid) + + except IndexError as e: + self.Irc.debug(f"system_reputation : {str(e)}") + + def system_reputation_timer(self) -> None: + try: + reputation_flag = int(self.defConfig['reputation']) + reputation_timer = int(self.defConfig['reputation_timer']) + reputation_seuil = self.defConfig['reputation_seuil'] + service_id = self.Config.SERVICE_ID + dchanlog = self.Config.SERVICE_CHANLOG + color_red = self.Config.CONFIG_COLOR['rouge'] + color_black = self.Config.CONFIG_COLOR['noire'] + salon_jail = self.Config.SALON_JAIL + + if reputation_flag == 0: + return None + elif reputation_timer == 0: + return None + + # self.Irc.debug(self.db_reputation) + uid_to_clean = [] + + for uid in self.db_reputation: + if not self.db_reputation[uid]['isWebirc']: # Si il ne vient pas de WebIRC + self.Irc.debug(f"Nickname: {self.db_reputation[uid]['nickname']} | uptime: {self.get_user_uptime_in_minutes(uid)} | reputation time: {reputation_timer}") + if self.get_user_uptime_in_minutes(uid) >= reputation_timer and int(self.db_reputation[uid]['score']) <= int(reputation_seuil): + self.Irc.send2socket(f":{service_id} PRIVMSG {dchanlog} :[{color_red} REPUTATION {color_black}] : Action sur {self.db_reputation[uid]['nickname']} aprés {str(reputation_timer)} minutes d'inactivité") + # if not system_reputation_timer_action(cglobal['reputation_timer_action'], uid, self.db_reputation[uid]['nickname']): + # return False + self.Irc.send2socket(f":{service_id} KILL {self.db_reputation[uid]['nickname']} After {str(reputation_timer)} minutes of inactivity you should reconnect and type the password code ") + + self.Irc.debug(f"Action sur {self.db_reputation[uid]['nickname']} aprés {str(reputation_timer)} minutes d'inactivité") + uid_to_clean.append(uid) + + for uid in uid_to_clean: + # Suppression des éléments dans {UID_DB} et {REPUTATION_DB} + for chan in self.Irc.db_chan: + if chan != salon_jail: + self.Irc.send2socket(f":{service_id} MODE {chan} -b {self.db_reputation[uid]['nickname']}!*@*") + + # Lorsqu'un utilisateur quitte, il doit être supprimé de {UID_DB}. + self.Irc.delete_db_uid(uid) + self.delete_db_reputation(uid) + + except AssertionError as ae: + self.Irc.debug(f'Assertion Error -> {ae}') + + def _execute_flood_action(self, action:str, channel:str) -> None: + """DO NOT EXECUTE THIS FUNCTION WITHOUT THREADING + + Args: + action (str): _description_ + timer (int): _description_ + nickname (str): _description_ + channel (str): _description_ + + Returns: + _type_: _description_ + """ + service_id = self.Config.SERVICE_ID + match action: + case 'mode-m': + # Action -m sur le salon + self.Irc.send2socket(f":{service_id} MODE {channel} -m") + case _: + pass + + return None + + def flood(self, detected_user:str, channel:str) -> None: + + if self.defConfig['flood'] == 0: + return None + + if not '#' in channel: + return None + + flood_time = self.defConfig['flood_time'] + flood_message = self.defConfig['flood_message'] + flood_timer = self.defConfig['flood_timer'] + service_id = self.Config.SERVICE_ID + dnickname = self.Config.SERVICE_NICKNAME + color_red = self.Config.CONFIG_COLOR['rouge'] + color_bold = self.Config.CONFIG_COLOR['gras'] + + get_detected_uid = self.Irc.get_uid(detected_user) + get_detected_nickname = self.Irc.get_nickname(detected_user) + + unixtime = self.Base.get_unixtime() + get_diff_secondes = 0 + + if not get_detected_uid in self.flood_system: + self.flood_system[get_detected_uid] = { + 'nbr_msg': 0, + 'first_msg_time': unixtime + } + + self.flood_system[get_detected_uid]['nbr_msg'] += 1 + get_diff_secondes = unixtime - self.flood_system[get_detected_uid]['first_msg_time'] + if get_diff_secondes > flood_time: + self.flood_system[get_detected_uid]['first_msg_time'] = unixtime + self.flood_system[get_detected_uid]['nbr_msg'] = 0 + get_diff_secondes = unixtime - self.flood_system[get_detected_uid]['first_msg_time'] + + elif self.flood_system[get_detected_uid]['nbr_msg'] > flood_message: + self.Irc.debug('system de flood detecté') + self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} : {color_red} {color_bold} Flood detected. Apply the +m mode (Ô_o)') + self.Irc.send2socket(f":{service_id} MODE {channel} +m") + self.Irc.debug(f'FLOOD Détecté sur {get_detected_nickname} mode +m appliqué sur le salon {channel}') + self.flood_system[get_detected_uid]['nbr_msg'] = 0 + self.flood_system[get_detected_uid]['first_msg_time'] = unixtime + + self.Base.create_timer(flood_timer, self._execute_flood_action, ('mode-m', channel)) + + def run_db_action_timer(self, wait_for: float = 0) -> None: + + query = "SELECT parameter FROM def_config" + res = self.Base.db_execute_query(query) + service_id = self.Config.SERVICE_ID + dchanlog = self.Config.SERVICE_CHANLOG + + for param in res.fetchall(): + if param[0] == 'reputation': + self.Irc.send2socket(f":{service_id} PRIVMSG {dchanlog} : ===> {param[0]}") + else: + self.Irc.send2socket(f":{service_id} PRIVMSG {dchanlog} : {param[0]}") + # print(f":{service_id} PRIVMSG {dchanlog} : {param[0]}") + + # self.Base.garbage_collector_timer() + + return None + + def scan_ports(self, remote_ip: str) -> None: + + for port in self.Config.PORTS_TO_SCAN: + newSocket = '' + newSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + newSocket.settimeout(0.5) + try: + connection = (remote_ip, self.Base.int_if_possible(port)) + newSocket.connect(connection) + self.Irc.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :[ {self.Config.CONFIG_COLOR['rouge']}PROXY_SCAN{self.Config.CONFIG_COLOR['noire']} ] : Port [{str(port)}] ouvert sur l'adresse ip [{remote_ip}]") + # print(f"=======> Le port {str(port)} est ouvert !!") + self.Base.running_sockets.append(newSocket) + # print(newSocket) + newSocket.shutdown(socket.SHUT_RDWR) + newSocket.close() + except (socket.timeout, ConnectionRefusedError): + self.Irc.debug(f"Le port {str(port)} est fermé") + except AttributeError as ae: + self.Irc.debug(f"AttributeError : {ae}") + finally: + # newSocket.shutdown(socket.SHUT_RDWR) + newSocket.close() + self.Irc.debug('=======> Fermeture de la socket') + + pass + + def get_ports_connexion(self, remote_ip: str) -> list[int]: + connections = psutil.net_connections(kind='inet') + + matching_ports = [conn.raddr.port for conn in connections if conn.raddr and conn.raddr.ip == remote_ip] + self.Irc.debug(f"Connexion of {remote_ip} using ports : {str(matching_ports)}") + + return matching_ports + + def abuseipdb_scan(self, remote_ip:str) -> dict[str, any] | None: + """Analyse l'ip avec AbuseIpDB + Cette methode devra etre lancer toujours via un thread ou un timer. + Args: + remote_ip (_type_): l'ip a analyser + + Returns: + dict[str, any] | None: les informations du provider + keys : 'score', 'country', 'isTor', 'totalReports' + """ + if self.defConfig['abuseipdb_scan'] == 0: + return None + + if self.abuseipdb_key == '': + return None + + url = 'https://api.abuseipdb.com/api/v2/check' + querystring = { + 'ipAddress': remote_ip, + 'maxAgeInDays': '90' + } + + headers = { + 'Accept': 'application/json', + 'Key': self.abuseipdb_key + } + + response = requests.request(method='GET', url=url, headers=headers, params=querystring) + + # Formatted output + decodedResponse = json.loads(response.text) + try: + result = { + 'score': decodedResponse['data']['abuseConfidenceScore'], + 'country': decodedResponse['data']['countryCode'], + 'isTor': decodedResponse['data']['isTor'], + 'totalReports': decodedResponse['data']['totalReports'] + } + + service_id = self.Config.SERVICE_ID + service_chanlog = self.Config.SERVICE_CHANLOG + color_red = self.Config.CONFIG_COLOR['rouge'] + color_black = self.Config.CONFIG_COLOR['noire'] + + self.Irc.send2socket(f":{service_id} PRIVMSG {service_chanlog} :[ {color_red}ABUSEIPDB_SCAN{color_black} ] : Connexion de {remote_ip} Score: {str(result['score'])} | Country : {result['country']} | Tor : {str(result['isTor'])} | Total Reports : {str(result['totalReports'])}") + + response.close() + + return result + except KeyError as ke: + self.Irc.debug(f"AbuseIpDb KeyError : {ke}") + + def cmd(self, data:list) -> None: + + service_id = self.Config.SERVICE_ID # Defender serveur id + cmd = data + + if len(cmd) < 2: + return None + + match cmd[1]: + + case 'REPUTATION': + # :001 REPUTATION 91.168.141.239 118 + try: + self.reputation_first_connexion['ip'] = cmd[2] + self.reputation_first_connexion['score'] = cmd[3] + + # self.Base.scan_ports(cmd[2]) + if self.defConfig['local_scan'] == 1: + self.Base.create_thread(self.scan_ports, (cmd[2], )) + + if self.defConfig['psutil_scan'] == 1: + self.Base.create_thread(self.get_ports_connexion, (cmd[2], )) + + if self.defConfig['abuseipdb_scan'] == 1: + self.Base.create_thread(self.abuseipdb_scan, (cmd[2], )) + # Possibilité de déclancher les bans a ce niveau. + except IndexError: + self.Irc.debug(f'cmd reputation: index error') + + match cmd[2]: + + case 'PRIVMSG': + cmd.pop(0) + user_trigger = str(cmd[0]).replace(':','') + channel = cmd[2] + find_nickname = self.Irc.get_nickname(user_trigger) + self.flood(find_nickname, channel) + + case 'SJOIN': + # ['@msgid=F9B7JeHL5pj9nN57cJ5pEr;time=2023-12-28T20:47:24.305Z', ':001', 'SJOIN', '1702138958', '#welcome', ':0015L1AHL'] + try: + cmd.pop(0) + parsed_chan = cmd[3] + self.Irc.insert_db_chan(parsed_chan) + + if self.defConfig['reputation'] == 1: + parsed_UID = cmd[4] + pattern = fr'^:[@|%|\+|~|\*]*' + parsed_UID = re.sub(pattern, '', parsed_UID) + if parsed_UID in self.db_reputation: + # print(f"====> {str(self.db_reputation)}") + isWebirc = self.db_reputation[parsed_UID]['isWebirc'] + if self.defConfig['reputation_ban_all_chan'] == 1 and not isWebirc: + if parsed_chan != self.Config.SALON_JAIL: + self.Irc.send2socket(f":{service_id} MODE {parsed_chan} +b {self.db_reputation[parsed_UID]['nickname']}!*@*") + self.Irc.send2socket(f":{service_id} KICK {parsed_chan} {self.db_reputation[parsed_UID]['nickname']}") + + self.Irc.debug(f'SJOIN parsed_uid : {parsed_UID}') + except KeyError as ke: + self.Irc.debug(f"key error SJOIN : {ke}") + + case 'UID': + + if self.Irc.INIT == 1: + return None + + if 'webirc' in cmd[0]: + isWebirc = True + else: + isWebirc = False + + # Supprimer la premiere valeur et finir le code normalement + cmd.pop(0) + + uid = str(cmd[7]) + nickname = str(cmd[2]) + username = str(cmd[5]) + hostname = str(cmd[6]) + umodes = str(cmd[9]) + vhost = str(cmd[10]) + + reputation_flag = self.Base.int_if_possible(self.defConfig['reputation']) + reputation_seuil = self.Base.int_if_possible(self.defConfig['reputation_seuil']) + + if self.Irc.INIT == 0: + # A chaque nouvelle connexion chargé les données dans reputation + client_ip = '' + client_score = 0 + if 'ip' in self.reputation_first_connexion: + client_ip = self.reputation_first_connexion['ip'] + if 'score' in self.reputation_first_connexion: + client_score = self.reputation_first_connexion['score'] + + # Si réputation activé lancer un whois sur le nickname connecté + # Si le user n'es pas un service ni un IrcOP alors whois + if not re.match(fr'^.*[S|o?].*$', umodes): + if reputation_flag == 1 and int(client_score) <= int(reputation_seuil): + # if not db_isTrusted_user(user_id): + self.insert_db_reputation(uid, client_ip, nickname, username, hostname, umodes, vhost, client_score, isWebirc) + # self.Irc.send2socket(f":{service_id} WHOIS {nickname}") + if uid in self.db_reputation: + if reputation_flag == 1 and int(client_score) <= int(reputation_seuil): + self.system_reputation(uid) + self.Irc.debug('Démarrer le systeme de reputation') + + case 'SLOG': + # self.Base.scan_ports(cmd[7]) + cmd.pop(0) + if self.defConfig['local_scan'] == 1: + self.Base.create_thread(self.scan_ports, (cmd[7], )) + + if self.defConfig['psutil_scan'] == 1: + self.Base.create_thread(self.get_ports_connexion, (cmd[7], )) + + if self.defConfig['abuseipdb_scan'] == 1: + self.Base.create_thread(self.abuseipdb_scan, (cmd[7], )) + + case 'NICK': + # :0010BS24L NICK [NEWNICK] 1697917711 + # Changement de nickname + cmd.pop(0) + uid = str(cmd[0]).replace(':','') + oldnick = self.db_reputation[uid]['nickname'] + newnickname = cmd[2] + + jail_salon = self.Config.SALON_JAIL + service_id = self.Config.SERVICE_ID + + self.update_db_reputation(uid, newnickname) + + if uid in self.db_reputation: + for chan in self.Irc.db_chan: + if chan != jail_salon: + self.Irc.send2socket(f":{service_id} MODE {chan} -b {oldnick}!*@*") + self.Irc.send2socket(f":{service_id} MODE {chan} +b {newnickname}!*@*") + + case 'QUIT': + # :001N1WD7L QUIT :Quit: free_znc_1 + cmd.pop(0) + user_id = str(cmd[0]).replace(':','') + final_UID = user_id + + jail_salon = self.Config.SALON_JAIL + service_id = self.Config.SERVICE_ID + + if final_UID in self.db_reputation: + final_nickname = self.db_reputation[user_id]['nickname'] + for chan in self.Irc.db_chan: + if chan != jail_salon: + self.Irc.send2socket(f":{service_id} MODE {chan} -b {final_nickname}!*@*") + self.delete_db_reputation(final_UID) + + def _hcmds(self, user:str, cmd: list) -> None: + + command = str(cmd[0]).lower() + fromuser = user + # print(command) + + dnickname = self.Config.SERVICE_NICKNAME # Defender nickname + dchanlog = self.Config.SERVICE_CHANLOG # Defender chan log + dumodes = self.Config.SERVICE_UMODES # Les modes de Defender + service_id = self.Config.SERVICE_ID # Defender serveur id + jail_chan = self.Config.SALON_JAIL # Salon pot de miel + jail_chan_mode = self.Config.SALON_JAIL_MODES # Mode du salon "pot de miel" + + match command: + + case 'timer': + try: + timer_sent = self.Base.int_if_possible(cmd[1]) + timer_sent = int(timer_sent) + # self.Irc.create_ping_timer(timer_sent, 'Defender', 'run_db_action_timer') + self.Base.create_timer(timer_sent, self.run_db_action_timer) + # self.Base.create_timer(timer_sent, self.Base.garbage_collector_sockets) + + except TypeError as te: + self.Irc.debug(f"Type Error -> {te}") + except ValueError as ve: + self.Irc.debug(f"Value Error -> {ve}") + + case 'show_reputation': + + if not self.db_reputation: + self.Irc.send2socket(f':{dnickname} PRIVMSG {dchanlog} : No one is suspected') + + for uid, nickname in self.db_reputation.items(): + self.Irc.send2socket(f':{dnickname} PRIVMSG {dchanlog} : Uid: {uid} | Nickname: {self.db_reputation[uid]["nickname"]} | Connected on: {self.db_reputation[uid]["connected_datetime"]} | Updated on: {self.db_reputation[uid]["updated_datetime"]}') + + case 'code': + try: + release_code = cmd[1] + jailed_nickname = self.Irc.get_nickname(fromuser) + jailed_UID = self.Irc.get_uid(fromuser) + jailed_IP = self.db_reputation[jailed_UID]['ip'] + jailed_salon = self.Config.SALON_JAIL + reputation_seuil = self.defConfig['reputation_seuil'] + welcome_salon = self.Config.SALON_LIBERER + + self.Irc.debug(f"IP de {jailed_nickname} : {jailed_IP}") + link = self.Config.SERVEUR_LINK + color_green = self.Config.CONFIG_COLOR['verte'] + color_black = self.Config.CONFIG_COLOR['noire'] + + + if jailed_UID in self.db_reputation: + if release_code == self.db_reputation[jailed_UID]['secret_code']: + self.Irc.send2socket(f':{dnickname} PRIVMSG {jailed_salon} : Bon mot de passe. Allez du vent !') + + if self.defConfig['reputation_ban_all_chan'] == 1: + for chan in self.Irc.db_chan: + if chan != jailed_salon: + self.Irc.send2socket(f":{service_id} MODE {chan} -b {jailed_nickname}!*@*") + + del self.db_reputation[jailed_UID] + self.Irc.debug(f'{jailed_UID} - {jailed_nickname} removed from REPUTATION_DB') + self.Irc.send2socket(f":{service_id} SAPART {jailed_nickname} {jailed_salon}") + self.Irc.send2socket(f":{service_id} SAJOIN {jailed_nickname} {welcome_salon}") + self.Irc.send2socket(f":{link} REPUTATION {jailed_IP} {int(reputation_seuil) + 1}") + self.Irc.send2socket(f":{service_id} PRIVMSG {jailed_nickname} :[{color_green} MOT DE PASS CORRECT {color_black}] : You have now the right to enjoy the network !") + + else: + self.Irc.send2socket(f':{dnickname} PRIVMSG {jailed_salon} : Mauvais password') + self.Irc.send2socket(f":{service_id} PRIVMSG {jailed_nickname} :[{color_green} MAUVAIS PASSWORD {color_black}]") + else: + self.Irc.send2socket(f":{dnickname} PRIVMSG {jailed_salon} : Ce n'est pas à toi de taper le mot de passe !") + + except IndexError: + self.Irc.debug('_hcmd code: out of index') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} code [code]') + pass + pass + + case 'reputation': + # .reputation [on/off] --> activate or deactivate reputation system + # .reputation set banallchan [on/off] --> activate or deactivate ban in all channel + # .reputation set limit [xxxx] --> change the reputation threshold + # .reputation [arg1] [arg2] [arg3] + try: + len_cmd = len(cmd) + activation = str(cmd[1]).lower() + + # Nous sommes dans l'activation ON / OFF + if len_cmd == 2: + key = 'reputation' + if activation == 'on': + + if self.defConfig[key] == 1: + self.Irc.send2socket(f":{dnickname} PRIVMSG {dchanlog} :[ {self.Config.CONFIG_COLOR['verte']}REPUTATION{self.Config.CONFIG_COLOR['noire']} ] : Already activated") + return False + + self.update_db_configuration('reputation', 1) + self.Irc.send2socket(f":{dnickname} PRIVMSG {dchanlog} :[ {self.Config.CONFIG_COLOR['verte']}REPUTATION{self.Config.CONFIG_COLOR['noire']} ] : Activated by {fromuser}") + self.Irc.send2socket(f":{service_id} JOIN {jail_chan}") + self.Irc.send2socket(f":{service_id} SAMODE {jail_chan} +{dumodes} {dnickname}") + self.Irc.send2socket(f":{service_id} MODE {jail_chan} +{jail_chan_mode}") + self.add_defender_channel(jail_chan) + + if activation == 'off': + + if self.defConfig[key] == 0: + self.Irc.send2socket(f":{dnickname} PRIVMSG {dchanlog} :[ {self.Config.CONFIG_COLOR['verte']}REPUTATION{self.Config.CONFIG_COLOR['noire']} ] : Already deactivated") + return False + + self.update_db_configuration('reputation', 0) + self.Irc.send2socket(f":{dnickname} PRIVMSG {dchanlog} :[ {self.Config.CONFIG_COLOR['rouge']}REPUTATION{self.Config.CONFIG_COLOR['noire']} ] : Deactivated by {fromuser}") + self.Irc.send2socket(f":{service_id} SAMODE {jail_chan} -{dumodes} {dnickname}") + self.Irc.send2socket(f":{service_id} MODE {jail_chan} -sS") + self.Irc.send2socket(f":{service_id} PART {jail_chan}") + self.delete_defender_channel(jail_chan) + + if len_cmd == 4: + get_set = str(cmd[1]).lower() + + if get_set != 'set': + return False + + get_options = str(cmd[2]).lower() + + match get_options: + case 'banallchan': + key = 'reputation_ban_all_chan' + get_value = str(cmd[3]).lower() + if get_value == 'on': + + if self.defConfig[key] == 1: + self.Irc.send2socket(f":{dnickname} PRIVMSG {dchanlog} :[ {self.Config.CONFIG_COLOR['rouge']}BAN ON ALL CHANS{self.Config.CONFIG_COLOR['noire']} ] : Already activated") + return False + + self.update_db_configuration(key, 1) + self.Irc.send2socket(f':{dnickname} PRIVMSG {dchanlog} :[ {self.Config.CONFIG_COLOR["verte"]}BAN ON ALL CHANS{self.Config.CONFIG_COLOR["noire"]} ] : Activated by {fromuser}') + + elif get_value == 'off': + print(get_value) + if self.defConfig[key] == 0: + self.Irc.send2socket(f":{dnickname} PRIVMSG {dchanlog} :[ {self.Config.CONFIG_COLOR['rouge']}BAN ON ALL CHANS{self.Config.CONFIG_COLOR['noire']} ] : Already deactivated") + return False + + self.update_db_configuration(key, 0) + self.Irc.send2socket(f':{dnickname} PRIVMSG {dchanlog} :[ {self.Config.CONFIG_COLOR["verte"]}BAN ON ALL CHANS{self.Config.CONFIG_COLOR["noire"]} ] : Deactivated by {fromuser}') + case 'limit': + reputation_seuil = int(cmd[3]) + key = 'reputation_seuil' + self.update_db_configuration(key, reputation_seuil) + + self.Irc.send2socket(f':{dnickname} PRIVMSG {dchanlog} :[ {self.Config.CONFIG_COLOR["verte"]}REPUTATION SEUIL{self.Config.CONFIG_COLOR["noire"]} ] : Limit set to {str(reputation_seuil)} by {fromuser}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Reputation set to {reputation_seuil}') + + case 'timer': + reputation_timer = int(cmd[3]) + key = 'reputation_timer' + self.update_db_configuration(key, reputation_timer) + self.Irc.send2socket(f':{dnickname} PRIVMSG {dchanlog} :[ {self.Config.CONFIG_COLOR["verte"]}REPUTATION TIMER{self.Config.CONFIG_COLOR["noire"]} ] : Timer set to {str(reputation_timer)} minute(s) by {fromuser}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Reputation set to {reputation_timer}') + + case _: + pass + + except IndexError: + self.Irc.debug('_hcmd reputation: out of index') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} reputation [ON/OFF]') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} reputation set banallchan [ON/OFF]') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} reputation set limit [1234]') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} reputation set timer [1234]') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} reputation set action [kill|None]') + + except ValueError: + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : La valeur devrait etre un entier >= 0') + + case 'proxy_scan': + + # .proxy_scan set local_scan on/off --> Va activer le scan des ports + # .proxy_scan set psutil_scan on/off --> Active les informations de connexion a la machine locale + # .proxy_scan set abuseipdb_scan on/off --> Active le scan via l'api abuseipdb + len_cmd = len(cmd) + color_green = self.Config.CONFIG_COLOR['verte'] + color_red = self.Config.CONFIG_COLOR['rouge'] + color_black = self.Config.CONFIG_COLOR['noire'] + + if len_cmd == 4: + set_key = str(cmd[1]).lower() + + if set_key != 'set': + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} proxy_scan set local_scan [ON/OFF]') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} proxy_scan set psutil_scan [ON/OFF]') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} proxy_scan set abuseipdb_scan [ON/OFF]') + + option = str(cmd[2]).lower() # => local_scan, psutil_scan, abuseipdb_scan + action = str(cmd[3]).lower() # => on / off + + match option: + case 'local_scan': + if action == 'on': + if self.defConfig[option] == 1: + self.Irc.send2socket(f":{dnickname} PRIVMSG {dchanlog} :[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Already activated") + return None + self.update_db_configuration(option, 1) + self.Irc.send2socket(f":{dnickname} PRIVMSG {dchanlog} :[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Activated by {fromuser}") + elif action == 'off': + if self.defConfig[option] == 0: + self.Irc.send2socket(f":{dnickname} PRIVMSG {dchanlog} :[ {color_red}PROXY_SCAN {option.upper()}{color_black} ] : Already Deactivated") + return None + self.update_db_configuration(option, 0) + self.Irc.send2socket(f":{dnickname} PRIVMSG {dchanlog} :[ {color_red}PROXY_SCAN {option.upper()}{color_black} ] : Deactivated by {fromuser}") + + case 'psutil_scan': + if action == 'on': + if self.defConfig[option] == 1: + self.Irc.send2socket(f":{dnickname} PRIVMSG {dchanlog} :[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Already activated") + return None + self.update_db_configuration(option, 1) + self.Irc.send2socket(f":{dnickname} PRIVMSG {dchanlog} :[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Activated by {fromuser}") + elif action == 'off': + if self.defConfig[option] == 0: + self.Irc.send2socket(f":{dnickname} PRIVMSG {dchanlog} :[ {color_red}PROXY_SCAN {option.upper()}{color_black} ] : Already Deactivated") + return None + self.update_db_configuration(option, 0) + self.Irc.send2socket(f":{dnickname} PRIVMSG {dchanlog} :[ {color_red}PROXY_SCAN {option.upper()}{color_black} ] : Deactivated by {fromuser}") + + case 'abuseipdb_scan': + if action == 'on': + if self.defConfig[option] == 1: + self.Irc.send2socket(f":{dnickname} PRIVMSG {dchanlog} :[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Already activated") + return None + self.update_db_configuration(option, 1) + self.Irc.send2socket(f":{dnickname} PRIVMSG {dchanlog} :[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Activated by {fromuser}") + elif action == 'off': + if self.defConfig[option] == 0: + self.Irc.send2socket(f":{dnickname} PRIVMSG {dchanlog} :[ {color_red}PROXY_SCAN {option.upper()}{color_black} ] : Already Deactivated") + return None + self.update_db_configuration(option, 0) + self.Irc.send2socket(f":{dnickname} PRIVMSG {dchanlog} :[ {color_red}PROXY_SCAN {option.upper()}{color_black} ] : Deactivated by {fromuser}") + + case _: + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} proxy_scan set local_scan [ON/OFF]') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} proxy_scan set psutil_scan [ON/OFF]') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} proxy_scan set abuseipdb_scan [ON/OFF]') + else: + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} proxy_scan set local_scan [ON/OFF]') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} proxy_scan set psutil_scan [ON/OFF]') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} proxy_scan set abuseipdb_scan [ON/OFF]') + + case 'status': + try: + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Reputation ==> {self.defConfig["reputation"]}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : reputation_seuil ==> {self.defConfig["reputation_seuil"]}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : reputation_ban_all_chan ==> {self.defConfig["reputation_ban_all_chan"]}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : reputation_timer ==> {self.defConfig["reputation_timer"]}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : [Proxy_scan]') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : local_scan ==> {self.defConfig["local_scan"]}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : psutil_scan ==> {self.defConfig["psutil_scan"]}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : abuseipdb_scan ==> {self.defConfig["abuseipdb_scan"]}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Flood ==> {self.defConfig["flood"]}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : flood_action ==> Coming soon') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : flood_message ==> {self.defConfig["flood_message"]}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : flood_time ==> {self.defConfig["flood_time"]}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : flood_timer ==> {self.defConfig["flood_timer"]}') + except KeyError as ke: + self.Irc.debug(f"Key Error : {ke}") + + case 'join': + + try: + channel = cmd[1] + self.Irc.send2socket(f':{service_id} JOIN {channel}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : {dnickname} JOINED {channel}') + self.add_defender_channel(channel) + except IndexError: + self.Irc.debug('_hcmd join: out of index') + + case 'part': + + try: + channel = cmd[1] + if channel == dchanlog: + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} : {dnickname} CAN'T LEFT {channel} AS IT IS LOG CHANNEL") + return False + + self.Irc.send2socket(f':{service_id} PART {channel}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : {dnickname} LEFT {channel}') + self.delete_defender_channel(channel) + except IndexError: + self.Irc.debug('_hcmd part: out of index') + + case 'op' | 'o': + # /mode #channel +o user + # .op #channel user + # [':adator', 'PRIVMSG', '#services', ':.o', '#services', 'dktmb'] + try: + print(cmd) + channel = cmd[1] + nickname = cmd[2] + self.Irc.send2socket(f":{service_id} MODE {channel} +o {nickname}") + except IndexError as e: + self.Irc.debug(f'_hcmd OP: {str(e)}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} op [#SALON] [NICKNAME]') + + case 'deop' | 'do': + # /mode #channel -o user + # .deop #channel user + try: + channel = cmd[1] + nickname = cmd[2] + self.Irc.send2socket(f":{service_id} MODE {channel} -o {nickname}") + except IndexError as e: + self.Irc.debug(f'_hcmd DEOP: {str(e)}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} deop [#SALON] [NICKNAME]') + + case 'owner' | 'q': + # /mode #channel +q user + # .owner #channel user + try: + channel = cmd[1] + nickname = cmd[2] + self.Irc.send2socket(f":{service_id} MODE {channel} +q {nickname}") + except IndexError as e: + self.Irc.debug(f'_hcmd OWNER: {str(e)}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} owner [#SALON] [NICKNAME]') + + case 'deowner' | 'dq': + # /mode #channel -q user + # .deowner #channel user + try: + channel = cmd[1] + nickname = cmd[2] + self.Irc.send2socket(f":{service_id} MODE {channel} -q {nickname}") + except IndexError as e: + self.Irc.debug(f'_hcmd DEOWNER: {str(e)}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} deowner [#SALON] [NICKNAME]') + + case 'halfop' | 'h': + # /mode #channel +h user + # .halfop #channel user + try: + channel = cmd[1] + nickname = cmd[2] + self.Irc.send2socket(f":{service_id} MODE {channel} +h {nickname}") + except IndexError as e: + self.Irc.debug(f'_hcmd halfop: {str(e)}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} halfop [#SALON] [NICKNAME]') + + case 'dehalfop' | 'dh': + # /mode #channel -h user + # .dehalfop #channel user + try: + channel = cmd[1] + nickname = cmd[2] + self.Irc.send2socket(f":{service_id} MODE {channel} -h {nickname}") + except IndexError as e: + self.Irc.debug(f'_hcmd DEHALFOP: {str(e)}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} dehalfop [#SALON] [NICKNAME]') + + case 'voice' | 'v': + # /mode #channel +v user + # .voice #channel user + try: + channel = cmd[1] + nickname = cmd[2] + self.Irc.send2socket(f":{service_id} MODE {channel} +v {nickname}") + except IndexError as e: + self.Irc.debug(f'_hcmd VOICE: {str(e)}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} voice [#SALON] [NICKNAME]') + + case 'devoice' | 'dv': + # /mode #channel -v user + # .devoice #channel user + try: + channel = cmd[1] + nickname = cmd[2] + self.Irc.send2socket(f":{service_id} MODE {channel} -v {nickname}") + except IndexError as e: + self.Irc.debug(f'_hcmd DEVOICE: {str(e)}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} devoice [#SALON] [NICKNAME]') + + case 'ban' | 'b': + # .ban #channel nickname + try: + channel = cmd[1] + nickname = cmd[2] + + self.Irc.send2socket(f":{service_id} MODE {channel} +b {nickname}!*@*") + self.Irc.debug(f'{fromuser} has banned {nickname} from {channel}') + except IndexError as e: + self.Irc.debug(f'_hcmd BAN: {str(e)}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} ban [#SALON] [NICKNAME]') + + case 'unban' | 'ub': + # .unban #channel nickname + try: + channel = cmd[1] + nickname = cmd[2] + + self.Irc.send2socket(f":{service_id} MODE {channel} -b {nickname}!*@*") + self.Irc.debug(f'{fromuser} has unbanned {nickname} from {channel}') + except IndexError as e: + self.Irc.debug(f'_hcmd UNBAN: {str(e)}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} unban [#SALON] [NICKNAME]') + + case 'kick' | 'k': + # .kick #channel nickname reason + try: + channel = cmd[1] + nickname = cmd[2] + reason = [] + + for i in range(3, len(cmd)): + reason.append(cmd[i]) + + final_reason = ' '.join(reason) + + self.Irc.send2socket(f":{service_id} KICK {channel} {nickname} {final_reason}") + self.Irc.debug(f'{fromuser} has kicked {nickname} from {channel} : {final_reason}') + except IndexError as e: + self.Irc.debug(f'_hcmd KICK: {str(e)}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} kick [#SALON] [NICKNAME] [REASON]') + + case 'kickban' | 'kb': + # .kickban #channel nickname reason + try: + channel = cmd[1] + nickname = cmd[2] + reason = [] + + for i in range(3, len(cmd)): + reason.append(cmd[i]) + + final_reason = ' '.join(reason) + + self.Irc.send2socket(f":{service_id} KICK {channel} {nickname} {final_reason}") + self.Irc.send2socket(f":{service_id} MODE {channel} +b {nickname}!*@*") + self.Irc.debug(f'{fromuser} has kicked and banned {nickname} from {channel} : {final_reason}') + except IndexError as e: + self.Irc.debug(f'_hcmd KICKBAN: {str(e)}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : Right command : /msg {dnickname} kickban [#SALON] [NICKNAME] [REASON]') + + case 'info': + try: + nickoruid = cmd[1] + uid_query = None + nickname_query = None + + if not self.Irc.get_nickname(nickoruid) is None: + nickname_query = self.Irc.get_nickname(nickoruid) + + if not self.Irc.get_uid(nickoruid) is None: + uid_query = self.Irc.get_uid(nickoruid) + + if nickname_query is None and uid_query is None: + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} : This user {nickoruid} doesn't exist") + else: + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : UID : {uid_query}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : NICKNAME : {self.Irc.db_uid[uid_query]["nickname"]}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : USERNAME : {self.Irc.db_uid[uid_query]["username"]}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : HOSTNAME : {self.Irc.db_uid[uid_query]["hostname"]}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : VHOST : {self.Irc.db_uid[uid_query]["vhost"]}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : MODES : {self.Irc.db_uid[uid_query]["umodes"]}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} : CONNECTION TIME : {self.Irc.db_uid[uid_query]["datetime"]}') + except KeyError as ke: + self.Irc.debug(f"Key error info user : {ke}") + + case 'show_users': + for uid, infousers in self.Irc.db_uid.items(): + # print(uid + " " + str(infousers)) + for info in infousers: + if info == 'nickname': + self.Irc.send2socket(f":{dnickname} PRIVMSG {dchanlog} :UID : {uid} - isWebirc: {infousers['isWebirc']} - {info}: {infousers[info]}") diff --git a/mods/mod_test.py b/mods/mod_test.py new file mode 100644 index 0000000..eb801e9 --- /dev/null +++ b/mods/mod_test.py @@ -0,0 +1,83 @@ +import threading +from core.irc import Irc + +# Le module crée devra réspecter quelques conditions +# 1. Importer le module de configuration +# 2. Le nom de class devra toujours s'appeler comme le module exemple => nom de class Dktmb | nom du module mod_dktmb +# 3. la fonction __init__ devra toujours avoir les parametres suivant (self, irc:object) +# 1 . Créer la variable irc dans le module +# 2 . Récuperer la configuration dans une variable +# 3 . Définir et enregistrer les nouvelles commandes +# 4. une fonction _hcmds(self, user:str, cmd: list) devra toujours etre crée. + +class Test(): + + def __init__(self, ircInstance:Irc) -> None: + print(f'Module {self.__class__.__name__} loaded ...') + + self.irc = ircInstance # Ajouter l'object mod_irc a la classe + + self.config = ircInstance.Config # Ajouter la configuration a la classe + + # Créer les nouvelles commandes du module + self.commands = ['test'] + + self.__set_commands(self.commands) # Enrigstrer les nouvelles commandes dans le code + + self.core = ircInstance.Base # Instance du module Base + + self.session = '' # Instancier une session pour la base de données + self.__create_db('mod_test') # Créer la base de données si necessaire + + def __set_commands(self, commands:list) -> None: + """Rajoute les commandes du module au programme principal + + Args: + commands (list): Liste des commandes du module + + Returns: + None: Aucun retour attendu + """ + for command in commands: + self.irc.commands.append(command) + + return True + + def __create_db(self, db_name:str) -> None: + """Methode qui va créer la base de donnée si elle n'existe pas. + Une Session unique pour cette classe sera crée, qui sera utilisé dans cette classe / module + Args: + database_name (str): Nom de la base de données ( pas d'espace dans le nom ) + + Returns: + None: Aucun retour n'es attendu + """ + db_directory = self.core.MODS_DB_PATH + + self.session = self.core.db_init(db_directory, db_name) + + table_logs = '''CREATE TABLE IF NOT EXISTS logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + datetime TEXT, + server_msg TEXT + ) + ''' + + self.core.db_execute_query(self.session, table_logs) + return None + + def _hcmds(self, user:str, cmd: list) -> None: + + command = cmd[0].lower() + + match command: + + case 'test': + try: + user_action = cmd[1] + self.irc.send2socket(f'PRIVMSG #webmail Je vais voicer {user}') + self.irc.send2socket(f'MODE #webmail +v {user_action}') + self.core.create_log(f"MODE +v sur {user_action}") + except KeyError as ke: + self.core.create_log(f"Key Error : {ke}") +