Created aggregator/pkg/common module with shared AgentFile type. Removed duplicate definitions from migration and services packages. Both agent and server now use common.AgentFile.
400 lines
12 KiB
Go
400 lines
12 KiB
Go
package migration
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Fimeg/RedFlag/aggregator/pkg/common"
|
|
)
|
|
|
|
// MigrationPlan represents a complete migration plan
|
|
type MigrationPlan struct {
|
|
Detection *MigrationDetection `json:"detection"`
|
|
TargetVersion string `json:"target_version"`
|
|
Config *FileDetectionConfig `json:"config"`
|
|
BackupPath string `json:"backup_path"`
|
|
EstimatedDuration time.Duration `json:"estimated_duration"`
|
|
RiskLevel string `json:"risk_level"` // low, medium, high
|
|
}
|
|
|
|
// MigrationResult represents the result of a migration execution
|
|
type MigrationResult struct {
|
|
Success bool `json:"success"`
|
|
StartTime time.Time `json:"start_time"`
|
|
EndTime time.Time `json:"end_time"`
|
|
Duration time.Duration `json:"duration"`
|
|
BackupPath string `json:"backup_path"`
|
|
MigratedFiles []string `json:"migrated_files"`
|
|
Errors []string `json:"errors"`
|
|
Warnings []string `json:"warnings"`
|
|
AppliedChanges []string `json:"applied_changes"`
|
|
RollbackAvailable bool `json:"rollback_available"`
|
|
}
|
|
|
|
// MigrationExecutor handles the execution of migration plans
|
|
type MigrationExecutor struct {
|
|
plan *MigrationPlan
|
|
result *MigrationResult
|
|
}
|
|
|
|
// NewMigrationExecutor creates a new migration executor
|
|
func NewMigrationExecutor(plan *MigrationPlan) *MigrationExecutor {
|
|
return &MigrationExecutor{
|
|
plan: plan,
|
|
result: &MigrationResult{},
|
|
}
|
|
}
|
|
|
|
// ExecuteMigration executes the complete migration plan
|
|
func (e *MigrationExecutor) ExecuteMigration() (*MigrationResult, error) {
|
|
e.result.StartTime = time.Now()
|
|
e.result.BackupPath = e.plan.BackupPath
|
|
|
|
fmt.Printf("[MIGRATION] Starting migration from %s to %s\n",
|
|
e.plan.Detection.CurrentAgentVersion, e.plan.TargetVersion)
|
|
|
|
// Phase 1: Create backups
|
|
if err := e.createBackups(); err != nil {
|
|
return e.completeMigration(false, fmt.Errorf("backup creation failed: %w", err))
|
|
}
|
|
e.result.AppliedChanges = append(e.result.AppliedChanges, "Created backups at "+e.plan.BackupPath)
|
|
|
|
// Phase 2: Directory migration
|
|
if contains(e.plan.Detection.RequiredMigrations, "directory_migration") {
|
|
if err := e.migrateDirectories(); err != nil {
|
|
return e.completeMigration(false, fmt.Errorf("directory migration failed: %w", err))
|
|
}
|
|
e.result.AppliedChanges = append(e.result.AppliedChanges, "Migrated directories")
|
|
}
|
|
|
|
// Phase 3: Configuration migration
|
|
if contains(e.plan.Detection.RequiredMigrations, "config_migration") {
|
|
if err := e.migrateConfiguration(); err != nil {
|
|
return e.completeMigration(false, fmt.Errorf("configuration migration failed: %w", err))
|
|
}
|
|
e.result.AppliedChanges = append(e.result.AppliedChanges, "Migrated configuration")
|
|
}
|
|
|
|
// Phase 4: Docker secrets migration (if available)
|
|
if contains(e.plan.Detection.RequiredMigrations, "docker_secrets_migration") {
|
|
if e.plan.Detection.DockerDetection == nil {
|
|
return e.completeMigration(false, fmt.Errorf("docker secrets migration requested but detection data missing"))
|
|
}
|
|
|
|
dockerExecutor := NewDockerSecretsExecutor(e.plan.Detection.DockerDetection, e.plan.Config)
|
|
if err := dockerExecutor.ExecuteDockerSecretsMigration(); err != nil {
|
|
return e.completeMigration(false, fmt.Errorf("docker secrets migration failed: %w", err))
|
|
}
|
|
e.result.AppliedChanges = append(e.result.AppliedChanges, "Migrated to Docker secrets")
|
|
}
|
|
|
|
// Phase 5: Security hardening
|
|
if contains(e.plan.Detection.RequiredMigrations, "security_hardening") {
|
|
if err := e.applySecurityHardening(); err != nil {
|
|
e.result.Warnings = append(e.result.Warnings,
|
|
fmt.Sprintf("Security hardening incomplete: %v", err))
|
|
} else {
|
|
e.result.AppliedChanges = append(e.result.AppliedChanges, "Applied security hardening")
|
|
}
|
|
}
|
|
|
|
// Phase 6: Validation
|
|
if err := e.validateMigration(); err != nil {
|
|
return e.completeMigration(false, fmt.Errorf("migration validation failed: %w", err))
|
|
}
|
|
|
|
return e.completeMigration(true, nil)
|
|
}
|
|
|
|
// createBackups creates backups of all existing files
|
|
func (e *MigrationExecutor) createBackups() error {
|
|
backupPath := e.plan.BackupPath
|
|
if backupPath == "" {
|
|
timestamp := time.Now().Format("2006-01-02-150405")
|
|
backupPath = fmt.Sprintf(e.plan.Config.BackupDirPattern, timestamp)
|
|
e.plan.BackupPath = backupPath
|
|
}
|
|
|
|
fmt.Printf("[MIGRATION] Creating backup at: %s\n", backupPath)
|
|
|
|
// Create backup directory
|
|
if err := os.MkdirAll(backupPath, 0755); err != nil {
|
|
return fmt.Errorf("failed to create backup directory: %w", err)
|
|
}
|
|
|
|
// Backup all files in inventory
|
|
allFiles := e.collectAllFiles()
|
|
for _, file := range allFiles {
|
|
if err := e.backupFile(file, backupPath); err != nil {
|
|
return fmt.Errorf("failed to backup file %s: %w", file.Path, err)
|
|
}
|
|
e.result.MigratedFiles = append(e.result.MigratedFiles, file.Path)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// migrateDirectories handles directory migration from old to new paths
|
|
func (e *MigrationExecutor) migrateDirectories() error {
|
|
fmt.Printf("[MIGRATION] Migrating directories...\n")
|
|
|
|
// Create new directories
|
|
newDirectories := []string{e.plan.Config.NewConfigPath, e.plan.Config.NewStatePath}
|
|
for _, newPath := range newDirectories {
|
|
if err := os.MkdirAll(newPath, 0755); err != nil {
|
|
return fmt.Errorf("failed to create directory %s: %w", newPath, err)
|
|
}
|
|
fmt.Printf("[MIGRATION] Created directory: %s\n", newPath)
|
|
}
|
|
|
|
// Migrate files from old to new directories
|
|
for _, oldDir := range e.plan.Detection.Inventory.OldDirectoryPaths {
|
|
newDir := e.getNewDirectoryPath(oldDir)
|
|
if newDir == "" {
|
|
continue
|
|
}
|
|
|
|
if err := e.migrateDirectoryContents(oldDir, newDir); err != nil {
|
|
return fmt.Errorf("failed to migrate directory %s to %s: %w", oldDir, newDir, err)
|
|
}
|
|
fmt.Printf("[MIGRATION] Migrated: %s → %s\n", oldDir, newDir)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// migrateConfiguration handles configuration file migration
|
|
func (e *MigrationExecutor) migrateConfiguration() error {
|
|
fmt.Printf("[MIGRATION] Migrating configuration...\n")
|
|
|
|
// Find and migrate config files
|
|
for _, configFile := range e.plan.Detection.Inventory.ConfigFiles {
|
|
if strings.Contains(configFile.Path, "config.json") {
|
|
newPath := e.getNewConfigPath(configFile.Path)
|
|
if newPath != "" && newPath != configFile.Path {
|
|
if err := e.migrateConfigFile(configFile.Path, newPath); err != nil {
|
|
return fmt.Errorf("failed to migrate config file: %w", err)
|
|
}
|
|
fmt.Printf("[MIGRATION] Migrated config: %s → %s\n", configFile.Path, newPath)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// applySecurityHardening applies security-related configurations
|
|
func (e *MigrationExecutor) applySecurityHardening() error {
|
|
fmt.Printf("[MIGRATION] Applying security hardening...\n")
|
|
|
|
// This would integrate with the config system to apply security defaults
|
|
// For now, just log what would be applied
|
|
|
|
for _, feature := range e.plan.Detection.MissingSecurityFeatures {
|
|
switch feature {
|
|
case "nonce_validation":
|
|
fmt.Printf("[MIGRATION] Enabling nonce validation\n")
|
|
case "machine_id_binding":
|
|
fmt.Printf("[MIGRATION] Configuring machine ID binding\n")
|
|
case "ed25519_verification":
|
|
fmt.Printf("[MIGRATION] Enabling Ed25519 verification\n")
|
|
case "subsystem_configuration":
|
|
fmt.Printf("[MIGRATION] Adding missing subsystem configurations\n")
|
|
case "system_subsystem":
|
|
fmt.Printf("[MIGRATION] Adding system scanner configuration\n")
|
|
case "updates_subsystem":
|
|
fmt.Printf("[MIGRATION] Adding updates subsystem configuration\n")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateMigration validates that the migration was successful
|
|
func (e *MigrationExecutor) validateMigration() error {
|
|
fmt.Printf("[MIGRATION] Validating migration...\n")
|
|
|
|
// Check that new directories exist
|
|
newDirectories := []string{e.plan.Config.NewConfigPath, e.plan.Config.NewStatePath}
|
|
for _, newDir := range newDirectories {
|
|
if _, err := os.Stat(newDir); err != nil {
|
|
return fmt.Errorf("new directory %s not found: %w", newDir, err)
|
|
}
|
|
}
|
|
|
|
// Check that config files exist in new location
|
|
for _, configFile := range e.plan.Detection.Inventory.ConfigFiles {
|
|
newPath := e.getNewConfigPath(configFile.Path)
|
|
if newPath != "" {
|
|
if _, err := os.Stat(newPath); err != nil {
|
|
return fmt.Errorf("migrated config file %s not found: %w", newPath, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.Printf("[MIGRATION] ✅ Migration validation successful\n")
|
|
return nil
|
|
}
|
|
|
|
// Helper methods
|
|
|
|
func (e *MigrationExecutor) collectAllFiles() []common.AgentFile {
|
|
var allFiles []common.AgentFile
|
|
allFiles = append(allFiles, e.plan.Detection.Inventory.ConfigFiles...)
|
|
allFiles = append(allFiles, e.plan.Detection.Inventory.StateFiles...)
|
|
allFiles = append(allFiles, e.plan.Detection.Inventory.BinaryFiles...)
|
|
allFiles = append(allFiles, e.plan.Detection.Inventory.LogFiles...)
|
|
allFiles = append(allFiles, e.plan.Detection.Inventory.CertificateFiles...)
|
|
return allFiles
|
|
}
|
|
|
|
func (e *MigrationExecutor) backupFile(file common.AgentFile, backupPath string) error {
|
|
relPath, err := filepath.Rel(e.plan.Config.OldConfigPath, file.Path)
|
|
if err != nil {
|
|
// Try relative to old state path
|
|
relPath, err = filepath.Rel(e.plan.Config.OldStatePath, file.Path)
|
|
if err != nil {
|
|
relPath = filepath.Base(file.Path)
|
|
}
|
|
}
|
|
|
|
backupFilePath := filepath.Join(backupPath, relPath)
|
|
backupDir := filepath.Dir(backupFilePath)
|
|
|
|
if err := os.MkdirAll(backupDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create backup directory: %w", err)
|
|
}
|
|
|
|
// Copy file to backup location
|
|
if err := copyFile(file.Path, backupFilePath); err != nil {
|
|
return fmt.Errorf("failed to copy file to backup: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *MigrationExecutor) migrateDirectoryContents(oldDir, newDir string) error {
|
|
return filepath.Walk(oldDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
relPath, err := filepath.Rel(oldDir, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newPath := filepath.Join(newDir, relPath)
|
|
newDirPath := filepath.Dir(newPath)
|
|
|
|
if err := os.MkdirAll(newDirPath, 0755); err != nil {
|
|
return fmt.Errorf("failed to create directory: %w", err)
|
|
}
|
|
|
|
if err := copyFile(path, newPath); err != nil {
|
|
return fmt.Errorf("failed to copy file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (e *MigrationExecutor) migrateConfigFile(oldPath, newPath string) error {
|
|
// This would use the config migration logic from the config package
|
|
// For now, just copy the file
|
|
return copyFile(oldPath, newPath)
|
|
}
|
|
|
|
func (e *MigrationExecutor) getNewDirectoryPath(oldPath string) string {
|
|
if oldPath == e.plan.Config.OldConfigPath {
|
|
return e.plan.Config.NewConfigPath
|
|
}
|
|
if oldPath == e.plan.Config.OldStatePath {
|
|
return e.plan.Config.NewStatePath
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (e *MigrationExecutor) getNewConfigPath(oldPath string) string {
|
|
if strings.HasPrefix(oldPath, e.plan.Config.OldConfigPath) {
|
|
relPath := strings.TrimPrefix(oldPath, e.plan.Config.OldConfigPath)
|
|
return filepath.Join(e.plan.Config.NewConfigPath, relPath)
|
|
}
|
|
if strings.HasPrefix(oldPath, e.plan.Config.OldStatePath) {
|
|
relPath := strings.TrimPrefix(oldPath, e.plan.Config.OldStatePath)
|
|
return filepath.Join(e.plan.Config.NewStatePath, relPath)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (e *MigrationExecutor) completeMigration(success bool, err error) (*MigrationResult, error) {
|
|
e.result.EndTime = time.Now()
|
|
e.result.Duration = e.result.EndTime.Sub(e.result.StartTime)
|
|
e.result.Success = success
|
|
e.result.RollbackAvailable = success && e.result.BackupPath != ""
|
|
|
|
if err != nil {
|
|
e.result.Errors = append(e.result.Errors, err.Error())
|
|
}
|
|
|
|
if success {
|
|
fmt.Printf("[MIGRATION] ✅ Migration completed successfully in %v\n", e.result.Duration)
|
|
if e.result.RollbackAvailable {
|
|
fmt.Printf("[MIGRATION] 📦 Rollback available at: %s\n", e.result.BackupPath)
|
|
}
|
|
} else {
|
|
fmt.Printf("[MIGRATION] ❌ Migration failed after %v\n", e.result.Duration)
|
|
if len(e.result.Errors) > 0 {
|
|
for _, errMsg := range e.result.Errors {
|
|
fmt.Printf("[MIGRATION] Error: %s\n", errMsg)
|
|
}
|
|
}
|
|
}
|
|
|
|
return e.result, err
|
|
}
|
|
|
|
// Utility functions
|
|
|
|
func contains(slice []string, item string) bool {
|
|
for _, s := range slice {
|
|
if s == item {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func copyFile(src, dst string) error {
|
|
sourceFile, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer sourceFile.Close()
|
|
|
|
destFile, err := os.Create(dst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer destFile.Close()
|
|
|
|
_, err = destFile.ReadFrom(sourceFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Preserve file permissions
|
|
sourceInfo, err := os.Stat(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.Chmod(dst, sourceInfo.Mode())
|
|
} |