Files
Vyntral 3a4c230aa7 feat: v2.0 full rewrite — event-driven pipeline, AI + Nuclei + proxy
Complete architectural overhaul. Replaces the v0.1 monolithic scanner
with an event-driven pipeline of auto-registered modules.

Foundation (internal/):
- eventbus: typed pub/sub, 20 event types, race-safe, drop counter
- module: registry with phase-based selection
- store: thread-safe host store with per-host locks + deep-copy reads
- pipeline: coordinator with phase barriers + panic recovery
- config: 5 scan profiles + 3 AI tiers + YAML loader + auto-discovery

Modules (26 auto-registered across 6 phases):
- Discovery: passive (26 sources), bruteforce, recursive, AXFR, GitHub
  dorks, CT streaming, permutation, reverse DNS, vhost, ASN, supply
  chain (npm + PyPI)
- Enrichment: HTTP probe + tech fingerprint + TLS appliance ID, ports
- Analysis: security checks, takeover (110+ sigs), cloud, JavaScript,
  GraphQL, JWT, headers (OWASP), HTTP smuggling, AI cascade, Nuclei
- Reporting: TXT/JSON/CSV writer + AI scan brief

AI layer (internal/ai/ + internal/modules/ai/):
- Three profiles: lean (16 GB), balanced (32 GB MoE), heavy (64 GB)
- Six event-driven handlers: CVE, JS file, HTTP response, secret
  filter, multi-agent vuln enrichment, anomaly + executive report
- Content-hash cache dedups Ollama calls across hosts
- Auto-pull of missing models via /api/pull with streaming progress
- End-of-scan AI SCAN BRIEF in terminal with top chains + next actions

Nuclei compat layer (internal/nucleitpl/):
- Executes ~13k community templates (HTTP subset)
- Auto-download of nuclei-templates ZIP to ~/.god-eye/nuclei-templates
- Scope filter rejects off-host templates (eliminates OSINT FPs)

Operations:
- Interactive wizard (internal/wizard/) — zero-flag launch
- LivePrinter (internal/tui/) — colorized event stream
- Diff engine + scheduler (internal/diff, internal/scheduler) for
  continuous ASM monitoring with webhook alerts
- Proxy support (internal/proxyconf/): http / https / socks5 / socks5h
  + basic auth

Fixes #1 — native SOCKS5 / Tor compatibility via --proxy flag.

185 unit tests across 15 packages, all race-detector clean.
2026-04-18 16:48:41 +02:00

250 lines
6.2 KiB
Go

package validator
import (
"strings"
"testing"
)
func TestValidateDomain(t *testing.T) {
v := DefaultDomainValidator()
tests := []struct {
name string
input string
wantErr bool
}{
{"simple domain", "example.com", false},
{"subdomain", "api.example.com", false},
{"deep subdomain", "a.b.c.example.com", false},
{"hyphen in middle", "my-site.example.com", false},
{"co.uk tld", "example.co.uk", false},
{"uppercase", "EXAMPLE.COM", false},
{"empty", "", true},
{"whitespace only", " ", true},
{"with scheme http", "http://example.com", true},
{"with scheme https", "https://example.com", true},
{"path traversal", "example.com/../etc", true},
{"shell metachar ;", "example.com;whoami", true},
{"shell metachar |", "example.com|whoami", true},
{"shell metachar &", "example.com&ls", true},
{"backtick", "example.com`id`", true},
{"dollar", "example.com$USER", true},
{"newline", "example.com\nmalicious", true},
{"null byte", "example.com\x00.evil", true},
{"leading hyphen label", "-example.com", true},
{"trailing hyphen label", "example-.com", true},
{"double dot", "example..com", true},
{"label too long", strings.Repeat("a", 64) + ".com", true},
{"domain too long", strings.Repeat("a.", 130) + "com", true},
{"numeric tld", "example.123", true},
{"single label", "localhost", true},
{"angle brackets", "<script>.com", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := v.ValidateDomain(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateDomain(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
}
})
}
}
func TestSanitizeDomain(t *testing.T) {
tests := []struct {
input string
want string
}{
{"example.com", "example.com"},
{" example.com ", "example.com"},
{"EXAMPLE.COM", "example.com"},
{"http://example.com", "example.com"},
{"https://example.com", "example.com"},
{"https://example.com/", "example.com"},
{"HTTPS://Example.com/", "example.com"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := SanitizeDomain(tt.input)
if got != tt.want {
t.Errorf("SanitizeDomain(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
func TestValidateIP(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{"ipv4", "8.8.8.8", false},
{"ipv4 with whitespace", " 1.1.1.1 ", false},
{"ipv6", "::1", false},
{"ipv6 full", "2001:db8::1", false},
{"empty", "", true},
{"invalid format", "not-an-ip", true},
{"out of range", "999.999.999.999", true},
{"with port (invalid for ParseIP)", "8.8.8.8:53", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateIP(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateIP(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
}
})
}
}
func TestValidatePort(t *testing.T) {
tests := []struct {
port int
wantErr bool
}{
{1, false},
{80, false},
{443, false},
{65535, false},
{0, true},
{-1, true},
{65536, true},
{100000, true},
}
for _, tt := range tests {
err := ValidatePort(tt.port)
if (err != nil) != tt.wantErr {
t.Errorf("ValidatePort(%d) error = %v, wantErr %v", tt.port, err, tt.wantErr)
}
}
}
func TestValidateWordlistPath(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{"empty allowed", "", false},
{"relative path", "wordlists/subdomains.txt", false},
{"absolute path", "/tmp/wordlist.txt", false},
{"path traversal", "../../../etc/passwd", true},
{"path traversal mid", "safe/../../../etc/passwd", true},
{"null byte", "wordlist.txt\x00.evil", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateWordlistPath(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateWordlistPath(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
}
})
}
}
func TestValidateOutputPath(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{"empty allowed", "", false},
{"relative output", "results.json", false},
{"tmp output", "/tmp/results.json", false},
{"path traversal", "../../../etc/passwd", true},
{"null byte", "output.txt\x00", true},
{"system path /etc/", "/etc/evil", true},
{"system path /var/", "/var/www/shell.php", true},
{"system path /usr/", "/usr/local/bin/backdoor", true},
{"system path /root/", "/root/.ssh/id_rsa", true},
{"system path /proc/", "/proc/self/mem", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateOutputPath(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateOutputPath(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
}
})
}
}
func TestValidateResolvers(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{"empty allowed", "", false},
{"single resolver", "8.8.8.8", false},
{"multiple resolvers", "8.8.8.8,1.1.1.1", false},
{"multiple with spaces", "8.8.8.8, 1.1.1.1 , 9.9.9.9", false},
{"invalid entry", "8.8.8.8,not-an-ip", true},
{"all invalid", "foo,bar", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateResolvers(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateResolvers(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
}
})
}
}
func TestValidateConcurrency(t *testing.T) {
tests := []struct {
n int
wantErr bool
}{
{1, false},
{1000, false},
{10000, false},
{0, true},
{-1, true},
{10001, true},
}
for _, tt := range tests {
err := ValidateConcurrency(tt.n)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateConcurrency(%d) err=%v wantErr=%v", tt.n, err, tt.wantErr)
}
}
}
func TestValidateTimeout(t *testing.T) {
tests := []struct {
n int
wantErr bool
}{
{1, false},
{5, false},
{300, false},
{0, true},
{-1, true},
{301, true},
}
for _, tt := range tests {
err := ValidateTimeout(tt.n)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateTimeout(%d) err=%v wantErr=%v", tt.n, err, tt.wantErr)
}
}
}
func TestValidationError_Error(t *testing.T) {
e := &ValidationError{Field: "domain", Message: "bad"}
if got := e.Error(); got != "domain: bad" {
t.Errorf("got %q, want %q", got, "domain: bad")
}
}