Files
hmac-file-server/cmd/server/config_validator.go

1132 lines
36 KiB
Go

// config_validator.go
package main
import (
"errors"
"fmt"
"net"
"os"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"time"
)
// ConfigValidationError represents a configuration validation error
type ConfigValidationError struct {
Field string
Value interface{}
Message string
}
func (e ConfigValidationError) Error() string {
return fmt.Sprintf("config validation error in field '%s': %s (value: %v)", e.Field, e.Message, e.Value)
}
// ConfigValidationResult contains the results of config validation
type ConfigValidationResult struct {
Errors []ConfigValidationError
Warnings []ConfigValidationError
Valid bool
}
// AddError adds a validation error
func (r *ConfigValidationResult) AddError(field string, value interface{}, message string) {
r.Errors = append(r.Errors, ConfigValidationError{Field: field, Value: value, Message: message})
r.Valid = false
}
// AddWarning adds a validation warning
func (r *ConfigValidationResult) AddWarning(field string, value interface{}, message string) {
r.Warnings = append(r.Warnings, ConfigValidationError{Field: field, Value: value, Message: message})
}
// HasErrors returns true if there are validation errors
func (r *ConfigValidationResult) HasErrors() bool {
return len(r.Errors) > 0
}
// HasWarnings returns true if there are validation warnings
func (r *ConfigValidationResult) HasWarnings() bool {
return len(r.Warnings) > 0
}
// ValidateConfigComprehensive performs comprehensive configuration validation
func ValidateConfigComprehensive(c *Config) *ConfigValidationResult {
result := &ConfigValidationResult{Valid: true}
// Validate each section
validateServerConfig(&c.Server, result)
validateSecurityConfig(&c.Security, result)
validateLoggingConfig(&c.Logging, result)
validateTimeoutConfig(&c.Timeouts, result)
validateUploadsConfig(&c.Uploads, result)
validateDownloadsConfig(&c.Downloads, result)
validateClamAVConfig(&c.ClamAV, result)
validateRedisConfig(&c.Redis, result)
validateWorkersConfig(&c.Workers, result)
validateVersioningConfig(&c.Versioning, result)
validateDeduplicationConfig(&c.Deduplication, result)
validateISOConfig(&c.ISO, result)
// Cross-section validations
validateCrossSection(c, result)
// Enhanced validations
validateSystemResources(result)
validateNetworkConnectivity(c, result)
validatePerformanceSettings(c, result)
validateSecurityHardening(c, result)
// Check disk space for storage paths
if c.Server.StoragePath != "" {
checkDiskSpace(c.Server.StoragePath, result)
}
if c.Deduplication.Enabled && c.Deduplication.Directory != "" {
checkDiskSpace(c.Deduplication.Directory, result)
}
return result
}
// validateServerConfig validates server configuration
func validateServerConfig(server *ServerConfig, result *ConfigValidationResult) {
// ListenAddress validation
if server.ListenAddress == "" {
result.AddError("server.listenport", server.ListenAddress, "listen address/port is required")
} else {
if !isValidPort(server.ListenAddress) {
result.AddError("server.listenport", server.ListenAddress, "invalid port number (must be 1-65535)")
}
}
// BindIP validation
if server.BindIP != "" {
if ip := net.ParseIP(server.BindIP); ip == nil {
result.AddError("server.bind_ip", server.BindIP, "invalid IP address format")
}
}
// StoragePath validation
if server.StoragePath == "" {
result.AddError("server.storagepath", server.StoragePath, "storage path is required")
} else {
if err := validateDirectoryPath(server.StoragePath, true); err != nil {
result.AddError("server.storagepath", server.StoragePath, err.Error())
}
}
// MetricsPort validation
if server.MetricsEnabled && server.MetricsPort != "" {
if !isValidPort(server.MetricsPort) {
result.AddError("server.metricsport", server.MetricsPort, "invalid metrics port number")
}
if server.MetricsPort == server.ListenAddress {
result.AddError("server.metricsport", server.MetricsPort, "metrics port cannot be the same as main listen port")
}
}
// Size validations
if server.MaxUploadSize != "" {
if _, err := parseSize(server.MaxUploadSize); err != nil {
result.AddError("server.max_upload_size", server.MaxUploadSize, "invalid size format")
}
}
if server.MinFreeBytes != "" {
if _, err := parseSize(server.MinFreeBytes); err != nil {
result.AddError("server.min_free_bytes", server.MinFreeBytes, "invalid size format")
}
}
// TTL validation
if server.FileTTLEnabled {
if server.FileTTL == "" {
result.AddError("server.filettl", server.FileTTL, "file TTL is required when TTL is enabled")
} else {
if _, err := parseTTL(server.FileTTL); err != nil {
result.AddError("server.filettl", server.FileTTL, "invalid TTL format")
}
}
}
// File naming validation
validFileNaming := []string{"HMAC", "original", "None"}
if !contains(validFileNaming, server.FileNaming) {
result.AddError("server.file_naming", server.FileNaming, "must be one of: HMAC, original, None")
}
// Protocol validation
validProtocols := []string{"ipv4", "ipv6", "auto", ""}
if !contains(validProtocols, server.ForceProtocol) {
result.AddError("server.force_protocol", server.ForceProtocol, "must be one of: ipv4, ipv6, auto, or empty")
}
// PID file validation
if server.PIDFilePath != "" {
dir := filepath.Dir(server.PIDFilePath)
if err := validateDirectoryPath(dir, false); err != nil {
result.AddError("server.pidfilepath", server.PIDFilePath, fmt.Sprintf("PID file directory invalid: %v", err))
}
}
// Worker threshold validation
if server.EnableDynamicWorkers {
if server.WorkerScaleUpThresh <= 0 {
result.AddError("server.worker_scale_up_thresh", server.WorkerScaleUpThresh, "must be positive when dynamic workers are enabled")
}
if server.WorkerScaleDownThresh <= 0 {
result.AddError("server.worker_scale_down_thresh", server.WorkerScaleDownThresh, "must be positive when dynamic workers are enabled")
}
if server.WorkerScaleDownThresh >= server.WorkerScaleUpThresh {
result.AddWarning("server.worker_scale_down_thresh", server.WorkerScaleDownThresh, "scale down threshold should be lower than scale up threshold")
}
}
// Extensions validation
for _, ext := range server.GlobalExtensions {
if !strings.HasPrefix(ext, ".") {
result.AddError("server.global_extensions", ext, "file extensions must start with a dot")
}
}
}
// validateSecurityConfig validates security configuration
func validateSecurityConfig(security *SecurityConfig, result *ConfigValidationResult) {
if security.EnableJWT {
// JWT validation
if strings.TrimSpace(security.JWTSecret) == "" {
result.AddError("security.jwtsecret", security.JWTSecret, "JWT secret is required when JWT is enabled")
} else if len(security.JWTSecret) < 32 {
result.AddWarning("security.jwtsecret", "[REDACTED]", "JWT secret should be at least 32 characters for security")
}
validAlgorithms := []string{"HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512"}
if !contains(validAlgorithms, security.JWTAlgorithm) {
result.AddError("security.jwtalgorithm", security.JWTAlgorithm, "unsupported JWT algorithm")
}
if security.JWTExpiration != "" {
if _, err := time.ParseDuration(security.JWTExpiration); err != nil {
result.AddError("security.jwtexpiration", security.JWTExpiration, "invalid JWT expiration format")
}
}
} else {
// HMAC validation
if strings.TrimSpace(security.Secret) == "" {
result.AddError("security.secret", security.Secret, "HMAC secret is required when JWT is disabled")
} else if len(security.Secret) < 16 {
result.AddWarning("security.secret", "[REDACTED]", "HMAC secret should be at least 16 characters for security")
}
}
}
// validateLoggingConfig validates logging configuration
func validateLoggingConfig(logging *LoggingConfig, result *ConfigValidationResult) {
validLevels := []string{"panic", "fatal", "error", "warn", "warning", "info", "debug", "trace"}
if !contains(validLevels, strings.ToLower(logging.Level)) {
result.AddError("logging.level", logging.Level, "invalid log level")
}
if logging.File != "" {
dir := filepath.Dir(logging.File)
if err := validateDirectoryPath(dir, false); err != nil {
result.AddError("logging.file", logging.File, fmt.Sprintf("log file directory invalid: %v", err))
}
}
if logging.MaxSize <= 0 {
result.AddWarning("logging.max_size", logging.MaxSize, "max size should be positive")
}
if logging.MaxBackups < 0 {
result.AddWarning("logging.max_backups", logging.MaxBackups, "max backups should be non-negative")
}
if logging.MaxAge < 0 {
result.AddWarning("logging.max_age", logging.MaxAge, "max age should be non-negative")
}
}
// validateTimeoutConfig validates timeout configuration
func validateTimeoutConfig(timeouts *TimeoutConfig, result *ConfigValidationResult) {
if timeouts.Read != "" {
if duration, err := time.ParseDuration(timeouts.Read); err != nil {
result.AddError("timeouts.read", timeouts.Read, "invalid read timeout format")
} else if duration <= 0 {
result.AddError("timeouts.read", timeouts.Read, "read timeout must be positive")
}
}
if timeouts.Write != "" {
if duration, err := time.ParseDuration(timeouts.Write); err != nil {
result.AddError("timeouts.write", timeouts.Write, "invalid write timeout format")
} else if duration <= 0 {
result.AddError("timeouts.write", timeouts.Write, "write timeout must be positive")
}
}
if timeouts.Idle != "" {
if duration, err := time.ParseDuration(timeouts.Idle); err != nil {
result.AddError("timeouts.idle", timeouts.Idle, "invalid idle timeout format")
} else if duration <= 0 {
result.AddError("timeouts.idle", timeouts.Idle, "idle timeout must be positive")
}
}
if timeouts.Shutdown != "" {
if duration, err := time.ParseDuration(timeouts.Shutdown); err != nil {
result.AddError("timeouts.shutdown", timeouts.Shutdown, "invalid shutdown timeout format")
} else if duration <= 0 {
result.AddError("timeouts.shutdown", timeouts.Shutdown, "shutdown timeout must be positive")
}
}
}
// validateUploadsConfig validates uploads configuration
func validateUploadsConfig(uploads *UploadsConfig, result *ConfigValidationResult) {
// Validate extensions
for _, ext := range uploads.AllowedExtensions {
if !strings.HasPrefix(ext, ".") {
result.AddError("uploads.allowed_extensions", ext, "file extensions must start with a dot")
}
}
// Validate chunk size
if uploads.ChunkSize != "" {
if _, err := parseSize(uploads.ChunkSize); err != nil {
result.AddError("uploads.chunk_size", uploads.ChunkSize, "invalid chunk size format")
}
}
// Validate resumable age
if uploads.MaxResumableAge != "" {
if _, err := time.ParseDuration(uploads.MaxResumableAge); err != nil {
result.AddError("uploads.max_resumable_age", uploads.MaxResumableAge, "invalid resumable age format")
}
}
}
// validateDownloadsConfig validates downloads configuration
func validateDownloadsConfig(downloads *DownloadsConfig, result *ConfigValidationResult) {
// Validate extensions
for _, ext := range downloads.AllowedExtensions {
if !strings.HasPrefix(ext, ".") {
result.AddError("downloads.allowed_extensions", ext, "file extensions must start with a dot")
}
}
// Validate chunk size
if downloads.ChunkSize != "" {
if _, err := parseSize(downloads.ChunkSize); err != nil {
result.AddError("downloads.chunk_size", downloads.ChunkSize, "invalid chunk size format")
}
}
}
// validateClamAVConfig validates ClamAV configuration
func validateClamAVConfig(clamav *ClamAVConfig, result *ConfigValidationResult) {
if clamav.ClamAVEnabled {
if clamav.ClamAVSocket == "" {
result.AddWarning("clamav.clamavsocket", clamav.ClamAVSocket, "ClamAV socket path not specified, using default")
} else {
// Check if socket file exists
if _, err := os.Stat(clamav.ClamAVSocket); os.IsNotExist(err) {
result.AddWarning("clamav.clamavsocket", clamav.ClamAVSocket, "ClamAV socket file does not exist")
}
}
if clamav.NumScanWorkers <= 0 {
result.AddError("clamav.numscanworkers", clamav.NumScanWorkers, "number of scan workers must be positive")
}
// Validate scan extensions
for _, ext := range clamav.ScanFileExtensions {
if !strings.HasPrefix(ext, ".") {
result.AddError("clamav.scanfileextensions", ext, "file extensions must start with a dot")
}
}
}
}
// validateRedisConfig validates Redis configuration
func validateRedisConfig(redis *RedisConfig, result *ConfigValidationResult) {
if redis.RedisEnabled {
if redis.RedisAddr == "" {
result.AddError("redis.redisaddr", redis.RedisAddr, "Redis address is required when Redis is enabled")
} else {
// Validate address format (host:port)
if !isValidHostPort(redis.RedisAddr) {
result.AddError("redis.redisaddr", redis.RedisAddr, "invalid Redis address format (should be host:port)")
}
}
if redis.RedisDBIndex < 0 || redis.RedisDBIndex > 15 {
result.AddWarning("redis.redisdbindex", redis.RedisDBIndex, "Redis DB index is typically 0-15")
}
if redis.RedisHealthCheckInterval != "" {
if _, err := time.ParseDuration(redis.RedisHealthCheckInterval); err != nil {
result.AddError("redis.redishealthcheckinterval", redis.RedisHealthCheckInterval, "invalid health check interval format")
}
}
}
}
// validateWorkersConfig validates workers configuration
func validateWorkersConfig(workers *WorkersConfig, result *ConfigValidationResult) {
if workers.NumWorkers <= 0 {
result.AddError("workers.numworkers", workers.NumWorkers, "number of workers must be positive")
}
if workers.UploadQueueSize <= 0 {
result.AddError("workers.uploadqueuesize", workers.UploadQueueSize, "upload queue size must be positive")
}
// Performance recommendations
if workers.NumWorkers > 50 {
result.AddWarning("workers.numworkers", workers.NumWorkers, "very high worker count may impact performance")
}
if workers.UploadQueueSize > 1000 {
result.AddWarning("workers.uploadqueuesize", workers.UploadQueueSize, "very large queue size may impact memory usage")
}
}
// validateVersioningConfig validates versioning configuration
func validateVersioningConfig(versioning *VersioningConfig, result *ConfigValidationResult) {
if versioning.Enabled {
if versioning.MaxRevs <= 0 {
result.AddError("versioning.maxversions", versioning.MaxRevs, "max versions must be positive when versioning is enabled")
}
validBackends := []string{"filesystem", "database", "s3", ""}
if !contains(validBackends, versioning.Backend) {
result.AddWarning("versioning.backend", versioning.Backend, "unknown versioning backend")
}
}
}
// validateDeduplicationConfig validates deduplication configuration
func validateDeduplicationConfig(dedup *DeduplicationConfig, result *ConfigValidationResult) {
if dedup.Enabled {
if dedup.Directory == "" {
result.AddError("deduplication.directory", dedup.Directory, "deduplication directory is required when deduplication is enabled")
} else {
if err := validateDirectoryPath(dedup.Directory, true); err != nil {
result.AddError("deduplication.directory", dedup.Directory, err.Error())
}
}
}
}
// validateISOConfig validates ISO configuration
func validateISOConfig(iso *ISOConfig, result *ConfigValidationResult) {
if iso.Enabled {
if iso.MountPoint == "" {
result.AddError("iso.mount_point", iso.MountPoint, "mount point is required when ISO is enabled")
}
if iso.Size != "" {
if _, err := parseSize(iso.Size); err != nil {
result.AddError("iso.size", iso.Size, "invalid ISO size format")
}
}
if iso.ContainerFile == "" {
result.AddWarning("iso.containerfile", iso.ContainerFile, "container file path not specified")
}
validCharsets := []string{"utf-8", "iso-8859-1", "ascii", ""}
if !contains(validCharsets, strings.ToLower(iso.Charset)) {
result.AddWarning("iso.charset", iso.Charset, "uncommon charset specified")
}
}
}
// validateCrossSection performs cross-section validations
func validateCrossSection(c *Config, result *ConfigValidationResult) {
// Storage path vs deduplication directory conflict
if c.Deduplication.Enabled && c.Server.StoragePath == c.Deduplication.Directory {
result.AddError("deduplication.directory", c.Deduplication.Directory, "deduplication directory cannot be the same as storage path")
}
// ISO mount point vs storage path conflict
if c.ISO.Enabled && c.Server.StoragePath == c.ISO.MountPoint {
result.AddWarning("iso.mount_point", c.ISO.MountPoint, "ISO mount point is the same as storage path")
}
// Extension conflicts between uploads and downloads
if len(c.Uploads.AllowedExtensions) > 0 && len(c.Downloads.AllowedExtensions) > 0 {
uploadExts := make(map[string]bool)
for _, ext := range c.Uploads.AllowedExtensions {
uploadExts[ext] = true
}
hasCommonExtensions := false
for _, ext := range c.Downloads.AllowedExtensions {
if uploadExts[ext] {
hasCommonExtensions = true
break
}
}
if !hasCommonExtensions {
result.AddWarning("uploads/downloads.allowed_extensions", "", "no common extensions between uploads and downloads - files may not be downloadable")
}
}
// Global extensions override warning
if len(c.Server.GlobalExtensions) > 0 && (len(c.Uploads.AllowedExtensions) > 0 || len(c.Downloads.AllowedExtensions) > 0) {
result.AddWarning("server.global_extensions", c.Server.GlobalExtensions, "global extensions will override upload/download extension settings")
}
}
// Enhanced Security Validation Functions
// checkSecretStrength analyzes the strength of secrets/passwords
func checkSecretStrength(secret string) (score int, issues []string) {
if len(secret) == 0 {
return 0, []string{"secret is empty"}
}
issues = []string{}
score = 0
// Length scoring
if len(secret) >= 32 {
score += 3
} else if len(secret) >= 16 {
score += 2
} else if len(secret) >= 8 {
score += 1
} else {
issues = append(issues, "secret is too short")
}
// Character variety scoring
hasLower := false
hasUpper := false
hasDigit := false
hasSpecial := false
for _, char := range secret {
switch {
case char >= 'a' && char <= 'z':
hasLower = true
case char >= 'A' && char <= 'Z':
hasUpper = true
case char >= '0' && char <= '9':
hasDigit = true
case strings.ContainsRune("!@#$%^&*()_+-=[]{}|;:,.<>?", char):
hasSpecial = true
}
}
varietyCount := 0
if hasLower {
varietyCount++
}
if hasUpper {
varietyCount++
}
if hasDigit {
varietyCount++
}
if hasSpecial {
varietyCount++
}
score += varietyCount
if varietyCount < 3 {
issues = append(issues, "secret should contain uppercase, lowercase, numbers, and special characters")
}
// Check for common patterns
lowerSecret := strings.ToLower(secret)
commonWeakPasswords := []string{
"password", "123456", "qwerty", "admin", "root", "test", "guest",
"secret", "hmac", "server", "default", "changeme", "example",
"demo", "temp", "temporary", "fileserver", "upload", "download",
}
for _, weak := range commonWeakPasswords {
if strings.Contains(lowerSecret, weak) {
issues = append(issues, fmt.Sprintf("contains common weak pattern: %s", weak))
score -= 2
}
}
// Check for repeated characters
if hasRepeatedChars(secret) {
issues = append(issues, "contains too many repeated characters")
score -= 1
}
// Ensure score doesn't go negative
if score < 0 {
score = 0
}
return score, issues
}
// hasRepeatedChars checks if a string has excessive repeated characters
func hasRepeatedChars(s string) bool {
if len(s) < 4 {
return false
}
for i := 0; i <= len(s)-3; i++ {
if s[i] == s[i+1] && s[i+1] == s[i+2] {
return true
}
}
return false
}
// isDefaultOrExampleSecret checks if a secret appears to be a default/example value
func isDefaultOrExampleSecret(secret string) bool {
defaultSecrets := []string{
"your-secret-key-here",
"change-this-secret",
"example-secret",
"default-secret",
"test-secret",
"demo-secret",
"sample-secret",
"placeholder",
"PUT_YOUR_SECRET_HERE",
"CHANGE_ME",
"YOUR_JWT_SECRET",
"your-hmac-secret",
"supersecret",
"secretkey",
"myverysecuresecret",
}
lowerSecret := strings.ToLower(strings.TrimSpace(secret))
for _, defaultSecret := range defaultSecrets {
if strings.Contains(lowerSecret, strings.ToLower(defaultSecret)) {
return true
}
}
// Check for obvious patterns
if strings.Contains(lowerSecret, "example") ||
strings.Contains(lowerSecret, "default") ||
strings.Contains(lowerSecret, "change") ||
strings.Contains(lowerSecret, "replace") ||
strings.Contains(lowerSecret, "todo") ||
strings.Contains(lowerSecret, "fixme") {
return true
}
return false
}
// calculateEntropy calculates the Shannon entropy of a string
func calculateEntropy(s string) float64 {
if len(s) == 0 {
return 0
}
// Count character frequencies
freq := make(map[rune]int)
for _, char := range s {
freq[char]++
}
// Calculate entropy
entropy := 0.0
length := float64(len(s))
for _, count := range freq {
if count > 0 {
p := float64(count) / length
entropy -= p * (float64(count) / length) // Simplified calculation
}
}
return entropy
}
// validateSecretSecurity performs comprehensive secret security validation
func validateSecretSecurity(fieldName, secret string, result *ConfigValidationResult) {
if secret == "" {
return // Already handled by other validators
}
// Check for default/example secrets
if isDefaultOrExampleSecret(secret) {
result.AddError(fieldName, "[REDACTED]", "appears to be a default or example secret - must be changed")
return
}
// Check secret strength
score, issues := checkSecretStrength(secret)
if score < 3 {
for _, issue := range issues {
result.AddError(fieldName, "[REDACTED]", fmt.Sprintf("weak secret: %s", issue))
}
} else if score < 6 {
for _, issue := range issues {
result.AddWarning(fieldName, "[REDACTED]", fmt.Sprintf("secret could be stronger: %s", issue))
}
}
// Check entropy (simplified)
entropy := calculateEntropy(secret)
if entropy < 3.0 {
result.AddWarning(fieldName, "[REDACTED]", "secret has low entropy - consider using more varied characters")
}
// Length-specific warnings
if len(secret) > 256 {
result.AddWarning(fieldName, "[REDACTED]", "secret is very long - may impact performance")
}
}
// validateSystemResources checks system resource availability
func validateSystemResources(result *ConfigValidationResult) {
// Check available CPU cores
cpuCores := runtime.NumCPU()
if cpuCores < 2 {
result.AddWarning("system.cpu", cpuCores, "minimum 2 CPU cores recommended for optimal performance")
} else if cpuCores < 4 {
result.AddWarning("system.cpu", cpuCores, "4+ CPU cores recommended for high-load environments")
}
// Check available memory (basic check through runtime)
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
// Basic memory availability check (simplified version)
// This checks current Go heap, but for production we'd want system memory
allocMB := float64(memStats.Alloc) / 1024 / 1024
if allocMB > 512 {
result.AddWarning("system.memory", allocMB, "current memory usage is high - ensure adequate system memory")
}
// Check for potential resource constraints
numGoroutines := runtime.NumGoroutine()
if numGoroutines > 1000 {
result.AddWarning("system.goroutines", numGoroutines, "high goroutine count may indicate resource constraints")
}
}
// validateNetworkConnectivity tests network connectivity to external services
func validateNetworkConnectivity(c *Config, result *ConfigValidationResult) {
// Test Redis connectivity if enabled
if c.Redis.RedisEnabled && c.Redis.RedisAddr != "" {
if err := testNetworkConnection("tcp", c.Redis.RedisAddr, 5*time.Second); err != nil {
result.AddWarning("redis.connectivity", c.Redis.RedisAddr, fmt.Sprintf("cannot connect to Redis: %v", err))
}
}
// Test ClamAV connectivity if enabled
if c.ClamAV.ClamAVEnabled && c.ClamAV.ClamAVSocket != "" {
// For Unix socket, test file existence and permissions
if strings.HasPrefix(c.ClamAV.ClamAVSocket, "/") {
if stat, err := os.Stat(c.ClamAV.ClamAVSocket); err != nil {
result.AddWarning("clamav.connectivity", c.ClamAV.ClamAVSocket, fmt.Sprintf("ClamAV socket not accessible: %v", err))
} else if stat.Mode()&os.ModeSocket == 0 {
result.AddWarning("clamav.connectivity", c.ClamAV.ClamAVSocket, "specified path is not a socket file")
}
} else {
// Assume TCP connection format
if err := testNetworkConnection("tcp", c.ClamAV.ClamAVSocket, 5*time.Second); err != nil {
result.AddWarning("clamav.connectivity", c.ClamAV.ClamAVSocket, fmt.Sprintf("cannot connect to ClamAV: %v", err))
}
}
}
}
// testNetworkConnection attempts to connect to a network address
func testNetworkConnection(network, address string, timeout time.Duration) error {
conn, err := net.DialTimeout(network, address, timeout)
if err != nil {
return err
}
defer conn.Close()
return nil
}
// validatePerformanceSettings analyzes configuration for performance implications
func validatePerformanceSettings(c *Config, result *ConfigValidationResult) {
// Check worker configuration against system resources
cpuCores := runtime.NumCPU()
if c.Workers.NumWorkers > cpuCores*4 {
result.AddWarning("workers.performance", c.Workers.NumWorkers,
fmt.Sprintf("worker count (%d) significantly exceeds CPU cores (%d) - may cause context switching overhead",
c.Workers.NumWorkers, cpuCores))
}
// Check ClamAV scan workers
if c.ClamAV.ClamAVEnabled && c.ClamAV.NumScanWorkers > cpuCores {
result.AddWarning("clamav.performance", c.ClamAV.NumScanWorkers,
fmt.Sprintf("scan workers (%d) exceed CPU cores (%d) - may impact scanning performance",
c.ClamAV.NumScanWorkers, cpuCores))
}
// Check timeout configurations for performance balance
if c.Timeouts.Read != "" {
if duration, err := time.ParseDuration(c.Timeouts.Read); err == nil {
if duration > 300*time.Second {
result.AddWarning("timeouts.performance", c.Timeouts.Read, "very long read timeout may impact server responsiveness")
}
}
}
// Check upload size vs available resources
if c.Server.MaxUploadSize != "" {
if size, err := parseSize(c.Server.MaxUploadSize); err == nil {
if size > 10*1024*1024*1024 { // 10GB
result.AddWarning("server.performance", c.Server.MaxUploadSize, "very large max upload size requires adequate disk space and memory")
}
}
}
// Check for potential memory-intensive configurations
if c.Workers.UploadQueueSize > 500 && c.Workers.NumWorkers > 20 {
result.AddWarning("workers.memory", fmt.Sprintf("queue:%d workers:%d", c.Workers.UploadQueueSize, c.Workers.NumWorkers),
"high queue size with many workers may consume significant memory")
}
}
// validateSecurityHardening performs advanced security validation
func validateSecurityHardening(c *Config, result *ConfigValidationResult) {
// Check for default or weak configurations
if c.Security.EnableJWT {
if c.Security.JWTSecret == "your-secret-key-here" || c.Security.JWTSecret == "changeme" {
result.AddError("security.jwtsecret", "[REDACTED]", "JWT secret appears to be a default value - change immediately")
}
// Check JWT algorithm strength
weakAlgorithms := []string{"HS256"} // HS256 is considered less secure than RS256
if contains(weakAlgorithms, c.Security.JWTAlgorithm) {
result.AddWarning("security.jwtalgorithm", c.Security.JWTAlgorithm, "consider using RS256 or ES256 for enhanced security")
}
} else {
if c.Security.Secret == "your-secret-key-here" || c.Security.Secret == "changeme" || c.Security.Secret == "secret" {
result.AddError("security.secret", "[REDACTED]", "HMAC secret appears to be a default value - change immediately")
}
}
// Check for insecure bind configurations
if c.Server.BindIP == "0.0.0.0" {
result.AddWarning("server.bind_ip", c.Server.BindIP, "binding to 0.0.0.0 exposes service to all interfaces - ensure firewall protection")
}
// Check for development/debug settings in production
if c.Logging.Level == "debug" || c.Logging.Level == "trace" {
result.AddWarning("logging.security", c.Logging.Level, "debug/trace logging may expose sensitive information - use 'info' or 'warn' in production")
}
// Check file permissions for sensitive paths
if c.Server.StoragePath != "" {
if stat, err := os.Stat(c.Server.StoragePath); err == nil {
mode := stat.Mode().Perm()
if mode&0077 != 0 { // World or group writable
result.AddWarning("server.storagepath.permissions", c.Server.StoragePath, "storage directory permissions allow group/world access - consider restricting to owner-only")
}
}
}
}
// checkDiskSpace validates available disk space for storage paths
func checkDiskSpace(path string, result *ConfigValidationResult) {
if stat, err := os.Stat(path); err == nil && stat.IsDir() {
// Get available space (platform-specific implementation would be more robust)
// This is a simplified check - in production, use syscall.Statfs on Unix or similar
// For now, we'll just check if we can write a test file
testFile := filepath.Join(path, ".disk_space_test")
if f, err := os.Create(testFile); err != nil {
result.AddWarning("system.disk_space", path, fmt.Sprintf("cannot write to storage directory: %v", err))
} else {
f.Close()
os.Remove(testFile)
// Additional check: try to write a larger test file to estimate space
const testSize = 1024 * 1024 // 1MB
testData := make([]byte, testSize)
if f, err := os.Create(testFile); err == nil {
if _, err := f.Write(testData); err != nil {
result.AddWarning("system.disk_space", path, "low disk space detected - ensure adequate storage for operations")
}
f.Close()
os.Remove(testFile)
}
}
}
}
// isValidPort checks if a string represents a valid port number
func isValidPort(port string) bool {
if p, err := strconv.Atoi(port); err != nil || p < 1 || p > 65535 {
return false
}
return true
}
// isValidHostPort checks if a string is a valid host:port combination
func isValidHostPort(hostPort string) bool {
host, port, err := net.SplitHostPort(hostPort)
if err != nil {
return false
}
// Validate port
if !isValidPort(port) {
return false
}
// Validate host (can be IP, hostname, or empty for localhost)
if host != "" {
if ip := net.ParseIP(host); ip == nil {
// If not an IP, check if it's a valid hostname
matched, _ := regexp.MatchString(`^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$`, host)
return matched
}
}
return true
}
// validateDirectoryPath validates a directory path
func validateDirectoryPath(path string, createIfMissing bool) error {
if path == "" {
return errors.New("directory path cannot be empty")
}
// Check if path exists
if stat, err := os.Stat(path); os.IsNotExist(err) {
if createIfMissing {
// Try to create the directory
if err := os.MkdirAll(path, 0755); err != nil {
return fmt.Errorf("cannot create directory: %v", err)
}
} else {
return fmt.Errorf("directory does not exist: %s", path)
}
} else if err != nil {
return fmt.Errorf("cannot access directory: %v", err)
} else if !stat.IsDir() {
return fmt.Errorf("path exists but is not a directory: %s", path)
}
// Check if directory is writable
testFile := filepath.Join(path, ".write_test")
if f, err := os.Create(testFile); err != nil {
return fmt.Errorf("directory is not writable: %v", err)
} else {
f.Close()
os.Remove(testFile)
}
return nil
}
// contains checks if a slice contains a string
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
// PrintValidationResults prints the validation results in a user-friendly format
func PrintValidationResults(result *ConfigValidationResult) {
if result.HasErrors() {
log.Error("❌ Configuration validation failed with the following errors:")
for _, err := range result.Errors {
log.Errorf(" • %s", err.Error())
}
fmt.Println()
}
if result.HasWarnings() {
log.Warn("⚠️ Configuration validation completed with warnings:")
for _, warn := range result.Warnings {
log.Warnf(" • %s", warn.Error())
}
fmt.Println()
}
if !result.HasErrors() && !result.HasWarnings() {
log.Info("✅ Configuration validation passed successfully!")
}
}
// runSpecializedValidation performs targeted validation based on flags
func runSpecializedValidation(c *Config, security, performance, connectivity, quiet, verbose, fixable bool) {
result := &ConfigValidationResult{Valid: true}
if verbose {
log.Info("Running specialized validation with detailed output...")
fmt.Println()
}
// Run only the requested validation types
if security {
if verbose {
log.Info("🔐 Running security validation checks...")
}
validateSecurityConfig(&c.Security, result)
validateSecurityHardening(c, result)
}
if performance {
if verbose {
log.Info("⚡ Running performance validation checks...")
}
validatePerformanceSettings(c, result)
validateSystemResources(result)
}
if connectivity {
if verbose {
log.Info("🌐 Running connectivity validation checks...")
}
validateNetworkConnectivity(c, result)
}
// If no specific type is requested, run basic validation
if !security && !performance && !connectivity {
if verbose {
log.Info("🔍 Running comprehensive validation...")
}
result = ValidateConfigComprehensive(c)
}
// Filter results based on flags
if fixable {
filterFixableIssues(result)
}
// Output results based on verbosity
if quiet {
printQuietValidationResults(result)
} else if verbose {
printVerboseValidationResults(result)
} else {
PrintValidationResults(result)
}
// Exit with appropriate code
if result.HasErrors() {
os.Exit(1)
}
}
// filterFixableIssues removes non-fixable issues from results
func filterFixableIssues(result *ConfigValidationResult) {
fixablePatterns := []string{
"permissions",
"directory",
"default value",
"debug logging",
"size format",
"timeout format",
"port number",
"IP address",
}
var fixableErrors []ConfigValidationError
var fixableWarnings []ConfigValidationError
for _, err := range result.Errors {
for _, pattern := range fixablePatterns {
if strings.Contains(strings.ToLower(err.Message), pattern) {
fixableErrors = append(fixableErrors, err)
break
}
}
}
for _, warn := range result.Warnings {
for _, pattern := range fixablePatterns {
if strings.Contains(strings.ToLower(warn.Message), pattern) {
fixableWarnings = append(fixableWarnings, warn)
break
}
}
}
result.Errors = fixableErrors
result.Warnings = fixableWarnings
result.Valid = len(fixableErrors) == 0
}
// printQuietValidationResults prints only errors
func printQuietValidationResults(result *ConfigValidationResult) {
if result.HasErrors() {
for _, err := range result.Errors {
fmt.Printf("ERROR: %s\n", err.Error())
}
}
}
// printVerboseValidationResults prints detailed validation information
func printVerboseValidationResults(result *ConfigValidationResult) {
fmt.Println("📊 DETAILED VALIDATION REPORT")
fmt.Println("============================")
fmt.Println()
// System information
fmt.Printf("🖥️ System: %d CPU cores, %d goroutines\n", runtime.NumCPU(), runtime.NumGoroutine())
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
fmt.Printf("💾 Memory: %.2f MB allocated\n", float64(memStats.Alloc)/1024/1024)
fmt.Println()
// Validation summary
fmt.Printf("✅ Checks passed: %d\n", countPassedChecks(result))
fmt.Printf("⚠️ Warnings: %d\n", len(result.Warnings))
fmt.Printf("❌ Errors: %d\n", len(result.Errors))
fmt.Println()
// Detailed results
if result.HasErrors() {
fmt.Println("🚨 CONFIGURATION ERRORS:")
for i, err := range result.Errors {
fmt.Printf(" %d. Field: %s\n", i+1, err.Field)
fmt.Printf(" Issue: %s\n", err.Message)
fmt.Printf(" Value: %v\n", err.Value)
fmt.Println()
}
}
if result.HasWarnings() {
fmt.Println("⚠️ CONFIGURATION WARNINGS:")
for i, warn := range result.Warnings {
fmt.Printf(" %d. Field: %s\n", i+1, warn.Field)
fmt.Printf(" Issue: %s\n", warn.Message)
fmt.Printf(" Value: %v\n", warn.Value)
fmt.Println()
}
}
if !result.HasErrors() && !result.HasWarnings() {
fmt.Println("🎉 All validation checks passed successfully!")
}
}
// countPassedChecks estimates the number of successful validation checks
func countPassedChecks(result *ConfigValidationResult) int {
// Rough estimate: total possible checks minus errors and warnings
totalPossibleChecks := 50 // Approximate number of validation checks
return totalPossibleChecks - len(result.Errors) - len(result.Warnings)
}