395 lines
10 KiB
Go
395 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"
|
|
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/common"
|
|
)
|
|
|
|
// 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 []common.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) ([]common.AgentFile, error) {
|
|
var secretFiles []common.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) ([]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
|
|
}
|
|
|
|
// 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 *[]common.AgentFile, path string, info os.FileInfo) error {
|
|
checksum, err := calculateFileChecksum(path)
|
|
if err != nil {
|
|
return nil // Skip files we can't read
|
|
}
|
|
|
|
file := common.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 []common.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
|
|
} |