diff --git a/Demo/Dockerfiles/Dockerfile.attaquant b/Demo/Dockerfiles/Dockerfile.attaquant new file mode 100644 index 0000000..8c52ada --- /dev/null +++ b/Demo/Dockerfiles/Dockerfile.attaquant @@ -0,0 +1,18 @@ +FROM python:alpine3.20 + +# Installation des paquets nécessaires pour scapy +RUN apk -U upgrade && \ + apk add --no-cache nmap iproute2 +RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories +RUN apk -U add --no-cache hping3 + +COPY Demo/Dockerfiles/attaquant-entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] + +# Copier le script d'attaque +#COPY attack.py /attack.py + +# Lancer le script d'attaque +#CMD ["python", "/attack.py"] + diff --git a/Demo/Dockerfiles/Dockerfile.cible b/Demo/Dockerfiles/Dockerfile.cible new file mode 100644 index 0000000..e951461 --- /dev/null +++ b/Demo/Dockerfiles/Dockerfile.cible @@ -0,0 +1,9 @@ +FROM httpd:alpine + +# Installation des paquets nécessaire pour le routage +RUN apk -U upgrade && apk add --no-cache iproute2 + +COPY Demo/Dockerfiles/cible-entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] +CMD ["httpd-foreground"] diff --git a/Demo/Dockerfiles/Dockerfile.db b/Demo/Dockerfiles/Dockerfile.db new file mode 100644 index 0000000..27ab9f8 --- /dev/null +++ b/Demo/Dockerfiles/Dockerfile.db @@ -0,0 +1,5 @@ +FROM mysql:latest + +# Copier le script SQL dans l'image +COPY sql/init.sql /docker-entrypoint-initdb.d/ + diff --git a/Demo/Dockerfiles/Dockerfile.idps b/Demo/Dockerfiles/Dockerfile.idps new file mode 100644 index 0000000..89f925e --- /dev/null +++ b/Demo/Dockerfiles/Dockerfile.idps @@ -0,0 +1,24 @@ +FROM python:alpine3.20 + +# Installation des paquets nécessaires pour scapy +RUN apk -U upgrade && \ + apk add --no-cache libpcap libpcap-dev gcc musl-dev libffi-dev iptables iproute2 +RUN pip install scapy mysql-connector-python + +# Copier le script de démarrage +COPY Demo/Dockerfiles/idps-entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# Copier le script de l'idps +WORKDIR /app + +# Copier le contenu du répertoire 'idps' du contexte de build vers '/app/idps' dans le conteneur +COPY idps /app/idps + +# Copie du fichier de configuration +COPY Demo/config/config-idps.json /app/config.json + +# Utiliser le script comme point d'entrée +ENTRYPOINT ["/entrypoint.sh"] +# Commande par défaut +CMD ["python", "/app/idps/main.py"] diff --git a/Demo/Dockerfiles/Dockerfile.ids b/Demo/Dockerfiles/Dockerfile.ids new file mode 100644 index 0000000..cded915 --- /dev/null +++ b/Demo/Dockerfiles/Dockerfile.ids @@ -0,0 +1,18 @@ +FROM python:alpine3.20 + +# Installation des paquets nécessaires pour scapy +RUN apk -U upgrade && \ + apk add --no-cache libpcap libpcap-dev gcc musl-dev libffi-dev +RUN pip install scapy mysql-connector-python + +# Copier le script de l'idps +WORKDIR /app + +# Copier le contenu du répertoire 'idps' du contexte de build vers '/app/idps' dans le conteneur +COPY idps /app/ids + +# Copie du fichier de configuration +COPY Demo/config/config-ids.json /app/config.json + +# Lancer le script de la sonde IDS +CMD ["python3", "/app/ids/main.py"] diff --git a/Demo/Dockerfiles/attaquant-entrypoint.sh b/Demo/Dockerfiles/attaquant-entrypoint.sh new file mode 100644 index 0000000..11792c1 --- /dev/null +++ b/Demo/Dockerfiles/attaquant-entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +ip route add 172.20.2.0/24 via 172.20.1.3 dev eth0 + +# Lancer l'application IDPS +exec "$@" diff --git a/Demo/Dockerfiles/cible-entrypoint.sh b/Demo/Dockerfiles/cible-entrypoint.sh new file mode 100644 index 0000000..74b24ae --- /dev/null +++ b/Demo/Dockerfiles/cible-entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +ip route add 172.20.1.0/24 via 172.20.2.2 dev eth0 + +# Lancer l'application IDPS +exec "$@" diff --git a/Demo/Dockerfiles/idps-entrypoint.sh b/Demo/Dockerfiles/idps-entrypoint.sh new file mode 100644 index 0000000..c69f83b --- /dev/null +++ b/Demo/Dockerfiles/idps-entrypoint.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +# Activer l'acheminement des paquets +echo 1 > /proc/sys/net/ipv4/ip_forward + +# Configurer les règles iptables +ip route add 172.20.2.0/24 via 172.20.2.2 dev eth1 +ip route add 172.20.1.0/24 via 172.20.1.3 dev eth2 + +iptables -A FORWARD -i eth1 -o eth2 -j ACCEPT +iptables -A FORWARD -i eth2 -o eth1 -j ACCEPT + +# Lancer l'application IDPS +exec "$@" diff --git a/Demo/architecture.png b/Demo/architecture.png new file mode 100644 index 0000000..96126b8 Binary files /dev/null and b/Demo/architecture.png differ diff --git a/Demo/config/config-idps.json b/Demo/config/config-idps.json new file mode 100644 index 0000000..7e734aa --- /dev/null +++ b/Demo/config/config-idps.json @@ -0,0 +1,41 @@ +{ + "rules_dirpath": "/app/idps/rules", + "ifaces": ["eth1"], + "db_host": "172.20.3.10", + "db_database": "sidps", + "db_user": "sidps", + "db_password": "SUPERPASSWORD", + "db_port": "3306", + "protection": 1, + "cef_version": 1, + "device_product": "SIDPS", + "device_vendor": "ArKa", + "device_version": "vAlpha", + "synscan_time": 180, + "synscan_count": 5, + "synscan_bantime": 300, + "tcpconnectscan_time": 180, + "tcpconnectscan_count": 5, + "tcpconnectscan_bantime": 300, + "ackscan_time": 180, + "ackscan_count": 5, + "ackscan_bantime": 300, + "finscan_time": 180, + "finscan_count": 5, + "finscan_bantime": 300, + "nullscan_time": 180, + "nullscan_count": 5, + "nullscan_bantime": 300, + "xmasscan_time": 180, + "xmasscan_count": 5, + "xmasscan_bantime": 300, + "synflood_time": 60, + "synflood_count": 100, + "synflood_bantime": 300, + "tcpconnectflood_time": 60, + "tcpconnectflood_count": 100, + "tcpconnectflood_bantime": 300, + "syndos_time": 60, + "syndos_count": 100, + "syndos_bantime": 300 +} diff --git a/Demo/config/config-ids.json b/Demo/config/config-ids.json new file mode 100644 index 0000000..4e091b7 --- /dev/null +++ b/Demo/config/config-ids.json @@ -0,0 +1,41 @@ +{ + "rules_dirpath": "/app/ids/rules", + "ifaces": ["br-c56b595383ad"], + "db_host": "172.20.3.10", + "db_database": "sidps", + "db_user": "sidps", + "db_password": "SUPERPASSWORD", + "db_port": "3306", + "protection": 0, + "cef_version": 1, + "device_product": "Sonde IDS", + "device_vendor": "ArKa", + "device_version": "vAlpha", + "synscan_time": 180, + "synscan_count": 5, + "synscan_bantime": 300, + "tcpconnectscan_time": 180, + "tcpconnectscan_count": 5, + "tcpconnectscan_bantime": 300, + "ackscan_time": 180, + "ackscan_count": 5, + "ackscan_bantime": 300, + "finscan_time": 180, + "finscan_count": 5, + "finscan_bantime": 300, + "nullscan_time": 180, + "nullscan_count": 5, + "nullscan_bantime": 300, + "xmasscan_time": 180, + "xmasscan_count": 5, + "xmasscan_bantime": 300, + "synflood_time": 60, + "synflood_count": 100, + "synflood_bantime": 300, + "tcpconnectflood_time": 60, + "tcpconnectflood_count": 100, + "tcpconnectflood_bantime": 300, + "syndos_time": 60, + "syndos_count": 100, + "syndos_bantime": 300 +} diff --git a/Demo/deploy.sh b/Demo/deploy.sh new file mode 100755 index 0000000..969b744 --- /dev/null +++ b/Demo/deploy.sh @@ -0,0 +1,6 @@ +#/bin/sh +docker compose build +docker compose stop +#docker rm ids idps +docker rm attaquant1 attaquant2 ids idps cible alert_db +docker compose up -d diff --git a/Demo/docker-compose.yml b/Demo/docker-compose.yml new file mode 100644 index 0000000..0811914 --- /dev/null +++ b/Demo/docker-compose.yml @@ -0,0 +1,108 @@ +version: "3.8" +services: + # Attaquant 1 + atk1: + build: + context: .. + dockerfile: Demo/Dockerfiles/Dockerfile.attaquant + container_name: attaquant1 + command: sleep infinity + cap_add: + - NET_ADMIN + networks: + net_public: + ipv4_address: 172.20.1.2 + restart: unless-stopped + + # IDPS + idps: + build: + context: .. + dockerfile: Demo/Dockerfiles/Dockerfile.idps + container_name: idps + cap_add: + - NET_ADMIN + - NET_RAW + networks: + net_public: + ipv4_address: 172.20.1.3 + net_private: + ipv4_address: 172.20.2.2 + net_data: + ipv4_address: 172.20.3.2 + restart: unless-stopped + + # Cible + cible: + build: + context: .. + dockerfile: Demo/Dockerfiles/Dockerfile.cible + container_name: cible + cap_add: + - NET_ADMIN + networks: + net_private: + ipv4_address: 172.20.2.3 + restart: unless-stopped + + # Attaquant 2 + atk2: + build: + context: .. + dockerfile: Demo/Dockerfiles/Dockerfile.attaquant + container_name: attaquant2 + command: sleep infinity + cap_add: + - NET_ADMIN + networks: + net_private: + ipv4_address: 172.20.2.4 + restart: unless-stopped + + # Sonde IDS + ids: + build: + context: .. + dockerfile: Demo/Dockerfiles/Dockerfile.ids + container_name: ids + cap_add: + - NET_ADMIN + - NET_RAW + #networks: + #net_private: + #ipv4_address: 172.20.2.5 + # Network mode host obligatoire pour que la sonde puisse sniffer le réseau + network_mode: host + restart: unless-stopped + + # BDD d'alertes + alert_db: + build: + context: .. + dockerfile: Demo/Dockerfiles/Dockerfile.db + container_name: alert_db + environment: + MYSQL_ROOT_PASSWORD: root + ports: + - "3306:3306" + networks: + net_data: + ipv4_address: 172.20.3.10 + restart: unless-stopped + +networks: + net_public: + driver: bridge + ipam: + config: + - subnet: 172.20.1.0/24 + net_private: + driver: bridge + ipam: + config: + - subnet: 172.20.2.0/24 + net_data: + driver: bridge + ipam: + config: + - subnet: 172.20.3.0/24 diff --git a/README.md b/README.md index fd414b4..28207bb 100644 --- a/README.md +++ b/README.md @@ -3,46 +3,30 @@ SIDPS est un outils de détection et de prévention d'intrusion. Il est capable de détecter & d'identifier différents types d'attaques réseaux, ainsi que de protéger contre certaines de ces attaques automatiquement. -**Attention, il s'agit d'un projet étudiant, dont le but est de réaliser une démonstration pour un projet Universitaire ! Ce projet ne sera probablement pas maintenu par la suite** +**Attention, il s'agit d'un projet étudiant, dont le but est de réaliser une démonstration pour un projet Universitaire !** +**Ce projet ne sera probablement pas maintenu par la suite** ## Fonctionnalités clés: - Rapidité - Interface web simple et intuitive - Messages d'alertes interopérable suivant la norme [CEF (Common Event Format)](https://www.microfocus.com/documentation/arcsight/arcsight-smartconnectors-8.4/pdfdoc/cef-implementation-standard/cef-implementation-standard.pdf) -- Détections de nombreuses attaques réseaux (scan, DOS, ...) +- Détections de nombreuses attaques réseaux (scan, DOS, exfiltration de données...) ## Installation -Pour son fonctionnement, ce projet utilise une base de donnée [redis](https://redis.io/). +Pour son fonctionnement, ce projet utilise une base de donnée [mysql](https://www.mysql.com/). -Un moyen simple d'avoir une base de donnée redis fonctionnel est d'utilisé docker: +## Structure de la base de données SQL -```bash -docker run -d --name redis-stack-server -p 6379:6379 redis/redis-stack-server:latest -``` - -Ou si vous souhaitez avoir une interface graphique pour Redis en plus de la base de donnée, vous pouvez installer un docker de [redis insight](https://redis.io/insight/). - -```bash -docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest -``` - -*Installation interface web & noyau de l'IDS* - -## Structure de la base de données Redis - -La base de données Redis est structurée de la façon suivante: -- stream `logs:alertes` contenant toutes les alertes envoyés par le noyau de l'IDPS -- stream `logs:correlations` contenant toutes les corrélations d'alertes, avec un message de corrélation et les alertes corrélées - -Ces deux streams suivent la norme CEF, mais sont structurées sous la forme d'objet et non d'une seule chaine de caractère. Afin de faciliter le parsing par la suite. +La base de données SQL est structurée de la façon suivante: +- une table pour les alertes `alertes`, contenant toutes les alertes. Ces alertes sont décrites selon la [norme CEF](https://www.microfocus.com/documentation/arcsight/arcsight-smartconnectors-8.4/pdfdoc/cef-implementation-standard/cef-implementation-standard.pdf) ## Interface de tests d'alertes -Un script python `tests/cef-generator.py` permet de générer des alertes CEF dans la base de données Redis. -Ce script peut être utile pour le développement d'interface d'affichage des alertes. Pour l'utiliser il faut une base de donnée redis, et mettre les identifiants dans le script. -De plus, ce script à besoin de la librairie `redis` pour pouvoir ajouter / faire des requêtes à la base de données Redis. +Un script python `sql/cef-generator.py` permet de générer des alertes CEF dans la base de données SQL. +Ce script peut être utile pour le développement d'interface d'affichage des alertes. Pour l'utiliser il faut une base de donnée sql, et mettre les identifiants dans le script. +De plus, ce script à besoin de la librairie `sql` pour pouvoir ajouter / faire des requêtes à la base de données MySQL. Pour cela, utiliser les commandes suivantes: @@ -55,11 +39,36 @@ pip install -r requirements.txt Puis executer le script `tests/cef-generator.py` avec le python3 du l'environnement virtuel. ```bash -.venv/bin/python3 tests/cef-generator.py +.venv/bin/python3 sql/cef-generator.py ``` -## TODO +## Demo -- Noyau d'analyse de l'IDS -- Interface web pour visualiser les alertes / rechercher dedans -- Moteur de corrélation des alertes (récupération + renvoi dans Redis). +Ce projet étant réaliser dans le cadre d'un cours à l'université, une démonstration est donc nécessaire. + +Ci-dessous, le schéma de l'architecture réseau de la démonstration: +![Architecture réseau démo](Demo/architecture.png) + +Cette démonstration peut être déployer facilement grâce à un docker-compose `Demo/docker-compose.yml`. + +Pour lancer cette démonstration, il faudra avoir `docker` & `docker compose` d'installer. Puis executer les commandes suivantes: + +```bash +cd Demo/ +./deploy.sh +``` + +La cible (172.20.2.3) héberge un serveur web apache avec la page par défaut sur son port 80. +Les conteneurs attaquants disposent tous les deux de nmap et de hping3 pour réaliser des scan et des floods / DOS. + +Rappel des commandes pour flood avec et sans charge utile avec hping3: + +```bash +hping3 -S --flood IP +hping3 -S --flood -d TAILLE IP +``` + +## TODO: + +- Moteur de corrélation des alertes (récupération + renvoi dans MySQL). +- Moteur de détection par comportement diff --git a/config.json b/config.json new file mode 100644 index 0000000..d827136 --- /dev/null +++ b/config.json @@ -0,0 +1,40 @@ +{ + "rules_dirpath": "/app/idps/rules", + "ifaces": ["eth1"], + "db_host": "172.20.3.10", + "db_database": "sidps", + "db_user": "sidps", + "db_password": "SUPERPASSWORD", + "db_port": "3306", + "cef_version": 1, + "device_product": "SIDPS", + "device_vendor": "ArKa", + "device_version": "vAlpha", + "synscan_time": 180, + "synscan_count": 5, + "synscan_bantime": 300, + "tcpconnectscan_time": 180, + "tcpconnectscan_count": 5, + "tcpconnectscan_bantime": 300, + "ackscan_time": 180, + "ackscan_count": 5, + "ackscan_bantime": 300, + "finscan_time": 180, + "finscan_count": 5, + "finscan_bantime": 300, + "nullscan_time": 180, + "nullscan_count": 5, + "nullscan_bantime": 300, + "xmasscan_time": 180, + "xmasscan_count": 5, + "xmasscan_bantime": 300, + "synflood_time": 60, + "synflood_count": 100, + "synflood_bantime": 300, + "tcpconnectflood_time": 60, + "tcpconnectflood_count": 100, + "tcpconnectflood_bantime": 300, + "syndos_time": 60, + "syndos_count": 100, + "syndos_bantime": 300 +} diff --git a/idps/database.py b/idps/database.py new file mode 100644 index 0000000..efdb6f0 --- /dev/null +++ b/idps/database.py @@ -0,0 +1,74 @@ +import mysql.connector + + +class Database: + """Classe pour effectuer les actions liées à la base de données (envoi d'alertes...)""" + + def __init__(self, config): + """Connexion à la base de données à partir des identifiants dans la config""" + self.conn = mysql.connector.connect(host=config["db_host"], database=config["db_database"], user=config["db_user"], password=config["db_password"], port=config["db_port"]) + self.config = config + + def send_alert(self, date_alert = None, agent_severity = None, device_event_class_id = None, + name = None, src = None, dst = None, dpt = None, spt = None, msg = None, + proto = None, bytesin = None, bytesout = None, reason = None, act = None): + """Ajoute une alerte dans la base de données + @param date_alert: Timestamp de l'alerte + @param agent_severity: Criticité de l'alerte (0 - 10) + @param device_event_class_id: Identifiant de signature, pour le moteur de corrélation + @param name: Nom descriptif de l'alerte + @param src: Adresse IP source + @param dst: Adresse IP destination + @param dpt: Port de destination + @param spt: Port source + @param msg: Champ de texte pour des notes ou commentaires additionnels + @param proto: Protocol couche 4 (réseau) utilisé + @param bytesin: Quantité de bytes (8 bits ici) entrants + @param bytesout: Quantité de bytes (8 bits ici) sortants + @param reason: Description de la raison de l'alerte + @param act: Action prise en réponse de l'alerte + """ + + try: + cursor = self.conn.cursor() + sql_query = """ + INSERT INTO alertes ( + cef_version, date_alerte, agent_severity, device_event_class_id, + device_product, device_vendor, device_version, name, dst, src, + dpt, spt, msg, proto, bytesin, bytesout, reason, act + ) VALUES ( + %(cef_version)s, %(date_alerte)s, %(agent_severity)s, %(device_event_class_id)s, + %(device_product)s, %(device_vendor)s, %(device_version)s, %(name)s, %(dst)s, + %(src)s, %(dpt)s, %(spt)s, %(msg)s, %(proto)s, %(bytesin)s, %(bytesout)s, + %(reason)s, %(act)s + ); + """ + + # Paramètres pour la requête SQL + params = { + "cef_version": self.config.get("cef_version", 1), + "date_alerte": date_alert, + "agent_severity": agent_severity, + "device_event_class_id": device_event_class_id, + "device_product": self.config.get("device_product", "SIDPS"), + "device_vendor": self.config.get("device_vendor", "ArKa"), + "device_version": self.config.get("device_version", "vAlpha"), + "name": name, + "src": src, + "dst": dst, + "dpt": dpt, + "spt": spt, + "msg": msg, + "proto": proto, + "bytesin": bytesin, + "bytesout": bytesout, + "reason": reason, + "act": act + } + + # Exécution de la requête d'insertion + cursor.execute(sql_query, params) + self.conn.commit() + cursor.close() + except mysql.connector.Error as err: + print("Erreur lors de l'envoi de l'alerte: {}".format(err)) diff --git a/idps/main.py b/idps/main.py new file mode 100644 index 0000000..379e8fb --- /dev/null +++ b/idps/main.py @@ -0,0 +1,149 @@ +import importlib.util +import os +import time +import tcp +import database +import json +import protection + +from scapy.all import sniff, TCP, IP +from scapy.config import conf +conf.debug_dissector = 2 + + +def check_frame_w_rules(packet, rules_functions, objets): + """Appliquer chaque règle des fonctions au paquet capturé. + @param packet: Paquet actuel à analyser + @param rules_functions: liste de fonctions de règles + @param objets: Dictionnaire contenant le dictionnaire de config, la liste des paquets tcp précédents, + la db, et le gestionnaire de règles Iptables""" + + for rule_func in rules_functions: + try: + rule_func(packet, objets) + except Exception as e: + print(f"Erreur lors de l'exécution de la règle : {e}") + + +def packet_callback(packet, rules_functions, objets): + """Callback réception d'un paquet + @param packet: Paquet actuel à classer + @param rules_functions: liste des fonctions de règles + @param objets: Dictionnaire contenant le dictionnaire de config, la liste des paquets tcp précédents, + la db, et le gestionnaire de règles Iptables""" + + # Nettoyage des règles et paquets TCP dépassé + objets["iptables_manager"].del_rules() + objets["tcp_packets"].clean_old_packets() + + if IP in packet and TCP in packet: + packet_origin = objets["tcp_packets"].add_packet(packet[IP].src, packet[TCP].sport, packet[IP].dst, packet[TCP].dport, packet[TCP].flags, time.time()) + + # Stockage du paquet originel lié à ce paquet pour identifier la provenance de l'attaque + objets['pkt_origin'] = packet_origin + + check_frame_w_rules(packet, rules_functions['TCP'], objets) + + +def load_rules(rules_dirpath = "/app/idps/rules"): + """Charger les fonctions de règles du répertoire de règles et des sous répertoires + @param rules_dirpath: Répertoire contenant les fichiers python de règles + """ + + if not os.path.exists(rules_dirpath): + raise ValueError(f"Le chemin spécifié n'existe pas: {rules_dirpath}") + + if not os.path.isdir(rules_dirpath): + raise ValueError(f"Le chemin spécifié n'est pas un répertoire: {rules_dirpath}") + + rules_functions = {} + # Liste de répertoires à explorer + dirs_to_explore = [rules_dirpath] + + # Explorer chaque répertoire / sous répertoire à la rechercher de fichier de règles + while dirs_to_explore: + current_dir = dirs_to_explore.pop() + + try: + for entry in os.scandir(current_dir): + # Ignorer les liens symboliques + if entry.is_symlink(): + continue + + # Ajouter les répertoires dans la liste à explorer + if entry.is_dir(): + dirs_to_explore.append(entry.path) + elif entry.is_file() and entry.name.endswith(".py"): + # Suppression de l'extension .py + module_name = entry.name[:-3] + + # Déterminer le protocole à partir du répertoire parent + if "TCP" in entry.path: + parent_dir = "TCP" + else: + parent_dir = "WTF" + + if parent_dir not in rules_functions: + rules_functions[parent_dir] = [] + + # Chargement du module + spec = importlib.util.spec_from_file_location(module_name, entry.path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # Vérification que le module possède bien la fonction rule + if hasattr(module, "rule"): + rules_functions[parent_dir].append(module.rule) + + except PermissionError: + raise PermissionError(f"Permission refusée pour accéder au répertoire: {current_dir}") + except OSError as e: + raise OSError(f"Erreur lors de l'accès au répertoire {current_dir}: {e}") + + return rules_functions + + +def read_config(config_filepath='config.json'): + """Charge les configurations depuis le fichier de config""" + + try: + with open(config_filepath, 'r', encoding='utf-8') as file: + config = json.load(file) + return config + except FileNotFoundError: + raise FileNotFoundError(f"Le fichier JSON {config_filepath} n'a pas été trouvé.") + except json.JSONDecodeError: + raise json.JSONDecodeError("Erreur lors de la lecture du fichier JSON. Le format peut être incorrect.") + + +def start_idps(): + """Charge les règles et démarre l'IDPS""" + + print("Récupération des configurations") + config = read_config() + print("Configurations chargées") + + print("Chargement des règles...") + rules_functions = load_rules(config["rules_dirpath"]) + print("Les règles sont chargées") + + print("Connexion à la base de données") + db = database.Database(config) + print("Connexion réussite à la base de données") + + tcp_packets = tcp.TCP(300) + protection_system = protection.Protection(config["protection"]) + + objets = {"config": config, "database": db, "tcp_packets": tcp_packets, "iptables_manager": protection_system} + + # Lancer scapy & envoyer le paquet à chaque règle de l'IDPS + sniff(iface=config["ifaces"], prn=lambda packet: packet_callback(packet, rules_functions, objets), store=0) + + +def main(): + print("Démarrage de l'IDPS") + start_idps() + + +if __name__ == "__main__": + main() diff --git a/idps/protection.py b/idps/protection.py new file mode 100644 index 0000000..aef2c50 --- /dev/null +++ b/idps/protection.py @@ -0,0 +1,49 @@ +import subprocess +import time + + +class Protection: + """Classe pour activer la protection du système avec iptables""" + + def __init__(self, activate = 0): + """Initialisation de la protection avec une liste pour stockées les règles créer""" + self.rules = [] + self.activate = int(activate) + + def add_rule(self, rule, duration): + """Ajouter une règle dans iptables + @param rule: Règle à ajouter + @param duration: Durée d'execution de la règle""" + + print(f"Rule: {rule}, {duration}, {self.activate}") + if self.activate == 0: + return + + print("Rule run") + try: + subprocess.run(rule.split(' '), check=True) + print(f"[iptables] Règle ajouter {rule}") + self.rules.append([rule, time.time(), duration]) + except subprocess.CalledProcessError as e: + print(f"[iptables] Erreur suppression de la règle {rule}: {e}") + + def del_rules(self): + """Supprimer les règles obsolètes de l'iptables""" + + if self.activate is False: + return + + curr = time.time() + for i, elt in enumerate(self.rules, 0): + if elt[1] + elt[2] <= curr: + self.del_rule(self, elt[0].replace("-I", "-D"), i) + + def del_rule(self, rule, i): + """Supprimer une règle dans iptables""" + + try: + subprocess.run(rule.split(' '), check=True) + print("[iptables] Règle supprimer {rule}") + self.rules.pop(i) + except subprocess.CalledProcessError as e: + print(f"[iptables] Erreur suppression de la règle {rule}: {e}") diff --git a/idps/rules/TCP/DOS/syndos.py b/idps/rules/TCP/DOS/syndos.py new file mode 100644 index 0000000..22ad701 --- /dev/null +++ b/idps/rules/TCP/DOS/syndos.py @@ -0,0 +1,39 @@ +from datetime import datetime +import time + + +def rule(packet, objets): + """ + Règle SYN Dos: + Un SYN DOS va envoyer des requêtes TCP avec le flag SYN en très grand nombre afin de surcharger le serveur ou la cible avec des grosses quantités de données + - Si le port est ouvert, le serveur répond avec: SYN-ACK, puis le client RESET la connexion. + - Si le port est fermé, le serveur répond avec: RST-ACK. + """ + + # Ne pas réagir si dans la période de cooldown + if (rule.cooldown + rule.time_window > time.time()): + return + + # Initialisation des paramètres à partir de la configuration si nécessaire + if (rule.seuil == 0 and rule.time_window == 0 and rule.ban_time == 0): + rule.time_window = objets["config"].get("synsdos_time", 60) + rule.seuil = objets["config"].get("syndos_count", 100) + rule.ban_time = objets["config"].get("syndos_bantime", 300) + + # Comptage des paquets TCP correspondant aux motifs spécifiques + syndeny_count = objets["tcp_packets"].count_packet_of_type(["S", "RA"], rule.time_window, True) + synaccept_count = objets["tcp_packets"].count_packet_of_type(["S", "SA", "R"], rule.time_window, True) + + # Détection si le seuil est dépassé et que la charge utile des paquets est non nulle + if (syndeny_count + synaccept_count >= rule.seuil and len(packet['TCP'].payload) > 0): + objets["database"].send_alert(datetime.now(), 5, None, "Syn DOS", objets["pkt_origin"][0], objets["pkt_origin"][2], proto="TCP", reason="Détection de nombreux patterns de Syn->SynACK->Reset et Syn->Reset ACK", act="Alerte") + objets["iptables_manager"].add_rule("iptables -I FORWARD -s " + objets["pkt_origin"][0] + " -j DROP", rule.ban_time) + print("Alerte, seuil dépassé, risque de Syn DOS détecté.") + rule.cooldown = time.time() + + +# Variables statiques +rule.cooldown = 0 +rule.time_window = 0 +rule.seuil = 0 +rule.ban_time = 0 diff --git a/idps/rules/TCP/DOS/synflood.py b/idps/rules/TCP/DOS/synflood.py new file mode 100644 index 0000000..8cc895b --- /dev/null +++ b/idps/rules/TCP/DOS/synflood.py @@ -0,0 +1,39 @@ +from datetime import datetime +import time + + +def rule(packet, objets): + """ + Règle SYN Flood: + Un SYN Flood va envoyer des requêtes TCP avec le flag SYN en très grand nombre afin de surcharger le serveur ou la cible + - Si le port est ouvert, le serveur répond avec: SYN-ACK, puis le client RESET la connexion. + - Si le port est fermé, le serveur répond avec: RST-ACK. + """ + + # Ne pas réagir si dans la période de cooldown + if (rule.cooldown + rule.time_window > time.time()): + return + + # Initialisation des paramètres à partir de la configuration si nécessaire + if (rule.seuil == 0 and rule.time_window == 0 and rule.ban_time == 0): + rule.time_window = objets["config"].get("synsflood_time", 60) + rule.seuil = objets["config"].get("synflood_count", 100) + rule.ban_time = objets["config"].get("synflood_bantime", 300) + + # Comptage des paquets TCP correspondant aux motifs spécifiques + syndeny_count = objets["tcp_packets"].count_packet_of_type(["S", "RA"], rule.time_window, True) + synaccept_count = objets["tcp_packets"].count_packet_of_type(["S", "SA", "R"], rule.time_window, True) + + # Détection si le seuil est dépassé + if (syndeny_count + synaccept_count >= rule.seuil): + objets["database"].send_alert(datetime.now(), 5, None, "Syn flood", objets["pkt_origin"][0], objets["pkt_origin"][2], proto="TCP", reason="Détection de nombreux patterns de Syn->SynACK->Reset et Syn->Reset ACK", act="Alerte") + objets["iptables_manager"].add_rule("iptables -I FORWARD -s " + objets["pkt_origin"][0] + " -j DROP", rule.ban_time) + print("Alerte, seuil dépassé, risque de Syn Flood détecté.") + rule.cooldown = time.time() + + +# Variables statiques +rule.cooldown = 0 +rule.time_window = 0 +rule.seuil = 0 +rule.ban_time = 0 diff --git a/idps/rules/TCP/DOS/tcpconnectflood.py b/idps/rules/TCP/DOS/tcpconnectflood.py new file mode 100644 index 0000000..d1536da --- /dev/null +++ b/idps/rules/TCP/DOS/tcpconnectflood.py @@ -0,0 +1,36 @@ +from datetime import datetime +import time + + +def rule(packet, objets): + """Règle TCPConnect Flood: + Un flood TCP connect va effectuer une connexion TCP en très grand nombre + Si le port est ouvert le serveur acceptera la connexion SYN -> SYN ACK -> ACK -> Reset ACK + Sinon le port est fermé et le serveur refusera la connexion SYN -> Reset ACK + """ + + if (rule.cooldown + rule.time_window > time.time()): + return + + # Vérification si nécessaire de récupérer les variables depuis la config + if (rule.seuil == 0 and rule.time_window == 0 and rule.ban_time == 0): + rule.time_window = objets["config"].get("tcpconnectflood_time", 60) + rule.seuil = objets["config"].get("tcpconnectflood_count", 100) + rule.ban_time = objets["config"].get("tcpconnectflood_bantime", 300) + + # Comptage du nombre de scan tcp connect acceptés et refusés + tcpconnectdeny_count = objets["tcp_packets"].count_packet_of_type(["S", "RA"], rule.time_window, True) + tcpconnectaccept_count = objets["tcp_packets"].count_packet_of_type(["S", "SA", "A", "RA"], rule.time_window, True) + + if (tcpconnectaccept_count + tcpconnectdeny_count >= rule.seuil): + objets["database"].send_alert(datetime.now(), 5, None, "TCPConnect Flood", objets["pkt_origin"][0], objets["pkt_origin"][2], proto="TCP", reason="Détection de nombreux patterns de Syn->SynACK->ACK->Reset ACK et Syn->Reset ACK", act="Alerte") + objets["iptables_manager"].add_rule("iptables -I FORWARD -s " + objets["pkt_origin"][0] + " -j DROP", rule.ban_time) + print("Alerte, seuils dépassés, risque de TCPconnect Flood") + rule.cooldown = time.time() + + +# Variables statiques +rule.cooldown = 0 +rule.time_window = 0 +rule.seuil = 0 +rule.ban_time = 0 diff --git a/idps/rules/TCP/Data/dataExfiltration.py b/idps/rules/TCP/Data/dataExfiltration.py new file mode 100644 index 0000000..10b285d --- /dev/null +++ b/idps/rules/TCP/Data/dataExfiltration.py @@ -0,0 +1,63 @@ +from collections import defaultdict +from scapy.all import IP +import time +from datetime import datetime + +data_transfer = defaultdict(lambda: {"current": 0, "daily": 0, "last_reset": time.time()}) + +def rule(packet, _, db): + """ + Règle pour détecter une exfiltration de données importantes. + Actuellement, ne fonctionne pas pour un débit supérieur à 4Mo + """ + global data_transfer + + if IP in packet: + src_ip = packet[IP].src + dst_ip = packet[IP].dst + payload_size = len(packet[IP].payload) + + # Mettre à jour le volume de données transférées + current_time = time.time() + if current_time - data_transfer[src_ip]["last_reset"] > rule.reset_time: + data_transfer[src_ip]["daily"] = 0 + data_transfer[src_ip]["last_reset"] = current_time + + data_transfer[src_ip]["current"] += payload_size + data_transfer[src_ip]["daily"] += payload_size + + # Exfiltration de données instantané + if data_transfer[src_ip]["current"] > rule.seuil_session: + db.send_alert( + datetime.now(), + 5, + None, + "Exfiltration de données détectée (instantané)", + src_ip, + dst_ip, + proto = "TCP", + reason="Exfiltration de données détectée (instantané)", + act="Alerte" + ) + data_transfer[src_ip]["current"] = 0 # Réinitialiser pour les prochaines sessions + print(f"Alerte, data transfer, transfert instantané important") + + # Exfiltration de données journalière + if data_transfer[src_ip]["daily"] > rule.seuil_journalier: + db.send_alert( + datetime.now(), + 5, + None, + "Exfiltration de données détectée (journalière)", + src_ip, + dst_ip, + proto = "TCP", + reason="Exfiltration de données détectée (journalière)", + act="Alerte" + ) + print(f"Alerte, data transfer, transfert journalier important") + + +rule.reset_time = 24 * 3600 # 24 heures en secondes +rule.seuil_session = 0.5 * 1024 * 1024 * 1024 # 500 Mo en octets +rule.seuil_journalier = 50 * 1024 * 1024 * 1024 # 50 Go en octets \ No newline at end of file diff --git a/idps/rules/TCP/Data/lateCommunication.py b/idps/rules/TCP/Data/lateCommunication.py new file mode 100644 index 0000000..5645449 --- /dev/null +++ b/idps/rules/TCP/Data/lateCommunication.py @@ -0,0 +1,48 @@ +from datetime import datetime, time, timedelta +from scapy.all import IP + +# Dictionnaire pour stocker les dernières alertes envoyées pour chaque IP +last_alert_time = {} + +def rule(packet, _, db): + """ + Règle pour détecter l'activité réseau entre une plage horaire donnée. + """ + global last_alert_time + + if IP in packet: + + src_ip = packet[IP].src + dst_ip = packet[IP].dst + + # Obtenir l'heure actuelle + current_time = datetime.now() + + # Vérifier si l'heure est dans la plage + if rule.start_time <= current_time.time() or current_time.time() <= rule.end_time: + # Vérifier si une alerte a déjà été envoyée récemment pour cette IP + if src_ip in last_alert_time: + time_since_last_alert = current_time - last_alert_time[src_ip] + if time_since_last_alert < timedelta(minutes=5): # 5 minutes de délai + return # Ne pas envoyer une nouvelle alerte + + # Envoyer une alerte + db.send_alert( + current_time, + 5, + None, + f"Activité réseau détectée entre {rule.start_time} et {rule.end_time}", + src_ip, + dst_ip, + proto="TCP", + reason=f"Activité réseau à {current_time.time()}", + act="Alerte" + ) + print(f"Alerte : activité réseau détectée à {current_time.time()} entre {src_ip} et {dst_ip}") + + # Mettre à jour le temps de la dernière alerte pour cette IP + last_alert_time[src_ip] = current_time + +# Définir la plage horaire +rule.start_time = time(21, 0) +rule.end_time = time(6, 0) diff --git a/idps/rules/TCP/Scan/ackscan.py b/idps/rules/TCP/Scan/ackscan.py new file mode 100644 index 0000000..916d2ff --- /dev/null +++ b/idps/rules/TCP/Scan/ackscan.py @@ -0,0 +1,35 @@ +from datetime import datetime +import time + + +def rule(packet, objets): + """Règle ACK Scan: + Un ACK Scan va envoyer des requêtes TCP avec le flag ACK + Si le firewall ne bloque pas, alors il répond avec le flag Reset + Sinon il répond rien + """ + if (rule.cooldown + rule.time_window > time.time()): + return + + # Vérification si nécessaire de récupérer les variables depuis la config + if (rule.seuil == 0 and rule.time_window == 0 and rule.ban_time == 0): + rule.time_window = objets["config"].get("ackscan_time", 180) + rule.seuil = objets["config"].get("ackscan_count", 5) + rule.ban_time = objets["config"].get("ackscan_bantime", 300) + + # Comptage nombre de scan ack acceptés et refusés + ackdeny_count = objets["tcp_packets"].count_packet_of_type(["A", "R"], rule.time_window, True) + ackaccept_count = objets["tcp_packets"].count_packet_of_type(["A"], rule.time_window, True) + + if (ackaccept_count + ackdeny_count >= rule.seuil): + objets["database"].send_alert(datetime.now(), 5, None, "ACK scan", objets["pkt_origin"][0], objets["pkt_origin"][2], proto="TCP", reason="Détection de nombreux patterns de Ack->Reset et Ack pas de réponse", act="Alerte") + print("Alerte, seuil dépassés, risque d'Ack scan") + objets["iptables_manager"].add_rule("iptables -I FORWARD -s " + objets["pkt_origin"][0] + " -j DROP", rule.ban_time) + rule.cooldown = time.time() + + +# Variables statiques +rule.cooldown = 0 +rule.time_window = 0 +rule.seuil = 0 +rule.ban_time = 0 diff --git a/idps/rules/TCP/Scan/finscan.py b/idps/rules/TCP/Scan/finscan.py new file mode 100644 index 0000000..b3a4801 --- /dev/null +++ b/idps/rules/TCP/Scan/finscan.py @@ -0,0 +1,36 @@ +from datetime import datetime +import time + + +def rule(packet, objets): + """Règle Fin Scan: + Un Fin Scan va envoyer des requêtes TCP avec le flag Fin + Si le port est ouvert alors le serveur répondra pas + Sinon le port est fermé et le serveur répondra: Reset ACK + """ + + if (rule.cooldown + rule.time_window > time.time()): + return + + # Vérification si nécessaire de récupérer les variables depuis la config + if (rule.seuil == 0 and rule.time_window == 0 and rule.ban_time == 0): + rule.time_window = objets["config"].get("finscan_time", 180) + rule.seuil = objets["config"].get("finscan_count", 5) + rule.ban_time = objets["config"].get("finscan_bantime", 300) + + # Comptage du nombre de scan fin acceptés et refusés + findeny_count = objets["tcp_packets"].count_packet_of_type(["F", "RA"], rule.time_window, True) + finaccept_count = objets["tcp_packets"].count_packet_of_type(["F"], rule.time_window, True) + + if (findeny_count + finaccept_count >= rule.seuil): + objets["database"].send_alert(datetime.now(), 5, None, "Fin scan", objets["pkt_origin"][0], objets["pkt_origin"][2], proto="TCP", reason="Détection de nombreux patterns de Fin->Reset Ack et Fin->rien", act="Alerte") + objets["iptables_manager"].add_rule("iptables -I FORWARD -s " + objets["pkt_origin"][0] + " -j DROP", rule.ban_time) + print("Alerte, seuil dépassés, risque de Fin Scan") + rule.cooldown = time.time() + + +# Variables statiques +rule.cooldown = 0 +rule.time_window = 0 +rule.seuil = 0 +rule.ban_time = 0 diff --git a/idps/rules/TCP/Scan/nullscan.py b/idps/rules/TCP/Scan/nullscan.py new file mode 100644 index 0000000..beb9427 --- /dev/null +++ b/idps/rules/TCP/Scan/nullscan.py @@ -0,0 +1,35 @@ +from datetime import datetime +import time + + +def rule(packet, objets): + """Règle Null Scan: + Un Null Scan va envoyer des requêtes TCP avec aucun flag d'actif + Si le port est ouvert alors le serveur ne répondra pas + Sinon le port est fermé et le serveur répondra: Reset ACK + """ + if (rule.cooldown + rule.time_window > time.time()): + return + + # Vérification si nécessaire de récupérer les variables depuis la config + if (rule.seuil == 0 and rule.time_window == 0 and rule.ban_time == 0): + rule.time_window = objets["config"].get("nullscan_time", 180) + rule.seuil = objets["config"].get("nullscan_count", 5) + rule.ban_time = objets["config"].get("nullscan_bantime", 300) + + # Comptage du nombre de scan null acceptés et refusés + nulldeny_count = objets["tcp_packets"].count_packet_of_type(["", "RA"], rule.time_window, True) + nullaccept_count = objets["tcp_packets"].count_packet_of_type([""], rule.time_window, True) + + if (nullaccept_count + nulldeny_count >= rule.seuil): + objets["database"].send_alert(datetime.now(), 5, None, "Null scan", objets["pkt_origin"][0], objets["pkt_origin"][2], proto="TCP", reason="Détection de nombreux patterns de None->Reset Ack et None -> rien", act="Alerte") + objets["iptables_manager"].add_rule("iptables -I FORWARD -s " + objets["pkt_origin"][0] + " -j DROP", rule.ban_time) + print("Alerte, seuil dépassés, risque de Null Scan") + rule.cooldown = time.time() + + +# Variables statiques +rule.cooldown = 0 +rule.time_window = 0 +rule.seuil = 0 +rule.ban_time = 0 diff --git a/idps/rules/TCP/Scan/synscan.py b/idps/rules/TCP/Scan/synscan.py new file mode 100644 index 0000000..2c522bc --- /dev/null +++ b/idps/rules/TCP/Scan/synscan.py @@ -0,0 +1,35 @@ +from datetime import datetime +import time + + +def rule(packet, objets): + """Règle SYN Scan: + Un SYNScan va envoyer des requêtes TCP avec le flag SYN + Si le port est ouvert alors le serveur répondra: Syn ACK, puis le client Reset la connexion + Sinon le port est fermé et le serveur répondra: Reset ACK + """ + if (rule.cooldown + rule.time_window > time.time()): + return + + # Vérification si nécessaire de récupérer les variables depuis la config + if (rule.seuil == 0 and rule.time_window == 0 and rule.ban_time == 0): + rule.time_window = objets["config"].get("synscan_time", 180) + rule.seuil = objets["config"].get("synscan_count", 5) + rule.ban_time = objets["config"].get("synscan_bantime", 300) + + # Comptage du nombre de scan syn acceptés et refusés + syndeny_count = objets["tcp_packets"].count_packet_of_type(["S", "RA"], rule.time_window, True) + synaccept_count = objets["tcp_packets"].count_packet_of_type(["S", "SA", "R"], rule.time_window, True) + + if (synaccept_count + syndeny_count >= rule.seuil): + objets["database"].send_alert(datetime.now(), 5, None, "Syn scan", objets["pkt_origin"][0], objets["pkt_origin"][2], proto="TCP", reason="Détection de nombreux patterns de Syn->SynACK->Reset et Syn->Reset ACK", act="Alerte") + objets["iptables_manager"].add_rule("/sbin/iptables -I FORWARD -s " + objets["pkt_origin"][0] + " -j DROP", rule.ban_time) + print("Alerte, seuil dépassés, risque de SynScan") + rule.cooldown = time.time() + + +# Variables statiques +rule.cooldown = 0 +rule.time_window = 0 +rule.seuil = 0 +rule.ban_time = 0 diff --git a/idps/rules/TCP/Scan/tcpconnectscan.py b/idps/rules/TCP/Scan/tcpconnectscan.py new file mode 100644 index 0000000..24bf1fa --- /dev/null +++ b/idps/rules/TCP/Scan/tcpconnectscan.py @@ -0,0 +1,36 @@ +from datetime import datetime +import time + + +def rule(packet, objets): + """Règle TCPConnect Scan: + Un scan TCP connect va effectuer une connexion TCP en entier sur chaque port scanné. + Si le port est ouvert le serveur acceptera la connexion SYN -> SYN ACK -> ACK -> Reset ACK + Sinon le port est fermé et le serveur refusera la connexion SYN -> Reset ACK + """ + + if (rule.cooldown + rule.time_window > time.time()): + return + + # Vérification si nécessaire de récupérer les variables depuis la config + if (rule.seuil == 0 and rule.time_window == 0 and rule.ban_time == 0): + rule.time_window = objets["config"].get("tcpconnectscan_time", 180) + rule.seuil = objets["config"].get("tcpconnectscan_count", 5) + rule.ban_time = objets["config"].get("tcpconnectscan_bantime", 300) + + # Comptage du nombre de scan tcp connect acceptés et refusés + tcpconnectdeny_count = objets["tcp_packets"].count_packet_of_type(["S", "RA"], rule.time_window, True) + tcpconnectaccept_count = objets["tcp_packets"].count_packet_of_type(["S", "SA", "A", "RA"], rule.time_window, True) + + if (tcpconnectaccept_count + tcpconnectdeny_count >= rule.seuil): + objets["database"].send_alert(datetime.now(), 5, None, "TCPConnect Scan", objets["pkt_origin"][0], objets["pkt_origin"][2], proto="TCP", reason="Détection de nombreux patterns de Syn->SynACK->ACK->Reset ACK et Syn->Reset ACK", act="Alerte") + objets["iptables_manager"].add_rule("iptables -I FORWARD -s " + objets["pkt_origin"][0] + " -j DROP", rule.ban_time) + print("Alerte, seuils dépassés, risque de TCPConnectScan") + rule.cooldown = time.time() + + +# Variables statiques +rule.cooldown = 0 +rule.time_window = 0 +rule.seuil = 0 +rule.ban_time = 0 diff --git a/idps/rules/TCP/Scan/xmasscan.py b/idps/rules/TCP/Scan/xmasscan.py new file mode 100644 index 0000000..4709e8f --- /dev/null +++ b/idps/rules/TCP/Scan/xmasscan.py @@ -0,0 +1,35 @@ +from datetime import datetime +import time + + +def rule(packet, objets): + """Règle XMAS Scan: + Un XMAS Scan va envoyer des requêtes TCP avec le flag Fin, Push, Urg + Si le port est ouvert alors le serveur répondra pas + Sinon le port est fermé et le serveur répondra: Reset ACK + """ + if (rule.cooldown + rule.time_window > time.time()): + return + + # Vérification si nécessaire de récupérer les variables depuis la config + if (rule.seuil == 0 and rule.time_window == 0 and rule.ban_time == 0): + rule.time_window = objets["config"].get("xmasscan_time", 180) + rule.seuil = objets["config"].get("xmasscan_count", 5) + rule.ban_time = objets["config"].get("xmasscan_bantime", 300) + + # Comptage du nombre de scan XMAS acceptés et refusés + xmasdeny_count = objets["tcp_packets"].count_packet_of_type(["FPU", "RA"], rule.time_window, True) + xmasaccept_count = objets["tcp_packets"].count_packet_of_type(["FPU"], rule.time_window, True) + + if (xmasaccept_count + xmasdeny_count >= rule.seuil): + objets["database"].send_alert(datetime.now(), 5, None, "XMAS scan", objets["pkt_origin"][0], objets["pkt_origin"][2], proto="TCP", reason="Détection de nombreux patterns de Fin Push Urg -> rien et Fin Push Urg->Reset ACK", act="Alerte") + objets["iptables_manager"].add_rule("iptables -I FORWARD -s " + objets["pkt_origin"][0] + " -j DROP", rule.ban_time) + print("Alerte, seuil dépassés, risque de XMAS Scan") + rule.cooldown = time.time() + + +# Variables statiques +rule.cooldown = 0 +rule.time_window = 0 +rule.seuil = 0 +rule.ban_time = 0 diff --git a/idps/tcp.py b/idps/tcp.py new file mode 100644 index 0000000..ca6d7ae --- /dev/null +++ b/idps/tcp.py @@ -0,0 +1,161 @@ +import time + + +class TCP: + def __init__(self, clean_time=300): + """Constructeur de la classe TCP + @param clean_time: temps avant qu'un paquet soit nettoyé""" + + self.packets = {} + self.clean_time = clean_time + + def add_packet(self, ip_src, port_src, ip_dst, port_dst, flags, timestamp): + """Ajoute le suivi d'un paquet dans le dictionnaire""" + + timestamp = int(timestamp) + + # Initialisation de la liste de paquets pour l'IP source + if ip_src not in self.packets: + self.packets[ip_src] = [] + + if flags == "S": + self.packets[ip_src].append([port_src, ip_dst, port_dst, ["S"], timestamp]) + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst) + + elif flags is None: + self.packets[ip_src].append([port_src, ip_dst, port_dst, [""], timestamp]) + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst) + + elif flags == "FPU": + self.packets[ip_src].append([port_src, ip_dst, port_dst, ["FPU"], timestamp]) + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst) + + elif flags == "SA": + i, ip = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "S") + + if i is not None: + self.packets[ip][i][3].append("SA") + self.packets[ip][i][4] = timestamp + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst, ip) + else: + self.packets[ip_src].append([port_src, ip_dst, port_dst, ["SA"], timestamp]) + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst) + + elif flags == "A": + i, ip = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "SA") + if i is None: + i, ip = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "R") + + if i is not None: + self.packets[ip][i][3].append("A") + self.packets[ip][i][4] = timestamp + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst, ip) + else: + self.packets[ip_src].append([port_src, ip_dst, port_dst, ["A"], timestamp]) + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst) + + elif flags == "RA": + i, ip = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "A") + + if i is None: + i, ip = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "S") + + if i is not None: + self.packets[ip][i][3].append("RA") + self.packets[ip][i][4] = timestamp + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst, ip) + else: + self.packets[ip_src].append([port_src, ip_dst, port_dst, ["RA"], timestamp]) + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst) + + elif flags == "R": + i, ip = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "A") + + if i is None: + i, ip = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "S") + + if i is not None: + self.packets[ip][i][3].append("R") + self.packets[ip][i][4] = timestamp + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst, ip) + else: + self.packets[ip_src].append([port_src, ip_dst, port_dst, ["R"], timestamp]) + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst) + + elif flags == "F": + i, ip = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "A") + + if i is not None: + self.packets[ip][i][3].append("F") + self.packets[ip][i][4] = timestamp + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst, ip) + else: + self.packets[ip_src].append([port_src, ip_dst, port_dst, ["F"], timestamp]) + return self.return_origin_packet(ip_src, port_src, ip_dst, port_dst) + + def find_packet_to_replace(self, ip_src, port_src, ip_dst, port_dst, flags): + """Cherche l'indice et le port de source du paquet dont le flag doit être remplacé""" + + # Recherche dans le sens src->dst + if ip_src in self.packets.keys(): + for i, [p_s, ip_d, p_d, f, stamp] in enumerate(self.packets[ip_src]): + if p_s == port_src and ip_d == ip_dst and p_d == port_dst and flags in f: + return i, ip_src + + # Recherche dans le sens dst->src + if ip_dst in self.packets.keys(): + for i, [p_d, ip_s, p_s, f, stamp] in enumerate(self.packets[ip_dst]): + if p_s == port_src and ip_s == ip_src and p_d == port_dst and flags in f: + return i, ip_dst + + return None, None + + def clean_old_packets(self): + """Supprime les paquets qui date de plus longtemps que le temps de clean""" + current_timestamp = time.time() + + # Parcours chaque ip_source de la liste + for ip_src in list(self.packets.keys()): + + # Vérification si le paquet doit être supprimé ou non + i = 0 + while i < len(self.packets[ip_src]): + packet = self.packets[ip_src][i] + if packet[4] <= current_timestamp - self.clean_time: + del self.packets[ip_src][i] + else: + i += 1 + + # Suppression de la case de l'ip source si elle n'existe plus + if not self.packets[ip_src]: + del self.packets[ip_src] + + def count_packet_of_type(self, flag, time_treshold, isList = False): + """Compte les paquets qui ont le flag choisi et qui sont dans la fenêtre de temps""" + count = 0 + + current_timestamp = time.time() + for ip in list(self.packets.keys()): + for packet in self.packets[ip]: + if isList and set(flag) == set(packet[3]) and packet[4] >= current_timestamp - time_treshold: + count += 1 + elif not isList and flag in packet[3] and packet[4] >= current_timestamp - time_treshold: + count += 1 + return count + + def __getitem__(self, src_ip): + """Retourne la liste des paquets liés à une adresse IP, pour du déboggage""" + + return self.packets.get(src_ip, None) + + def return_origin_packet(self, ip_src, port_src, ip_dst, port_dst, ip = None): + """Retourne le paquet d'origine par rapport à l'ip de référence""" + + if ip is None: + return [ip_src, port_src, ip_dst, port_dst] + else: + if ip == ip_src: + return [ip_src, port_src, ip_dst, port_dst] + elif ip == ip_dst: + return [ip_dst, port_dst, ip_src, port_src] + diff --git a/requirements.txt b/requirements.txt index f6832c6..6429e50 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -hiredis==3.0.0 -redis==5.2.0 +mysql-connector-python==9.1.0 +scapy=2.6.1 diff --git a/sql/cef-generator.py b/sql/cef-generator.py new file mode 100644 index 0000000..affabda --- /dev/null +++ b/sql/cef-generator.py @@ -0,0 +1,150 @@ +# Générateur d'alertes CEF (Common Event Format) +# Pratique pour le moteur de corrélation et le site web + +# Une alerte CEF est formattée de cette façon: +# CEF:Version|Device Vendor|Device Product|Device Version|Device Event Class ID|Name|Severity|[Extension] + +import mysql.connector +import time +import random +from datetime import datetime + +def generate_alert(alert_type): + # Dictionnaire pour différents types d'alertes réseau et fichiers + alert_templates = { + "network": { + "Syn Flood": { + "Device_event_class_id": "1001", + "name": "Syn Flood Detected", + "src": f"{generate_ip()}", + "dst": f"{generate_ip()}", + "agent_severity": "8" + }, + "Port Scanning": { + "Device_event_class_id": "1002", + "name": "Port Scanning Activity", + "src": f"{generate_ip()}", + "dst": f"{generate_ip()}", + "cs1": f"{generate_ports()}", + "agent_severity": "5" + } + }, + "file": { + "Suspicious File Creation": { + "Device_event_class_id": "2001", + "name": "Suspicious File Created", + "fname": f"{generate_filename()}", + "fsize": f"{random.randint(10, 1000)}kb", + "agent_severity": "7" + }, + "Critical File Deletion Attempt": { + "Device_event_class_id": "2002", + "name": "Critical File Deletion Attempt", + "fname": f"{generate_filename()}", + "agent_severity": "9" + } + } + } + + # Sélectionner le bon template en fonction du type d'alerte + category = "network" if alert_type in alert_templates["network"] else "file" + alert_info = alert_templates[category].get(alert_type, {}) + + if not alert_info: + raise ValueError(f"Unknown alert type: {alert_type}") + + return alert_info + +def generate_ip(): + # Générer une adresse IP aléatoire + return ".".join(str(random.randint(0, 255)) for _ in range(4)) + +def generate_ports(): + # Générer une liste de ports scannés + return ",".join(str(random.randint(20, 1024)) for _ in range(5)) + +def generate_filename(): + # Générer un nom de fichier aléatoire + filenames = ["config.txt", "database.db", "system32.dll", "passwd", "shadow", "sensitive_data.doc"] + return random.choice(filenames) + +def generate_alerts(conn, cursor, main_headers): + # Récupérer ces données depuis une fonction + alertes = ["Syn Flood", "Port Scanning"] #, "Suspicious File Creation", "Critical File Deletion Attempt"] + + while True: + data = generate_alert(random.choice(alertes)) + merged = main_headers.copy() + merged.update(data) + + # Préparer la requête SQL d'insertion + sql_query = """ + INSERT INTO alertes ( + cef_version, date_alerte, agent_severity, device_event_class_id, + device_product, device_vendor, device_version, name, dst, src, + dpt, spt, msg, proto, bytesin, bytesout, reason, act + ) VALUES ( + %(cef_version)s, %(date_alerte)s, %(agent_severity)s, %(device_event_class_id)s, + %(device_product)s, %(device_vendor)s, %(device_version)s, %(name)s, %(dst)s, + %(src)s, %(dpt)s, %(spt)s, %(msg)s, %(proto)s, %(bytesin)s, %(bytesout)s, + %(reason)s, %(act)s + ); + """ + + # Paramètres pour la requête SQL + params = { + "cef_version": merged["CEF"], + "date_alerte": datetime.now(), + "agent_severity": int(merged["agent_severity"]), + "device_event_class_id": None, + "device_product": merged["Device Product"], + "device_vendor": merged["Device Vendor"], + "device_version": merged["Device Version"], + "name": merged["name"], + "src": merged["src"], + "dst": merged["dst"], + "dpt": None, + "spt": None, + "msg": "Message", + "proto": "TCP", + "bytesin": None, + "bytesout": None, + "reason": "Activité suspecte", + "act": "Alert" + } + + # Exécution de la requête d'insertion + cursor.execute(sql_query, params) + conn.commit() + + # Attente avant de générer la prochaine alerte + time.sleep(random.randint(1, 10)) + +def main(): + # Connexion à la base de données MySQL/MariaDB + conn = mysql.connector.connect( + host="172.20.2.10", # À adapter selon votre configuration + database="sidps", # Nom de la base de données + user="sidps", # Nom d'utilisateur + password="SUPERPASSWORD", # Mot de passe + port=3306 # Port MySQL par défaut (peut être 3306 ou autre selon la configuration) + ) + + cursor = conn.cursor() + + main_headers = { + "CEF": 1, + "Device Vendor": "ArKa", + "Device Product": "SIDPS", + "Device Version": "vAlpha" + } + + # Lancer la génération d'alertes + generate_alerts(conn, cursor, main_headers) + + # Fermer la connexion à la base de données + cursor.close() + conn.close() + +if __name__ == "__main__": + main() diff --git a/sql/db-schema.sql b/sql/db-schema.sql new file mode 100644 index 0000000..94772e1 --- /dev/null +++ b/sql/db-schema.sql @@ -0,0 +1,32 @@ +#----------------------------------------------- +# Nettoyage des tables dans la base de données +#----------------------------------------------- + +DROP TABLE IF EXISTS alertes; + +#----------------------------------------------- +# Table: alertes +#---------------------------------------------- + +CREATE TABLE alertes ( + id SERIAL PRIMARY KEY, -- Identifiant unique pour chaque alerte + cef_version VARCHAR(10) DEFAULT 'CEF:1', -- Version du format CEF utilisé + date_alerte TIMESTAMP(3) NOT NULL, -- Date et heure de l'alerte avec une précision de millisecondes + agent_severity INT CHECK (agent_severity >= 0 AND agent_severity <= 10), -- Niveau de gravité de l'alerte sur une échelle de 0 à 10 + device_event_class_id VARCHAR(1023), -- Identifiant de la signature permettant d'aider les moteurs de corrélations + device_product VARCHAR(63), -- Nom du produit à l'origine de l'alerte + device_vendor VARCHAR(63), -- Nom du fournisseur ou fabricant du produit + device_version VARCHAR(31), -- Version du produit ou dispositif ayant généré l'alerte + name VARCHAR(512), -- Nom descriptif de l'alerte + -- Champ d'extension du CEF + dst VARCHAR(45), -- Adresse IP de destination impliquée dans l'alerte + src VARCHAR(45), -- Adresse IP source impliquée dans l'alerte + dpt INT, -- Port de destination utilisé pour l'événement ou l'alerte + spt INT, -- Port source de l'événement ou de l'alerte + msg VARCHAR(1023), -- Champ texte pour des notes ou commentaires additionnels concernant l'alerte + proto VARCHAR(10), -- Protocole réseau impliqué (ex : TCP, UDP) + bytesin INT, -- Quantité de bytes (8 bits ici) entrant (cas de flood ou DOS) + bytesout INT, -- Quantité des bytes (8 bits ici) sortants + reason VARCHAR(1023), -- Description de la raison de l'alerte expliquant pourquoi elle a été générée + act VARCHAR(50) -- Action entreprise en réponse à l'alerte (ex : bloqué, alerté uniquement, ...) +); diff --git a/sql/init.sql b/sql/init.sql new file mode 100644 index 0000000..61f7aa8 --- /dev/null +++ b/sql/init.sql @@ -0,0 +1,37 @@ +CREATE DATABASE IF NOT EXISTS sidps DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; +CREATE USER 'sidps'@'%' IDENTIFIED BY 'SUPERPASSWORD'; +GRANT ALL PRIVILEGES ON sidps.* TO 'sidps'@'%'; +FLUSH PRIVILEGES; +use sidps; + +#----------------------------------------------- +# Nettoyage des tables dans la base de données +#----------------------------------------------- + +DROP TABLE IF EXISTS alertes; + +#----------------------------------------------- +# Table: alertes +#---------------------------------------------- + +CREATE TABLE alertes ( + id SERIAL PRIMARY KEY, -- Identifiant unique pour chaque alerte + cef_version VARCHAR(10) DEFAULT 'CEF:1', -- Version du format CEF utilisé + date_alerte TIMESTAMP(3) NOT NULL, -- Date et heure de l'alerte avec une précision de millisecondes + agent_severity INT CHECK (agent_severity >= 0 AND agent_severity <= 10), -- Niveau de gravité de l'alerte sur une échelle de 0 à 10 + device_event_class_id VARCHAR(1023), -- Identifiant de la signature permettant d'aider les moteurs de corrélations + device_product VARCHAR(63), -- Nom du produit à l'origine de l'alerte + device_vendor VARCHAR(63), -- Nom du fournisseur ou fabricant du produit + device_version VARCHAR(31), -- Version du produit ou dispositif ayant généré l'alerte + name VARCHAR(512), -- Nom descriptif de l'alerte + dst VARCHAR(45), -- Adresse IP de destination impliquée dans l'alerte + src VARCHAR(45), -- Adresse IP source impliquée dans l'alerte + dpt INT, -- Port de destination utilisé pour l'événement ou l'alerte + spt INT, -- Port source de l'événement ou de l'alerte + msg VARCHAR(1023), -- Champ texte pour des notes ou commentaires additionnels concernant l'alerte + proto VARCHAR(10), -- Protocole réseau impliqué (ex : TCP, UDP) + bytesin INT, -- Quantité de bits entrant (cas de flood ou DOS) + bytesout INT, -- Quantité des bits sortants + reason VARCHAR(1023), -- Description de la raison de l'alerte expliquant pourquoi elle a été générée + act VARCHAR(50) -- Action entreprise en réponse à l'alerte (ex : bloqué, alerté uniquement, ...) +); diff --git a/tests/cef-generator.py b/tests/cef-generator.py deleted file mode 100644 index b228105..0000000 --- a/tests/cef-generator.py +++ /dev/null @@ -1,102 +0,0 @@ -# Générateur d'alertes CEF (Common Event Format) -# Pratique pour le moteur de corrélation et le site web - -# Une alerte CEF est formattée de cette façon: -# CEF:Version|Device Vendor|Device Product|Device Version|Device Event Class ID|Name|Severity|[Extension] - -import redis -import time -import random - -def generate_alert(alert_type): - # Dictionnaire pour différents types d'alertes réseau et fichiers - alert_templates = { - "network": { - "Syn Flood": { - "Device_event_class_id": "1001", - "name": "Syn Flood Detected", - "src" : f"{generate_ip()}", - "dst" : f"{generate_ip()}", - "agent_severity": "8" - }, - "Port Scanning": { - "Device_event_class_id": "1002", - "name": "Port Scanning Activity", - "src" : f"{generate_ip()}", - "dst" : f"{generate_ip()}", - "cs1" : f"{generate_ports()}", - "agent_severity": "5" - } - }, - "file": { - "Suspicious File Creation": { - "Device_event_class_id": "2001", - "name": "Suspicious File Created", - "fname" : f"{generate_filename()}", - "fsize" : f"{random.randint(10,1000)}kb", - "agent_severity": "7" - }, - "Critical File Deletion Attempt": { - "Device_event_class_id": "2002", - "name": "Critical File Deletion Attempt", - "fname" : f"{generate_filename()}", - "agent_severity": "9" - } - } - } - - # Sélectionner le bon template en fonction du type d'alerte - category = "network" if alert_type in alert_templates["network"] else "file" - alert_info = alert_templates[category].get(alert_type, {}) - - if not alert_info: - raise ValueError(f"Unknown alert type: {alert_type}") - - return alert_info - -def generate_ip(): - # Générer une adresse IP aléatoire - return ".".join(str(random.randint(0, 255)) for _ in range(4)) - -def generate_ports(): - # Générer une liste de ports scannés - return ",".join(str(random.randint(20, 1024)) for _ in range(5)) - -def generate_filename(): - # Générer un nom de fichier aléatoire - filenames = ["config.txt", "database.db", "system32.dll", "passwd", "shadow", "sensitive_data.doc"] - return random.choice(filenames) - -def generate_alerts(db, main_headers): - # Récupérer ces données depuis une fonction - alertes = ["Syn Flood", "Port Scanning", "Suspicious File Creation", "Critical File Deletion Attempt"] - - while (1): - data = generate_alert(random.choice(alertes)) - merged = main_headers.copy() - merged.update(data) - - # Ajout dans redis - response = db.xadd("logs:alertes", merged) - time.sleep(random.randint(1, 10)) - - -def main(): - - # Connexion à Redis (si besoin changer l'host et le port) - db = redis.Redis(host='localhost', port=6379, decode_responses=True) - - # Pour une db en production (https://redis.io/docs/latest/operate/oss_and_stack/management/security/acl/) - #db = redis.Redis(host="my-redis.cloud.redislabs.com", port=6379, username="default", password="secret", ssl=True, ssl_certfile="./redis_user.crt", ssl_keyfile="./redis_user_private.key", ssl_ca_certs="./redis_ca.pem") - - CEF_version=1 - Device_vendor="ArKa" - Device_product="SIDPS" - Device_version="vAlpha" - - main_headers = {"CEF": CEF_version, "Device Vendor" : Device_vendor, "Device Product" : Device_product, "Device_version" : Device_version} - - generate_alerts(db, main_headers) - -if __name__ == "__main__": - main()