mirror of
https://github.com/Vyntral/god-eye.git
synced 2026-05-15 21:28:18 +02:00
3a4c230aa7
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.
250 lines
6.2 KiB
Go
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")
|
|
}
|
|
}
|