fetch_ml/internal/validation/framework.go
Jeremie Fraeys e4d286f2e5
feat: add security monitoring and validation framework
- Implement anomaly detection monitor (brute force, path traversal, etc.)
- Add input validation framework with safety rules
- Add environment-based secrets manager with redaction
- Add security test suite for path traversal and injection
- Add CI security scanning workflow
2026-02-19 15:34:25 -05:00

183 lines
4.5 KiB
Go

// Package validation provides input validation utilities for security
package validation
import (
"fmt"
"path/filepath"
"regexp"
"strings"
)
// ValidationRule is a function that validates a string value
type ValidationRule func(value string) error
// Validator provides reusable validation rules
type Validator struct {
errors []string
}
// NewValidator creates a new validator
func NewValidator() *Validator {
return &Validator{errors: make([]string, 0)}
}
// Add adds a field to validate with the given rules
func (v *Validator) Add(name, value string, rules ...ValidationRule) {
for _, rule := range rules {
if err := rule(value); err != nil {
v.errors = append(v.errors, fmt.Sprintf("%s: %v", name, err))
}
}
}
// Valid returns nil if validation passed, otherwise returns an error
func (v *Validator) Valid() error {
if len(v.errors) > 0 {
return fmt.Errorf("validation failed: %s", strings.Join(v.errors, "; "))
}
return nil
}
// Common validation rules
// SafeName validates alphanumeric + underscore + hyphen only
var SafeName ValidationRule = func(v string) error {
if matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]+$`, v); !matched {
return fmt.Errorf("must contain only alphanumeric characters, underscores, and hyphens")
}
return nil
}
// MaxLength validates maximum string length
func MaxLength(max int) ValidationRule {
return func(v string) error {
if len(v) > max {
return fmt.Errorf("exceeds maximum length of %d", max)
}
return nil
}
}
// MinLength validates minimum string length
func MinLength(min int) ValidationRule {
return func(v string) error {
if len(v) < min {
return fmt.Errorf("must be at least %d characters", min)
}
return nil
}
}
// NoPathTraversal validates no path traversal sequences
var NoPathTraversal ValidationRule = func(v string) error {
if strings.Contains(v, "..") || strings.Contains(v, "../") || strings.Contains(v, "..\\") {
return fmt.Errorf("path traversal sequence detected")
}
return nil
}
// NoShellMetacharacters validates no shell metacharacters
var NoShellMetacharacters ValidationRule = func(v string) error {
dangerous := []string{";", "|", "&", "`", "$", "(", ")", "<", ">", "*", "?"}
for _, char := range dangerous {
if strings.Contains(v, char) {
return fmt.Errorf("shell metacharacter '%s' detected", char)
}
}
return nil
}
// NoNullBytes validates no null bytes
var NoNullBytes ValidationRule = func(v string) error {
if strings.Contains(v, "\x00") {
return fmt.Errorf("null byte detected")
}
return nil
}
// ValidPath validates a path is within a base directory
func ValidPath(basePath string) ValidationRule {
return func(v string) error {
cleaned := filepath.Clean(v)
absPath, err := filepath.Abs(cleaned)
if err != nil {
return fmt.Errorf("invalid path: %w", err)
}
absBase, err := filepath.Abs(basePath)
if err != nil {
return fmt.Errorf("invalid base path: %w", err)
}
if !strings.HasPrefix(absPath, absBase) {
return fmt.Errorf("path escapes base directory")
}
return nil
}
}
// MatchesPattern validates against a regex pattern
func MatchesPattern(pattern, description string) ValidationRule {
re := regexp.MustCompile(pattern)
return func(v string) error {
if !re.MatchString(v) {
return fmt.Errorf("must match pattern: %s", description)
}
return nil
}
}
// Whitelist validates against a whitelist of allowed values
func Whitelist(allowed ...string) ValidationRule {
return func(v string) error {
for _, a := range allowed {
if v == a {
return nil
}
}
return fmt.Errorf("value not in whitelist")
}
}
// Sanitize removes dangerous characters from input
func Sanitize(input string) string {
// Remove null bytes
input = strings.ReplaceAll(input, "\x00", "")
// Remove control characters
input = strings.ReplaceAll(input, "\r", "")
return input
}
// ValidateJobName validates a job name is safe
func ValidateJobName(jobName string) error {
validator := NewValidator()
validator.Add("job_name", jobName,
MinLength(1),
MaxLength(64),
SafeName,
NoPathTraversal,
NoShellMetacharacters,
)
return validator.Valid()
}
// ValidateExperimentID validates an experiment ID is safe
func ValidateExperimentID(id string) error {
validator := NewValidator()
validator.Add("experiment_id", id,
MinLength(1),
MaxLength(128),
SafeName,
NoPathTraversal,
)
return validator.Valid()
}
// ValidateCommand validates a command string is safe
func ValidateCommand(cmd string) error {
validator := NewValidator()
validator.Add("command", cmd,
MinLength(1),
MaxLength(1024),
NoShellMetacharacters,
)
return validator.Valid()
}