517 lines
15 KiB
Go
517 lines
15 KiB
Go
package migration
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/common"
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/constants"
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/version"
|
|
)
|
|
|
|
// AgentFileInventory represents all files associated with an agent installation
|
|
type AgentFileInventory struct {
|
|
ConfigFiles []common.AgentFile `json:"config_files"`
|
|
StateFiles []common.AgentFile `json:"state_files"`
|
|
BinaryFiles []common.AgentFile `json:"binary_files"`
|
|
LogFiles []common.AgentFile `json:"log_files"`
|
|
CertificateFiles []common.AgentFile `json:"certificate_files"`
|
|
OldDirectoryPaths []string `json:"old_directory_paths"`
|
|
NewDirectoryPaths []string `json:"new_directory_paths"`
|
|
}
|
|
|
|
// MigrationDetection represents the result of migration detection
|
|
type MigrationDetection struct {
|
|
CurrentAgentVersion string `json:"current_agent_version"`
|
|
CurrentConfigVersion int `json:"current_config_version"`
|
|
RequiresMigration bool `json:"requires_migration"`
|
|
RequiredMigrations []string `json:"required_migrations"`
|
|
MissingSecurityFeatures []string `json:"missing_security_features"`
|
|
Inventory *AgentFileInventory `json:"inventory"`
|
|
DockerDetection *DockerDetection `json:"docker_detection,omitempty"`
|
|
DetectionTime time.Time `json:"detection_time"`
|
|
}
|
|
|
|
// SecurityFeature represents a security feature that may be missing
|
|
type SecurityFeature struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Required bool `json:"required"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
// FileDetectionConfig holds configuration for file detection
|
|
type FileDetectionConfig struct {
|
|
OldConfigPath string
|
|
OldStatePath string
|
|
NewConfigPath string
|
|
NewStatePath string
|
|
BackupDirPattern string
|
|
}
|
|
|
|
// NewFileDetectionConfig creates a default detection configuration
|
|
func NewFileDetectionConfig() *FileDetectionConfig {
|
|
return &FileDetectionConfig{
|
|
OldConfigPath: constants.LegacyConfigPath,
|
|
OldStatePath: constants.LegacyStatePath,
|
|
NewConfigPath: constants.GetAgentConfigDir(),
|
|
NewStatePath: constants.GetAgentStateDir(),
|
|
BackupDirPattern: constants.GetMigrationBackupDir() + "/%d",
|
|
}
|
|
}
|
|
|
|
// DetectMigrationRequirements scans for existing agent installations and determines migration needs
|
|
func DetectMigrationRequirements(config *FileDetectionConfig) (*MigrationDetection, error) {
|
|
detection := &MigrationDetection{
|
|
DetectionTime: time.Now(),
|
|
Inventory: &AgentFileInventory{},
|
|
}
|
|
|
|
// Scan for existing installations
|
|
inventory, err := scanAgentFiles(config)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan agent files: %w", err)
|
|
}
|
|
detection.Inventory = inventory
|
|
|
|
// Detect version information
|
|
version, configVersion, err := detectVersionInfo(inventory)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to detect version: %w", err)
|
|
}
|
|
detection.CurrentAgentVersion = version
|
|
detection.CurrentConfigVersion = configVersion
|
|
|
|
// Identify required migrations
|
|
requiredMigrations := determineRequiredMigrations(detection, config)
|
|
detection.RequiredMigrations = requiredMigrations
|
|
detection.RequiresMigration = len(requiredMigrations) > 0
|
|
|
|
// Identify missing security features
|
|
missingFeatures := identifyMissingSecurityFeatures(detection)
|
|
detection.MissingSecurityFeatures = missingFeatures
|
|
|
|
// Detect Docker secrets requirements if in Docker environment
|
|
if IsDockerEnvironment() {
|
|
dockerDetection, err := DetectDockerSecretsRequirements(config)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to detect Docker secrets requirements: %w", err)
|
|
}
|
|
detection.DockerDetection = dockerDetection
|
|
}
|
|
|
|
return detection, nil
|
|
}
|
|
|
|
// scanAgentFiles scans for agent-related files in old and new locations
|
|
func scanAgentFiles(config *FileDetectionConfig) (*AgentFileInventory, error) {
|
|
inventory := &AgentFileInventory{
|
|
OldDirectoryPaths: []string{config.OldConfigPath, config.OldStatePath},
|
|
NewDirectoryPaths: []string{config.NewConfigPath, config.NewStatePath},
|
|
}
|
|
|
|
// Define file patterns to look for
|
|
filePatterns := map[string][]string{
|
|
"config": {
|
|
"config.json",
|
|
"agent.key",
|
|
"server.key",
|
|
"ca.crt",
|
|
},
|
|
"state": {
|
|
"pending_acks.json",
|
|
"public_key.cache",
|
|
"last_scan.json",
|
|
"metrics.json",
|
|
},
|
|
"binary": {
|
|
"redflag-agent",
|
|
"redflag-agent.exe",
|
|
},
|
|
"log": {
|
|
"redflag-agent.log",
|
|
"redflag-agent.*.log",
|
|
},
|
|
"certificate": {
|
|
"*.crt",
|
|
"*.key",
|
|
"*.pem",
|
|
},
|
|
}
|
|
|
|
// Scan both old and new directory paths
|
|
allPaths := append(inventory.OldDirectoryPaths, inventory.NewDirectoryPaths...)
|
|
for _, dirPath := range allPaths {
|
|
if _, err := os.Stat(dirPath); err == nil {
|
|
files, err := scanDirectory(dirPath, filePatterns)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan directory %s: %w", dirPath, err)
|
|
}
|
|
|
|
// Categorize files
|
|
for _, file := range files {
|
|
switch {
|
|
case ContainsAny(file.Path, filePatterns["config"]):
|
|
inventory.ConfigFiles = append(inventory.ConfigFiles, file)
|
|
case ContainsAny(file.Path, filePatterns["state"]):
|
|
inventory.StateFiles = append(inventory.StateFiles, file)
|
|
case ContainsAny(file.Path, filePatterns["binary"]):
|
|
inventory.BinaryFiles = append(inventory.BinaryFiles, file)
|
|
case ContainsAny(file.Path, filePatterns["log"]):
|
|
inventory.LogFiles = append(inventory.LogFiles, file)
|
|
case ContainsAny(file.Path, filePatterns["certificate"]):
|
|
inventory.CertificateFiles = append(inventory.CertificateFiles, file)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return inventory, nil
|
|
}
|
|
|
|
// scanDirectory scans a directory for files matching specific patterns
|
|
func scanDirectory(dirPath string, patterns map[string][]string) ([]common.AgentFile, error) {
|
|
var files []common.AgentFile
|
|
|
|
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
// Calculate checksum
|
|
checksum, err := calculateFileChecksum(path)
|
|
if err != nil {
|
|
// Skip files we can't read
|
|
return nil
|
|
}
|
|
|
|
file := common.AgentFile{
|
|
Path: path,
|
|
Size: info.Size(),
|
|
ModifiedTime: info.ModTime(),
|
|
Checksum: checksum,
|
|
Required: isRequiredFile(path, patterns),
|
|
Migrate: shouldMigrateFile(path, patterns),
|
|
Description: getFileDescription(path),
|
|
}
|
|
|
|
// Try to detect version from filename or content
|
|
if version := detectFileVersion(path, info); version != "" {
|
|
file.Version = version
|
|
}
|
|
|
|
files = append(files, file)
|
|
return nil
|
|
})
|
|
|
|
return files, err
|
|
}
|
|
|
|
// detectVersionInfo attempts to detect agent and config versions from files
|
|
func detectVersionInfo(inventory *AgentFileInventory) (string, int, error) {
|
|
var detectedVersion string
|
|
configVersion := 0
|
|
|
|
// Try to read config file for version information
|
|
for _, configFile := range inventory.ConfigFiles {
|
|
if strings.Contains(configFile.Path, "config.json") {
|
|
version, cfgVersion, err := readConfigVersion(configFile.Path)
|
|
if err == nil {
|
|
detectedVersion = version
|
|
configVersion = cfgVersion
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no version found in config, try binary files
|
|
if detectedVersion == "" {
|
|
for _, binaryFile := range inventory.BinaryFiles {
|
|
if version := detectBinaryVersion(binaryFile.Path); version != "" {
|
|
detectedVersion = version
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default to unknown if nothing found
|
|
if detectedVersion == "" {
|
|
detectedVersion = "unknown"
|
|
}
|
|
|
|
return detectedVersion, configVersion, nil
|
|
}
|
|
|
|
// readConfigVersion reads version information from a config file
|
|
func readConfigVersion(configPath string) (string, int, error) {
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
|
|
var config map[string]interface{}
|
|
if err := json.Unmarshal(data, &config); err != nil {
|
|
return "", 0, err
|
|
}
|
|
|
|
// Try to extract version info
|
|
var agentVersion string
|
|
var cfgVersion int
|
|
|
|
if version, ok := config["agent_version"].(string); ok {
|
|
agentVersion = version
|
|
}
|
|
if version, ok := config["version"].(float64); ok {
|
|
cfgVersion = int(version)
|
|
}
|
|
|
|
return agentVersion, cfgVersion, nil
|
|
}
|
|
|
|
// determineRequiredMigrations determines what migrations are needed
|
|
func determineRequiredMigrations(detection *MigrationDetection, config *FileDetectionConfig) []string {
|
|
var migrations []string
|
|
|
|
// Check migration state to skip already completed migrations
|
|
configPath := filepath.Join(config.NewConfigPath, "config.json")
|
|
stateManager := NewStateManager(configPath)
|
|
|
|
// Check if old directories exist
|
|
for _, oldDir := range detection.Inventory.OldDirectoryPaths {
|
|
if _, err := os.Stat(oldDir); err == nil {
|
|
// Check if directory migration was already completed
|
|
completed, err := stateManager.IsMigrationCompleted("directory_migration")
|
|
if err == nil && !completed {
|
|
migrations = append(migrations, "directory_migration")
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
// Check for legacy installation (old path migration)
|
|
hasLegacyDirs := false
|
|
for _, oldDir := range detection.Inventory.OldDirectoryPaths {
|
|
if _, err := os.Stat(oldDir); err == nil {
|
|
hasLegacyDirs = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// Legacy migration: always migrate if old directories exist
|
|
if hasLegacyDirs {
|
|
if detection.CurrentConfigVersion < 4 {
|
|
// Check if already completed
|
|
completed, err := stateManager.IsMigrationCompleted("config_migration")
|
|
if err == nil && !completed {
|
|
migrations = append(migrations, "config_migration")
|
|
}
|
|
}
|
|
|
|
// Check if Docker secrets migration is needed (v5)
|
|
if detection.CurrentConfigVersion < 5 {
|
|
// Check if already completed
|
|
completed, err := stateManager.IsMigrationCompleted("config_v5_migration")
|
|
if err == nil && !completed {
|
|
migrations = append(migrations, "config_v5_migration")
|
|
}
|
|
}
|
|
} else {
|
|
// Version-based migration: compare current config version with expected
|
|
// This handles upgrades for agents already in correct location
|
|
// Use version package for single source of truth
|
|
agentVersion := version.Version
|
|
expectedConfigVersionStr := version.ExtractConfigVersionFromAgent(agentVersion)
|
|
// Convert to int for comparison (e.g., "6" -> 6)
|
|
expectedConfigVersion := 6 // Default fallback
|
|
if expectedConfigInt, err := strconv.Atoi(expectedConfigVersionStr); err == nil {
|
|
expectedConfigVersion = expectedConfigInt
|
|
}
|
|
|
|
// If config file exists but version is old, migrate
|
|
if detection.CurrentConfigVersion < expectedConfigVersion {
|
|
if detection.CurrentConfigVersion < 4 {
|
|
// Check if already completed
|
|
completed, err := stateManager.IsMigrationCompleted("config_migration")
|
|
if err == nil && !completed {
|
|
migrations = append(migrations, "config_migration")
|
|
}
|
|
}
|
|
|
|
// Check if Docker secrets migration is needed (v5)
|
|
if detection.CurrentConfigVersion < 5 {
|
|
// Check if already completed
|
|
completed, err := stateManager.IsMigrationCompleted("config_v5_migration")
|
|
if err == nil && !completed {
|
|
migrations = append(migrations, "config_v5_migration")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if Docker secrets migration is needed
|
|
if detection.DockerDetection != nil && detection.DockerDetection.MigrateToSecrets {
|
|
// Check if already completed
|
|
completed, err := stateManager.IsMigrationCompleted("docker_secrets_migration")
|
|
if err == nil && !completed {
|
|
migrations = append(migrations, "docker_secrets_migration")
|
|
}
|
|
}
|
|
|
|
// Check if security features need to be applied
|
|
if len(detection.MissingSecurityFeatures) > 0 {
|
|
// Check if already completed
|
|
completed, err := stateManager.IsMigrationCompleted("security_hardening")
|
|
if err == nil && !completed {
|
|
migrations = append(migrations, "security_hardening")
|
|
}
|
|
}
|
|
|
|
return migrations
|
|
}
|
|
|
|
// identifyMissingSecurityFeatures identifies security features that need to be enabled
|
|
func identifyMissingSecurityFeatures(detection *MigrationDetection) []string {
|
|
var missingFeatures []string
|
|
|
|
// Check config for security features
|
|
if detection.Inventory.ConfigFiles != nil {
|
|
for _, configFile := range detection.Inventory.ConfigFiles {
|
|
if strings.Contains(configFile.Path, "config.json") {
|
|
features := checkConfigSecurityFeatures(configFile.Path)
|
|
missingFeatures = append(missingFeatures, features...)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default missing features for old versions
|
|
if detection.CurrentConfigVersion < 4 {
|
|
missingFeatures = append(missingFeatures,
|
|
"nonce_validation",
|
|
"machine_id_binding",
|
|
"ed25519_verification",
|
|
"subsystem_configuration",
|
|
)
|
|
}
|
|
|
|
return missingFeatures
|
|
}
|
|
|
|
// checkConfigSecurityFeatures checks a config file for security feature settings
|
|
func checkConfigSecurityFeatures(configPath string) []string {
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return []string{}
|
|
}
|
|
|
|
var config map[string]interface{}
|
|
if err := json.Unmarshal(data, &config); err != nil {
|
|
return []string{}
|
|
}
|
|
|
|
var missingFeatures []string
|
|
|
|
// Check for subsystem configuration
|
|
if subsystems, ok := config["subsystems"].(map[string]interface{}); ok {
|
|
if _, hasSystem := subsystems["system"]; !hasSystem {
|
|
missingFeatures = append(missingFeatures, "system_subsystem")
|
|
}
|
|
if _, hasUpdates := subsystems["updates"]; !hasUpdates {
|
|
missingFeatures = append(missingFeatures, "updates_subsystem")
|
|
}
|
|
} else {
|
|
missingFeatures = append(missingFeatures, "subsystem_configuration")
|
|
}
|
|
|
|
// Check for machine ID
|
|
if _, hasMachineID := config["machine_id"]; !hasMachineID {
|
|
missingFeatures = append(missingFeatures, "machine_id_binding")
|
|
}
|
|
|
|
return missingFeatures
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func calculateFileChecksum(filePath string) (string, error) {
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer file.Close()
|
|
|
|
hash := sha256.New()
|
|
if _, err := io.Copy(hash, file); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return fmt.Sprintf("%x", hash.Sum(nil)), nil
|
|
}
|
|
|
|
func ContainsAny(path string, patterns []string) bool {
|
|
for _, pattern := range patterns {
|
|
if matched, _ := filepath.Match(pattern, filepath.Base(path)); matched {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isRequiredFile(path string, patterns map[string][]string) bool {
|
|
base := filepath.Base(path)
|
|
return base == "config.json" || base == "pending_acks.json"
|
|
}
|
|
|
|
func shouldMigrateFile(path string, patterns map[string][]string) bool {
|
|
return !ContainsAny(path, []string{"*.log", "*.tmp"})
|
|
}
|
|
|
|
func getFileDescription(path string) string {
|
|
base := filepath.Base(path)
|
|
switch {
|
|
case base == "config.json":
|
|
return "Agent configuration file"
|
|
case base == "pending_acks.json":
|
|
return "Pending command acknowledgments"
|
|
case base == "public_key.cache":
|
|
return "Server public key cache"
|
|
case strings.Contains(base, ".log"):
|
|
return "Agent log file"
|
|
case strings.Contains(base, ".key"):
|
|
return "Private key file"
|
|
case strings.Contains(base, ".crt"):
|
|
return "Certificate file"
|
|
default:
|
|
return "Agent file"
|
|
}
|
|
}
|
|
|
|
func detectFileVersion(path string, info os.FileInfo) string {
|
|
// Try to extract version from filename
|
|
base := filepath.Base(path)
|
|
if strings.Contains(base, "v0.1.") {
|
|
// Extract version from filename like "redflag-agent-v0.1.22"
|
|
parts := strings.Split(base, "v0.1.")
|
|
if len(parts) > 1 {
|
|
return "v0.1." + strings.Split(parts[1], "-")[0]
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func detectBinaryVersion(binaryPath string) string {
|
|
// This would involve reading binary headers or executing with --version flag
|
|
// For now, return empty
|
|
return ""
|
|
}
|