diff --git a/scripts/import_adresse_correction.py b/scripts/import_adresse_correction.py deleted file mode 100644 index b8de9f72..00000000 --- a/scripts/import_adresse_correction.py +++ /dev/null @@ -1,197 +0,0 @@ -#!/usr/bin/env python3 -""" -Import adresse_correction depuis data2.txt (format psql pipe-séparé, avec -gestion des valeurs multi-lignes et des lignes fusionnées sans newline). -Usage : python3 import_adresse_correction.py [--dry-run] -""" - -import os -import re -import sys - -import psycopg2 - -DB_CONFIG = { - "host": "localhost", - "port": 5432, - "user": "", - "password": "", - "dbname": "", -} - -DATA_FILE = os.path.join(os.path.dirname(__file__), "data2.txt") -DRY_RUN = "--dry-run" in sys.argv - -SOURCE_COLUMNS = [ - "id", - "invalid_address", - "correct_address", - "created_at", - "updated_at", -] - - -def preprocess(content: str) -> list[str]: - """ - Sépare les lignes physiquement fusionnées : - ex: "...2026-03-13 22:33:10.353601 79 | 55 Quai..." → deux lignes. - Pattern : timestamp immédiatement suivi (sans \n) d'un entier + '|'. - """ - content = re.sub( - r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+)\s{2,}(\d+\s*\|)", - r"\1\n \2", - content, - ) - return content.splitlines() - - -def parse_psql_rows(path: str) -> list[list[str]]: - """ - Parse le format aligné psql : - - Lignes de continuation (sans id, commençant par ' |') : valeur multi-ligne. - - Champ se terminant par '+' avant '|' : continuation sur la ligne suivante. - """ - with open(path, encoding="utf-8") as f: - raw = f.read() - - lines = preprocess(raw) - rows: list[list[str]] = [] - current: list[str] | None = None - - for line in lines: - line = line.rstrip("\n") - stripped = line.strip() - - # Ligne vide - if not stripped: - continue - - # Ligne séparateur psql (----+----) - if ( - set( - stripped.replace("|", "") - .replace("+", "") - .replace("-", "") - .replace(" ", "") - ) - == set() - ): - continue - - parts = line.split("|") - if len(parts) < 2: - continue - - first = parts[0].strip() - is_continuation = current is not None and (first == "" or not first.isdigit()) - - if is_continuation: - # Fusionner avec la ligne en cours - cont = parts[:-1] if parts[-1].strip() == "" else parts - for i, cf in enumerate(cont): - if i < len(current): - prev = current[i].rstrip("+").rstrip() - add = cf.strip() - current[i] = (prev + (" " if prev and add else "") + add).strip() - continue - - # Sauvegarder la ligne précédente - if current is not None: - rows.append(current) - - # Nouvelle ligne de données : nettoyer le '+' de continuation - raw_fields = parts[:-1] if parts[-1].strip() == "" else parts - current = [f.strip().rstrip("+").strip() for f in raw_fields] - - if current is not None: - rows.append(current) - - return rows - - -def parse_value(v: str): - v = v.strip() - if v == "" or v.lower() == "null": - return None - return v - - -def main(): - raw_rows = parse_psql_rows(DATA_FILE) - if not raw_rows: - print("❌ Aucune ligne trouvée dans data2.txt") - sys.exit(1) - - # Filtrer l'en-tête éventuel - rows = [[parse_value(v) for v in r] for r in raw_rows if r[0].strip() != "id"] - - print(f"📂 {len(rows)} lignes de données à importer depuis {DATA_FILE}") - - insert_cols = [c for c in SOURCE_COLUMNS if c != "id"] - insert_cols_all = ["id"] + insert_cols - placeholders = ", ".join(["%s"] * len(insert_cols_all)) - col_names = ", ".join(insert_cols_all) - update_expr = ", ".join(f"{c} = EXCLUDED.{c}" for c in insert_cols) - - sql = f""" - INSERT INTO adresse_correction ({col_names}) - VALUES ({placeholders}) - ON CONFLICT (invalid_address) DO UPDATE SET {update_expr} - """ - - conn = psycopg2.connect(**DB_CONFIG) - conn.autocommit = False - cur = conn.cursor() - - inserted = updated = errors = 0 - - for i, row in enumerate(rows, 1): - if len(row) < len(SOURCE_COLUMNS): - print(f" ⚠️ Ligne {i} ignorée ({len(row)} champs) : {row}") - continue - - row_dict = {SOURCE_COLUMNS[j]: row[j] for j in range(len(SOURCE_COLUMNS))} - values = [row_dict[c] for c in insert_cols_all] - inv_addr = row_dict.get("invalid_address", "?") - - if DRY_RUN: - print( - f" [DRY-RUN] {i:3}. {inv_addr!r} → {row_dict.get('correct_address')!r}" - ) - inserted += 1 - continue - - try: - cur.execute( - "SELECT 1 FROM adresse_correction WHERE invalid_address = %s", - (inv_addr,), - ) - exists = cur.fetchone() is not None - cur.execute(sql, values) - if exists: - updated += 1 - else: - inserted += 1 - - except Exception as e: - print(f" ❌ Erreur ligne {i} ({inv_addr!r}) : {e}") - conn.rollback() - errors += 1 - - if not DRY_RUN: - cur.execute( - "SELECT setval('adresse_correction_id_seq', COALESCE((SELECT MAX(id) FROM adresse_correction), 1))" - ) - conn.commit() - - cur.close() - conn.close() - - print(f"\n{'🔍 DRY-RUN — ' if DRY_RUN else ''}✅ Terminé") - print(f" Insérés : {inserted}") - print(f" Mis à jour : {updated}") - print(f" Erreurs : {errors}") - - -if __name__ == "__main__": - main() diff --git a/scripts/import_clients.py b/scripts/import_clients.py deleted file mode 100644 index eea7e5e5..00000000 --- a/scripts/import_clients.py +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/env python3 -""" -Import clients depuis data.txt (format psql pipe-séparé) vers PostgreSQL. -Usage : python3 import_clients.py [--dry-run] -""" - -import os -import sys - -import psycopg2 - -# ── Connexion ────────────────────────────────────────────────────────────────── -DB_CONFIG = { - "host": "localhost", - "port": 5432, - "user": "", - "password": "", - "dbname": "", -} - -DATA_FILE = os.path.join(os.path.dirname(__file__), "data.txt") -DRY_RUN = "--dry-run" in sys.argv - -# ── Ordre des colonnes dans data.txt (DB production, construite incrémentalement) -# None = colonne inconnue/obsolète à ignorer -SOURCE_COLUMNS = [ - "id", - "username", - "password", - "nom", - "prenom", - "telephone", - "command", - "amende", - "cancel_commande", - "referral_balance", # position 9 : NUMERIC(10,2) → "0.00" - "cancellations_count", # position 10 : INTEGER → "0" - None, # position 11 : colonne obsolète/inconnue, ignorée - "last_penalty_reason", # position 12 : TEXT - "updated_at", - "created_at", - "must_change_password", # position 15 : BOOLEAN → "f"/"t" -] - - -def parse_value(v: str): - """Convertit une valeur brute psql en type Python (None si vide).""" - v = v.strip() - if v == "" or v.lower() == "null": - return None - if v.lower() == "t": - return True - if v.lower() == "f": - return False - return v - - -def load_data(path: str) -> list[list]: - rows = [] - with open(path, encoding="utf-8") as f: - for line in f: - line = line.rstrip("\n") - if not line.strip(): - continue - # Lignes séparateur psql (ex: "----+----") - if ( - set( - line.replace("|", "") - .replace("+", "") - .replace("-", "") - .replace(" ", "") - ) - == set() - ): - continue - parts = line.split("|") - values = ( - [parse_value(p) for p in parts[:-1]] - if parts[-1].strip() == "" - else [parse_value(p) for p in parts] - ) - if values: - rows.append(values) - return rows - - -def main(): - rows = load_data(DATA_FILE) - if not rows: - print("❌ Aucune ligne trouvée dans data.txt") - sys.exit(1) - - print(f"📂 {len(rows)} lignes lues depuis {DATA_FILE}") - - data_col_count = len(rows[0]) - print( - f"📋 Colonnes data.txt : {data_col_count} | SOURCE_COLUMNS définis : {len(SOURCE_COLUMNS)}" - ) - - if data_col_count != len(SOURCE_COLUMNS): - print( - f"⚠️ Ajuste SOURCE_COLUMNS dans le script ({len(SOURCE_COLUMNS)} définis, {data_col_count} dans le fichier)" - ) - sys.exit(1) - - # Colonnes à insérer (on filtre les None = colonnes ignorées) - insert_cols = [col for col in SOURCE_COLUMNS if col is not None and col != "id"] - # On conserve aussi l'id pour FORCE_ID - insert_cols_with_id = ["id"] + insert_cols - - placeholders = ", ".join(["%s"] * len(insert_cols_with_id)) - col_names = ", ".join(insert_cols_with_id) - update_expr = ", ".join(f"{c} = EXCLUDED.{c}" for c in insert_cols) - - sql = f""" - INSERT INTO clients ({col_names}) - VALUES ({placeholders}) - ON CONFLICT (username) DO UPDATE SET {update_expr} - """ - - conn = psycopg2.connect(**DB_CONFIG) - conn.autocommit = False - cur = conn.cursor() - - inserted = 0 - updated = 0 - errors = 0 - - for i, row in enumerate(rows, 1): - # Construire un dict {col_name: value} depuis SOURCE_COLUMNS - row_dict = {} - for pos, col in enumerate(SOURCE_COLUMNS): - if col is None: - continue - row_dict[col] = row[pos] if pos < len(row) else None - - values = [row_dict.get(c) for c in insert_cols_with_id] - username = row_dict.get("username", "?") - - try: - if DRY_RUN: - print(f" [DRY-RUN] ligne {i} : {username} → {row_dict}") - inserted += 1 - continue - - cur.execute("SELECT 1 FROM clients WHERE username = %s", (username,)) - exists = cur.fetchone() is not None - - cur.execute(sql, values) - - if exists: - updated += 1 - else: - inserted += 1 - - except Exception as e: - print(f" ❌ Erreur ligne {i} ({username}) : {e}") - conn.rollback() - errors += 1 - continue - - if not DRY_RUN: - cur.execute( - "SELECT setval('clients_id_seq', COALESCE((SELECT MAX(id) FROM clients), 1))" - ) - conn.commit() - - cur.close() - conn.close() - - print(f"\n{'🔍 DRY-RUN — ' if DRY_RUN else ''}✅ Terminé") - print(f" Insérés : {inserted}") - print(f" Mis à jour : {updated}") - print(f" Erreurs : {errors}") - print(f" ℹ️ Séquence clients_id_seq resynchronisée sur MAX(id)") - - -if __name__ == "__main__": - main() diff --git a/scripts/import_users.py b/scripts/import_users.py deleted file mode 100644 index 7b509daf..00000000 --- a/scripts/import_users.py +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env python3 -""" -Import users depuis data3.txt (format psql pipe-séparé) vers PostgreSQL. -Usage : python3 import_users.py [--dry-run] -""" - -import os -import sys - -import psycopg2 - -DB_CONFIG = { - "host": "localhost", - "port": 5432, - "user": "", - "password": "", - "dbname": "", -} - -DATA_FILE = os.path.join(os.path.dirname(__file__), "data3.txt") -DRY_RUN = "--dry-run" in sys.argv - -# Colonnes dans data3.txt — None = ignoré (push_token supprimé) -SOURCE_COLUMNS = [ - "id", - "username", - "password", - "role", - "total", - "livraison", - "created_at", - "updated_at", -] - - -def parse_value(v: str): - v = v.strip() - if v == "" or v.lower() == "null": - return None - if v.lower() == "t": - return True - if v.lower() == "f": - return False - return v - - -def load_data(path: str) -> list[list]: - rows = [] - with open(path, encoding="utf-8") as f: - for line in f: - line = line.rstrip("\n") - if not line.strip(): - continue - if ( - set( - line.replace("|", "") - .replace("+", "") - .replace("-", "") - .replace(" ", "") - ) - == set() - ): - continue - parts = line.split("|") - values = ( - [parse_value(p) for p in parts[:-1]] - if parts[-1].strip() == "" - else [parse_value(p) for p in parts] - ) - # Ignorer la ligne d'en-tête - if values and values[0] == "id": - continue - if values: - rows.append(values) - return rows - - -def main(): - rows = load_data(DATA_FILE) - if not rows: - print("❌ Aucune ligne trouvée dans data3.txt") - sys.exit(1) - - print(f"📂 {len(rows)} lignes lues depuis {DATA_FILE}") - - data_col_count = len(rows[0]) - print( - f"📋 Colonnes data3.txt : {data_col_count} | SOURCE_COLUMNS définis : {len(SOURCE_COLUMNS)}" - ) - - if data_col_count != len(SOURCE_COLUMNS): - print( - f"⚠️ Ajuste SOURCE_COLUMNS ({len(SOURCE_COLUMNS)} définis, {data_col_count} dans le fichier)" - ) - sys.exit(1) - - insert_cols = [col for col in SOURCE_COLUMNS if col is not None and col != "id"] - insert_cols_with_id = ["id"] + insert_cols - - placeholders = ", ".join(["%s"] * len(insert_cols_with_id)) - col_names = ", ".join(insert_cols_with_id) - update_expr = ", ".join(f"{c} = EXCLUDED.{c}" for c in insert_cols) - - sql = f""" - INSERT INTO users ({col_names}) - VALUES ({placeholders}) - ON CONFLICT (username) DO UPDATE SET {update_expr} - """ - - conn = psycopg2.connect(**DB_CONFIG) - conn.autocommit = False - cur = conn.cursor() - - inserted = updated = errors = 0 - - for i, row in enumerate(rows, 1): - row_dict = {} - for pos, col in enumerate(SOURCE_COLUMNS): - if col is None: - continue - row_dict[col] = row[pos] if pos < len(row) else None - - values = [row_dict.get(c) for c in insert_cols_with_id] - username = row_dict.get("username", "?") - - try: - if DRY_RUN: - print(f" [DRY-RUN] {i}. {username} → {row_dict}") - inserted += 1 - continue - - cur.execute("SELECT 1 FROM users WHERE username = %s", (username,)) - exists = cur.fetchone() is not None - cur.execute(sql, values) - if exists: - updated += 1 - else: - inserted += 1 - - except Exception as e: - print(f" ❌ Erreur ligne {i} ({username}) : {e}") - conn.rollback() - errors += 1 - - if not DRY_RUN: - cur.execute( - "SELECT setval('users_id_seq', COALESCE((SELECT MAX(id) FROM users), 1))" - ) - conn.commit() - - cur.close() - conn.close() - - print(f"\n{'🔍 DRY-RUN — ' if DRY_RUN else ''}✅ Terminé") - print(f" Insérés : {inserted}") - print(f" Mis à jour : {updated}") - print(f" Erreurs : {errors}") - print(" ℹ️ Séquence users_id_seq resynchronisée sur MAX(id)") - - -if __name__ == "__main__": - main() diff --git a/scripts/ssh-monitor.service b/scripts/ssh-monitor.service deleted file mode 100644 index 0f78edac..00000000 --- a/scripts/ssh-monitor.service +++ /dev/null @@ -1,25 +0,0 @@ -[Unit] -Description=SSH Monitor — alertes Telegram + bannissement automatique -Documentation=https://github.com/pyTelegramBotAPI/pyTelegramBotAPI -After=network-online.target -Wants=network-online.target - -[Service] -Type=simple -ExecStart=/usr/bin/python3 /home/xor_fakers/projet_gestion_commande/scripts/ssh_monitor.py -Restart=always -RestartSec=5 - -# Root requis pour iptables -User=root - -# Sortie dans journald (journalctl -u ssh-monitor -f) -StandardOutput=journal -StandardError=journal -SyslogIdentifier=ssh-monitor - -# Redémarrer même si le process quitte proprement (ex : erreur init) -RestartForceExitStatus=1 - -[Install] -WantedBy=multi-user.target diff --git a/scripts/ssh_monitor.py b/scripts/ssh_monitor.py deleted file mode 100644 index 9c7eb197..00000000 --- a/scripts/ssh_monitor.py +++ /dev/null @@ -1,230 +0,0 @@ - #!/usr/bin/env python3 - """ - pip install pyTelegramBotAPI - """ - - import os - import re - import subprocess - import sys - import time - from collections import defaultdict - from datetime import datetime - - import telebot - - # ============================================================ - # CONFIGURATION — à modifier avant de lancer - # ============================================================ - - BOT_TOKEN = "7671151228:AAHI_xJqaXkbf2RnDGotn6ZwO_fWgoY-WJk" - CHAT_ID = "7950889398" - - LOG_FILE = "/var/log/auth.log" - - BRUTE_FORCE_THRESHOLD = 5 - BRUTE_FORCE_WINDOW = 60 - - # Fenêtre de déduplication : évite d'envoyer 2 fois le même type - # d'alerte pour la même IP en moins de DEDUP_WINDOW secondes. - # Toute IP hors whitelist est considérée malveillante → on alerte toujours. - DEDUP_WINDOW = 10 - - WHITELIST_IPS = {"86.217.21.144"} - - ACTIVE_RESPONSE = True - - BAN_ON_BRUTE_FORCE = True - BAN_ON_MAX_AUTH = True - - banned_ips: set[str] = set() - - RE_FAILED = re.compile( - r"sshd\[\d+\]: Failed (?:password|publickey) for (?:invalid user )?(\S+) from ([\d.]+)" - ) - RE_INVALID_USER = re.compile(r"sshd\[\d+\]: Invalid user (\S+) from ([\d.]+)") - RE_ACCEPTED = re.compile( - r"sshd\[\d+\]: Accepted (?:password|publickey) for (\S+) from ([\d.]+)" - ) - RE_MAX_AUTH = re.compile( - r"sshd\[\d+\]: error: maximum authentication attempts exceeded.*from ([\d.]+)" - ) - RE_DISCONNECT_INVALID = re.compile( - r"sshd\[\d+\]: Disconnected from invalid user (\S+) ([\d.]+)" - ) - - - failed_attempts: dict[str, list[float]] = defaultdict(list) - - last_sent: dict[tuple, float] = {} - - known_ips: set[str] = set() - - # ============================================================ - # TELEGRAM - # ============================================================ - - bot = telebot.TeleBot(BOT_TOKEN, parse_mode="HTML") - - - def send_alert(message: str) -> None: - try: - bot.send_message(CHAT_ID, message) - except Exception as e: - print(f"[TELEGRAM ERROR] {e}", file=sys.stderr) - - - def dedup_ok(ip: str, event: str) -> bool: - """Retourne True si on peut envoyer l'alerte (pas envoyée depuis DEDUP_WINDOW s).""" - key = (ip, event) - now = time.time() - if key in last_sent and now - last_sent[key] < DEDUP_WINDOW: - return False - last_sent[key] = now - return True - - - def ts() -> str: - return datetime.now().strftime("%d/%m/%Y %H:%M:%S") - - - # ============================================================ - # ANALYSE DES LIGNES - # ============================================================ - - - def process_line(line: str) -> None: - line = line.strip() - if not line: - return - - m = RE_ACCEPTED.search(line) - if m: - user, ip = m.group(1), m.group(2) - if ip in WHITELIST_IPS: - return - icon = "🔴" if user == "root" else "🟡" - new_ip = "" if ip in known_ips else "(nouvelle IP !)" - known_ips.add(ip) - send_alert( - f"{icon}IP Alertes Connecter: {new_ip}\n" - f"👤 Utilisateur : {user}\n" - f"🌐 IP : {ip}\n" - f"🕐 {ts()}" - ) - return - - m = RE_MAX_AUTH.search(line) - if m: - ip = m.group(1) - if ip in WHITELIST_IPS: - return - if dedup_ok(ip, "maxauth"): - send_alert(f"🚫 Max authentifications atteint\n🌐 IP : {ip}\n🕐 {ts()}") - return - - # --- Utilisateur invalide --- - m = RE_INVALID_USER.search(line) - if m: - user, ip = m.group(1), m.group(2) - if ip in WHITELIST_IPS: - return - _record_failure(ip) - _alert(ip, user) - return - - # --- Mot de passe / clé échouée --- - m = RE_FAILED.search(line) - if m: - user, ip = m.group(1), m.group(2) - if ip in WHITELIST_IPS: - return - _record_failure(ip) - _alert(ip, user) - return - - - def _record_failure(ip: str) -> None: - now = time.time() - failed_attempts[ip].append(now) - failed_attempts[ip] = [ - t for t in failed_attempts[ip] if now - t <= BRUTE_FORCE_WINDOW - ] - - - def _alert(ip: str, user: str) -> None: - count = len(failed_attempts[ip]) - - if count >= BRUTE_FORCE_THRESHOLD: - if dedup_ok(ip, "bruteforce"): - send_alert( - f"🔥 Brute force SSH détecté !\n" - f"🌐 IP : {ip}\n" - f"👤 Dernier user : {user}\n" - f"📊 {count} échecs en {BRUTE_FORCE_WINDOW}s\n" - f"🕐 {ts()}" - ) - return - - if dedup_ok(ip, "fail"): - send_alert( - f"⚠️ Tentative SSH échouée\n" - f"🌐 IP : {ip}\n" - f"👤 User : {user}\n" - f"📊 {count} échec(s) récent(s)\n" - f"🕐 {ts()}" - ) - - - # ============================================================ - # LECTURE EN CONTINU (tail -f) - # ============================================================ - - - def follow(filepath: str): - if not os.path.exists(filepath): - print(f"[ERREUR] Fichier introuvable : {filepath}", file=sys.stderr) - sys.exit(1) - - with open(filepath, "r", encoding="utf-8", errors="ignore") as f: - f.seek(0, os.SEEK_END) - print(f"[INFO] Surveillance de {filepath} démarrée.") - send_alert( - f"✅ SSH Monitor démarré\n📂 {filepath}\n🕐 {ts()}" - ) - - while True: - line = f.readline() - if line: - yield line - else: - try: - if os.stat(filepath).st_ino != os.fstat(f.fileno()).st_ino: - print("[INFO] Rotation du fichier détectée, réouverture.") - f.close() - f = open(filepath, "r", encoding="utf-8", errors="ignore") - except OSError: - pass - time.sleep(0.2) - - - # ============================================================ - # POINT D'ENTRÉE - # ============================================================ - - if __name__ == "__main__": - if BOT_TOKEN == "VOTRE_BOT_TOKEN" or CHAT_ID == "VOTRE_CHAT_ID": - print("[ERREUR] Configure BOT_TOKEN et CHAT_ID avant de lancer le script.") - sys.exit(1) - - print( - f"[INFO] Brute force : {BRUTE_FORCE_THRESHOLD} échecs en {BRUTE_FORCE_WINDOW}s" - ) - print(f"[INFO] Déduplication : {DEDUP_WINDOW}s par (IP, type d'événement)") - - try: - for log_line in follow(LOG_FILE): - process_line(log_line) - except KeyboardInterrupt: - print("\n[INFO] Arrêt du moniteur.") - send_alert(f"🛑 SSH Monitor arrêté\n🕐 {ts()}")