chore: add go.work and optional monorepo orchestrator
This commit is contained in:
parent
fb8411c6ad
commit
afea976f57
10 changed files with 124 additions and 136 deletions
3
go.work
Normal file
3
go.work
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
go 1.23.8
|
||||
|
||||
use ./veza-backend-api
|
||||
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue