diff --git a/cmd/env.go b/cmd/env.go index 5859a9f6..4e207441 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -8,7 +8,6 @@ import ( "os" "os/signal" "path/filepath" - "regexp" "strings" "syscall" @@ -37,36 +36,17 @@ Use the subcommands to set, get, list, and delete environment variables and secr } var ( - hasTTY = tui.HasTTY - looksLikeSecret = regexp.MustCompile(`(?i)KEY|SECRET|TOKEN|PASSWORD|sk_`) - isAgentuityEnv = regexp.MustCompile(`(?i)AGENTUITY_`) + hasTTY = tui.HasTTY ) -func descriptionLookingLikeASecret(description string) bool { - if description == "" { - return false - } - val := strings.ToLower(description) - if strings.Contains(val, "secret") { - return true - } - if strings.Contains(val, "password") { - return true - } - if strings.Contains(val, "key") { - return true - } - return false -} - func loadEnvFile(le []env.EnvLineComment, forceSecret bool) (map[string]string, map[string]string) { envs := make(map[string]string) secrets := make(map[string]string) for _, ev := range le { - if isAgentuityEnv.MatchString(ev.Key) { + if envutil.IsAgentuityEnv.MatchString(ev.Key) { continue } - if looksLikeSecret.MatchString(ev.Key) || forceSecret || descriptionLookingLikeASecret(ev.Comment) { + if envutil.LooksLikeSecret.MatchString(ev.Key) || forceSecret || envutil.DescriptionLookingLikeASecret(ev.Comment) { secrets[ev.Key] = ev.Val } else { envs[ev.Key] = ev.Val @@ -79,7 +59,7 @@ func loadOSEnv() map[string]string { osenv := make(map[string]string) for _, line := range os.Environ() { parts := strings.SplitN(line, "=", 2) - if len(parts) == 2 && !isAgentuityEnv.MatchString(parts[0]) { + if len(parts) == 2 && !envutil.IsAgentuityEnv.MatchString(parts[0]) { osenv[parts[0]] = parts[1] } } @@ -152,7 +132,7 @@ Examples: le, _ := env.ParseEnvFile(envfile) var added bool for _, ev := range le { - if !isAgentuityEnv.MatchString(ev.Key) { + if !envutil.IsAgentuityEnv.MatchString(ev.Key) { localenv[ev.Key] = ev.Val added = true } @@ -164,14 +144,14 @@ Examples: if len(args) == 0 && hasEnvFile && len(localenv) > 0 && !hasSetFromFile && !noConfirm { var options []tui.Option for k := range localenv { - if !isAgentuityEnv.MatchString(k) { + if !envutil.IsAgentuityEnv.MatchString(k) { options = append(options, tui.Option{ID: k, Text: k, Selected: true}) } } results := tui.MultiSelect(logger, "Set environment variables from .env", "", options) for _, result := range results { val := localenv[result] - if looksLikeSecret.MatchString(result) || forceSecret { + if envutil.LooksLikeSecret.MatchString(result) || forceSecret { secrets[result] = val } else { envs[result] = val @@ -206,7 +186,7 @@ Examples: askMore = false } } - isSecret = looksLikeSecret.MatchString(key) || forceSecret + isSecret = envutil.LooksLikeSecret.MatchString(key) || forceSecret if key != "" && value == "" && !noConfirm { if len(envs) == 0 && len(secrets) == 0 { fi, _ := os.Stdin.Stat() diff --git a/internal/envutil/envutil.go b/internal/envutil/envutil.go index 9d3cc0f9..ca54a127 100644 --- a/internal/envutil/envutil.go +++ b/internal/envutil/envutil.go @@ -27,7 +27,7 @@ var redDiff = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#990 var LooksLikeSecret = looksLikeSecret var IsAgentuityEnv = isAgentuityEnv -var looksLikeSecret = regexp.MustCompile(`(?i)KEY|SECRET|TOKEN|PASSWORD|sk_`) +var looksLikeSecret = regexp.MustCompile(`(?i)(^|_|-)(APIKEY|API_KEY|PRIVATE_KEY|KEY|SECRET|TOKEN|CREDENTIAL|CREDENTIALS|PASSWORD|sk_[a-zA-Z0-9_-]*|BEARER|AUTH|JWT|WEBHOOK)($|_|-)`) var isAgentuityEnv = regexp.MustCompile(`(?i)AGENTUITY_`) // ProcessEnvFiles handles .env and template env processing @@ -181,10 +181,17 @@ func HandleMissingProjectEnvs(ctx context.Context, logger logger.Logger, le []en } if force { for key, val := range keyvalue { - if projectData.Env == nil { - projectData.Env = make(map[string]string) + if looksLikeSecret.MatchString(key) { + if projectData.Secrets == nil { + projectData.Secrets = make(map[string]string) + } + projectData.Secrets[key] = cstr.Mask(val) + } else { + if projectData.Env == nil { + projectData.Env = make(map[string]string) + } + projectData.Env[key] = val } - projectData.Env[key] = val } _, err := theproject.SetProjectEnv(ctx, logger, apiUrl, token, projectData.Env, projectData.Secrets) if err != nil { @@ -266,17 +273,7 @@ func DescriptionLookingLikeASecret(description string) bool { if description == "" { return false } - val := strings.ToLower(description) - if strings.Contains(val, "secret") { - return true - } - if strings.Contains(val, "password") { - return true - } - if strings.Contains(val, "key") { - return true - } - return false + return looksLikeSecret.MatchString(description) } // LoadOSEnv loads the OS environment variables diff --git a/internal/envutil/envutil_test.go b/internal/envutil/envutil_test.go new file mode 100644 index 00000000..ab3c8bb6 --- /dev/null +++ b/internal/envutil/envutil_test.go @@ -0,0 +1,78 @@ +package envutil + +import ( + "testing" +) + +func TestLooksLikeSecret(t *testing.T) { + tests := []struct { + input string + matches bool + }{ + {"API_KEY", true}, + {"SECRET_TOKEN", true}, + {"MY_PASSWORD", true}, + {"CREDENTIALS", true}, + {"sk_test_123", true}, + {"ACCESS_TOKEN", true}, + {"DATABASE_URL", false}, + {"USERNAME", false}, + {"EMAIL", false}, + {"AGENTUITY_SECRET", true}, // should match because of SECRET + {"SOME_RANDOM_VAR", false}, + {"PRIVATE_KEY", true}, + {"MY_APIKEY", true}, + {"MY_API_KEY", true}, + {"MY_API-KEY", true}, + {"MONKEY", false}, + + {"api_key", true}, + {"secret_token", true}, + {"my_password", true}, + {"credentials", true}, + {"sk_test_123", true}, + {"access_token", true}, + {"database_url", false}, + {"username", false}, + {"email", false}, + {"agentuity_secret", true}, + {"some_random_var", false}, + } + + for _, tt := range tests { + if LooksLikeSecret.MatchString(tt.input) != tt.matches { + t.Errorf("LooksLikeSecret.MatchString(%q) = %v, want %v", tt.input, LooksLikeSecret.MatchString(tt.input), tt.matches) + } + } +} + +func TestIsAgentuityEnv(t *testing.T) { + tests := []struct { + input string + matches bool + }{ + {"AGENTUITY_API_KEY", true}, + {"AGENTUITY_SECRET", true}, + {"AGENTUITY_TOKEN", true}, + {"AGENTUITY_SOMETHING", true}, + {"SOME_AGENTUITY_VAR", true}, + {"API_KEY", false}, + {"SECRET", false}, + {"DATABASE_URL", false}, + + {"agentuity_api_key", true}, + {"agentuity_secret", true}, + {"agentuity_token", true}, + {"agentuity_something", true}, + {"some_agentuity_var", true}, + {"api_key", false}, + {"secret", false}, + {"database_url", false}, + } + + for _, tt := range tests { + if IsAgentuityEnv.MatchString(tt.input) != tt.matches { + t.Errorf("IsAgentuityEnv.MatchString(%q) = %v, want %v", tt.input, IsAgentuityEnv.MatchString(tt.input), tt.matches) + } + } +}