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.
447 lines
13 KiB
Go
447 lines
13 KiB
Go
package migration
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Fimeg/RedFlag/aggregator/pkg/common"
|
|
)
|
|
|
|
// 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: "/etc/aggregator",
|
|
OldStatePath: "/var/lib/aggregator",
|
|
NewConfigPath: "/etc/redflag",
|
|
NewStatePath: "/var/lib/redflag",
|
|
BackupDirPattern: "/etc/redflag.backup.%s",
|
|
}
|
|
}
|
|
|
|
// 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 if old directories exist
|
|
for _, oldDir := range detection.Inventory.OldDirectoryPaths {
|
|
if _, err := os.Stat(oldDir); err == nil {
|
|
migrations = append(migrations, "directory_migration")
|
|
break
|
|
}
|
|
}
|
|
|
|
// Check config version compatibility
|
|
if detection.CurrentConfigVersion < 4 {
|
|
migrations = append(migrations, "config_migration")
|
|
}
|
|
|
|
// Check if Docker secrets migration is needed (v5)
|
|
if detection.CurrentConfigVersion < 5 {
|
|
migrations = append(migrations, "config_v5_migration")
|
|
}
|
|
|
|
// Check if Docker secrets migration is needed
|
|
if detection.DockerDetection != nil && detection.DockerDetection.MigrateToSecrets {
|
|
migrations = append(migrations, "docker_secrets_migration")
|
|
}
|
|
|
|
// Check if security features need to be applied
|
|
if len(detection.MissingSecurityFeatures) > 0 {
|
|
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 ""
|
|
} |