import customtkinter as ctk from tkinter import filedialog, messagebox import os import threading import subprocess import urllib.request import time import random import platform import datetime import glob import hashlib import string import secrets from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler # ========================================== # CONFIGURATION GLOBALE # ========================================== UPDATE_URL = "https://git.7ka1.com/7ka1/7LnA_Antivirus_Linux_Free_ClamAV_Based/raw/branch/main/7lna.py" QUARANTINE_DIR = os.path.expanduser("~/.7lna_quarantine") WATCH_FOLDER = os.path.expanduser("~/Téléchargements") ctk.set_appearance_mode("dark") ctk.set_default_color_theme("blue") # ========================================== # UTILITAIRES SYSTÈME # ========================================== def send_desktop_notification(title, message, is_critical=False): """Envoie une notification native sous Linux""" try: urgency = 'critical' if is_critical else 'normal' subprocess.Popen(['notify-send', '-u', urgency, '-a', '7LnA Security', title, message]) except Exception: pass # Ignore si notify-send n'est pas installé # ========================================== # GESTIONNAIRE DU BOUCLIER TEMPS RÉEL # ========================================== class RealTimeShieldHandler(FileSystemEventHandler): def __init__(self, app_instance): self.app = app_instance def on_created(self, event): if not event.is_directory: time.sleep(1.5) # Laisse le temps au fichier d'être copié/téléchargé self.app.trigger_realtime_scan(event.src_path) # ========================================== # APPLICATION PRINCIPALE # ========================================== class Antivirus7LnA(ctk.CTk): def __init__(self): super().__init__() self.title("7LnA Security Suite - Ultimate Edition V7") self.geometry("1200x800") self.minsize(950, 650) os.makedirs(QUARANTINE_DIR, exist_ok=True) self.shield_observer = None self.shield_active = False self.check_dependencies() self.setup_ui() def check_dependencies(self): try: subprocess.run(['clamscan', '--version'], capture_output=True, check=True) self.clamav_installed = True except (subprocess.CalledProcessError, FileNotFoundError): self.clamav_installed = False def setup_ui(self): self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(1, weight=1) # --- BARRE LATÉRALE --- self.sidebar = ctk.CTkFrame(self, width=260, corner_radius=0, fg_color="#111827") self.sidebar.grid(row=0, column=0, sticky="nsew") self.sidebar.grid_rowconfigure(8, weight=1) ctk.CTkLabel(self.sidebar, text="🛡️ 7LnA Sec", font=ctk.CTkFont(size=30, weight="bold"), text_color="#3B82F6").grid(row=0, column=0, padx=20, pady=(30, 20)) self.btn_dash = self.create_nav_button("📊 Tableau de Bord", 1, "dashboard") self.btn_scan = self.create_nav_button("🔍 Scanner Manuel", 2, "scanner") self.btn_shield = self.create_nav_button("⚡ Bouclier Actif", 3, "shield") self.btn_audit = self.create_nav_button("⚙️ Audit Réseau", 4, "audit") self.btn_tools = self.create_nav_button("🧰 Outils Avancés", 5, "tools") self.btn_quarantine = self.create_nav_button("📦 Quarantaine", 6, "quarantine") self.btn_update = self.create_nav_button("🔄 Mise à jour", 7, "update") self.version_label = ctk.CTkLabel(self.sidebar, text="v7.0 - Ultimate", text_color="#6B7280", font=ctk.CTkFont(weight="bold")) self.version_label.grid(row=8, column=0, pady=20, sticky="s") # --- CONTENEUR DES VUES --- self.views = {} self.init_dashboard_view() self.init_scanner_view() self.init_realtime_view() self.init_audit_view() self.init_tools_view() self.init_quarantine_view() self.init_update_view() self.select_view("dashboard") def create_nav_button(self, text, row, view_name): btn = ctk.CTkButton(self.sidebar, text=text, anchor="w", fg_color="transparent", text_color="#D1D5DB", hover_color="#1F2937", font=ctk.CTkFont(size=15), height=40, command=lambda: self.select_view(view_name)) btn.grid(row=row, column=0, padx=20, pady=5, sticky="ew") return btn def select_view(self, view_name): for view in self.views.values(): view.grid_forget() if view_name in self.views: self.views[view_name].grid(row=0, column=1, sticky="nsew", padx=30, pady=30) if view_name == "quarantine": self.refresh_quarantine_list() # --- UTILITAIRES DE CONSOLE --- def setup_console_tags(self, console): console.tag_config("danger", foreground="#EF4444") console.tag_config("success", foreground="#10B981") console.tag_config("warning", foreground="#F59E0B") console.tag_config("info", foreground="#3B82F6") def get_time_prefix(self): return datetime.datetime.now().strftime("[%H:%M:%S] ") # --- VUE : TABLEAU DE BORD --- def init_dashboard_view(self): frame = ctk.CTkFrame(self, fg_color="transparent") self.views["dashboard"] = frame ctk.CTkLabel(frame, text="État du Système", font=ctk.CTkFont(size=34, weight="bold")).pack(anchor="w", pady=(0, 20)) status_card = ctk.CTkFrame(frame, fg_color="#064E3B" if self.clamav_installed else "#7F1D1D", corner_radius=15) status_card.pack(fill="x", pady=10, ipady=20) status_text = "Moteur ClamAV Opérationnel" if self.clamav_installed else "Moteur ClamAV Introuvable" ctk.CTkLabel(status_card, text=f"{'✅' if self.clamav_installed else '❌'} {status_text}", font=ctk.CTkFont(size=24, weight="bold"), text_color="white").pack(expand=True) sys_frame = ctk.CTkFrame(frame, fg_color="#1F2937", corner_radius=10) sys_frame.pack(fill="x", pady=10, ipady=10) sys_info = f"🖥️ OS : {platform.system()} {platform.release()} | 👤 Compte : {os.getlogin()}" ctk.CTkLabel(sys_frame, text=sys_info, font=ctk.CTkFont(size=16, weight="bold")).pack(padx=20, pady=10, anchor="w") info_frame = ctk.CTkFrame(frame, fg_color="#1F2937", corner_radius=10) info_frame.pack(fill="x", pady=10, ipady=10) ctk.CTkLabel(info_frame, text=f"📂 Dossier Quarantaine : {QUARANTINE_DIR}\n📁 Dossier Surveillé : {WATCH_FOLDER}", justify="left", font=ctk.CTkFont(size=14)).pack(padx=20, pady=10, anchor="w") # --- VUE : SCANNER MANUEL --- def init_scanner_view(self): frame = ctk.CTkFrame(self, fg_color="transparent") frame.grid_rowconfigure(3, weight=1) frame.grid_columnconfigure((0, 1, 2), weight=1) self.views["scanner"] = frame ctk.CTkLabel(frame, text="Analyse Profonde", font=ctk.CTkFont(size=34, weight="bold")).grid(row=0, column=0, columnspan=3, sticky="w", pady=(0, 20)) self.btn_scan_f = ctk.CTkButton(frame, text="📄 Analyser Fichier", command=lambda: self.start_manual_scan(is_dir=False), height=45, fg_color="#2563EB", hover_color="#1D4ED8") self.btn_scan_f.grid(row=1, column=0, padx=(0, 5), sticky="ew") self.btn_scan_d = ctk.CTkButton(frame, text="📁 Analyser Dossier", command=lambda: self.start_manual_scan(is_dir=True), height=45, fg_color="#4F46E5", hover_color="#4338CA") self.btn_scan_d.grid(row=1, column=1, padx=(5, 5), sticky="ew") self.btn_db_update = ctk.CTkButton(frame, text="🔄 MaJ Signatures (freshclam)", command=self.update_virus_db, height=45, fg_color="#059669", hover_color="#047857") self.btn_db_update.grid(row=1, column=2, padx=(5, 0), sticky="ew") self.scan_progress = ctk.CTkProgressBar(frame, mode="indeterminate", height=10) self.scan_progress.grid(row=2, column=0, columnspan=3, pady=(20, 0), sticky="ew") self.scan_progress.set(0) self.scan_console = ctk.CTkTextbox(frame, font=ctk.CTkFont(family="Consolas", size=13), fg_color="#111827", corner_radius=10) self.scan_console.grid(row=3, column=0, columnspan=3, pady=20, sticky="nsew") self.setup_console_tags(self.scan_console) self.scan_console.insert("end", f"{self.get_time_prefix()}[*] Moteur de détection V7 prêt...\n", "info") def update_virus_db(self): threading.Thread(target=self._run_freshclam, daemon=True).start() def _run_freshclam(self): self.scan_console.insert("end", f"\n{self.get_time_prefix()}[*] Mise à jour des signatures...\n", "info") self.scan_progress.start() self.btn_db_update.configure(state="disabled") try: process = subprocess.Popen(['freshclam'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) for line in process.stdout: self.scan_console.insert("end", line) self.scan_console.see("end") process.wait() if process.returncode == 0: self.scan_console.insert("end", f"{self.get_time_prefix()}[+] Base de données virale à jour.\n", "success") send_desktop_notification("Mise à jour réussie", "Les signatures ClamAV sont à jour.") else: self.scan_console.insert("end", f"{self.get_time_prefix()}[-] Échec ou droits root requis (sudo freshclam).\n", "warning") except Exception as e: self.scan_console.insert("end", f"{self.get_time_prefix()}❌ Erreur : {e}\n", "danger") finally: self.scan_progress.stop() self.scan_progress.set(0) self.btn_db_update.configure(state="normal") self.scan_console.see("end") def start_manual_scan(self, is_dir): path = filedialog.askdirectory() if is_dir else filedialog.askopenfilename() if path: threading.Thread(target=self.run_clamav_scan, args=(path, is_dir, self.scan_console), daemon=True).start() # --- VUE : BOUCLIER TEMPS RÉEL --- def init_realtime_view(self): frame = ctk.CTkFrame(self, fg_color="transparent") frame.grid_rowconfigure(2, weight=1) frame.grid_columnconfigure(0, weight=1) self.views["shield"] = frame header = ctk.CTkFrame(frame, fg_color="transparent") header.grid(row=0, column=0, sticky="ew", pady=(0, 20)) ctk.CTkLabel(header, text="⚡ Bouclier Actif", font=ctk.CTkFont(size=34, weight="bold")).pack(side="left") self.btn_toggle_shield = ctk.CTkButton(header, text="Démarrer la Surveillance", fg_color="#059669", hover_color="#047857", command=self.toggle_shield, height=45, font=ctk.CTkFont(weight="bold")) self.btn_toggle_shield.pack(side="right") self.rt_console = ctk.CTkTextbox(frame, font=ctk.CTkFont(family="Consolas", size=13), fg_color="#111827", corner_radius=10) self.rt_console.grid(row=2, column=0, sticky="nsew") self.setup_console_tags(self.rt_console) self.rt_console.insert("end", f"{self.get_time_prefix()}[-] Sentinelle en attente d'activation.\n") def toggle_shield(self): if not self.shield_active: self.shield_observer = Observer() self.shield_observer.schedule(RealTimeShieldHandler(self), WATCH_FOLDER, recursive=False) self.shield_observer.start() self.shield_active = True self.btn_toggle_shield.configure(text="Arrêter le Bouclier", fg_color="#DC2626", hover_color="#B91C1C") self.rt_console.insert("end", f"\n{self.get_time_prefix()}[+] BOUCLIER ARMÉ : {WATCH_FOLDER}\n", "success") send_desktop_notification("Bouclier Activé", f"Surveillance en direct de {WATCH_FOLDER}") else: self.shield_observer.stop() self.shield_active = False self.btn_toggle_shield.configure(text="Démarrer la Surveillance", fg_color="#059669", hover_color="#047857") self.rt_console.insert("end", f"\n{self.get_time_prefix()}[-] Bouclier désactivé.\n", "warning") send_desktop_notification("Bouclier Désactivé", "La surveillance en temps réel est coupée.") self.rt_console.see("end") def trigger_realtime_scan(self, filepath): self.rt_console.insert("end", f"\n{self.get_time_prefix()}⚡ Nouveau fichier : {os.path.basename(filepath)}\n", "info") threading.Thread(target=self.run_clamav_scan, args=(filepath, False, self.rt_console), daemon=True).start() # --- MOTEUR CLAMAV (Cœur) --- def run_clamav_scan(self, path, is_dir, console): if not self.clamav_installed: console.insert("end", f"{self.get_time_prefix()}❌ Moteur introuvable.\n", "danger") return console.insert("end", f"{self.get_time_prefix()}[*] Analyse : {path}\n") if console == self.scan_console: self.scan_progress.start() try: cmd = ['clamscan', '-i', '--no-summary', f'--move={QUARANTINE_DIR}'] if is_dir: cmd.append('-r') cmd.append(path) process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) infected = 0 for line in process.stdout: clean_line = line.strip() if "FOUND" in clean_line: console.insert("end", f"{self.get_time_prefix()}☠️ MENACE DÉTECTÉE : {clean_line}\n", "danger") console.insert("end", f"{self.get_time_prefix()}🛡️ Action : Fichier mis en quarantaine.\n", "warning") infected += 1 elif clean_line: console.insert("end", f" -> {clean_line}\n") console.see("end") process.wait() if infected == 0: console.insert("end", f"{self.get_time_prefix()}[+] Fichier propre.\n", "success") else: console.insert("end", f"{self.get_time_prefix()}🚨 {infected} MENACE(S) NEUTRALISÉE(S) !\n", "danger") # Envoi d'une notification critique au système send_desktop_notification("🚨 VIRUS NEUTRALISÉ", f"{infected} menace(s) trouvée(s) et isolée(s) dans {os.path.basename(path)}.", is_critical=True) except Exception as e: console.insert("end", f"{self.get_time_prefix()}❌ Erreur Moteur : {e}\n", "danger") finally: if console == self.scan_console: self.scan_progress.stop() self.scan_progress.set(0) console.see("end") # --- VUE : AUDIT & PARE-FEU --- def init_audit_view(self): frame = ctk.CTkFrame(self, fg_color="transparent") frame.grid_rowconfigure(2, weight=1) frame.grid_columnconfigure(0, weight=1) self.views["audit"] = frame ctk.CTkLabel(frame, text="Audit Réseau & Pare-feu", font=ctk.CTkFont(size=34, weight="bold")).grid(row=0, column=0, sticky="w", pady=(0, 20)) btn_frame = ctk.CTkFrame(frame, fg_color="transparent") btn_frame.grid(row=1, column=0, sticky="ew", pady=(0, 10)) ctk.CTkButton(btn_frame, text="Lancer l'Audit Processus", command=self.run_audit_thread, fg_color="#D97706", hover_color="#B45309", height=40).pack(side="left", padx=(0, 10)) ctk.CTkButton(btn_frame, text="Vérifier Statut UFW", command=self.check_firewall, fg_color="#4B5563", hover_color="#374151", height=40).pack(side="left") self.audit_console = ctk.CTkTextbox(frame, font=ctk.CTkFont(family="Consolas", size=13), fg_color="#111827", corner_radius=10) self.audit_console.grid(row=2, column=0, sticky="nsew") self.setup_console_tags(self.audit_console) def run_audit_thread(self): self.audit_console.delete("0.0", "end") threading.Thread(target=self.perform_audit, daemon=True).start() def perform_audit(self): self.audit_console.insert("end", f"{self.get_time_prefix()}[*] Recherche de connexions fantômes (Ports ouverts)...\n", "info") try: self.audit_console.insert("end", subprocess.check_output(['ss', '-tuln'], text=True)) except: pass self.audit_console.insert("end", f"\n{self.get_time_prefix()}[*] Top 10 Processus (Charge CPU)...\n", "info") try: res = subprocess.check_output(['ps', '-eo', 'pid,user,%cpu,%mem,cmd', '--sort=-%cpu'], text=True) self.audit_console.insert("end", "\n".join(res.split('\n')[:11]) + "\n") except: pass def check_firewall(self): self.audit_console.delete("0.0", "end") self.audit_console.insert("end", f"{self.get_time_prefix()}[*] Interrogation du pare-feu Ubuntu (UFW)...\n", "info") try: res = subprocess.check_output(['systemctl', 'is-active', 'ufw'], text=True).strip() if res == "active": self.audit_console.insert("end", f"{self.get_time_prefix()}✅ PARE-FEU UFW ACTIF.\n", "success") else: self.audit_console.insert("end", f"{self.get_time_prefix()}⚠️ PARE-FEU INACTIF.\nTerminal: 'sudo ufw enable'.\n", "danger") except Exception: self.audit_console.insert("end", f"{self.get_time_prefix()}[-] Impossible de déterminer l'état de UFW.\n", "warning") # --- VUE : OUTILS AVANCÉS (V7 - Scrollable) --- def init_tools_view(self): # On utilise un CTkScrollableFrame pour empiler les outils proprement frame = ctk.CTkScrollableFrame(self, fg_color="transparent") self.views["tools"] = frame ctk.CTkLabel(frame, text="Boîte à Outils", font=ctk.CTkFont(size=34, weight="bold")).pack(anchor="w", pady=(0, 20)) # --- Outil 1 : Destructeur de fichiers --- shred_card = ctk.CTkFrame(frame, fg_color="#1F2937", corner_radius=10) shred_card.pack(fill="x", pady=10, ipady=15) ctk.CTkLabel(shred_card, text="🔥 Destructeur de Fichiers (File Shredder)", font=ctk.CTkFont(size=18, weight="bold"), text_color="#EF4444").pack(anchor="w", padx=20, pady=(10,0)) ctk.CTkLabel(shred_card, text="Écrase le fichier avec des données aléatoires (3 passes) pour empêcher sa récupération.", text_color="#9CA3AF").pack(anchor="w", padx=20, pady=(5, 10)) ctk.CTkButton(shred_card, text="Détruire un fichier à jamais", fg_color="#DC2626", hover_color="#991B1B", command=self.run_shredder).pack(anchor="w", padx=20) # --- Outil 2 : Calculateur de Hash --- hash_card = ctk.CTkFrame(frame, fg_color="#1F2937", corner_radius=10) hash_card.pack(fill="x", pady=10, ipady=15) ctk.CTkLabel(hash_card, text="🧬 Extracteur d'Empreintes (Hash)", font=ctk.CTkFont(size=18, weight="bold"), text_color="#3B82F6").pack(anchor="w", padx=20, pady=(10,0)) ctk.CTkLabel(hash_card, text="Calcule les empreintes MD5 et SHA-256 d'un fichier pour vérifier sa légitimité.", text_color="#9CA3AF").pack(anchor="w", padx=20, pady=(5, 10)) self.hash_result_var = ctk.StringVar(value="Aucun fichier sélectionné.") ctk.CTkButton(hash_card, text="Sélectionner un fichier", fg_color="#2563EB", hover_color="#1D4ED8", command=self.calculate_hash).pack(anchor="w", padx=20, pady=(0, 10)) ctk.CTkEntry(hash_card, textvariable=self.hash_result_var, state="readonly", width=500, fg_color="#111827").pack(anchor="w", padx=20) # --- Outil 3 : Générateur de Mot de Passe --- pwd_card = ctk.CTkFrame(frame, fg_color="#1F2937", corner_radius=10) pwd_card.pack(fill="x", pady=10, ipady=15) ctk.CTkLabel(pwd_card, text="🔑 Générateur de Mot de Passe Blindé", font=ctk.CTkFont(size=18, weight="bold"), text_color="#10B981").pack(anchor="w", padx=20, pady=(10,0)) ctk.CTkLabel(pwd_card, text="Génère un mot de passe cryptographiquement sécurisé (20 caractères).", text_color="#9CA3AF").pack(anchor="w", padx=20, pady=(5, 10)) self.pwd_result_var = ctk.StringVar(value="*****") btn_pwd_frame = ctk.CTkFrame(pwd_card, fg_color="transparent") btn_pwd_frame.pack(anchor="w", padx=20, fill="x") ctk.CTkButton(btn_pwd_frame, text="Générer", fg_color="#059669", hover_color="#047857", command=self.generate_password).pack(side="left", padx=(0, 10)) ctk.CTkEntry(btn_pwd_frame, textvariable=self.pwd_result_var, state="normal", width=300, fg_color="#111827", font=ctk.CTkFont(family="Consolas", size=14)).pack(side="left") def run_shredder(self): filepath = filedialog.askopenfilename(title="SÉLECTIONNEZ LE FICHIER À DÉTRUIRE") if not filepath: return if messagebox.askyesno("DANGER", f"Détruire DÉFINITIVEMENT ?\n\n{filepath}"): threading.Thread(target=self.shred_file, args=(filepath,), daemon=True).start() def shred_file(self, filepath): try: file_size = os.path.getsize(filepath) for _ in range(3): with open(filepath, "ba+", buffering=0) as f: f.seek(0) f.write(os.urandom(file_size)) os.remove(filepath) messagebox.showinfo("Succès", "Fichier détruit de manière irréversible.") except Exception as e: messagebox.showerror("Erreur", f"Erreur : {e}") def calculate_hash(self): filepath = filedialog.askopenfilename(title="SÉLECTIONNEZ UN FICHIER") if not filepath: return try: sha256_hash = hashlib.sha256() with open(filepath, "rb") as f: for byte_block in iter(lambda: f.read(4096), b""): sha256_hash.update(byte_block) self.hash_result_var.set(f"SHA-256: {sha256_hash.hexdigest()}") except Exception as e: self.hash_result_var.set(f"Erreur de lecture.") def generate_password(self): alphabet = string.ascii_letters + string.digits + "!@#$%^&*" pwd = ''.join(secrets.choice(alphabet) for i in range(20)) self.pwd_result_var.set(pwd) # --- VUE : QUARANTAINE --- def init_quarantine_view(self): frame = ctk.CTkFrame(self, fg_color="transparent") frame.grid_rowconfigure(1, weight=1) frame.grid_columnconfigure(0, weight=1) self.views["quarantine"] = frame header = ctk.CTkFrame(frame, fg_color="transparent") header.grid(row=0, column=0, sticky="ew", pady=(0, 20)) ctk.CTkLabel(header, text="📦 Quarantaine", font=ctk.CTkFont(size=34, weight="bold")).pack(side="left") ctk.CTkButton(header, text="🗑️ Vider", fg_color="#DC2626", hover_color="#991B1B", command=self.empty_quarantine, height=40).pack(side="right") self.quarantine_listbox = ctk.CTkTextbox(frame, font=ctk.CTkFont(family="Consolas", size=14), fg_color="#111827", corner_radius=10) self.quarantine_listbox.grid(row=1, column=0, sticky="nsew") def refresh_quarantine_list(self): self.quarantine_listbox.delete("0.0", "end") files = glob.glob(os.path.join(QUARANTINE_DIR, "*")) if not files: self.quarantine_listbox.insert("end", "\n ✅ Aucun fichier malveillant isolé.\n") else: self.quarantine_listbox.insert("end", f" ⚠️ {len(files)} menace(s) :\n\n") for f in files: size_kb = os.path.getsize(f) / 1024 self.quarantine_listbox.insert("end", f" -> {os.path.basename(f)} ({size_kb:.1f} KB)\n") def empty_quarantine(self): files = glob.glob(os.path.join(QUARANTINE_DIR, "*")) if not files: return if messagebox.askyesno("Purger", "Supprimer définitivement les virus isolés ?"): for f in files: try: os.remove(f) except: pass self.refresh_quarantine_list() # --- VUE : MISE À JOUR (OTA) --- def init_update_view(self): frame = ctk.CTkFrame(self, fg_color="transparent") self.views["update"] = frame ctk.CTkLabel(frame, text="Mise à jour Cloud", font=ctk.CTkFont(size=34, weight="bold")).pack(anchor="w", pady=(0, 20)) ctk.CTkLabel(frame, text="Synchronisation avec le dépôt 7ka1 Git.", text_color="#9CA3AF", font=ctk.CTkFont(size=14)).pack(anchor="w", pady=(0, 20)) self.btn_update_soft = ctk.CTkButton(frame, text="⬇️ Mettre à jour", command=self.run_software_update, fg_color="#8B5CF6", hover_color="#7C3AED", height=45) self.btn_update_soft.pack(anchor="w") self.update_console = ctk.CTkTextbox(frame, height=200, font=ctk.CTkFont(family="Consolas", size=13), fg_color="#111827", corner_radius=10) self.update_console.pack(fill="x", pady=20) self.setup_console_tags(self.update_console) def run_software_update(self): self.btn_update_soft.configure(state="disabled") self.update_console.insert("end", f"{self.get_time_prefix()}[*] Négociation avec git.7ka1.com...\n", "info") try: current_file_path = os.path.abspath(__file__) urllib.request.urlretrieve(UPDATE_URL, current_file_path) self.update_console.insert("end", f"{self.get_time_prefix()}[+] Code source mis à jour !\n", "success") messagebox.showinfo("Succès", "Redémarrage requis.") self.destroy() except Exception as e: self.update_console.insert("end", f"{self.get_time_prefix()}[-] Échec : {e}\n", "danger") self.btn_update_soft.configure(state="normal") if __name__ == "__main__": app = Antivirus7LnA() app.mainloop()