package security import ( "os" "strings" "testing" "github.com/jfraeys/fetch_ml/internal/worker" ) // TestSecretsEnvExpansionE2E tests environment variable expansion through full config load func TestSecretsEnvExpansionE2E(t *testing.T) { // Set test environment variables os.Setenv("TEST_REDIS_PASSWORD", "secret_redis_pass_123") os.Setenv("TEST_AWS_ACCESS_KEY", "AKIAIOSFODNN7EXAMPLE") os.Setenv("TEST_AWS_SECRET_KEY", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY") defer func() { os.Unsetenv("TEST_REDIS_PASSWORD") os.Unsetenv("TEST_AWS_ACCESS_KEY") os.Unsetenv("TEST_AWS_SECRET_KEY") }() // Create a config with environment variable references config := &worker.Config{ RedisPassword: "${TEST_REDIS_PASSWORD}", SnapshotStore: worker.SnapshotStoreConfig{ AccessKey: "${TEST_AWS_ACCESS_KEY}", SecretKey: "${TEST_AWS_SECRET_KEY}", }, } // Expand secrets config.ExpandSecrets() // Verify expansion worked if config.RedisPassword != "secret_redis_pass_123" { t.Errorf("Redis password not expanded correctly: got %s", config.RedisPassword) } if config.SnapshotStore.AccessKey != "AKIAIOSFODNN7EXAMPLE" { t.Errorf("AWS access key not expanded correctly: got %s", config.SnapshotStore.AccessKey) } if config.SnapshotStore.SecretKey != "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" { t.Errorf("AWS secret key not expanded correctly: got %s", config.SnapshotStore.SecretKey) } } // TestSecretsPlaintextDetectionE2E tests that plaintext secrets fail validation func TestSecretsPlaintextDetectionE2E(t *testing.T) { tests := []struct { name string redisPass string accessKey string secretKey string shouldFail bool description string }{ { name: "plaintext AWS key should fail", redisPass: "${REDIS_PASSWORD}", accessKey: "AKIAIOSFODNN7EXAMPLE", secretKey: "${AWS_SECRET_KEY}", shouldFail: true, description: "AWS access key in plaintext should be detected", }, { name: "plaintext high-entropy secret should fail", redisPass: "${REDIS_PASSWORD}", accessKey: "${AWS_ACCESS_KEY}", secretKey: "super_secret_key_with_high_entropy_12345!@#$%", shouldFail: true, description: "High entropy secret should be detected as plaintext", }, { name: "env references should pass", redisPass: "${REDIS_PASSWORD}", accessKey: "${AWS_ACCESS_KEY}", secretKey: "${AWS_SECRET_KEY}", shouldFail: false, description: "Environment variable references should be allowed", }, { name: "empty values should pass", redisPass: "", accessKey: "", secretKey: "", shouldFail: false, description: "Empty values should be allowed", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { config := &worker.Config{ RedisPassword: tt.redisPass, SnapshotStore: worker.SnapshotStoreConfig{ AccessKey: tt.accessKey, SecretKey: tt.secretKey, }, } err := config.ValidateNoPlaintextSecrets() if tt.shouldFail && err == nil { t.Errorf("%s: expected validation to fail but it passed", tt.description) } if !tt.shouldFail && err != nil { t.Errorf("%s: expected validation to pass but got error: %v", tt.description, err) } }) } } // TestSecretsRotation tests hot-reload capability for secrets func TestSecretsRotation(t *testing.T) { // Initial secret value os.Setenv("ROTATING_SECRET", "initial_secret_value") defer os.Unsetenv("ROTATING_SECRET") // Load config initially config1 := &worker.Config{ RedisPassword: "${ROTATING_SECRET}", } config1.ExpandSecrets() if config1.RedisPassword != "initial_secret_value" { t.Fatalf("Initial expansion failed: got %s", config1.RedisPassword) } // Simulate secret rotation by changing env var os.Setenv("ROTATING_SECRET", "rotated_secret_value") // Create new config (simulating reload) config2 := &worker.Config{ RedisPassword: "${ROTATING_SECRET}", } config2.ExpandSecrets() if config2.RedisPassword != "rotated_secret_value" { t.Errorf("Secret rotation failed: expected 'rotated_secret_value', got '%s'", config2.RedisPassword) } // Verify old config still has old value (no shared state) if config1.RedisPassword != "initial_secret_value" { t.Logf("Old config retained initial value as expected: %s", config1.RedisPassword) } } // TestSecretsPatternDetection validates specific secret pattern detection func TestSecretsPatternDetection(t *testing.T) { patterns := []struct { value string shouldMatch bool pattern string }{ // AWS patterns {"AKIAIOSFODNN7EXAMPLE", true, "AWS Access Key"}, {"ASIAIOSFODNN7EXAMPLE", true, "AWS Session Key"}, {"AKIA1234567890", true, "AWS Access Key short"}, // GitHub patterns {"ghp_xxxxxxxxxxxxxxxxxxxx", true, "GitHub Personal Token"}, {"gho_xxxxxxxxxxxxxxxxxxxx", true, "GitHub OAuth"}, {"github_pat_xxxxxxxxxx", true, "GitHub App Token"}, // GitLab patterns {"glpat-xxxxxxxxxxxxxxxx", true, "GitLab Token"}, // OpenAI/Stripe patterns {"sk-xxxxxxxxxxxxxxxxxxxxxxxx", true, "OpenAI/Stripe Secret"}, {"sk_test_xxxxxxxxxxxxxx", true, "Stripe Test Key"}, {"sk_live_xxxxxxxxxxxxxx", true, "Stripe Live Key"}, // Non-secrets that should NOT match {"not-a-secret", false, "Plain text"}, {"password123", false, "Simple password"}, {"${ENV_VAR}", false, "Env reference"}, {"AKIA", false, "Too short"}, // Too short to be a real key {"", false, "Empty string"}, } for _, p := range patterns { t.Run(p.pattern, func(t *testing.T) { // Check if the value looks like a secret looksLikeSecret := looksLikeSecretHelper(p.value) if p.shouldMatch && !looksLikeSecret { t.Errorf("Expected '%s' to be detected as secret but wasn't", p.value) } if !p.shouldMatch && looksLikeSecret { t.Logf("Note: '%s' was detected as secret (may be acceptable based on entropy)", p.value) } }) } } // looksLikeSecretHelper mirrors the internal detection logic for testing func looksLikeSecretHelper(value string) bool { if value == "" || strings.HasPrefix(value, "${") { return false } // Check for known secret patterns patterns := []string{ "AKIA", // AWS Access Key "ASIA", // AWS Session Key "ghp_", // GitHub personal "gho_", // GitHub OAuth "github_pat_", // GitHub app "glpat-", // GitLab "sk-", // OpenAI/Stripe } for _, pattern := range patterns { if strings.Contains(value, pattern) { return true } } // High entropy check (simplified) if len(value) >= 20 { return true } return false }