first commit

This commit is contained in:
adator85
2024-01-02 00:30:02 +01:00
parent a236854eb1
commit a45a99681e
10 changed files with 3096 additions and 0 deletions

65
README_1.md Normal file
View File

@@ -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

0
core/__init__.py Normal file
View File

413
core/base.py Normal file
View File

@@ -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

View File

@@ -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
}

66
core/installation.py Normal file
View File

@@ -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")

1145
core/irc.py Normal file

File diff suppressed because it is too large Load Diff

26
main.py Normal file
View File

@@ -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()

0
mods/__init__.py Normal file
View File

1245
mods/mod_defender.py Normal file

File diff suppressed because it is too large Load Diff

83
mods/mod_test.py Normal file
View File

@@ -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}")