Files
god-eye/internal/validator/validator.go
Vyntral 14c26dc726 feat: Add Multi-Agent AI Orchestration with 8 specialized agents
- Implement 8 specialized AI agents (XSS, SQLi, Auth, API, Crypto, Secrets, Headers, General)
- Add fast type-based routing for finding classification
- Include OWASP-aligned knowledge bases per agent
- Add agent handoff logic for cross-vulnerability detection
- Optimize timeouts and parallelism for local LLM
- Add new modules: cache, network, fingerprint, secrets, cloud, API, discovery
- Update documentation with multi-agent feature
2025-11-21 15:23:11 +01:00

279 lines
6.6 KiB
Go

package validator
import (
"fmt"
"net"
"regexp"
"strings"
)
// ValidationError represents an input validation error
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
// DomainValidator validates domain inputs
type DomainValidator struct {
MaxLength int
AllowWildcard bool
AllowSubdomains bool
}
// DefaultDomainValidator returns a validator with sensible defaults
func DefaultDomainValidator() *DomainValidator {
return &DomainValidator{
MaxLength: 253, // RFC 1035 max domain length
AllowWildcard: false,
AllowSubdomains: true,
}
}
// ValidateDomain validates a domain name for security and correctness
func (v *DomainValidator) ValidateDomain(domain string) error {
// Trim whitespace
domain = strings.TrimSpace(domain)
// Check empty
if domain == "" {
return &ValidationError{Field: "domain", Message: "domain cannot be empty"}
}
// Check length
if len(domain) > v.MaxLength {
return &ValidationError{
Field: "domain",
Message: fmt.Sprintf("domain exceeds maximum length of %d characters", v.MaxLength),
}
}
// Check for dangerous characters (path traversal, command injection)
dangerousChars := []string{
"..", "/", "\\", ";", "|", "&", "$", "`", "'", "\"",
"\n", "\r", "\t", "\x00", "%", "<", ">", "(", ")", "{", "}",
}
for _, char := range dangerousChars {
if strings.Contains(domain, char) {
return &ValidationError{
Field: "domain",
Message: fmt.Sprintf("domain contains invalid character: %q", char),
}
}
}
// Check for URL scheme (common mistake)
if strings.HasPrefix(strings.ToLower(domain), "http://") ||
strings.HasPrefix(strings.ToLower(domain), "https://") {
return &ValidationError{
Field: "domain",
Message: "domain should not include protocol (http:// or https://)",
}
}
// Validate domain format using regex
// Valid: example.com, sub.example.com, test-site.co.uk
// Invalid: -example.com, example-.com, example..com
domainRegex := regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`)
if !domainRegex.MatchString(domain) {
return &ValidationError{
Field: "domain",
Message: "invalid domain format",
}
}
// Check each label length (max 63 chars per RFC 1035)
labels := strings.Split(domain, ".")
for _, label := range labels {
if len(label) > 63 {
return &ValidationError{
Field: "domain",
Message: fmt.Sprintf("domain label %q exceeds 63 characters", label),
}
}
if len(label) == 0 {
return &ValidationError{
Field: "domain",
Message: "domain contains empty label",
}
}
}
// Check TLD is not just numbers
tld := labels[len(labels)-1]
if regexp.MustCompile(`^\d+$`).MatchString(tld) {
return &ValidationError{
Field: "domain",
Message: "TLD cannot be numeric only",
}
}
return nil
}
// ValidateIP validates an IP address
func ValidateIP(ip string) error {
ip = strings.TrimSpace(ip)
if ip == "" {
return &ValidationError{Field: "ip", Message: "IP cannot be empty"}
}
parsed := net.ParseIP(ip)
if parsed == nil {
return &ValidationError{Field: "ip", Message: "invalid IP address format"}
}
return nil
}
// ValidatePort validates a port number
func ValidatePort(port int) error {
if port < 1 || port > 65535 {
return &ValidationError{
Field: "port",
Message: fmt.Sprintf("port must be between 1 and 65535, got %d", port),
}
}
return nil
}
// ValidateWordlistPath validates a wordlist file path for security
func ValidateWordlistPath(path string) error {
path = strings.TrimSpace(path)
if path == "" {
return nil // Empty is allowed (uses default)
}
// Check for path traversal attempts
if strings.Contains(path, "..") {
return &ValidationError{
Field: "wordlist",
Message: "path traversal not allowed in wordlist path",
}
}
// Check for null bytes (truncation attack)
if strings.Contains(path, "\x00") {
return &ValidationError{
Field: "wordlist",
Message: "null bytes not allowed in path",
}
}
return nil
}
// ValidateOutputPath validates output file path for security
func ValidateOutputPath(path string) error {
path = strings.TrimSpace(path)
if path == "" {
return nil // Empty is allowed (no output file)
}
// Check for path traversal attempts
if strings.Contains(path, "..") {
return &ValidationError{
Field: "output",
Message: "path traversal not allowed in output path",
}
}
// Check for null bytes
if strings.Contains(path, "\x00") {
return &ValidationError{
Field: "output",
Message: "null bytes not allowed in path",
}
}
// Disallow writing to sensitive paths
sensitivePatterns := []string{
"/etc/", "/var/", "/usr/", "/bin/", "/sbin/",
"/root/", "/home/", "/proc/", "/sys/", "/dev/",
}
lowerPath := strings.ToLower(path)
for _, pattern := range sensitivePatterns {
if strings.HasPrefix(lowerPath, pattern) {
return &ValidationError{
Field: "output",
Message: fmt.Sprintf("cannot write to system path: %s", pattern),
}
}
}
return nil
}
// ValidateResolvers validates a comma-separated list of DNS resolvers
func ValidateResolvers(resolvers string) error {
resolvers = strings.TrimSpace(resolvers)
if resolvers == "" {
return nil // Empty uses defaults
}
parts := strings.Split(resolvers, ",")
for _, resolver := range parts {
resolver = strings.TrimSpace(resolver)
if resolver == "" {
continue
}
// Check if it's a valid IP
if err := ValidateIP(resolver); err != nil {
return &ValidationError{
Field: "resolvers",
Message: fmt.Sprintf("invalid resolver IP: %s", resolver),
}
}
}
return nil
}
// ValidateConcurrency validates concurrency settings
func ValidateConcurrency(concurrency int) error {
if concurrency < 1 {
return &ValidationError{
Field: "concurrency",
Message: "concurrency must be at least 1",
}
}
if concurrency > 10000 {
return &ValidationError{
Field: "concurrency",
Message: "concurrency exceeds maximum of 10000",
}
}
return nil
}
// ValidateTimeout validates timeout settings
func ValidateTimeout(timeout int) error {
if timeout < 1 {
return &ValidationError{
Field: "timeout",
Message: "timeout must be at least 1 second",
}
}
if timeout > 300 {
return &ValidationError{
Field: "timeout",
Message: "timeout exceeds maximum of 300 seconds",
}
}
return nil
}
// SanitizeDomain returns a cleaned domain string
func SanitizeDomain(domain string) string {
domain = strings.TrimSpace(domain)
domain = strings.ToLower(domain)
domain = strings.TrimPrefix(domain, "http://")
domain = strings.TrimPrefix(domain, "https://")
domain = strings.TrimSuffix(domain, "/")
return domain
}