Rechercher des virus dans les fichiers en Go avec ClamAV
Les applications qui acceptent des téléversements ont besoin d’une règle claire pour libérer les fichiers après l’analyse. Ce guide utilise ClamAV et Go pour distinguer quatre résultats : sain, infecté, erreur du scanner et limite de taille. Seul un résultat sain explicite permet à l’application de poursuivre le traitement.
Introduction à l’analyse antivirus en Go
Conservez les nouveaux fichiers téléversés dans une quarantaine privée jusqu’à la fin de l’analyse. Un échec de connexion, un dépassement de délai, une réponse vide ou une limite d’analyse dépassée ne prouve pas qu’un fichier est sain. L’analyse antivirus n’est qu’une couche de protection ; elle ne peut pas établir que chaque fichier est inoffensif.
Nous allons transmettre en flux un instantané borné d’un fichier complet à un démon local. L’exemple n’analyse jamais un répertoire, ne supprime aucun fichier téléversé et ne tente jamais de réparer un fichier. Votre application doit garder l’instantané analysé immuable et ne publier ces mêmes octets qu’après un verdict sain.
Présentation de ClamAV et de ses capacités
ClamAV fournit la commande clamscan, le démon persistant clamd et freshclam pour la mise à jour
des signatures. Un démon évite de charger la base de signatures à chaque requête. Ce guide utilise
le client Go pour le protocole du démon, et non les liaisons go-clamav séparées pour libclamav.
Le protocole TCP du démon ne comporte aucune authentification. Utilisez un socket Unix aux permissions restreintes sur le même hôte et n’exposez pas clamd au réseau public. Consultez le guide d’analyse de ClamAV.
Mise en place de ClamAV dans votre environnement Go
L’exemple exécutable ci-dessous a été testé sous Linux avec Bash, Go 1.26.8 et ClamAV 1.5.4.
Installez Go à l’aide du guide d’installation officiel si go version
ne fonctionne pas. Utilisez une version maintenue de ClamAV avec des signatures à jour ; consultez la
politique de support de ClamAV pour la version de votre distribution.
Sur Debian ou Ubuntu avec systemd, installez le scanner et le démon à l’aide des paquets de la
distribution. Collez chaque bloc Bash en entier : && empêche une étape en échec de lancer la suivante.
sudo apt-get update &&
sudo apt-get install clamav clamav-daemon &&
sudo systemctl status clamav-freshclam
Laissez le programme de mise à jour des signatures terminer le téléchargement initial de la base.
Évitez de lancer un second processus freshclam alors que son service gère déjà les mises à jour.
Relisez le clamd.conf fourni par votre distribution avant de démarrer ou de redémarrer clamav-daemon ;
utilisez les limites ci-dessous et vérifiez le chemin du socket et les permissions du groupe. Les
chemins des services diffèrent sur les autres systèmes d’exploitation.
sudo systemctl restart clamav-daemon &&
sudo systemctl status clamav-daemon
Mise en œuvre de go-clamd pour l’analyse antivirus
Exécutez ceci depuis le répertoire où vous souhaitez créer un nouveau projet scan-example. Le bloc crée un
module Go distinct et épingle la version du client utilisée ici. Un répertoire existant provoque une
erreur ; choisissez un autre répertoire parent plutôt que de supprimer votre projet. En cas de succès,
votre shell entre dans le nouveau projet. Si la création, la navigation ou l’installation des
dépendances échoue, il reste dans le répertoire d’origine ; examinez l’erreur avant de continuer.
(
mkdir scan-example &&
cd scan-example &&
GOWORK=off go mod init example.com/scan-example &&
GOWORK=off go get github.com/dutchcoders/go-clamd@v0.0.0-20170520113014-b970184f4d9e
) && cd scan-example
GOWORK=off rend ce module autonome indépendant d’un éventuel fichier go.work englobant. Utilisez-le
aussi pour les commandes de compilation et de test ; consultez la
documentation des espaces de travail Go.
Ce client est ancien et de petite taille. Son ScanResult
possède un champ Path, et non Filename. ScanStream renvoie un canal et une erreur, et accepte un canal
d’abandon. La fermeture de ce canal d’abandon ferme une connexion établie, mais le paquet ne fournit
pas de délai complet tenant compte du contexte pour l’établissement de la connexion et toutes les E/S.
L’exemple exécute donc le client dans un processus worker de courte durée. Le exec.CommandContext de Go
termine ce processus en cas de dépassement de délai et attend sa sortie, ce qui libère son socket et
ses goroutines. Cela coûte un processus par analyse. Dans un service, bornez la concurrence des
workers ; ne lancez pas une goroutine ou un processus sans limite pour chaque fichier téléversé entrant.
Exemples pratiques et extraits de code
Enregistrez le programme complet sous main.go. Il accepte un fichier régulier choisi par
l’application, lit au plus 10 MiB plus un octet, et transmet cet instantané en flux. CLAMD_SOCKET
est un paramètre de configuration du serveur, pas un paramètre de téléversement. L’absence de
réponse, des entrées mal formées ou une deuxième réponse exposée par le canal du client entraînent
un échec en mode fermé. Les diagnostics bruts du démon restent hors de la sortie publique du programme.
Le client épinglé peut ignorer une réponse finale non terminée et masquer un échec de lecture après une réponse complète. Le délai du worker empêche les blocages ; il ne corrige pas l’analyseur de réponses. Un verdict sain signifie que le client a exposé exactement une réponse saine provenant de votre démon local de confiance. Utilisez un client qui signale explicitement les erreurs de transport si votre intégration exige des garanties plus fortes.
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", "--stdin")
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) == 3 && os.Args[1] == "--worker" && os.Args[2] == "--stdin" {
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)
}
}
Compilez l’exécutable avant de le lancer ; l’enveloppe de délai lance ce même exécutable en mode worker. Les codes de sortie sont 0 pour sain, 1 pour infecté, 2 pour erreur du scanner et 3 pour limite de taille.
GOWORK=off go build -o scan-example . &&
CLAMD_SOCKET=/var/run/clamav/clamd.ctl ./scan-example "/path/to/quarantine/completed-upload"
Remplacez le chemin du fichier par celui d’un fichier complet de votre répertoire de quarantaine
privé, et le chemin du socket par le LocalSocket de votre démon s’il diffère. Un fichier sain affiche
clean ; EICAR affiche infected. La compilation doit réussir avant qu’une analyse ne
s’exécute ; ainsi, une recompilation échouée ne peut pas lancer un binaire obsolète.
Le délai borne la communication avec le démon après la lecture de l’instantané local. Le stockage de quarantaine doit être local et contrôlé par votre application ; il ne s’agit ni d’un délai pour des fichiers arbitraires montés en réseau, ni d’un contrôle d’autorisation sur des chemins fournis par l’utilisateur.
Configuration de ClamAV pour la production
Alignez les limites du démon sur la limite applicative de 10 MiB. Par exemple, voici des
paramètres clamd.conf, et non des
commandes 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
Définissez LocalSocketGroup sur le groupe partagé avec votre application et supprimez tout écouteur TCP
dont vous n’avez pas besoin. Réservez le répertoire du socket aux processus de confiance. Les limites
de taille et d’archive évitent un travail excessif, mais peuvent aussi laisser du contenu non
analysé. AlertExceedsMax yes fait produire aux limites applicables du moteur des résultats Heuristics.Limits.Exceeded…, que
ce programme signale comme size-limit plutôt que comme logiciel malveillant. Le même état couvre
les limites de récursion et de nombre de fichiers.
Le rejet lié à StreamMaxLength est une erreur de protocole distincte. Il peut survenir avant que le client
ait fini d’écrire ; dans ce cas, un échec de connexion ou d’écriture est signalé comme
scanner-error. Les deux états bloquent la libération ; aucun ne signifie que le fichier entier a
été analysé. Les autres restrictions de format et limitations d’analyseur de ClamAV s’appliquent
toujours, même lorsque le verdict global est sain.
Maintenez la base de signatures officielle à jour avec freshclam. Surveillez les échecs de mise à jour,
les journaux du démon, la latence des analyses et l’arriéré de la quarantaine. Après toute
modification de configuration, redémarrez ou rechargez les services selon le système de paquets de
votre plateforme. Ajustez la concurrence à la mémoire et au CPU disponibles, y compris les coûts de
décompression, plutôt qu’au seul nombre de requêtes HTTP.
Tests avec EICAR
EICAR publie une chaîne de test inoffensive de 68 octets
que les moteurs antivirus détectent intentionnellement. Générez-la uniquement dans un répertoire de
test isolé. Enregistrez ceci sous main_test.go ; ce fichier de test Go utilise les octets EICAR exacts
du paquet client et le programme compilé à l’étape précédente. Définissez CLAMD_SOCKET sur le socket privé de
votre démon de test.
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)
}
}
GOWORK=off go test -run '^TestEICAR$' -count=1 .
Un démon arrêté doit faire échouer ce test, et non compter comme une détection réussie de logiciel malveillant. Testez aussi un petit fichier de test sain, un fichier de test trop volumineux, des fichiers manquants, des réponses du démon mal formées ou vides, et un démon qui ne répond jamais. Les jeux de test du protocole peuvent vérifier la gestion des erreurs de manière déterministe ; ils ne remplacent pas une vérification EICAR avec le scanner réel et sa base configurée.
Bonnes pratiques d’analyse antivirus dans les applications Go
- Mettre en quarantaine d’abord : stockez les fichiers téléversés complets en privé, analysez un instantané immuable et ne libérez que ces mêmes octets après un résultat sain. N’utilisez pas de répertoire de téléversement public.
- Échouer en mode fermé : rejetez ou conservez les fichiers en cas d’erreur du scanner ou de limite atteinte. Réessayez via une file bornée avec un temps d’attente croissant entre les tentatives ; l’épuisement du budget de tentatives ne doit pas libérer le fichier.
- Borner les ressources : plafonnez la taille des téléversements, les processus workers simultanés, la profondeur des archives, les octets décompressés, la durée d’analyse et la longueur de la file. Surveillez chaque type d’analyse rejetée.
- Isoler le scanner : utilisez un socket local restreint et un compte de service dédié. L’application transmet les octets en flux ; le démon n’a donc pas besoin d’un accès direct aux fichiers en quarantaine.
- Conserver un statut utile : exposez des verdicts stables aux appelants ; conservez les diagnostics détaillés dans des journaux à accès contrôlé. Ne traitez jamais chaque erreur renvoyée comme « virus trouvé ».
- Examiner la dépendance : le client épinglé est ancien et signale les erreurs d’E/S de façon limitée. L’isolation des processus assure ici l’annulation, mais n’en fait pas une API moderne tenant compte du contexte.
Résolution des problèmes courants
- Erreurs du scanner : vérifiez les permissions du socket, la disponibilité de la base, l’état du démon et les journaux. Vérifiez que le socket configuré correspond à celui utilisé par Go. Ne rendez pas le socket public.
- Dépassements de délai : examinez la profondeur de la file et les ressources du démon avant d’augmenter le budget de cinq secondes. Un dépassement de délai tue le worker et bloque la libération du fichier.
- Limites de taille : comparez la taille applicative,
StreamMaxLength,MaxFileSizeet les limites d’archive décompressée. Augmenter une seule valeur peut laisser une autre limite en vigueur. - EICAR non détecté : vérifiez le fichier de test de 68 octets, la base de signatures chargée et le verdict réel. Une erreur de connexion n’est pas une détection positive.
- Résultats sains inattendus : confirmez
HeuristicAlertsetAlertExceedsMax, examinez les formats pris en charge et les limites d’analyse, et assurez-vous de publier les octets qui ont été analysés.
Conclusion et ressources complémentaires
ClamAV peut ajouter une analyse utile des logiciels malveillants à un pipeline de téléversement Go lorsque ses états d’échec sont explicites. L’application, la configuration du démon et la politique de libération doivent s’accorder sur ce que signifie une analyse réussie. Appuyez-vous sur les références principales pour adapter cet exemple :
Le Robot 🤖 /file/virusscan (English) de Transloadit
utilise ClamAV avec des mises à jour quotidiennes des signatures dans le cadre de notre
service de filtrage des fichiers. Son option
error_on_decline détermine si un fichier rejeté arrête l’Assembly ou est exclu du traitement
ultérieur. Consultez la documentation du Robot pour choisir ce comportement.
