diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 243056523..eda86a8c3 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -68,6 +68,24 @@ services: cpus: '0.50' memory: 256M + clamav: + image: clamav/clamav:latest + container_name: veza_clamav + restart: unless-stopped + networks: + - veza-network + healthcheck: + test: ["CMD", "clamdscan", "--ping", "1"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 180s + deploy: + resources: + limits: + cpus: '0.5' + memory: 1G + # ============================================================================ # PAYMENT ROUTER (Hyperswitch) # ============================================================================ @@ -144,6 +162,9 @@ services: - HYPERSWITCH_WEBHOOK_SECRET=${HYPERSWITCH_WEBHOOK_SECRET:-} - HYPERSWITCH_ENABLED=${HYPERSWITCH_ENABLED:-false} - CHECKOUT_SUCCESS_URL=${CHECKOUT_SUCCESS_URL:-https://veza.fr/purchases} + - ENABLE_CLAMAV=true + - CLAMAV_REQUIRED=true + - CLAMAV_ADDRESS=clamav:3310 depends_on: postgres: condition: service_healthy @@ -151,6 +172,8 @@ services: condition: service_healthy rabbitmq: condition: service_healthy + clamav: + condition: service_started networks: - veza-network healthcheck: diff --git a/docker-compose.yml b/docker-compose.yml index f349a58fd..09601cb5e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -55,6 +55,27 @@ services: reservations: memory: 32M + # ClamAV - Virus scanning for uploads (v0.101) + clamav: + image: clamav/clamav:latest + container_name: veza_clamav + restart: unless-stopped + ports: + - "${PORT_CLAMAV:-13310}:3310" + networks: + - veza-net + healthcheck: + test: ["CMD", "clamdscan", "--ping", "1"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 180s + deploy: + resources: + limits: + cpus: '0.5' + memory: 1G + # RabbitMQ - Message Broker # Limit: 256MB RAM. Host 15672->AMQP(5672), 25672->Management(15672) rabbitmq: @@ -153,6 +174,9 @@ services: - COOKIE_PATH=/ - CORS_ALLOWED_ORIGINS=http://veza.fr:3000,http://veza.fr:5173 - RABBITMQ_URL=amqp://${RABBITMQ_DEFAULT_USER:-veza}:${RABBITMQ_DEFAULT_PASS:-devpassword}@rabbitmq:5672/ + - ENABLE_CLAMAV=true + - CLAMAV_REQUIRED=false + - CLAMAV_ADDRESS=clamav:3310 ports: - "${PORT_BACKEND:-18080}:8080" depends_on: @@ -162,6 +186,8 @@ services: condition: service_healthy rabbitmq: condition: service_healthy + clamav: + condition: service_started networks: - veza-net healthcheck: diff --git a/veza-backend-api/Dockerfile b/veza-backend-api/Dockerfile index 0204d8c40..1865ca4b5 100644 --- a/veza-backend-api/Dockerfile +++ b/veza-backend-api/Dockerfile @@ -27,8 +27,8 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ # Runtime stage FROM alpine:latest -# Install runtime dependencies -RUN apk --no-cache add ca-certificates tzdata wget +# Install runtime dependencies (clamav for virus scanning in v0.101) +RUN apk --no-cache add ca-certificates tzdata wget clamav # Create non-root user for security RUN addgroup -g 1001 -S app && \ diff --git a/veza-backend-api/Dockerfile.production b/veza-backend-api/Dockerfile.production index dfdc8db2f..1ee26ca18 100644 --- a/veza-backend-api/Dockerfile.production +++ b/veza-backend-api/Dockerfile.production @@ -32,10 +32,8 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ # Runtime stage - minimal alpine FROM alpine:3.21 -# Install only runtime dependencies -RUN apk --no-cache add ca-certificates tzdata && \ - # Add wget for health checks - apk --no-cache add wget && \ +# Install only runtime dependencies (clamav for virus scanning in v0.101) +RUN apk --no-cache add ca-certificates tzdata wget clamav && \ # Clean up apk cache rm -rf /var/cache/apk/* diff --git a/veza-backend-api/internal/config/services_init.go b/veza-backend-api/internal/config/services_init.go index d88fe712d..a5070d1ee 100644 --- a/veza-backend-api/internal/config/services_init.go +++ b/veza-backend-api/internal/config/services_init.go @@ -35,6 +35,10 @@ func (c *Config) initServices() error { if p := getEnv("CLAMAV_CLAMD_PATH", ""); p != "" { uploadConfig.ClamAVClamdPath = p } + // Adresse ClamAV pour connexion TCP (ex: clamav:3310 en Docker) + if addr := getEnv("CLAMAV_ADDRESS", ""); addr != "" { + uploadConfig.ClamAVAddress = addr + } var err error c.UploadValidator, err = services.NewUploadValidator(uploadConfig, c.Logger) if err != nil { diff --git a/veza-backend-api/internal/services/upload_validator.go b/veza-backend-api/internal/services/upload_validator.go index 4b40e595f..4983d21e7 100644 --- a/veza-backend-api/internal/services/upload_validator.go +++ b/veza-backend-api/internal/services/upload_validator.go @@ -21,6 +21,7 @@ import ( type UploadValidator struct { logger *zap.Logger clamdPath string // Chemin vers clamdscan (ex: clamdscan, /usr/bin/clamdscan) + clamAVConfigPath string // Config pour connexion TCP distante (ex: clamav:3310) 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 @@ -115,6 +116,21 @@ func NewUploadValidator(config *UploadConfig, logger *zap.Logger) (*UploadValida } fmt.Printf("🛡️ [UPLOAD VALIDATOR] ClamAV activé - Utilisation de %s (exec)\n", clamdPath) + // Config pour connexion TCP distante (ex: clamav:3310 en Docker) + clamAVConfigPath := "" + if config.ClamAVAddress != "" && strings.Contains(config.ClamAVAddress, ":") { + parts := strings.SplitN(config.ClamAVAddress, ":", 2) + host, port := parts[0], parts[1] + configContent := fmt.Sprintf("TCPSocket %s\nTCPAddr %s\n", port, host) + tmpFile, err := os.CreateTemp("", "veza-clamd-*.conf") + if err == nil { + _, _ = tmpFile.WriteString(configContent) + _ = tmpFile.Close() + clamAVConfigPath = tmpFile.Name() + fmt.Printf("🛡️ [UPLOAD VALIDATOR] Config TCP: %s -> %s\n", config.ClamAVAddress, clamAVConfigPath) + } + } + clamAVRequiredButUnavailable := false // Test disponibilité: clamdscan --version pingCtx, pingCancel := context.WithTimeout(context.Background(), 2*time.Second) @@ -141,6 +157,7 @@ func NewUploadValidator(config *UploadConfig, logger *zap.Logger) (*UploadValida return &UploadValidator{ logger: logger, clamdPath: clamdPath, + clamAVConfigPath: clamAVConfigPath, quarantineDir: config.QuarantineDir, clamAVRequiredButUnavailable: clamAVRequiredButUnavailable, clamAVRequired: config.ClamAVRequired, @@ -495,7 +512,12 @@ func (uv *UploadValidator) scanWithClamAV(ctx context.Context, file io.Reader) ( return false, fmt.Errorf("failed to close temp file: %w", closeErr) } - cmd := exec.CommandContext(scanCtx, uv.clamdPath, "--no-summary", tmpPath) + args := []string{"--no-summary"} + if uv.clamAVConfigPath != "" { + args = append(args, "-c", uv.clamAVConfigPath) + } + args = append(args, tmpPath) + cmd := exec.CommandContext(scanCtx, uv.clamdPath, args...) var stderr bytes.Buffer cmd.Stderr = &stderr runErr := cmd.Run()