Files
Redflag/aggregator-agent/internal/migration/docker.go
Fimeg c95cc7d91f cleanup: remove 2,369 lines of dead code
Removed backup files and unused legacy scanner function.
All code verified as unreferenced.
2025-11-10 21:20:42 -05:00

393 lines
10 KiB
Go

package migration
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"time"
)
// DockerDetection represents Docker secrets detection results
type DockerDetection struct {
DockerAvailable bool `json:"docker_available"`
SecretsMountPath string `json:"secrets_mount_path"`
RequiredSecrets []string `json:"required_secrets"`
ExistingSecrets []string `json:"existing_secrets"`
MigrateToSecrets bool `json:"migrate_to_secrets"`
SecretFiles []AgentFile `json:"secret_files"`
DetectionTime time.Time `json:"detection_time"`
}
// SecretFile represents a file that should be migrated to Docker secrets
type SecretFile struct {
Name string `json:"name"`
SourcePath string `json:"source_path"`
SecretPath string `json:"secret_path"`
Encrypted bool `json:"encrypted"`
Checksum string `json:"checksum"`
Size int64 `json:"size"`
}
// DockerConfig holds Docker secrets configuration
type DockerConfig struct {
Enabled bool `json:"enabled"`
SecretsPath string `json:"secrets_path"`
EncryptionKey string `json:"encryption_key,omitempty"`
Secrets map[string]string `json:"secrets,omitempty"`
}
// GetDockerSecretsPath returns the platform-specific Docker secrets path
func GetDockerSecretsPath() string {
if runtime.GOOS == "windows" {
return `C:\ProgramData\Docker\secrets`
}
return "/run/secrets"
}
// DetectDockerSecretsRequirements detects if Docker secrets migration is needed
func DetectDockerSecretsRequirements(config *FileDetectionConfig) (*DockerDetection, error) {
detection := &DockerDetection{
DetectionTime: time.Now(),
SecretsMountPath: GetDockerSecretsPath(),
}
// Check if Docker secrets directory exists
if _, err := os.Stat(detection.SecretsMountPath); err == nil {
detection.DockerAvailable = true
fmt.Printf("[DOCKER] Docker secrets mount path detected: %s\n", detection.SecretsMountPath)
} else {
fmt.Printf("[DOCKER] Docker secrets not available: %s\n", err)
return detection, nil
}
// Scan for sensitive files that should be migrated to secrets
secretFiles, err := scanSecretFiles(config)
if err != nil {
return nil, fmt.Errorf("failed to scan for secret files: %w", err)
}
detection.SecretFiles = secretFiles
detection.MigrateToSecrets = len(secretFiles) > 0
// Identify required secrets
detection.RequiredSecrets = identifyRequiredSecrets(secretFiles)
// Check existing secrets
detection.ExistingSecrets = scanExistingSecrets(detection.SecretsMountPath)
return detection, nil
}
// scanSecretFiles scans for files containing sensitive data
func scanSecretFiles(config *FileDetectionConfig) ([]AgentFile, error) {
var secretFiles []AgentFile
// Define sensitive file patterns
secretPatterns := []string{
"agent.key",
"server.key",
"ca.crt",
"*.pem",
"*.key",
"config.json", // Will be filtered for sensitive content
}
// Scan new directory paths for secret files
for _, dirPath := range []string{config.NewConfigPath, config.NewStatePath} {
if _, err := os.Stat(dirPath); err == nil {
files, err := scanSecretDirectory(dirPath, secretPatterns)
if err != nil {
return nil, fmt.Errorf("failed to scan directory %s for secrets: %w", dirPath, err)
}
secretFiles = append(secretFiles, files...)
}
}
return secretFiles, nil
}
// scanSecretDirectory scans a directory for files that may contain secrets
func scanSecretDirectory(dirPath string, patterns []string) ([]AgentFile, error) {
var files []AgentFile
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
// Check if file matches secret patterns
if !matchesSecretPattern(path, patterns) {
// For config.json, check if it contains sensitive data
if filepath.Base(path) == "config.json" {
if hasSensitiveContent(path) {
return addSecretFile(&files, path, info)
}
}
return nil
}
return addSecretFile(&files, path, info)
})
return files, err
}
// addSecretFile adds a file to the secret files list
func addSecretFile(files *[]AgentFile, path string, info os.FileInfo) error {
checksum, err := calculateFileChecksum(path)
if err != nil {
return nil // Skip files we can't read
}
file := AgentFile{
Path: path,
Size: info.Size(),
ModifiedTime: info.ModTime(),
Checksum: checksum,
Required: true,
Migrate: true,
Description: getSecretFileDescription(path),
}
*files = append(*files, file)
return nil
}
// matchesSecretPattern checks if a file path matches secret patterns
func matchesSecretPattern(path string, patterns []string) bool {
base := filepath.Base(path)
for _, pattern := range patterns {
if matched, _ := filepath.Match(pattern, base); matched {
return true
}
}
return false
}
// hasSensitiveContent checks if a config file contains sensitive data
func hasSensitiveContent(configPath string) bool {
data, err := os.ReadFile(configPath)
if err != nil {
return false
}
var config map[string]interface{}
if err := json.Unmarshal(data, &config); err != nil {
return false
}
// Check for sensitive fields
sensitiveFields := []string{
"password", "token", "key", "secret", "credential",
"proxy", "tls", "certificate", "private",
}
for _, field := range sensitiveFields {
if containsSensitiveField(config, field) {
return true
}
}
return false
}
// containsSensitiveField recursively checks for sensitive fields in config
func containsSensitiveField(config map[string]interface{}, field string) bool {
for key, value := range config {
if containsString(key, field) {
return true
}
if nested, ok := value.(map[string]interface{}); ok {
if containsSensitiveField(nested, field) {
return true
}
}
}
return false
}
// containsString checks if a string contains a substring (case-insensitive)
func containsString(s, substr string) bool {
s = strings.ToLower(s)
substr = strings.ToLower(substr)
return strings.Contains(s, substr)
}
// identifyRequiredSecrets identifies which secrets need to be created
func identifyRequiredSecrets(secretFiles []AgentFile) []string {
var secrets []string
for _, file := range secretFiles {
secretName := filepath.Base(file.Path)
if file.Path == "config.json" {
secrets = append(secrets, "config.json.enc")
} else {
secrets = append(secrets, secretName)
}
}
return secrets
}
// scanExistingSecrets scans the Docker secrets directory for existing secrets
func scanExistingSecrets(secretsPath string) []string {
var secrets []string
entries, err := os.ReadDir(secretsPath)
if err != nil {
return secrets
}
for _, entry := range entries {
if !entry.IsDir() {
secrets = append(secrets, entry.Name())
}
}
return secrets
}
// getSecretFileDescription returns a description for a secret file
func getSecretFileDescription(path string) string {
base := filepath.Base(path)
switch {
case base == "agent.key":
return "Agent private key"
case base == "server.key":
return "Server private key"
case base == "ca.crt":
return "Certificate authority certificate"
case strings.Contains(base, ".key"):
return "Private key file"
case strings.Contains(base, ".crt") || strings.Contains(base, ".pem"):
return "Certificate file"
case base == "config.json":
return "Configuration file with sensitive data"
default:
return "Secret file"
}
}
// EncryptFile encrypts a file using AES-256-GCM
func EncryptFile(inputPath, outputPath, key string) error {
// Generate key from passphrase
keyBytes := sha256.Sum256([]byte(key))
// Read input file
plaintext, err := os.ReadFile(inputPath)
if err != nil {
return fmt.Errorf("failed to read input file: %w", err)
}
// Create cipher
block, err := aes.NewCipher(keyBytes[:])
if err != nil {
return fmt.Errorf("failed to create cipher: %w", err)
}
// Create GCM
gcm, err := cipher.NewGCM(block)
if err != nil {
return fmt.Errorf("failed to create GCM: %w", err)
}
// Generate nonce
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return fmt.Errorf("failed to generate nonce: %w", err)
}
// Encrypt
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
// Write encrypted file
if err := os.WriteFile(outputPath, ciphertext, 0600); err != nil {
return fmt.Errorf("failed to write encrypted file: %w", err)
}
return nil
}
// DecryptFile decrypts a file using AES-256-GCM
func DecryptFile(inputPath, outputPath, key string) error {
// Generate key from passphrase
keyBytes := sha256.Sum256([]byte(key))
// Read encrypted file
ciphertext, err := os.ReadFile(inputPath)
if err != nil {
return fmt.Errorf("failed to read encrypted file: %w", err)
}
// Create cipher
block, err := aes.NewCipher(keyBytes[:])
if err != nil {
return fmt.Errorf("failed to create cipher: %w", err)
}
// Create GCM
gcm, err := cipher.NewGCM(block)
if err != nil {
return fmt.Errorf("failed to create GCM: %w", err)
}
// Check minimum length
if len(ciphertext) < gcm.NonceSize() {
return fmt.Errorf("ciphertext too short")
}
// Extract nonce and ciphertext
nonce := ciphertext[:gcm.NonceSize()]
ciphertext = ciphertext[gcm.NonceSize():]
// Decrypt
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return fmt.Errorf("failed to decrypt: %w", err)
}
// Write decrypted file
if err := os.WriteFile(outputPath, plaintext, 0600); err != nil {
return fmt.Errorf("failed to write decrypted file: %w", err)
}
return nil
}
// GenerateEncryptionKey generates a random encryption key
func GenerateEncryptionKey() (string, error) {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("failed to generate encryption key: %w", err)
}
return hex.EncodeToString(bytes), nil
}
// IsDockerEnvironment checks if running in Docker environment
func IsDockerEnvironment() bool {
// Check for .dockerenv file
if _, err := os.Stat("/.dockerenv"); err == nil {
return true
}
// Check for Docker in cgroup
if data, err := os.ReadFile("/proc/1/cgroup"); err == nil {
if containsString(string(data), "docker") {
return true
}
}
return false
}