chore: add go.work and optional monorepo orchestrator

This commit is contained in:
senke 2026-02-14 18:21:39 +01:00
parent fb8411c6ad
commit afea976f57
10 changed files with 124 additions and 136 deletions

3
go.work Normal file
View file

@ -0,0 +1,3 @@
go 1.23.8
use ./veza-backend-api

View file

@ -3,6 +3,7 @@ github.com/Microsoft/hcsshim v0.11.5/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/cilium/ebpf v0.9.1/go.mod h1:+OhNOIXx/Fnu1IE8bJz2dzOA+VSfyTfdNUVdlQnxUFY=
github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=
github.com/containerd/btrfs/v2 v2.0.0/go.mod h1:swkD/7j9HApWpzl8OHfrHNxppPd9l44DFZdF94BUj9k=
@ -30,6 +31,7 @@ github.com/dhowden/plist v0.0.0-20141002110153-5db6e0d9931a/go.mod h1:sLjdR6uwx3
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -87,6 +89,7 @@ k8s.io/component-base v0.26.2/go.mod h1:DxbuIe9M3IZPRxPIzhch2m1eT7uFrSBJUBuVCQEB
k8s.io/cri-api v0.27.1/go.mod h1:+Ts/AVYbIo04S86XbTD73UPp/DkTiYxtsFeOFEu32L0=
k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

View file

@ -6,9 +6,9 @@ Le backend Veza API intègre **ClamAV** pour scanner tous les fichiers uploadés
**MOD-P1-001**: Le scan ClamAV est maintenant **obligatoire** et se fait **AVANT toute persistance**. Si ClamAV est indisponible, tous les uploads sont rejetés (fail-secure).
### Limitation connue (go-clamd)
### Implémentation (clamdscan exec)
Le client ClamAV utilisé est **github.com/dutchcoders/go-clamd**, abandonné (dernier commit 2017). En cas de désactivation via `ENABLE_CLAMAV=false`, le scan est entièrement ignoré (aucune connexion à clamd). Pour un environnement sans ClamAV, documenter en interne que le scan antivirus est désactivé et gérer les risques associés.
Le scan utilise **clamdscan** (exécutable système) au lieu de la bibliothèque go-clamd abandonnée. Aucune dépendance Go externe pour ClamAV. Le daemon clamd doit être démarré ; clamdscan s'y connecte via socket Unix ou TCP selon la configuration système.
---
@ -20,9 +20,9 @@ Le client ClamAV utilisé est **github.com/dutchcoders/go-clamd**, abandonné (d
# Activer/désactiver ClamAV (par défaut: true)
CLAMAV_ENABLED=true
# Adresse du daemon ClamAV (par défaut: localhost:3310)
# Format: host:port (ex: localhost:3310, 192.168.1.100:3310)
CLAMAV_ADDRESS=localhost:3310
# Chemin vers clamdscan (par défaut: clamdscan)
# Ex: clamdscan, /usr/bin/clamdscan
CLAMAV_CLAMD_PATH=clamdscan
```
### Exemples de configuration
@ -30,14 +30,11 @@ CLAMAV_ADDRESS=localhost:3310
#### Développement local
```bash
CLAMAV_ENABLED=true
CLAMAV_ADDRESS=localhost:3310
CLAMAV_CLAMD_PATH=clamdscan
```
#### Production avec ClamAV distant
```bash
CLAMAV_ENABLED=true
CLAMAV_ADDRESS=clamav.internal:3310
```
#### Production (clamd sur machine distante)
Configurer clamd.conf pour pointer vers le daemon distant, ou utiliser un wrapper. Par défaut clamdscan utilise le socket Unix local.
#### Désactiver ClamAV (non recommandé en production)
```bash
@ -139,10 +136,10 @@ go test -tags=clamav ./internal/services -run TestUploadValidator_ClamAV_CleanFi
### Configuration pour les tests d'intégration
Par défaut, les tests utilisent `localhost:3310`. Pour utiliser une autre adresse :
Par défaut, les tests utilisent `clamdscan`. Pour un chemin personnalisé :
```bash
export CLAMAV_ADDRESS=192.168.1.100:3310
export CLAMAV_CLAMD_PATH=/usr/bin/clamdscan
go test -tags=clamav ./internal/services -run TestUploadValidator_ClamAV -v
```
@ -206,7 +203,7 @@ go test -tags=clamav ./internal/services -run TestUploadValidator_ClamAV -v
**Causes possibles** :
1. ClamAV n'est pas démarré : `sudo systemctl status clamav-daemon`
2. Mauvaise adresse : Vérifier `CLAMAV_ADDRESS`
2. clamdscan introuvable : Vérifier `CLAMAV_CLAMD_PATH` et que clamd est installé
3. Firewall bloque le port 3310
**Solution** :
@ -227,7 +224,7 @@ sudo systemctl restart clamav-daemon
**Solution** :
1. Vérifier que ClamAV est démarré
2. Vérifier la variable `CLAMAV_ADDRESS` si ClamAV est sur une autre machine
2. Vérifier que clamd est démarré et que clamdscan peut s'y connecter
3. Vérifier les logs : `sudo journalctl -u clamav-daemon -f`
---
@ -235,5 +232,4 @@ sudo systemctl restart clamav-daemon
## Références
- [ClamAV Documentation](https://docs.clamav.net/)
- [go-clamd Library](https://github.com/dutchcoders/go-clamd)
- [EICAR Test File](https://en.wikipedia.org/wiki/EICAR_test_file)

View file

@ -4,7 +4,6 @@ go 1.23.8
require (
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/Lyimmi/go-clamd v1.0.0
github.com/aws/aws-sdk-go-v2 v1.41.0
github.com/aws/aws-sdk-go-v2/config v1.32.6
github.com/aws/aws-sdk-go-v2/credentials v1.19.6

View file

@ -10,8 +10,6 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7Oputl
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Lyimmi/go-clamd v1.0.0 h1:w+px/HYUTMf/LeBXq+fem6nhd6J+bcbiYQO593sqUh0=
github.com/Lyimmi/go-clamd v1.0.0/go.mod h1:V9dsJa+47pc0C+3AqhEicGiNXhM1uwPiPAF+Ehj0EzQ=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=

View file

@ -65,21 +65,21 @@ type Config struct {
ConfigWatcher *ConfigWatcher
// Configuration
Env string // Environnement: development, test, production (P0-SECURITY)
AppPort int // Port pour le serveur HTTP (T0031)
AppDomain string // Domaine applicatif (APP_DOMAIN) — single source of truth pour URLs & CORS
JWTSecret string
JWTIssuer string // T0204: Issuer claim validation (P1-SECURITY)
JWTAudience string // T0204: Audience claim validation (P1-SECURITY)
ChatJWTSecret string // Secret pour les tokens WebSocket Chat
RedisURL string
RedisEnable bool // Enable/Disable Redis
DatabaseURL string
UploadDir string // Répertoire d'upload
Env string // Environnement: development, test, production (P0-SECURITY)
AppPort int // Port pour le serveur HTTP (T0031)
AppDomain string // Domaine applicatif (APP_DOMAIN) — single source of truth pour URLs & CORS
JWTSecret string
JWTIssuer string // T0204: Issuer claim validation (P1-SECURITY)
JWTAudience string // T0204: Audience claim validation (P1-SECURITY)
ChatJWTSecret string // Secret pour les tokens WebSocket Chat
RedisURL string
RedisEnable bool // Enable/Disable Redis
DatabaseURL string
UploadDir string // Répertoire d'upload
StreamServerURL string // URL du serveur de streaming
StreamServerInternalAPIKey string // API key for /internal/jobs/transcode (P1.1.2 - same as stream server INTERNAL_API_KEY)
ChatServerURL string // URL du serveur de chat
CORSOrigins []string // Liste des origines CORS autorisées
CORSOrigins []string // Liste des origines CORS autorisées
// S3 Storage Configuration (BE-SVC-005)
S3Bucket string // Nom du bucket S3
@ -263,12 +263,12 @@ func NewConfig() (*Config, error) {
RedisURL: getEnv("REDIS_URL", "redis://"+appDomain+":6379"),
RedisEnable: getEnvBool("REDIS_ENABLE", true),
// SECURITY: DATABASE_URL est REQUIS - contient des credentials sensibles
DatabaseURL: databaseURL,
UploadDir: getEnv("UPLOAD_DIR", "uploads"),
DatabaseURL: databaseURL,
UploadDir: getEnv("UPLOAD_DIR", "uploads"),
StreamServerURL: getEnv("STREAM_SERVER_URL", "http://"+appDomain+":8082"),
StreamServerInternalAPIKey: getEnv("STREAM_SERVER_INTERNAL_API_KEY", ""),
StreamServerInternalAPIKey: getEnv("STREAM_SERVER_INTERNAL_API_KEY", ""),
ChatServerURL: getEnv("CHAT_SERVER_URL", "http://"+appDomain+":8081"),
CORSOrigins: corsOrigins,
CORSOrigins: corsOrigins,
// S3 Storage Configuration (BE-SVC-005)
S3Bucket: getEnv("AWS_S3_BUCKET", ""),
@ -712,6 +712,10 @@ func (c *Config) initServices() error {
if !clamAVRequired {
c.Logger.Warn("CLAMAV_REQUIRED=false - Uploads will be accepted even if ClamAV is unavailable (degraded mode). This should only be used in development or with alternative security measures.")
}
// Chemin vers clamdscan (exec) - remplace go-clamd abandonné
if p := getEnv("CLAMAV_CLAMD_PATH", ""); p != "" {
uploadConfig.ClamAVClamdPath = p
}
var err error
c.UploadValidator, err = services.NewUploadValidator(uploadConfig, c.Logger)
if err != nil {

View file

@ -7,21 +7,20 @@ import (
"fmt"
"io"
"mime/multipart"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/Lyimmi/go-clamd"
"go.uber.org/zap"
)
// UploadValidator service pour valider les uploads de fichiers
type UploadValidator struct {
logger *zap.Logger
clamdClient *clamd.Clamd
clamdPath string // Chemin vers clamdscan (ex: clamdscan, /usr/bin/clamdscan)
quarantineDir string
clamAVRequiredButUnavailable bool // MOD-P1-001-REFINEMENT: Flag pour fail-secure localisé
clamAVRequired bool // MOD-P1-002: Si false, accepte uploads même si ClamAV down
@ -39,10 +38,11 @@ type UploadConfig struct {
AllowedImageTypes []string
AllowedVideoTypes []string
// Configuration ClamAV
ClamAVEnabled bool // Active/désactive ClamAV
ClamAVRequired bool // MOD-P1-002: Si false, accepte uploads même si ClamAV down (mode dégradé)
ClamAVAddress string
// Configuration ClamAV (utilise clamdscan en exec, remplace go-clamd abandonné)
ClamAVEnabled bool // Active/désactive ClamAV
ClamAVRequired bool // MOD-P1-002: Si false, accepte uploads même si ClamAV down (mode dégradé)
ClamAVAddress string // Gardé pour compat config, non utilisé (clamdscan utilise socket par défaut)
ClamAVClamdPath string // Chemin vers clamdscan (ex: clamdscan, /usr/bin/clamdscan)
// Dossier de quarantaine
QuarantineDir string
@ -79,10 +79,11 @@ func DefaultUploadConfig() *UploadConfig {
"video/avi",
},
ClamAVEnabled: true,
ClamAVRequired: true, // MOD-P1-002: Par défaut, ClamAV est requis (fail-secure)
ClamAVAddress: "localhost:3310",
QuarantineDir: "/quarantine",
ClamAVEnabled: true,
ClamAVRequired: true, // MOD-P1-002: Par défaut, ClamAV est requis (fail-secure)
ClamAVAddress: "localhost:3310",
ClamAVClamdPath: "clamdscan",
QuarantineDir: "/quarantine",
}
}
@ -92,85 +93,55 @@ func DefaultUploadConfig() *UploadConfig {
func NewUploadValidator(config *UploadConfig, logger *zap.Logger) (*UploadValidator, error) {
fmt.Printf("🔧 [UPLOAD VALIDATOR] Initialisation - ClamAVEnabled=%v, ClamAVRequired=%v\n", config.ClamAVEnabled, config.ClamAVRequired)
// EARLY RETURN: Si ClamAV est désactivé, ne JAMAIS toucher au réseau ClamAV
// EARLY RETURN: Si ClamAV est désactivé, ne JAMAIS toucher à clamdscan
if !config.ClamAVEnabled {
fmt.Printf("✅ [UPLOAD VALIDATOR] ClamAV désactivé - Aucune connexion réseau ne sera tentée\n")
fmt.Printf("✅ [UPLOAD VALIDATOR] ClamAV désactivé - Aucun scan antivirus\n")
logger.Info("ClamAV is disabled - virus scanning will be skipped",
zap.Bool("clamav_enabled", config.ClamAVEnabled),
zap.Bool("clamav_required", config.ClamAVRequired),
)
return &UploadValidator{
logger: logger,
clamdClient: nil, // Explicitement nil
clamdPath: "",
quarantineDir: config.QuarantineDir,
clamAVRequiredButUnavailable: false,
clamAVRequired: config.ClamAVRequired,
}, nil
}
// ClamAV est activé - initialiser le client
fmt.Printf("🛡️ [UPLOAD VALIDATOR] ClamAV activé - Initialisation client sur %s\n", config.ClamAVAddress)
var clamdClient *clamd.Clamd
// ClamAV activé - utiliser clamdscan (exec) au lieu de go-clamd abandonné
clamdPath := config.ClamAVClamdPath
if clamdPath == "" {
clamdPath = "clamdscan"
}
fmt.Printf("🛡️ [UPLOAD VALIDATOR] ClamAV activé - Utilisation de %s (exec)\n", clamdPath)
clamAVRequiredButUnavailable := false
// Parser host:port depuis config.ClamAVAddress
host, portStr, parseErr := net.SplitHostPort(config.ClamAVAddress)
if parseErr != nil {
return nil, fmt.Errorf("invalid ClamAV address %q: %w", config.ClamAVAddress, parseErr)
}
port, parseErr := strconv.Atoi(portStr)
if parseErr != nil {
return nil, fmt.Errorf("invalid ClamAV port %q: %w", portStr, parseErr)
}
// Créer le client ClamAV (Lyimmi fork - API avec WithTCP)
clamdClient = clamd.NewClamd(clamd.WithTCP(host, port))
fmt.Printf("🔌 [UPLOAD VALIDATOR] Client ClamAV créé - Test de connexion (Ping avec timeout 2s)...\n")
// Test connection avec timeout pour éviter blocage - MOD-P1-001-REFINEMENT: Ne pas bloquer le démarrage
// Test disponibilité: clamdscan --version
pingCtx, pingCancel := context.WithTimeout(context.Background(), 2*time.Second)
defer pingCancel()
ok, err := clamdClient.Ping(pingCtx)
if !ok || err != nil {
if err == nil {
err = fmt.Errorf("ClamAV ping failed")
}
fmt.Printf("🔌 [UPLOAD VALIDATOR] Ping terminé - erreur: %v\n", err)
} else {
fmt.Printf("🔌 [UPLOAD VALIDATOR] Ping terminé - OK\n")
}
if err != nil {
fmt.Printf("⚠️ [UPLOAD VALIDATOR] ClamAV Ping échoué: %v\n", err)
if err := exec.CommandContext(pingCtx, clamdPath, "--version").Run(); err != nil {
fmt.Printf("⚠️ [UPLOAD VALIDATOR] clamdscan non disponible: %v\n", err)
if config.ClamAVRequired {
// MOD-P1-002: Si ClamAV est requis, rejeter les uploads (fail-secure)
logger.Warn("ClamAV is enabled and required but unavailable - uploads will be rejected until ClamAV is available",
logger.Warn("ClamAV is enabled and required but clamdscan unavailable - uploads will be rejected",
zap.Error(err),
zap.String("address", config.ClamAVAddress),
zap.String("clamd_path", clamdPath),
)
clamAVRequiredButUnavailable = true
} else {
// MOD-P1-002: Si ClamAV n'est pas requis, accepter uploads avec warning (mode dégradé)
logger.Warn("ClamAV is enabled but unavailable and not required - uploads will be accepted without virus scanning (degraded mode)",
logger.Warn("ClamAV enabled but clamdscan unavailable and not required - degraded mode",
zap.Error(err),
zap.String("address", config.ClamAVAddress),
zap.String("security_warning", "Virus scanning is disabled. This should only be used in development or with alternative security measures."),
zap.String("clamd_path", clamdPath),
)
clamAVRequiredButUnavailable = false
}
// Ne pas retourner d'erreur - le serveur peut démarrer
} else {
fmt.Printf("✅ [UPLOAD VALIDATOR] ClamAV Ping réussi - Connexion OK\n")
logger.Info("ClamAV connection successful")
fmt.Printf("✅ [UPLOAD VALIDATOR] clamdscan disponible - OK\n")
logger.Info("ClamAV (clamdscan) available for virus scanning")
}
fmt.Printf("✅ [UPLOAD VALIDATOR] Validateur initialisé - clamdClient=%v, requiredButUnavailable=%v\n",
clamdClient != nil, clamAVRequiredButUnavailable)
return &UploadValidator{
logger: logger,
clamdClient: clamdClient,
clamdPath: clamdPath,
quarantineDir: config.QuarantineDir,
clamAVRequiredButUnavailable: clamAVRequiredButUnavailable,
clamAVRequired: config.ClamAVRequired,
@ -192,12 +163,12 @@ type ValidationResult struct {
func (uv *UploadValidator) ValidateFile(ctx context.Context, fileHeader *multipart.FileHeader, fileType string) (*ValidationResult, error) {
// DEBUG: Log de début de validation
fmt.Printf("🚀 [UPLOAD VALIDATE] Début validation fichier: %s (taille: %d bytes, type: %s)\n", fileHeader.Filename, fileHeader.Size, fileType)
fmt.Printf("🔍 [UPLOAD VALIDATE] État ClamAV - client=%v, required=%v, requiredButUnavailable=%v\n",
uv.clamdClient != nil, uv.clamAVRequired, uv.clamAVRequiredButUnavailable)
fmt.Printf("🔍 [UPLOAD VALIDATE] État ClamAV - enabled=%v, required=%v, requiredButUnavailable=%v\n",
uv.clamdPath != "", uv.clamAVRequired, uv.clamAVRequiredButUnavailable)
// EARLY CHECK: Si ClamAV est complètement désactivé, on skip immédiatement
if uv.clamdClient == nil {
fmt.Printf("⏭️ [UPLOAD VALIDATE] ClamAV désactivé (client=nil) - scan antivirus ignoré\n")
if uv.clamdPath == "" {
fmt.Printf("⏭️ [UPLOAD VALIDATE] ClamAV désactivé - scan antivirus ignoré\n")
}
result := &ValidationResult{
@ -279,8 +250,8 @@ func (uv *UploadValidator) ValidateFile(ctx context.Context, fileHeader *multipa
// Continuer sans scan ClamAV
}
// Scanner avec ClamAV si disponible
if uv.clamdClient != nil {
// Scanner avec ClamAV si disponible (clamdscan exec)
if uv.clamdPath != "" {
fmt.Printf("🛡️ [UPLOAD VALIDATE] ClamAV activé - Début scan antivirus...\n")
file.Seek(0, 0)
infected, err := uv.scanWithClamAV(ctx, file)
@ -312,7 +283,7 @@ func (uv *UploadValidator) ValidateFile(ctx context.Context, fileHeader *multipa
zap.String("status", "clean"),
)
} else {
fmt.Printf("⏭️ [UPLOAD VALIDATE] ClamAV désactivé (client=nil) - scan ignoré, validation continue\n")
fmt.Printf("⏭️ [UPLOAD VALIDATE] ClamAV désactivé - scan ignoré, validation continue\n")
}
// Valider l'extension du fichier
@ -512,27 +483,45 @@ func (uv *UploadValidator) validateVideoMagicBytes(header []byte) error {
return fmt.Errorf("invalid video file signature")
}
// scanWithClamAV scanne le fichier avec ClamAV avec timeout strict
// scanWithClamAV scanne le fichier avec clamdscan (exec) - remplace go-clamd abandonné
// MOD-P1-001: Timeout strict via context pour éviter les blocages
// Lyimmi go-clamd: ScanStream(ctx, r) returns (ok bool, err error) - ok=true means clean, ok=false means virus
// clamdscan exit 0 = clean, exit 1 = infected, exit 2 = error
func (uv *UploadValidator) scanWithClamAV(ctx context.Context, file io.Reader) (infected bool, err error) {
// Timeout strict: 30 secondes max pour le scan
scanCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
// Lire tout le fichier en mémoire pour le scan
var buf bytes.Buffer
if _, copyErr := io.Copy(&buf, file); copyErr != nil {
return false, fmt.Errorf("failed to read file for scan: %w", copyErr)
tmpFile, err := os.CreateTemp("", "veza-clamav-scan-*")
if err != nil {
return false, fmt.Errorf("failed to create temp file for scan: %w", err)
}
tmpPath := tmpFile.Name()
defer func() { _ = os.Remove(tmpPath) }()
if _, copyErr := io.Copy(tmpFile, file); copyErr != nil {
_ = tmpFile.Close()
return false, fmt.Errorf("failed to write file for scan: %w", copyErr)
}
if closeErr := tmpFile.Close(); closeErr != nil {
return false, fmt.Errorf("failed to close temp file: %w", closeErr)
}
// Lyimmi: ScanStream(ctx, io.Reader) -> (ok bool, err error)
ok, scanErr := uv.clamdClient.ScanStream(scanCtx, &buf)
if scanErr != nil {
return false, fmt.Errorf("clamav scan failed: %w", scanErr)
cmd := exec.CommandContext(scanCtx, uv.clamdPath, "--no-summary", tmpPath)
var stderr bytes.Buffer
cmd.Stderr = &stderr
runErr := cmd.Run()
if runErr != nil {
if exitErr, ok := runErr.(*exec.ExitError); ok {
switch exitErr.ExitCode() {
case 1:
return true, nil // Virus détecté
case 2:
return false, fmt.Errorf("clamdscan error: %s", stderr.String())
}
}
return false, fmt.Errorf("clamdscan failed: %w", runErr)
}
// ok=true: clean, ok=false: virus detected
return !ok, nil
return false, nil // Exit 0 = clean
}
// QuarantineFile met un fichier en quarantaine

View file

@ -18,15 +18,15 @@ import (
// Pour exécuter: go test -tags=clamav ./internal/services -run TestUploadValidator_ClamAV_EICAR_Rejected
// MOD-P1-001: Test que le fichier EICAR (test virus standard) est rejeté
func TestUploadValidator_ClamAV_EICAR_Rejected(t *testing.T) {
// Vérifier que ClamAV est disponible
clamavAddr := os.Getenv("CLAMAV_ADDRESS")
if clamavAddr == "" {
clamavAddr = "localhost:3310"
// Vérifier que clamdscan est disponible (exec)
clamdPath := os.Getenv("CLAMAV_CLAMD_PATH")
if clamdPath == "" {
clamdPath = "clamdscan"
}
logger, _ := zap.NewDevelopment()
// Configuration avec ClamAV enabled
// Configuration avec ClamAV enabled (utilise clamdscan exec)
config := &UploadConfig{
MaxAudioSize: 100 * 1024 * 1024,
MaxImageSize: 10 * 1024 * 1024,
@ -35,7 +35,7 @@ func TestUploadValidator_ClamAV_EICAR_Rejected(t *testing.T) {
AllowedImageTypes: []string{"image/jpeg"},
AllowedVideoTypes: []string{"video/mp4"},
ClamAVEnabled: true,
ClamAVAddress: clamavAddr,
ClamAVClamdPath: clamdPath,
QuarantineDir: "/tmp/quarantine",
}
@ -66,15 +66,13 @@ func TestUploadValidator_ClamAV_EICAR_Rejected(t *testing.T) {
// TestUploadValidator_ClamAV_CleanFile_Accepted vérifie qu'un fichier propre est accepté
// MOD-P1-001: Test qu'un fichier non infecté passe le scan ClamAV
func TestUploadValidator_ClamAV_CleanFile_Accepted(t *testing.T) {
// Vérifier que ClamAV est disponible
clamavAddr := os.Getenv("CLAMAV_ADDRESS")
if clamavAddr == "" {
clamavAddr = "localhost:3310"
clamdPath := os.Getenv("CLAMAV_CLAMD_PATH")
if clamdPath == "" {
clamdPath = "clamdscan"
}
logger, _ := zap.NewDevelopment()
// Configuration avec ClamAV enabled
config := &UploadConfig{
MaxAudioSize: 100 * 1024 * 1024,
MaxImageSize: 10 * 1024 * 1024,
@ -83,7 +81,7 @@ func TestUploadValidator_ClamAV_CleanFile_Accepted(t *testing.T) {
AllowedImageTypes: []string{"image/jpeg"},
AllowedVideoTypes: []string{"video/mp4"},
ClamAVEnabled: true,
ClamAVAddress: clamavAddr,
ClamAVClamdPath: clamdPath,
QuarantineDir: "/tmp/quarantine",
}

View file

@ -43,7 +43,7 @@ func createTestFileHeader(t *testing.T, filename string, content []byte) *multip
func TestUploadValidator_ClamAVDown_RejectsUploads(t *testing.T) {
logger, _ := zap.NewDevelopment()
// Configuration avec ClamAV enabled mais adresse invalide (simule ClamAV down)
// Configuration avec ClamAV enabled mais chemin inexistant (simule ClamAV down)
// MOD-P1-002: ClamAVRequired doit être true pour que les uploads soient rejetés
config := &UploadConfig{
MaxAudioSize: 100 * 1024 * 1024,
@ -53,8 +53,8 @@ func TestUploadValidator_ClamAVDown_RejectsUploads(t *testing.T) {
AllowedImageTypes: []string{"image/jpeg"},
AllowedVideoTypes: []string{"video/mp4"},
ClamAVEnabled: true,
ClamAVRequired: true, // MOD-P1-002: ClamAV requis (fail-secure)
ClamAVAddress: "localhost:99999", // Port invalide pour simuler ClamAV down
ClamAVRequired: true,
ClamAVClamdPath: "/nonexistent/clamdscan", // Simule clamdscan indisponible
QuarantineDir: "/tmp/quarantine",
}
@ -98,7 +98,6 @@ func TestUploadValidator_ClamAVDisabled_AllowsUploads(t *testing.T) {
AllowedImageTypes: []string{"image/jpeg"},
AllowedVideoTypes: []string{"video/mp4"},
ClamAVEnabled: false, // ClamAV désactivé
ClamAVAddress: "localhost:3310",
QuarantineDir: "/tmp/quarantine",
}
@ -139,8 +138,8 @@ func TestUploadValidator_ClamAVDown_NotRequired_AcceptsUploads(t *testing.T) {
AllowedImageTypes: []string{"image/jpeg"},
AllowedVideoTypes: []string{"video/mp4"},
ClamAVEnabled: true,
ClamAVRequired: false, // MOD-P1-002: ClamAV non requis
ClamAVAddress: "localhost:99999", // Port invalide pour simuler ClamAV down
ClamAVRequired: false, // MOD-P1-002: ClamAV non requis
ClamAVClamdPath: "/nonexistent/clamdscan", // Simule clamdscan indisponible
QuarantineDir: "/tmp/quarantine",
}

View file

@ -102,7 +102,6 @@ func setupUploadTestRouter(t *testing.T) (*gin.Engine, *gorm.DB, uuid.UUID, func
AllowedAudioTypes: []string{"audio/mpeg", "audio/mp3"},
ClamAVEnabled: false, // Disable for performance tests
ClamAVRequired: false,
ClamAVAddress: "",
QuarantineDir: t.TempDir(),
}
uploadValidator, err := services.NewUploadValidator(uploadConfig, logger)