claw-code/.guardrails/mcp-server/internal/team/validation.go

129 lines
3.7 KiB
Go

package team
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
)
// ValidationError represents a validation error
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error for %s: %s", e.Field, e.Message)
}
// SecurityError represents a security violation
type SecurityError struct {
Message string
}
func (e *SecurityError) Error() string {
return fmt.Sprintf("security error: %s", e.Message)
}
// Validation constants
const (
MaxProjectNameLength = 64
MaxRoleNameLength = 128
MaxPersonNameLength = 256
)
var (
projectNameRegex = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
roleNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\s\-_/\&\(\)\.]+$`)
personNameRegex = regexp.MustCompile(`^[a-zA-Z\s\-'']+$`)
)
// ValidateProjectName validates a project name
func ValidateProjectName(name string) error {
if name == "" {
return &ValidationError{Field: "project_name", Message: "project name is required"}
}
if len(name) > MaxProjectNameLength {
return &ValidationError{Field: "project_name", Message: fmt.Sprintf("project name must be %d characters or less", MaxProjectNameLength)}
}
if !projectNameRegex.MatchString(name) {
return &ValidationError{Field: "project_name", Message: "project name must contain only letters, numbers, hyphens, and underscores"}
}
return nil
}
// ValidateProjectPath validates project path to prevent path traversal
func ValidateProjectPath(projectName, baseDir string) (string, error) {
if baseDir == "" {
baseDir = ".teams"
}
if err := ValidateProjectName(projectName); err != nil {
return "", err
}
dangerousPatterns := []string{"..", "/", "\\", "\x00"}
for _, pattern := range dangerousPatterns {
if strings.Contains(projectName, pattern) {
return "", &SecurityError{Message: fmt.Sprintf("path traversal detected: project_name contains forbidden pattern '%s'", pattern)}
}
}
configPath := filepath.Join(baseDir, fmt.Sprintf("%s.json", projectName))
absPath, err := filepath.Abs(configPath)
if err != nil {
return "", fmt.Errorf("failed to resolve path: %w", err)
}
baseAbs, err := filepath.Abs(baseDir)
if err != nil {
return "", fmt.Errorf("failed to resolve base directory: %w", err)
}
if !strings.HasPrefix(absPath, baseAbs+string(filepath.Separator)) && absPath != baseAbs {
return "", &SecurityError{Message: "path traversal detected: resolved path is outside the base directory"}
}
return absPath, nil
}
// ValidateRoleName validates a role name
func ValidateRoleName(name string) error {
if name == "" {
return &ValidationError{Field: "role_name", Message: "role name is required"}
}
if len(name) > MaxRoleNameLength {
return &ValidationError{Field: "role_name", Message: fmt.Sprintf("role name must be %d characters or less", MaxRoleNameLength)}
}
return nil
}
// ValidatePersonName validates a person name
func ValidatePersonName(name string) error {
if name == "" {
return &ValidationError{Field: "person", Message: "person name is required"}
}
if len(name) > MaxPersonNameLength {
return &ValidationError{Field: "person", Message: fmt.Sprintf("person name must be %d characters or less", MaxPersonNameLength)}
}
return nil
}
// ValidatePhase validates a phase name
func ValidatePhase(phase string) error {
validPhases := []string{
"Phase 1: Strategy, Governance & Planning",
"Phase 2: Platform & Foundation",
"Phase 3: The Build Squads",
"Phase 4: Validation & Hardening",
"Phase 5: Delivery & Sustainment",
}
for _, valid := range validPhases {
if phase == valid {
return nil
}
}
return &ValidationError{Field: "phase", Message: fmt.Sprintf("invalid phase: %s", phase)}
}
// EnsureDir ensures a directory exists
func EnsureDir(path string) error {
return os.MkdirAll(path, 0755)
}