Analiza archivos en busca de virus en Go con ClamAV
Las aplicaciones que aceptan subidas necesitan una regla clara para liberar los archivos después del análisis. Esta guía usa ClamAV y Go para distinguir cuatro resultados: limpio, infectado, error del analizador y límite de tamaño. Solo un resultado explícito de archivo limpio permite que la aplicación continúe con el procesamiento.
Introducción al análisis de virus en Go
Mantén las nuevas subidas en cuarentena privada hasta que termine el análisis. Un fallo de conexión, un tiempo de espera agotado, una respuesta vacía o un límite de análisis superado no demuestran que un archivo esté limpio. El análisis antivirus es una capa de protección; no puede determinar que todos los archivos sean inofensivos.
Transmitiremos una instantánea de tamaño limitado de un archivo cuya subida haya finalizado a un daemon local. El ejemplo nunca analiza un directorio, elimina una subida ni intenta reparar un archivo. Tu aplicación debe mantener inmutable la instantánea analizada y publicar esos mismos bytes solo después de un veredicto de archivo limpio.
Descripción general de ClamAV y sus capacidades
ClamAV proporciona el comando clamscan, el daemon persistente
clamd y freshclam para actualizar las firmas.
Usar un daemon evita cargar la base de datos de firmas en cada solicitud. Esta guía usa el cliente de
Go para el protocolo del daemon, no los bindings independientes go-clamav
para libclamav.
El protocolo TCP del daemon no tiene autenticación. Usa un socket Unix con permisos restringidos en el mismo host y no expongas clamd a la red pública. Consulta la guía de análisis de ClamAV.
Configura ClamAV en tu entorno de Go
En Debian o Ubuntu, instala el analizador y el daemon mediante los paquetes de la distribución:
sudo apt-get update
sudo apt-get install clamav clamav-daemon
sudo systemctl status clamav-freshclam
Deja que el actualizador de firmas termine la descarga inicial de su base de datos. Evita ejecutar
un segundo proceso freshclam mientras su servicio ya gestiona las actualizaciones.
Revisa el archivo clamd.conf de tu distribución antes de iniciar o reiniciar
clamav-daemon; usa los límites que se indican a continuación y confirma la ruta del
socket y los permisos de grupo. Las rutas de los servicios varían en otros sistemas operativos.
sudo systemctl restart clamav-daemon
sudo systemctl status clamav-daemon
Implementa go-clamd para analizar archivos en busca de virus
Crea un módulo de Go independiente y fija la versión del cliente que se usa aquí:
mkdir scan-example
cd scan-example
go mod init example.com/scan-example
go get github.com/dutchcoders/go-clamd@v0.0.0-20170520113014-b970184f4d9e
Este cliente es antiguo y pequeño. Su ScanResult
tiene un campo Path, no Filename.
ScanStream devuelve un canal y un error, y acepta un canal de cancelación.
Cerrar ese canal de cancelación cierra una conexión establecida, pero el paquete no proporciona un
plazo completo que tenga en cuenta el contexto para establecer la conexión y realizar todas las
operaciones de E/S.
Por eso, el ejemplo ejecuta el cliente en un proceso de trabajo de corta duración. La función
exec.CommandContext de Go termina ese proceso cuando se agota el tiempo de espera y espera
a que finalice, liberando su socket y sus goroutines. Esto requiere un proceso por análisis. Limita
la concurrencia de los procesos de trabajo en un servicio; no inicies goroutines o procesos sin
límite para cada subida entrante.
Ejemplos prácticos y fragmentos de código
Guarda el programa completo como main.go. Acepta un archivo regular
seleccionado por la aplicación, lee como máximo 10 MiB más un byte y transmite esa instantánea.
CLAMD_SOCKET es una configuración del servidor, no un parámetro de subida.
Un resultado vacío o ambiguo bloquea la liberación del archivo por seguridad, incluida una segunda
respuesta inesperada. Los diagnósticos sin procesar del daemon quedan fuera de la salida pública
del programa.
package main
import (
"bytes"
"context"
"fmt"
"io"
"os"
"os/exec"
"strings"
"time"
"github.com/dutchcoders/go-clamd"
)
type Verdict string
const (
Clean Verdict = "clean"
Infected Verdict = "infected"
ScannerError Verdict = "scanner-error"
SizeLimit Verdict = "size-limit"
maxBytes = 10 * 1024 * 1024
)
func scanStream(data []byte, socket string) Verdict {
abort := make(chan bool)
defer close(abort)
replies, err := clamd.NewClamd("unix:" + socket).ScanStream(bytes.NewReader(data), abort)
if err != nil {
return ScannerError
}
verdict := ScannerError
count := 0
for result := range replies {
count++
if result == nil || count != 1 {
return ScannerError
}
if result.Raw == "INSTREAM size limit exceeded. ERROR" ||
(result.Status == clamd.RES_FOUND &&
strings.HasPrefix(result.Description, "Heuristics.Limits.Exceeded")) {
verdict = SizeLimit
continue
}
switch {
case result.Raw == "stream: OK":
verdict = Clean
case result.Status == clamd.RES_FOUND && result.Path == "stream" && result.Description != "":
verdict = Infected
default:
verdict = ScannerError
}
}
return verdict
}
func scanFile(filePath, executable string, timeout time.Duration) Verdict {
// The caller supplies a completed, immutable file in its private quarantine directory.
info, err := os.Lstat(filePath)
if err != nil || !info.Mode().IsRegular() {
return ScannerError
}
if info.Size() > maxBytes {
return SizeLimit
}
file, err := os.Open(filePath)
if err != nil {
return ScannerError
}
defer file.Close()
data, err := io.ReadAll(io.LimitReader(file, maxBytes+1))
if err != nil {
return ScannerError
}
if len(data) > maxBytes {
return SizeLimit
}
socket := os.Getenv("CLAMD_SOCKET")
if socket == "" {
socket = "/var/run/clamav/clamd.ctl"
}
if !strings.HasPrefix(socket, "/") || timeout <= 0 {
return ScannerError
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
command := exec.CommandContext(ctx, executable, "--worker")
command.Env = []string{"CLAMD_SOCKET=" + socket}
command.Stdin = bytes.NewReader(data)
output, err := command.Output()
if err != nil || ctx.Err() != nil {
return ScannerError
}
switch verdict := Verdict(output); verdict {
case Clean, Infected, ScannerError, SizeLimit:
return verdict
default:
return ScannerError
}
}
func main() {
if len(os.Args) == 2 && os.Args[1] == "--worker" {
data, err := io.ReadAll(io.LimitReader(os.Stdin, maxBytes+1))
verdict := ScannerError
if err == nil && len(data) <= maxBytes {
verdict = scanStream(data, os.Getenv("CLAMD_SOCKET"))
}
fmt.Print(verdict)
return
}
if len(os.Args) != 2 {
fmt.Fprintln(os.Stderr, "Usage: scan-example <quarantined-file>")
os.Exit(2)
}
executable, err := os.Executable()
if err != nil {
fmt.Println(ScannerError)
os.Exit(2)
}
verdict := scanFile(os.Args[1], executable, 5*time.Second)
fmt.Println(verdict)
switch verdict {
case Clean:
return
case Infected:
os.Exit(1)
case SizeLimit:
os.Exit(3)
default:
os.Exit(2)
}
}
Compila el ejecutable antes de ejecutarlo; el envoltorio que controla el tiempo de espera inicia ese mismo ejecutable en modo de proceso de trabajo. Los códigos de salida son 0 para limpio, 1 para infectado, 2 para error del analizador y 3 para límite de tamaño.
go build -o scan-example .
CLAMD_SOCKET=/var/run/clamav/clamd.ctl ./scan-example /path/to/quarantine/completed-upload
El tiempo de espera limita la comunicación con el daemon después de leer la instantánea local. El almacenamiento de cuarentena debe ser local y estar controlado por tu aplicación; este plazo no se aplica a archivos arbitrarios montados en red ni constituye una comprobación de autorización sobre las rutas proporcionadas por el usuario.
Configura ClamAV para su uso en producción
Ajusta los límites del daemon al límite de 10 MiB de la aplicación. Por ejemplo, estos son
ajustes de clamd.conf, no comandos del shell:
LocalSocket /var/run/clamav/clamd.ctl
LocalSocketMode 660
StreamMaxLength 10M
MaxFileSize 10M
MaxScanSize 50M
MaxRecursion 16
MaxFiles 1000
MaxThreads 4
HeuristicAlerts yes
AlertExceedsMax yes
Establece LocalSocketGroup en el grupo compartido con tu aplicación y elimina cualquier
socket de escucha TCP que no necesites. Restringe el acceso al directorio del socket a los procesos
de confianza. Los límites de tamaño y de archivos contenedores evitan un procesamiento excesivo,
pero también pueden dejar contenido sin analizar. AlertExceedsMax yes hace que los límites
aplicables del motor generen detecciones Heuristics.Limits.Exceeded…, que este programa comunica
como size-limit en lugar de malware. El mismo estado abarca los límites de recursión y de
cantidad de archivos.
El rechazo por StreamMaxLength es un error de protocolo independiente. Puede llegar
antes de que el cliente termine de escribir, en cuyo caso un fallo de conexión o escritura se
comunica como scanner-error. Ambos estados bloquean la liberación del archivo; ninguno significa
que se haya analizado el archivo completo. Las demás restricciones de formato y limitaciones del
análisis sintáctico de ClamAV siguen vigentes incluso cuando el veredicto general es limpio.
Mantén actualizada la base de datos oficial de firmas mediante freshclam.
Supervisa los fallos de actualización, los registros del daemon, la latencia del análisis y los
archivos acumulados en cuarentena. Reinicia o recarga los servicios según el sistema de paquetes de
tu plataforma después de cambiar la configuración. Ajusta la concurrencia a la memoria y la CPU
disponibles, incluidos los costos de descompresión, en lugar de basarte solo en el número de
solicitudes HTTP.
Prueba con eicar
EICAR publica una cadena de prueba inofensiva de 68 bytes que los
motores antivirus detectan intencionalmente. Genérala solo en un directorio de pruebas aislado.
Guarda lo siguiente como main_test.go; usa los bytes exactos de EICAR del paquete
del cliente y el programa compilado en el paso anterior. Establece CLAMD_SOCKET
en el socket privado de tu daemon de prueba.
package main
import (
"os"
"path/filepath"
"testing"
"time"
"github.com/dutchcoders/go-clamd"
)
func TestEICAR(t *testing.T) {
if len(clamd.EICAR) != 68 {
t.Fatal("EICAR fixture must contain exactly 68 bytes")
}
executable, err := filepath.Abs("scan-example")
if err != nil {
t.Fatal(err)
}
file := filepath.Join(t.TempDir(), "eicar.txt")
if err := os.WriteFile(file, clamd.EICAR, 0600); err != nil {
t.Fatal(err)
}
if verdict := scanFile(file, executable, 5*time.Second); verdict != Infected {
t.Fatalf("EICAR must be detected as infected; got %s", verdict)
}
}
go test -run '^TestEICAR$' -count=1 .
Si el daemon está detenido, esta prueba debe fallar, no contar como una detección correcta de malware. Prueba también con un archivo de prueba pequeño y limpio, otro que exceda el tamaño máximo, archivos que no existan, respuestas del daemon malformadas o vacías y un daemon que nunca responda. Los casos de prueba del protocolo pueden comprobar el manejo de errores de forma determinista; no sustituyen una comprobación con EICAR contra el analizador real y su base de datos configurada.
Buenas prácticas para analizar virus en aplicaciones de Go
- Pon los archivos en cuarentena primero: almacena las subidas finalizadas de forma privada, analiza una instantánea inmutable y libera solo esos mismos bytes después de un resultado limpio. No uses un directorio público para las subidas.
- Bloquea la liberación ante fallos: rechaza o retén los archivos si el analizador presenta errores o alcanza sus límites. Reintenta mediante una cola acotada con esperas progresivas; agotar los reintentos permitidos no debe liberar el archivo.
- Limita los recursos: establece límites para el tamaño de las subidas, los procesos de trabajo simultáneos, la profundidad de anidamiento de los archivos contenedores, los bytes descomprimidos, la duración del análisis y la longitud de la cola. Supervisa cada tipo de análisis rechazado.
- Aísla el analizador: usa un socket local restringido y una cuenta de servicio dedicada. La aplicación transmite bytes, por lo que el daemon no necesita acceso directo a los archivos en cuarentena.
- Conserva información útil del estado: proporciona veredictos estables a quienes llaman al servicio; guarda los diagnósticos detallados en registros con control de acceso. Nunca interpretes todos los errores devueltos como «virus encontrado».
- Revisa la dependencia: la versión fijada del cliente es antigua y ofrece información limitada sobre errores de E/S. Aquí, el aislamiento del proceso permite la cancelación, pero no convierte al cliente en una API moderna que tenga en cuenta el contexto.
Resuelve problemas comunes
- Errores del analizador: comprueba los permisos del socket, la disponibilidad de la base de datos, el estado del daemon y los registros. Verifica que el socket configurado coincida con el que usa Go. No hagas público el socket.
- Tiempos de espera agotados: examina la cantidad de elementos en la cola y los recursos del daemon antes de aumentar el plazo de cinco segundos. Agotar el tiempo de espera termina el proceso de trabajo y bloquea la liberación del archivo.
- Límites de tamaño: compara el límite de tamaño de la aplicación,
StreamMaxLength,MaxFileSizey los límites del contenido descomprimido de los archivos contenedores. Aumentar un solo valor puede dejar otro límite vigente. - EICAR no detectado: verifica el archivo de prueba de 68 bytes, la base de datos de firmas cargada y el veredicto real. Un error de conexión no es una detección positiva.
- Resultados limpios inesperados: confirma
HeuristicAlertsyAlertExceedsMax, revisa los formatos compatibles y los límites de análisis, y asegúrate de publicar los bytes que se analizaron.
Conclusión y recursos adicionales
ClamAV puede añadir una comprobación útil de malware a un pipeline de subida en Go cuando sus estados de fallo son explícitos. La aplicación, la configuración del daemon y la política de liberación deben coincidir en lo que significa un análisis satisfactorio. Usa las fuentes primarias al adaptar este ejemplo:
- Documentación de ClamAV
- Código fuente de la versión fijada de go-clamd
- Documentación del paquete go-clamd
El Robot 🤖 /file/virusscan
de Transloadit usa ClamAV con actualizaciones diarias de firmas como parte de nuestro
servicio de filtrado de archivos. Su opción
error_on_decline controla si un archivo rechazado detiene la Assembly o se excluye
del procesamiento posterior. Consulta la documentación del Robot al elegir ese comportamiento.
