mirror of
https://github.com/Oxbian/SIDPS.git
synced 2025-07-06 20:05:42 +02:00
feat: idps + détection scan TCPConnect, SynScan
This commit is contained in:
@ -6,7 +6,14 @@ RUN apk -U upgrade && \
|
||||
RUN pip install scapy
|
||||
|
||||
# Copier le script de l'idps
|
||||
#COPY idps.py /idps.py
|
||||
WORKDIR /app
|
||||
|
||||
# Lancer le script de détection
|
||||
#CMD ["python", "/idps.py"]
|
||||
# Copier le contenu du répertoire 'idps' du contexte de build vers '/app/idps' dans le conteneur
|
||||
COPY idps /app/idps
|
||||
|
||||
# Autres commandes nécessaires pour ton projet
|
||||
# Par exemple, pour installer des dépendances :
|
||||
# RUN pip install -r /app/idps/requirements.txt (si applicable)
|
||||
|
||||
# Commande par défaut
|
||||
CMD ["python", "/app/idps/main.py"]
|
||||
|
@ -3,8 +3,8 @@ services:
|
||||
# Attaquant 1
|
||||
atk1:
|
||||
build:
|
||||
context: Dockerfiles/.
|
||||
dockerfile: Dockerfile.attaquant
|
||||
context: ..
|
||||
dockerfile: Demo/Dockerfiles/Dockerfile.attaquant
|
||||
container_name: attaquant1
|
||||
command: sleep infinity
|
||||
networks:
|
||||
@ -15,8 +15,8 @@ services:
|
||||
# IDPS
|
||||
idps:
|
||||
build:
|
||||
context: Dockerfiles/.
|
||||
dockerfile: Dockerfile.idps
|
||||
context: ..
|
||||
dockerfile: Demo/Dockerfiles/Dockerfile.idps
|
||||
container_name: idps
|
||||
command: sleep infinity
|
||||
cap_add:
|
||||
|
@ -62,4 +62,4 @@ docker compose up -d
|
||||
|
||||
- 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 MySQL).
|
||||
- Moteur de corrélation des alertes (récupération + renvoi dans MySQL).
|
||||
|
108
idps/main.py
Normal file
108
idps/main.py
Normal file
@ -0,0 +1,108 @@
|
||||
from scapy.all import sniff, TCP, IP
|
||||
from scapy.config import conf
|
||||
conf.debug_dissector = 2
|
||||
import importlib.util
|
||||
import os
|
||||
import time
|
||||
import tcp
|
||||
|
||||
|
||||
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"""
|
||||
|
||||
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:
|
||||
print(f"Permission refusée pour accéder au répertoire: {current_dir}")
|
||||
except OSError as e:
|
||||
print(f"Erreur lors de l'accès au répertoire {current_dir}: {e}")
|
||||
|
||||
return rules_functions
|
||||
|
||||
|
||||
def check_frame_w_rules(packet, rules_functions, packets):
|
||||
"""Appliquer chaque règle des fonctions au paquet capturé."""
|
||||
|
||||
for rule_func in rules_functions:
|
||||
try:
|
||||
rule_func(packet, packets)
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de l'exécution de la règle : {e}")
|
||||
|
||||
|
||||
def packet_callback(packet, rules_functions, tcp_packets):
|
||||
#print(packet)
|
||||
if IP in packet and TCP in packet:
|
||||
tcp_packets.add_packet(packet[IP].src, packet[TCP].sport, packet[IP].dst, packet[TCP].dport, packet[TCP].flags, time.time())
|
||||
#print(tcp_packets[packet[IP].src])
|
||||
check_frame_w_rules(packet, rules_functions['TCP'], tcp_packets)
|
||||
tcp_packets.clean_old_packets()
|
||||
|
||||
|
||||
def start_idps(IDS_IFACES = ["eth0","eth1"]):
|
||||
"""Charge les règles et démarre l'IDPS"""
|
||||
print(f"Chargement des règles...")
|
||||
rules_functions = load_rules()
|
||||
print(f"Les règles sont chargées")
|
||||
|
||||
# Opti possible: charger les règles par protocole, permettant des filtrages et donc optimiser
|
||||
# le nombre de fonctions vérifiant le paquet (snort s'arrête à la première corrélation par exemple)
|
||||
|
||||
tcp_packets = tcp.TCP(300)
|
||||
|
||||
# Lancer scapy & envoyer le paquet à chaque règle de l'IDPS
|
||||
sniff(iface=IDS_IFACES, prn=lambda packet: packet_callback(packet, rules_functions, tcp_packets), store=0)
|
||||
#wrpcap("idps.pcap", capture)
|
||||
|
||||
|
||||
def main():
|
||||
print(f"Démarrage de l'IDPS")
|
||||
start_idps()
|
||||
print(f"IDPS opérationel")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
8
idps/rules/TCP/Scan/synscan.py
Normal file
8
idps/rules/TCP/Scan/synscan.py
Normal file
@ -0,0 +1,8 @@
|
||||
# Seuils
|
||||
TIME_WINDOW = 180
|
||||
NB_SEUIL = 5
|
||||
|
||||
|
||||
def rule(packet, tcp_packets):
|
||||
if (tcp_packets.count_packet_of_type("RA", TIME_WINDOW) + tcp_packets.count_packet_of_type("SA", TIME_WINDOW)) + tcp_packets.count_packet_of_type("R", TIME_WINDOW) >= NB_SEUIL:
|
||||
print(f"Alerte, seuil dépassés, risque de SynScan")
|
8
idps/rules/TCP/Scan/tcpconnectscan.py
Normal file
8
idps/rules/TCP/Scan/tcpconnectscan.py
Normal file
@ -0,0 +1,8 @@
|
||||
# Seuils
|
||||
TIME_WINDOW = 180 # 180 secondes pour avoir X paquets
|
||||
NB_SEUIL = 5
|
||||
|
||||
|
||||
def rule(packet, tcp_packets):
|
||||
if (tcp_packets.count_packet_of_type("A", TIME_WINDOW) + tcp_packets.count_packet_of_type("RA", TIME_WINDOW)) >= NB_SEUIL:
|
||||
print(f"Alerte, seuils dépassés, risque de TCPConnectScan")
|
118
idps/tcp.py
Normal file
118
idps/tcp.py
Normal file
@ -0,0 +1,118 @@
|
||||
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"""
|
||||
|
||||
# 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, flags, timestamp])
|
||||
return
|
||||
|
||||
elif flags == "SA":
|
||||
i = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "S", True)
|
||||
|
||||
if i is not None:
|
||||
self.packets[ip_dst][i][3] = "SA"
|
||||
self.packets[ip_dst][i][4] = timestamp
|
||||
return
|
||||
else:
|
||||
self.packets[ip_src].append([port_src, ip_dst, port_dst, flags, timestamp])
|
||||
return
|
||||
|
||||
elif flags == "A":
|
||||
i = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "SA")
|
||||
if i is None:
|
||||
i = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "R", True)
|
||||
|
||||
if i is not None:
|
||||
self.packets[ip_src][i][3] = "A"
|
||||
self.packets[ip_src][i][4] = timestamp
|
||||
return
|
||||
else:
|
||||
self.packets[ip_src].append([port_src, ip_dst, port_dst, flags, timestamp])
|
||||
return
|
||||
|
||||
elif flags == "RA":
|
||||
i = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "A")
|
||||
|
||||
if i is None:
|
||||
i = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "S")
|
||||
|
||||
if i is not None:
|
||||
self.packets[ip_src][i][3] = "RA"
|
||||
self.packets[ip_src][i][4] = timestamp
|
||||
return
|
||||
else:
|
||||
self.packets[ip_src].append([port_src, ip_dst, port_dst, flags, timestamp])
|
||||
return
|
||||
|
||||
elif flags == "R":
|
||||
i = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "A")
|
||||
|
||||
if i is None:
|
||||
i = self.find_packet_to_replace(ip_src, port_src, ip_dst, port_dst, "S")
|
||||
|
||||
if i is not None:
|
||||
self.packets[ip_src][i][3] = "R"
|
||||
self.packets[ip_src][i][4] = timestamp
|
||||
return
|
||||
else:
|
||||
self.packets[ip_src].append([port_src, ip_dst, port_dst, flags, timestamp])
|
||||
return
|
||||
|
||||
def find_packet_to_replace(self, ip_src, port_src, ip_dst, port_dst, flags, reverse=False):
|
||||
"""Cherche l'indice du paquet dont le flag doit être remplacé"""
|
||||
if reverse is True:
|
||||
ip_src, ip_dst = ip_dst, ip_src
|
||||
port_src, port_dst = port_dst, port_src
|
||||
|
||||
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 f == flags:
|
||||
return i
|
||||
return 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):
|
||||
"""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 packet[3] == flag and packet[4] >= current_timestamp - time_treshold:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
def __getitem__(self, src_ip):
|
||||
return self.packets.get(src_ip, None)
|
Reference in New Issue
Block a user