fetch_ml/tests/unit/security/secrets_test.go
Jeremie Fraeys b33c6c4878
test(security): Add PHI denylist tests to secrets validation
Add comprehensive PHI detection tests:
- patient_id rejection
- ssn rejection
- medical_record_number rejection
- diagnosis_code rejection
- Mixed secrets with PHI rejection
- Normal secrets acceptance (HF_TOKEN, WANDB_API_KEY, etc.)

Ensures AllowedSecrets PHI denylist validation works correctly
across all PHI pattern variations.

Part of: PHI denylist validation from security plan
2026-02-23 19:44:33 -05:00

125 lines
3.2 KiB
Go

package security
import (
"strings"
"testing"
"github.com/jfraeys/fetch_ml/internal/worker"
)
func TestExpandSecrets_FromEnv(t *testing.T) {
// Set environment variables for testing
t.Setenv("TEST_REDIS_PASS", "secret_redis_password")
t.Setenv("TEST_ACCESS_KEY", "AKIAIOSFODNN7EXAMPLE")
t.Setenv("TEST_SECRET_KEY", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY")
cfg := &worker.Config{
RedisPassword: "${TEST_REDIS_PASS}",
SnapshotStore: worker.SnapshotStoreConfig{
AccessKey: "${TEST_ACCESS_KEY}",
SecretKey: "${TEST_SECRET_KEY}",
},
}
// Apply security defaults (needed before expandSecrets)
cfg.Sandbox.ApplySecurityDefaults()
// Use the exported method directly
err := cfg.ExpandSecrets()
if err != nil {
t.Fatalf("ExpandSecrets failed: %v", err)
}
// Verify secrets were expanded
if cfg.RedisPassword != "secret_redis_password" {
t.Errorf("RedisPassword not expanded: got %q, want %q", cfg.RedisPassword, "secret_redis_password")
}
if cfg.SnapshotStore.AccessKey != "AKIAIOSFODNN7EXAMPLE" {
t.Errorf("AccessKey not expanded: got %q", cfg.SnapshotStore.AccessKey)
}
if cfg.SnapshotStore.SecretKey != "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" {
t.Errorf("SecretKey not expanded: got %q", cfg.SnapshotStore.SecretKey)
}
}
func TestValidateNoPlaintextSecrets_DetectsPlaintext(t *testing.T) {
// Test that plaintext secrets are detected
tests := []struct {
name string
value string
wantErr bool
}{
{
name: "AWS-like access key",
value: "AKIAIOSFODNN7EXAMPLE12345",
wantErr: true,
},
{
name: "GitHub token",
value: "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
wantErr: true,
},
{
name: "high entropy secret",
value: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0",
wantErr: true,
},
{
name: "short value (not a secret)",
value: "password",
wantErr: false,
},
{
name: "low entropy value",
value: "aaaaaaaaaaaaaaaaaaaaaaa",
wantErr: false, // Low entropy, not a secret
},
{
name: "empty value",
value: "",
wantErr: false,
},
{
name: "env reference syntax",
value: "${ENV_VAR_NAME}",
wantErr: false, // Using env reference is correct
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Skip env references as they don't trigger the check
if strings.HasPrefix(tt.value, "${") {
return
}
result := worker.LooksLikeSecret(tt.value)
if result != tt.wantErr {
t.Errorf("LooksLikeSecret(%q) = %v, want %v", tt.value, result, tt.wantErr)
}
})
}
}
func TestCalculateEntropy(t *testing.T) {
tests := []struct {
input string
expected float64 // approximate
}{
{"aaaaaaaa", 0.0}, // Low entropy
{"abcdefgh", 3.0}, // Medium entropy
{"a1b2c3d4e5f6", 3.5}, // Higher entropy
{"wJalrXUtnFEMI/K7MDENG", 4.5}, // Very high entropy (secret-like)
{"", 0.0}, // Empty
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
entropy := worker.CalculateEntropy(tt.input)
// Allow some tolerance for floating point
if entropy < tt.expected-0.5 || entropy > tt.expected+0.5 {
t.Errorf("CalculateEntropy(%q) = %.2f, want approximately %.2f", tt.input, entropy, tt.expected)
}
})
}
}