 131868bdca
			
		
	
	131868bdca
	
	
	
		
			
			Major security, observability, and configuration improvements:
## Security Hardening
- Implemented configurable CORS (no more wildcards)
- Added comprehensive auth middleware for admin endpoints
- Enhanced webhook HMAC validation
- Added input validation and rate limiting
- Security headers and CSP policies
## Configuration Management
- Made N8N webhook URL configurable (WHOOSH_N8N_BASE_URL)
- Replaced all hardcoded endpoints with environment variables
- Added feature flags for LLM vs heuristic composition
- Gitea fetch hardening with EAGER_FILTER and FULL_RESCAN options
## API Completeness
- Implemented GetCouncilComposition function
- Added GET /api/v1/councils/{id} endpoint
- Council artifacts API (POST/GET /api/v1/councils/{id}/artifacts)
- /admin/health/details endpoint with component status
- Database lookup for repository URLs (no hardcoded fallbacks)
## Observability & Performance
- Added OpenTelemetry distributed tracing with goal/pulse correlation
- Performance optimization database indexes
- Comprehensive health monitoring
- Enhanced logging and error handling
## Infrastructure
- Production-ready P2P discovery (replaces mock implementation)
- Removed unused Redis configuration
- Enhanced Docker Swarm integration
- Added migration files for performance indexes
## Code Quality
- Comprehensive input validation
- Graceful error handling and failsafe fallbacks
- Backwards compatibility maintained
- Following security best practices
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
		
	
		
			
				
	
	
		
			307 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			307 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package validation
 | |
| 
 | |
| import (
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"regexp"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/go-chi/render"
 | |
| 	"github.com/rs/zerolog/log"
 | |
| )
 | |
| 
 | |
| // Common validation patterns
 | |
| var (
 | |
| 	// AlphaNumeric allows letters, numbers, hyphens and underscores
 | |
| 	AlphaNumeric = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
 | |
| 	
 | |
| 	// ProjectName allows alphanumeric, spaces, hyphens, underscores (max 100 chars)
 | |
| 	ProjectName = regexp.MustCompile(`^[a-zA-Z0-9\s_-]{1,100}$`)
 | |
| 	
 | |
| 	// GitURL validates basic git URL structure
 | |
| 	GitURL = regexp.MustCompile(`^https?:\/\/[a-zA-Z0-9.-]+\/[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+(?:\.git)?$`)
 | |
| 	
 | |
| 	// TaskTitle allows reasonable task title characters (max 200 chars)
 | |
| 	TaskTitle = regexp.MustCompile(`^[a-zA-Z0-9\s.,!?()_-]{1,200}$`)
 | |
| 	
 | |
| 	// AgentID should be alphanumeric with hyphens (max 50 chars)
 | |
| 	AgentID = regexp.MustCompile(`^[a-zA-Z0-9-]{1,50}$`)
 | |
| 	
 | |
| 	// UUID pattern for council IDs, task IDs, etc.
 | |
| 	UUID = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
 | |
| )
 | |
| 
 | |
| // ValidationError represents a validation error
 | |
| type ValidationError struct {
 | |
| 	Field   string `json:"field"`
 | |
| 	Message string `json:"message"`
 | |
| }
 | |
| 
 | |
| // ValidationErrors is a slice of validation errors
 | |
| type ValidationErrors []ValidationError
 | |
| 
 | |
| func (v ValidationErrors) Error() string {
 | |
| 	if len(v) == 0 {
 | |
| 		return ""
 | |
| 	}
 | |
| 	if len(v) == 1 {
 | |
| 		return fmt.Sprintf("%s: %s", v[0].Field, v[0].Message)
 | |
| 	}
 | |
| 	return fmt.Sprintf("validation failed for %d fields", len(v))
 | |
| }
 | |
| 
 | |
| // Validator provides request validation utilities
 | |
| type Validator struct {
 | |
| 	maxBodySize int64
 | |
| }
 | |
| 
 | |
| // NewValidator creates a new validator with default settings
 | |
| func NewValidator() *Validator {
 | |
| 	return &Validator{
 | |
| 		maxBodySize: 1024 * 1024, // 1MB default
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // WithMaxBodySize sets the maximum request body size
 | |
| func (v *Validator) WithMaxBodySize(size int64) *Validator {
 | |
| 	v.maxBodySize = size
 | |
| 	return v
 | |
| }
 | |
| 
 | |
| // DecodeAndValidateJSON safely decodes JSON with size limits and validation
 | |
| func (v *Validator) DecodeAndValidateJSON(r *http.Request, dest interface{}) error {
 | |
| 	// Limit request body size to prevent DoS attacks
 | |
| 	r.Body = http.MaxBytesReader(nil, r.Body, v.maxBodySize)
 | |
| 	
 | |
| 	// Decode JSON
 | |
| 	if err := json.NewDecoder(r.Body).Decode(dest); err != nil {
 | |
| 		log.Warn().Err(err).Msg("JSON decode error")
 | |
| 		return fmt.Errorf("invalid JSON: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // ValidateProjectRequest validates project creation/update requests
 | |
| func ValidateProjectRequest(req map[string]interface{}) ValidationErrors {
 | |
| 	var errors ValidationErrors
 | |
| 	
 | |
| 	// Validate name
 | |
| 	name, ok := req["name"].(string)
 | |
| 	if !ok || name == "" {
 | |
| 		errors = append(errors, ValidationError{
 | |
| 			Field:   "name",
 | |
| 			Message: "name is required",
 | |
| 		})
 | |
| 	} else if !ProjectName.MatchString(name) {
 | |
| 		errors = append(errors, ValidationError{
 | |
| 			Field:   "name",
 | |
| 			Message: "name contains invalid characters or is too long (max 100 chars)",
 | |
| 		})
 | |
| 	}
 | |
| 	
 | |
| 	// Validate repo_url
 | |
| 	repoURL, ok := req["repo_url"].(string)
 | |
| 	if !ok || repoURL == "" {
 | |
| 		errors = append(errors, ValidationError{
 | |
| 			Field:   "repo_url",
 | |
| 			Message: "repo_url is required",
 | |
| 		})
 | |
| 	} else {
 | |
| 		if !GitURL.MatchString(repoURL) {
 | |
| 			errors = append(errors, ValidationError{
 | |
| 				Field:   "repo_url",
 | |
| 				Message: "invalid git repository URL format",
 | |
| 			})
 | |
| 		} else {
 | |
| 			// Additional URL validation
 | |
| 			if _, err := url.Parse(repoURL); err != nil {
 | |
| 				errors = append(errors, ValidationError{
 | |
| 					Field:   "repo_url",
 | |
| 					Message: "malformed URL",
 | |
| 				})
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	// Validate optional description
 | |
| 	if desc, exists := req["description"]; exists {
 | |
| 		if descStr, ok := desc.(string); ok && len(descStr) > 1000 {
 | |
| 			errors = append(errors, ValidationError{
 | |
| 				Field:   "description",
 | |
| 				Message: "description too long (max 1000 chars)",
 | |
| 			})
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	return errors
 | |
| }
 | |
| 
 | |
| // ValidateTaskRequest validates task creation/update requests
 | |
| func ValidateTaskRequest(req map[string]interface{}) ValidationErrors {
 | |
| 	var errors ValidationErrors
 | |
| 	
 | |
| 	// Validate title
 | |
| 	title, ok := req["title"].(string)
 | |
| 	if !ok || title == "" {
 | |
| 		errors = append(errors, ValidationError{
 | |
| 			Field:   "title",
 | |
| 			Message: "title is required",
 | |
| 		})
 | |
| 	} else if !TaskTitle.MatchString(title) {
 | |
| 		errors = append(errors, ValidationError{
 | |
| 			Field:   "title",
 | |
| 			Message: "title contains invalid characters or is too long (max 200 chars)",
 | |
| 		})
 | |
| 	}
 | |
| 	
 | |
| 	// Validate description
 | |
| 	description, ok := req["description"].(string)
 | |
| 	if !ok || description == "" {
 | |
| 		errors = append(errors, ValidationError{
 | |
| 			Field:   "description",
 | |
| 			Message: "description is required",
 | |
| 		})
 | |
| 	} else if len(description) > 5000 {
 | |
| 		errors = append(errors, ValidationError{
 | |
| 			Field:   "description",
 | |
| 			Message: "description too long (max 5000 chars)",
 | |
| 		})
 | |
| 	}
 | |
| 	
 | |
| 	// Validate priority (if provided)
 | |
| 	if priority, exists := req["priority"]; exists {
 | |
| 		if priorityStr, ok := priority.(string); ok {
 | |
| 			validPriorities := []string{"low", "medium", "high", "critical"}
 | |
| 			isValid := false
 | |
| 			for _, valid := range validPriorities {
 | |
| 				if strings.ToLower(priorityStr) == valid {
 | |
| 					isValid = true
 | |
| 					break
 | |
| 				}
 | |
| 			}
 | |
| 			if !isValid {
 | |
| 				errors = append(errors, ValidationError{
 | |
| 					Field:   "priority",
 | |
| 					Message: "priority must be one of: low, medium, high, critical",
 | |
| 				})
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	return errors
 | |
| }
 | |
| 
 | |
| // ValidateAgentRequest validates agent registration requests
 | |
| func ValidateAgentRequest(req map[string]interface{}) ValidationErrors {
 | |
| 	var errors ValidationErrors
 | |
| 	
 | |
| 	// Validate agent_id
 | |
| 	agentID, ok := req["agent_id"].(string)
 | |
| 	if !ok || agentID == "" {
 | |
| 		errors = append(errors, ValidationError{
 | |
| 			Field:   "agent_id",
 | |
| 			Message: "agent_id is required",
 | |
| 		})
 | |
| 	} else if !AgentID.MatchString(agentID) {
 | |
| 		errors = append(errors, ValidationError{
 | |
| 			Field:   "agent_id",
 | |
| 			Message: "agent_id contains invalid characters or is too long (max 50 chars)",
 | |
| 		})
 | |
| 	}
 | |
| 	
 | |
| 	// Validate capabilities (if provided)
 | |
| 	if capabilities, exists := req["capabilities"]; exists {
 | |
| 		if capArray, ok := capabilities.([]interface{}); ok {
 | |
| 			if len(capArray) > 50 {
 | |
| 				errors = append(errors, ValidationError{
 | |
| 					Field:   "capabilities",
 | |
| 					Message: "too many capabilities (max 50)",
 | |
| 				})
 | |
| 			}
 | |
| 			for i, cap := range capArray {
 | |
| 				if capStr, ok := cap.(string); !ok || len(capStr) > 100 {
 | |
| 					errors = append(errors, ValidationError{
 | |
| 						Field:   fmt.Sprintf("capabilities[%d]", i),
 | |
| 						Message: "capability must be string with max 100 chars",
 | |
| 					})
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	return errors
 | |
| }
 | |
| 
 | |
| // ValidatePathParameter validates URL path parameters
 | |
| func ValidatePathParameter(param, value, paramType string) error {
 | |
| 	if value == "" {
 | |
| 		return fmt.Errorf("%s is required", param)
 | |
| 	}
 | |
| 	
 | |
| 	switch paramType {
 | |
| 	case "uuid":
 | |
| 		if !UUID.MatchString(value) {
 | |
| 			return fmt.Errorf("invalid %s format (must be UUID)", param)
 | |
| 		}
 | |
| 	case "alphanumeric":
 | |
| 		if !AlphaNumeric.MatchString(value) {
 | |
| 			return fmt.Errorf("invalid %s format (alphanumeric only)", param)
 | |
| 		}
 | |
| 	case "agent_id":
 | |
| 		if !AgentID.MatchString(value) {
 | |
| 			return fmt.Errorf("invalid %s format", param)
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // SanitizeString removes potentially dangerous characters
 | |
| func SanitizeString(input string) string {
 | |
| 	// Remove null bytes
 | |
| 	input = strings.ReplaceAll(input, "\x00", "")
 | |
| 	
 | |
| 	// Trim whitespace
 | |
| 	input = strings.TrimSpace(input)
 | |
| 	
 | |
| 	return input
 | |
| }
 | |
| 
 | |
| // ValidateAndRespond validates data and responds with errors if validation fails
 | |
| func (v *Validator) ValidateAndRespond(w http.ResponseWriter, r *http.Request, errors ValidationErrors) bool {
 | |
| 	if len(errors) > 0 {
 | |
| 		log.Warn().Interface("errors", errors).Msg("Validation failed")
 | |
| 		render.Status(r, http.StatusBadRequest)
 | |
| 		render.JSON(w, r, map[string]interface{}{
 | |
| 			"error":  "validation failed",
 | |
| 			"errors": errors,
 | |
| 		})
 | |
| 		return false
 | |
| 	}
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| // SecurityHeaders adds security headers to the response
 | |
| func SecurityHeaders(next http.Handler) http.Handler {
 | |
| 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | |
| 		// Content Security Policy
 | |
| 		w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'")
 | |
| 		
 | |
| 		// X-Frame-Options
 | |
| 		w.Header().Set("X-Frame-Options", "DENY")
 | |
| 		
 | |
| 		// X-Content-Type-Options
 | |
| 		w.Header().Set("X-Content-Type-Options", "nosniff")
 | |
| 		
 | |
| 		// X-XSS-Protection
 | |
| 		w.Header().Set("X-XSS-Protection", "1; mode=block")
 | |
| 		
 | |
| 		// Referrer Policy
 | |
| 		w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
 | |
| 		
 | |
| 		next.ServeHTTP(w, r)
 | |
| 	})
 | |
| } |