chore: delete script
This commit is contained in:
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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 : <code>{user}</code>\n"
|
||||
f"🌐 IP : <code>{ip}</code>\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"🔥 <b>Brute force SSH détecté !</b>\n"
|
||||
f"🌐 IP : <code>{ip}</code>\n"
|
||||
f"👤 Dernier user : <code>{user}</code>\n"
|
||||
f"📊 {count} échecs en {BRUTE_FORCE_WINDOW}s\n"
|
||||
f"🕐 {ts()}"
|
||||
)
|
||||
return
|
||||
|
||||
if dedup_ok(ip, "fail"):
|
||||
send_alert(
|
||||
f"⚠️ <b>Tentative SSH échouée</b>\n"
|
||||
f"🌐 IP : <code>{ip}</code>\n"
|
||||
f"👤 User : <code>{user}</code>\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"✅ <b>SSH Monitor démarré</b>\n📂 <code>{filepath}</code>\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"🛑 <b>SSH Monitor arrêté</b>\n🕐 {ts()}")
|
||||
Reference in New Issue
Block a user