Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions internal/commands/create_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package commands

import (
"fmt"
"regexp"
"strings"

"github.com/charmbracelet/huh"
Expand All @@ -17,21 +18,30 @@ type createNamePrompt struct {
Kind string
}

// validNamePattern is an allow-list for scaffolded names: letters, numbers,
// hyphens and underscores only. This also rules out path separators and
// "..", since these names end up joined into filesystem paths.
var validNamePattern = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)

func validateCreateName(kind string) func(string) error {
return func(s string) error {
if s == "" {
return fmt.Errorf("%s name cannot be empty", kind)
}
if strings.Contains(s, " ") {
return fmt.Errorf("%s name cannot contain spaces", kind)
if !validNamePattern.MatchString(s) {
return fmt.Errorf("%s name must only contain letters, numbers, hyphens and underscores", kind)
}
return nil
}
}

func getNameFromArgsOrPrompt(args []string, p createNamePrompt) (string, error) {
if len(args) > 0 {
return args[0], nil
name := args[0]
if err := validateCreateName(p.Kind)(name); err != nil {
return "", err
}
return name, nil
}

var name string
Expand Down
41 changes: 41 additions & 0 deletions internal/commands/create_helpers_names_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package commands

import "testing"

func TestValidateCreateName(t *testing.T) {
validate := validateCreateName("resource")

valid := []string{"my-resource", "my_resource", "Resource123", "a"}
for _, name := range valid {
if err := validate(name); err != nil {
t.Errorf("expected %q to be valid, got error: %v", name, err)
}
}

invalid := []string{"", "has space", "../../etc/passwd", "../escape", "nested/path", "a/b", "."}
for _, name := range invalid {
if err := validate(name); err == nil {
t.Errorf("expected %q to be rejected, got no error", name)
}
}
}

func TestGetNameFromArgsOrPromptRejectsTraversalInArgs(t *testing.T) {
prompt := createNamePrompt{Title: "Name", Description: "desc", Kind: "resource"}

if _, err := getNameFromArgsOrPrompt([]string{"../../etc/passwd"}, prompt); err == nil {
t.Fatal("expected an error for a traversal name passed as a CLI argument, got nil")
}
}

func TestGetNameFromArgsOrPromptAcceptsValidArgs(t *testing.T) {
prompt := createNamePrompt{Title: "Name", Description: "desc", Kind: "resource"}

name, err := getNameFromArgsOrPrompt([]string{"my-resource"}, prompt)
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
if name != "my-resource" {
t.Fatalf("expected name %q, got %q", "my-resource", name)
}
}