Escaner gráfico para ClamAV
Para el uso del antivirus ClamAV y su motor de busqueda clamscan
en modo gráfico suelo recomendar usar la aplicación ClamTk
, la cual funciona correctamente.

Sin embargo, me apetecía disponer de una aplicación más sencilla para el uso del antivirus.
Así que con un poco de Python y las librerías de Tkinter me he creado mi aplicación gráfica para poder usar el antivirus ClamAV y disponer de un "modo oscuro" en su interfaz.

¿Cómo se instala?
Si tienes configurado el repositorio de SoloConLinux, tienes la aplicación disponible para Debian 12 y Debian 13, tan sólo tienes que ejecutar:
# Actualizar e instalar
sudo apt update
sudo apt -y install antivirus-clamav
# Crear Icono en escritorio
cp /usr/share/applications/antivirus.desktop ~/Escritorio/
Puedes configurar el repositorio SoloConLinux para tu Debian 13 simplemente con las instrucciones del artículo:

La aplicación está creada para que use de forma automática su propio entorno virtual
de Python3 y no haya problemas de dependencias en los equipos Debian que lo vayan a usar.
Código fuente
Para los que tengais curiosidad por el código fuente (versión 9 y última del código) os lo dejo a continuación.
Nota: Perdonad, pero no me ha dado tiempo a dejarlo en mi repositorio en modo público.
Para que os funcione teneis que tener instalado ClamAV, Python3 y customTKinter
# Instalar CustomTKinter
pip install customtkinter
#!/usr/bin/env python3
# LuisGuLo (SoloConLinux)
import customtkinter as ctk
from tkinter import filedialog
import subprocess
import threading
import os
# Inicializar apariencia
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue")
# Variable global para controlar el proceso
proceso_activo = None
# Función para seleccionar ruta (inicia en HOME)
def seleccionar_ruta():
home_dir = os.path.expanduser("~")
ruta = filedialog.askdirectory(initialdir=home_dir)
if ruta:
ruta_entry.delete(0, ctk.END)
ruta_entry.insert(0, ruta)
actualizar_comando()
# Función para actualizar el comando
def actualizar_comando():
ruta = ruta_entry.get()
comando = ["clamscan"]
if recursivo_var.get():
comando.append("-r")
if comprimidos_var.get():
comando.append("--scan-archive=yes")
if pua_var.get():
comando.append("--detect-pua=yes")
if max_filesize_var.get() != "Sin límite":
comando.append(f"--max-filesize={max_filesize_var.get()}")
if max_scansize_var.get() != "Sin límite":
comando.append(f"--max-scansize={max_scansize_var.get()}")
if ruta:
comando.append(ruta)
comando_label.configure(text="Comando: " + " ".join(comando))
# Función para ejecutar escaneo en hilo
def ejecutar_escaneo():
progreso.start()
resultado_text.delete("1.0", ctk.END)
threading.Thread(target=escaneo_en_hilo).start()
def escaneo_en_hilo():
global proceso_activo
ruta = ruta_entry.get()
if not ruta:
resultado_text.insert(ctk.END, " Por favor selecciona una ruta.\n")
progreso.stop()
return
comando = comando_label.cget("text").replace("Comando: ", "").split()
try:
proceso_activo = subprocess.Popen(comando, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
stdout, stderr = proceso_activo.communicate()
resultado_text.insert(ctk.END, stdout)
if stderr:
resultado_text.insert(ctk.END, "\n Errores:\n" + stderr)
except Exception as e:
resultado_text.insert(ctk.END, f" Error al ejecutar: {e}")
finally:
progreso.stop()
proceso_activo = None
# Función para detener escaneo
def detener_escaneo():
global proceso_activo
if proceso_activo and proceso_activo.poll() is None:
proceso_activo.terminate()
resultado_text.insert(ctk.END, "\n Escaneo detenido por el usuario.\n")
progreso.stop()
proceso_activo = None
# Crear ventana
ventana = ctk.CTk()
ventana.title("🛡️ Escaneo con ClamAV")
ventana.geometry("900x750")
# Título visual
ctk.CTkLabel(
ventana,
text="🛡️ Escáner Antivirus con ClamAV",
font=ctk.CTkFont(size=24, weight="bold"),
text_color="white"
).grid(row=0, column=0, columnspan=3, padx=10, pady=(20, 10))
# Ruta
ctk.CTkLabel(ventana, text="Ruta a escanear:").grid(row=1, column=0, padx=10, pady=10, sticky="w")
ruta_entry = ctk.CTkEntry(ventana, width=500)
ruta_entry.grid(row=1, column=1, padx=10, pady=10)
ctk.CTkButton(ventana, text="Seleccionar", command=seleccionar_ruta).grid(row=1, column=2, padx=10, pady=10)
# Opciones
recursivo_var = ctk.BooleanVar()
comprimidos_var = ctk.BooleanVar()
pua_var = ctk.BooleanVar()
ctk.CTkCheckBox(ventana, text="Búsqueda recursiva", variable=recursivo_var, command=actualizar_comando).grid(row=2, column=0, columnspan=3, padx=10, pady=5, sticky="w")
ctk.CTkCheckBox(ventana, text="Escanear comprimidos", variable=comprimidos_var, command=actualizar_comando).grid(row=3, column=0, columnspan=3, padx=10, pady=5, sticky="w")
ctk.CTkCheckBox(ventana, text="Localizar PUA", variable=pua_var, command=actualizar_comando).grid(row=4, column=0, columnspan=3, padx=10, pady=5, sticky="w")
# Tamaños
ctk.CTkLabel(ventana, text="Max filesize:").grid(row=5, column=0, padx=10, pady=10, sticky="w")
max_filesize_var = ctk.StringVar(value="Sin límite")
ctk.CTkOptionMenu(ventana, variable=max_filesize_var, values=["Sin límite", "50M", "100M", "500M"], command=lambda _: actualizar_comando()).grid(row=5, column=1, padx=10, pady=10)
ctk.CTkLabel(ventana, text="Max scansize:").grid(row=6, column=0, padx=10, pady=10, sticky="w")
max_scansize_var = ctk.StringVar(value="Sin límite")
ctk.CTkOptionMenu(ventana, variable=max_scansize_var, values=["Sin límite", "200M", "500M", "2G"], command=lambda _: actualizar_comando()).grid(row=6, column=1, padx=10, pady=10)
# Comando generado
comando_label = ctk.CTkLabel(ventana, text="Comando: clamscan", text_color="lightblue", wraplength=800, anchor="w", justify="left")
comando_label.grid(row=7, column=0, columnspan=3, padx=10, pady=10)
# Botones
ctk.CTkButton(ventana, text="Ejecutar escaneo", command=ejecutar_escaneo).grid(row=8, column=0, padx=10, pady=10)
ctk.CTkButton(ventana, text="Detener escaneo", command=detener_escaneo).grid(row=8, column=1, padx=10, pady=10)
# Barra de progreso
progreso = ctk.CTkProgressBar(ventana, mode="indeterminate", width=600)
progreso.grid(row=9, column=0, columnspan=3, padx=10, pady=10)
# Informe con barra de desplazamiento
ctk.CTkLabel(ventana, text="📋 Informe escaneo:").grid(row=10, column=0, columnspan=3, padx=10, pady=10, sticky="w")
informe_frame = ctk.CTkFrame(ventana)
informe_frame.grid(row=11, column=0, columnspan=3, padx=10, pady=10)
resultado_text = ctk.CTkTextbox(informe_frame, width=820, height=300)
resultado_text.grid(row=0, column=0, sticky="nsew")
scrollbar = ctk.CTkScrollbar(informe_frame, orientation="vertical", command=resultado_text.yview)
scrollbar.grid(row=0, column=1, sticky="ns")
resultado_text.configure(yscrollcommand=scrollbar.set)
# Ejecutar ventana
ventana.mainloop()
Espero que os sea útil