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

172 lines
4.0 KiB
Go

package sources
import (
"reflect"
"sort"
"testing"
"time"
)
func TestExtractSubdomains(t *testing.T) {
target := "example.com"
tests := []struct {
name string
text string
want []string
}{
{
name: "empty text",
text: "",
want: nil,
},
{
name: "no matches",
text: "some text with no domains",
want: nil,
},
{
name: "apex only",
text: "found example.com here",
want: []string{"example.com"},
},
{
name: "single subdomain",
text: "api.example.com was found",
want: []string{"api.example.com"},
},
{
name: "multiple subdomains",
text: "api.example.com and admin.example.com and dev.example.com",
want: []string{"admin.example.com", "api.example.com", "dev.example.com"},
},
{
name: "deduplication",
text: "api.example.com api.example.com api.example.com",
want: []string{"api.example.com"},
},
{
name: "uppercase normalized",
text: "API.EXAMPLE.COM and Api.Example.com",
want: []string{"api.example.com"},
},
{
name: "wildcard prefix stripped",
text: "*.example.com is a wildcard",
want: []string{"example.com"},
},
{
name: "different domain filtered",
text: "api.example.com and other.different.org and sub.example.com",
want: []string{"api.example.com", "sub.example.com"},
},
{
name: "partial match not allowed",
text: "evilexample.com should not match",
want: nil,
},
{
name: "json-wrapped",
text: `{"name":"api.example.com","type":"A"}`,
want: []string{"api.example.com"},
},
{
name: "mixed with urls",
text: `Visit https://api.example.com and https://docs.example.com/path`,
want: []string{"api.example.com", "docs.example.com"},
},
{
// Regex is greedy: only the longest leftmost match is returned,
// not every suffix. This is the v1 baseline behavior.
name: "deep subdomain longest match only",
text: "a.b.c.example.com",
want: []string{"a.b.c.example.com"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ExtractSubdomains(tt.text, target)
sort.Strings(got)
sort.Strings(tt.want)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ExtractSubdomains(%q)\n got: %v\n want: %v", tt.text, got, tt.want)
}
})
}
}
func TestGetClientForTimeout(t *testing.T) {
tests := []struct {
timeout time.Duration
want string // identify by Timeout field
}{
{5 * time.Second, "fast"},
{10 * time.Second, "fast"},
{15 * time.Second, "standard"},
{30 * time.Second, "standard"},
{60 * time.Second, "slow"},
{120 * time.Second, "slow"},
}
for _, tt := range tests {
c := GetClientForTimeout(tt.timeout)
if c == nil {
t.Fatalf("GetClientForTimeout(%v) returned nil", tt.timeout)
}
var gotClient string
switch c {
case FastClient:
gotClient = "fast"
case StandardClient:
gotClient = "standard"
case SlowClient:
gotClient = "slow"
default:
gotClient = "unknown"
}
if gotClient != tt.want {
t.Errorf("GetClientForTimeout(%v) = %s, want %s", tt.timeout, gotClient, tt.want)
}
}
}
func TestClientsInitialized(t *testing.T) {
if FastClient == nil {
t.Error("FastClient is nil")
}
if StandardClient == nil {
t.Error("StandardClient is nil")
}
if SlowClient == nil {
t.Error("SlowClient is nil")
}
if FastClient.Timeout != 10*time.Second {
t.Errorf("FastClient.Timeout = %v, want 10s", FastClient.Timeout)
}
if StandardClient.Timeout != 15*time.Second {
t.Errorf("StandardClient.Timeout = %v, want 15s", StandardClient.Timeout)
}
if SlowClient.Timeout != 120*time.Second {
t.Errorf("SlowClient.Timeout = %v, want 120s", SlowClient.Timeout)
}
}
func TestRegexCompiled(t *testing.T) {
if SubdomainRegex == nil {
t.Error("SubdomainRegex not compiled")
}
if EmailDomainRegex == nil {
t.Error("EmailDomainRegex not compiled")
}
if URLDomainRegex == nil {
t.Error("URLDomainRegex not compiled")
}
if JSONSubdomainRegex == nil {
t.Error("JSONSubdomainRegex not compiled")
}
if WildcardPrefixRegex == nil {
t.Error("WildcardPrefixRegex not compiled")
}
}