package validation import ( "fmt" "os" "path/filepath" "strings" "time" "github.com/Fimeg/RedFlag/aggregator-agent/internal/common" "github.com/Fimeg/RedFlag/aggregator-agent/internal/event" "github.com/Fimeg/RedFlag/aggregator-agent/internal/migration/pathutils" "github.com/Fimeg/RedFlag/aggregator-agent/internal/models" "github.com/google/uuid" ) // FileValidator handles comprehensive file validation for migration type FileValidator struct { pathManager *pathutils.PathManager eventBuffer *event.Buffer agentID uuid.UUID } // NewFileValidator creates a new file validator func NewFileValidator(pm *pathutils.PathManager, eventBuffer *event.Buffer, agentID uuid.UUID) *FileValidator { return &FileValidator{ pathManager: pm, eventBuffer: eventBuffer, agentID: agentID, } } // ValidationResult holds validation results type ValidationResult struct { Valid bool `json:"valid"` Errors []string `json:"errors"` Warnings []string `json:"warnings"` Inventory *FileInventory `json:"inventory"` Statistics *ValidationStats `json:"statistics"` } // FileInventory represents validated files type FileInventory struct { ValidFiles []common.AgentFile `json:"valid_files"` InvalidFiles []InvalidFile `json:"invalid_files"` MissingFiles []string `json:"missing_files"` SkippedFiles []SkippedFile `json:"skipped_files"` Directories []string `json:"directories"` } // InvalidFile represents a file that failed validation type InvalidFile struct { Path string `json:"path"` Reason string `json:"reason"` ErrorType string `json:"error_type"` // "not_found", "permission", "traversal", "other" Expected string `json:"expected"` } // SkippedFile represents a file that was intentionally skipped type SkippedFile struct { Path string `json:"path"` Reason string `json:"reason"` } // ValidationStats holds statistics about validation type ValidationStats struct { TotalFiles int `json:"total_files"` ValidFiles int `json:"valid_files"` InvalidFiles int `json:"invalid_files"` MissingFiles int `json:"missing_files"` SkippedFiles int `json:"skipped_files"` ValidationTime int64 `json:"validation_time_ms"` TotalSizeBytes int64 `json:"total_size_bytes"` } // ValidateInventory performs comprehensive validation of file inventory func (v *FileValidator) ValidateInventory(files []common.AgentFile, requiredPatterns []string) (*ValidationResult, error) { start := time.Now() result := &ValidationResult{ Valid: true, Errors: []string{}, Warnings: []string{}, Inventory: &FileInventory{ ValidFiles: []common.AgentFile{}, InvalidFiles: []InvalidFile{}, MissingFiles: []string{}, SkippedFiles: []SkippedFile{}, Directories: []string{}, }, Statistics: &ValidationStats{}, } // Group files by directory and collect statistics dirMap := make(map[string]bool) var totalSize int64 for _, file := range files { result.Statistics.TotalFiles++ // Skip log files (.log, .tmp) as they shouldn't be migrated if containsAny(file.Path, []string{"*.log", "*.tmp"}) { result.Inventory.SkippedFiles = append(result.Inventory.SkippedFiles, SkippedFile{ Path: file.Path, Reason: "Log/temp files are not migrated", }) result.Statistics.SkippedFiles++ continue } // Validate file path and existence if err := v.pathManager.ValidatePath(file.Path); err != nil { result.Valid = false result.Statistics.InvalidFiles++ errorType := "other" reason := err.Error() if os.IsNotExist(err) { errorType = "not_found" reason = fmt.Sprintf("File does not exist: %s", file.Path) } else if os.IsPermission(err) { errorType = "permission" reason = fmt.Sprintf("Permission denied: %s", file.Path) } result.Errors = append(result.Errors, reason) result.Inventory.InvalidFiles = append(result.Inventory.InvalidFiles, InvalidFile{ Path: file.Path, Reason: reason, ErrorType: errorType, }) // Log the validation failure v.bufferEvent("file_validation_failed", "warning", "migration_validator", reason, map[string]interface{}{ "file_path": file.Path, "error_type": errorType, "file_size": file.Size, }) continue } // Track directory dir := filepath.Dir(file.Path) if !dirMap[dir] { dirMap[dir] = true result.Inventory.Directories = append(result.Inventory.Directories, dir) } result.Inventory.ValidFiles = append(result.Inventory.ValidFiles, file) result.Statistics.ValidFiles++ totalSize += file.Size } result.Statistics.TotalSizeBytes = totalSize // Check for required files for _, pattern := range requiredPatterns { found := false for _, file := range result.Inventory.ValidFiles { if matched, _ := filepath.Match(pattern, filepath.Base(file.Path)); matched { found = true break } } if !found { result.Valid = false missing := fmt.Sprintf("Required file pattern not found: %s", pattern) result.Errors = append(result.Errors, missing) result.Inventory.MissingFiles = append(result.Inventory.MissingFiles, pattern) result.Statistics.MissingFiles++ // Log missing required file v.bufferEvent("required_file_missing", "error", "migration_validator", missing, map[string]interface{}{ "required_pattern": pattern, "phase": "validation", }) } } result.Statistics.ValidationTime = time.Since(start).Milliseconds() // Log validation completion v.bufferEvent("validation_completed", "info", "migration_validator", fmt.Sprintf("File validation completed: %d total, %d valid, %d invalid, %d skipped", result.Statistics.TotalFiles, result.Statistics.ValidFiles, result.Statistics.InvalidFiles, result.Statistics.SkippedFiles), map[string]interface{}{ "stats": result.Statistics, "valid": result.Valid, }) return result, nil } // ValidateBackupLocation validates backup location is writable and safe func (v *FileValidator) ValidateBackupLocation(backupPath string) error { // Normalize path normalized, err := v.pathManager.NormalizeToAbsolute(backupPath) if err != nil { return fmt.Errorf("invalid backup path: %w", err) } // Ensure backup path isn't in system directories if strings.HasPrefix(normalized, "/bin/") || strings.HasPrefix(normalized, "/sbin/") || strings.HasPrefix(normalized, "/usr/bin/") || strings.HasPrefix(normalized, "/usr/sbin/") { return fmt.Errorf("backup path cannot be in system directory: %s", normalized) } // Ensure parent directory exists and is writable parent := filepath.Dir(normalized) if err := v.pathManager.EnsureDirectory(parent); err != nil { return fmt.Errorf("cannot create backup directory: %w", err) } // Test write permission (create a temp file) testFile := filepath.Join(parent, ".migration_test_"+uuid.New().String()[:8]) if err := os.WriteFile(testFile, []byte("test"), 0600); err != nil { return fmt.Errorf("backup directory not writable: %w", err) } // Clean up test file _ = os.Remove(testFile) return nil } // PreValidate validates all conditions before migration starts func (v *FileValidator) PreValidate(detection *MigrationDetection, backupPath string) (*ValidationResult, error) { v.bufferEvent("pre_validation_started", "info", "migration_validator", "Starting comprehensive migration validation", map[string]interface{}{ "agent_version": detection.CurrentAgentVersion, "config_version": detection.CurrentConfigVersion, }) // Collect all files from inventory allFiles := v.collectAllFiles(detection.Inventory) // Define required patterns based on migration needs requiredPatterns := []string{ "config.json", // Config is essential // Note: agent.key files are generated if missing } // Validate inventory result, err := v.ValidateInventory(allFiles, requiredPatterns) if err != nil { v.bufferEvent("validation_error", "error", "migration_validator", fmt.Sprintf("Validation failed: %v", err), map[string]interface{}{ "error": err.Error(), "phase": "pre_validation", }) return nil, fmt.Errorf("validation failed: %w", err) } // Validate backup location if err := v.ValidateBackupLocation(backupPath); err != nil { result.Valid = false result.Errors = append(result.Errors, fmt.Sprintf("Backup location invalid: %v", err)) v.bufferEvent("backup_validation_failed", "error", "migration_validator", fmt.Sprintf("Backup validation failed: %v", err), map[string]interface{}{ "backup_path": backupPath, "error": err.Error(), "phase": "validation", }) } // Validate new directories can be created (but don't create them yet) newDirs := []string{ v.pathManager.GetConfig().NewConfigPath, v.pathManager.GetConfig().NewStatePath, } for _, dir := range newDirs { normalized, err := v.pathManager.NormalizeToAbsolute(dir) if err != nil { result.Valid = false result.Errors = append(result.Errors, fmt.Sprintf("Invalid new directory %s: %v", dir, err)) continue } // Check if parent is writable parent := filepath.Dir(normalized) if _, err := os.Stat(parent); err != nil { if os.IsNotExist(err) { result.Warnings = append(result.Warnings, fmt.Sprintf("Parent directory for %s does not exist: %s", dir, parent)) } } } // Log final validation status v.bufferEvent("pre_validation_completed", "info", "migration_validator", fmt.Sprintf("Pre-validation completed: %s", func() string { if result.Valid { return "PASSED" } return "FAILED" }()), map[string]interface{}{ "errors_count": len(result.Errors), "warnings_count": len(result.Warnings), "files_valid": result.Statistics.ValidFiles, "files_invalid": result.Statistics.InvalidFiles, "files_skipped": result.Statistics.SkippedFiles, }) return result, nil } // collectAllFiles collects all files from the migration inventory func (v *FileValidator) collectAllFiles(inventory *AgentFileInventory) []common.AgentFile { var allFiles []common.AgentFile if inventory != nil { allFiles = append(allFiles, inventory.ConfigFiles...) allFiles = append(allFiles, inventory.StateFiles...) allFiles = append(allFiles, inventory.BinaryFiles...) allFiles = append(allFiles, inventory.LogFiles...) allFiles = append(allFiles, inventory.CertificateFiles...) } return allFiles } // bufferEvent logs an event to the event buffer func (v *FileValidator) bufferEvent(eventSubtype, severity, component, message string, metadata map[string]interface{}) { if v.eventBuffer == nil { return } event := &models.SystemEvent{ ID: uuid.New(), AgentID: &v.agentID, EventType: models.EventTypeAgentMigration, // Using model constant EventSubtype: eventSubtype, Severity: severity, Component: component, Message: message, Metadata: metadata, CreatedAt: time.Now(), } if err := v.eventBuffer.BufferEvent(event); err != nil { fmt.Printf("[VALIDATION] Warning: Failed to buffer event: %v\n", err) } } // containsAny checks if path matches any of the patterns func containsAny(path string, patterns []string) bool { for _, pattern := range patterns { if matched, _ := filepath.Match(pattern, filepath.Base(path)); matched { return true } } return false } // ValidateFileForBackup validates a single file before backup func (v *FileValidator) ValidateFileForBackup(file common.AgentFile) error { // Check if file exists if _, err := os.Stat(file.Path); err != nil { if os.IsNotExist(err) { v.bufferEvent("backup_file_missing", "warning", "migration_validator", fmt.Sprintf("Skipping backup of non-existent file: %s", file.Path), map[string]interface{}{ "file_path": file.Path, "phase": "backup", }) return fmt.Errorf("file does not exist: %s", file.Path) } return fmt.Errorf("failed to access file %s: %w", file.Path, err) } // Additional validation for sensitive files if strings.Contains(file.Path, ".key") || strings.Contains(file.Path, "config") { // Key files should be readable only by owner info, err := os.Stat(file.Path) if err == nil { perm := info.Mode().Perm() // Check if others have read permission if perm&0004 != 0 { v.bufferEvent("insecure_file_permissions", "warning", "migration_validator", fmt.Sprintf("Sensitive file has world-readable permissions: %s (0%o)", file.Path, perm), map[string]interface{}{ "file_path": file.Path, "permissions": perm, }) } } } return nil }