Files
god-eye/internal/discovery/recursive.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

272 lines
6.5 KiB
Go

package discovery
import (
"context"
"fmt"
"sort"
"strings"
"sync"
"god-eye/internal/dns"
)
// RecursiveDiscovery performs recursive subdomain enumeration
type RecursiveDiscovery struct {
domain string
resolvers []string
timeout int
maxDepth int
concurrency int
// Results tracking
found map[string]bool
foundMu sync.RWMutex
// Pattern learning
patterns *PatternLearner
}
// RecursiveConfig contains configuration for recursive discovery
type RecursiveConfig struct {
Domain string
Resolvers []string
Timeout int
MaxDepth int // Maximum recursion depth (default: 3)
Concurrency int
}
// NewRecursiveDiscovery creates a new recursive discovery engine
func NewRecursiveDiscovery(cfg RecursiveConfig) *RecursiveDiscovery {
if cfg.MaxDepth == 0 {
cfg.MaxDepth = 3
}
if cfg.Concurrency == 0 {
cfg.Concurrency = 50
}
return &RecursiveDiscovery{
domain: cfg.Domain,
resolvers: cfg.Resolvers,
timeout: cfg.Timeout,
maxDepth: cfg.MaxDepth,
concurrency: cfg.Concurrency,
found: make(map[string]bool),
patterns: NewPatternLearner(),
}
}
// Discover performs recursive discovery starting from initial subdomains
func (rd *RecursiveDiscovery) Discover(ctx context.Context, initial []string) []string {
// Add initial subdomains
rd.foundMu.Lock()
for _, sub := range initial {
rd.found[sub] = true
rd.patterns.Learn(sub, rd.domain)
}
rd.foundMu.Unlock()
// Process each depth level
currentLevel := initial
for depth := 1; depth <= rd.maxDepth; depth++ {
select {
case <-ctx.Done():
break
default:
}
// Generate permutations for current level
candidates := rd.generateCandidates(currentLevel, depth)
if len(candidates) == 0 {
break
}
// Resolve candidates
newFound := rd.resolveParallel(ctx, candidates)
if len(newFound) == 0 {
break
}
// Learn patterns from new discoveries
rd.foundMu.Lock()
for _, sub := range newFound {
rd.patterns.Learn(sub, rd.domain)
}
rd.foundMu.Unlock()
currentLevel = newFound
}
// Return all found subdomains
rd.foundMu.RLock()
defer rd.foundMu.RUnlock()
result := make([]string, 0, len(rd.found))
for sub := range rd.found {
result = append(result, sub)
}
sort.Strings(result)
return result
}
// generateCandidates generates subdomain candidates based on patterns
func (rd *RecursiveDiscovery) generateCandidates(bases []string, depth int) []string {
seen := make(map[string]bool)
var candidates []string
// Common prefixes for recursion
commonPrefixes := []string{
"api", "v1", "v2", "v3", "internal", "staging", "dev", "test",
"prod", "admin", "app", "web", "cdn", "static", "assets",
"auth", "login", "portal", "dashboard", "backend", "frontend",
"data", "db", "cache", "redis", "elastic", "kafka", "queue",
"mail", "smtp", "imap", "mx", "ns", "dns",
"vpn", "proxy", "gateway", "lb", "loadbalancer",
"monitor", "metrics", "logs", "trace", "health",
"git", "svn", "repo", "ci", "cd", "jenkins", "gitlab",
"k8s", "kubernetes", "docker", "container", "pod",
}
// Add learned prefixes from patterns
learnedPrefixes := rd.patterns.GetLearnedPrefixes()
commonPrefixes = append(commonPrefixes, learnedPrefixes...)
// Common suffixes
commonSuffixes := []string{
"01", "02", "03", "1", "2", "3",
"a", "b", "c",
"east", "west", "eu", "us", "asia",
"primary", "secondary", "backup",
}
for _, base := range bases {
// Extract the subdomain part (remove domain suffix)
subPart := strings.TrimSuffix(base, "."+rd.domain)
if subPart == base {
continue // Not a subdomain of target
}
// Generate prefix variations: prefix.existing.domain.com
for _, prefix := range commonPrefixes {
candidate := fmt.Sprintf("%s.%s", prefix, base)
if !seen[candidate] && !rd.isFound(candidate) {
seen[candidate] = true
candidates = append(candidates, candidate)
}
}
// Generate suffix variations for multi-part subdomains
parts := strings.Split(subPart, ".")
if len(parts) >= 1 {
basePart := parts[0]
for _, suffix := range commonSuffixes {
// api.example.com -> api1.example.com, api-01.example.com
var newBase string
if len(parts) > 1 {
newBase = fmt.Sprintf("%s%s.%s.%s", basePart, suffix, strings.Join(parts[1:], "."), rd.domain)
} else {
newBase = fmt.Sprintf("%s%s.%s", basePart, suffix, rd.domain)
}
if !seen[newBase] && !rd.isFound(newBase) {
seen[newBase] = true
candidates = append(candidates, newBase)
}
// With dash: api-1.example.com
if len(parts) > 1 {
newBase = fmt.Sprintf("%s-%s.%s.%s", basePart, suffix, strings.Join(parts[1:], "."), rd.domain)
} else {
newBase = fmt.Sprintf("%s-%s.%s", basePart, suffix, rd.domain)
}
if !seen[newBase] && !rd.isFound(newBase) {
seen[newBase] = true
candidates = append(candidates, newBase)
}
}
}
}
// Limit candidates per depth to avoid explosion
maxCandidates := 5000 / depth
if len(candidates) > maxCandidates {
candidates = candidates[:maxCandidates]
}
return candidates
}
// resolveParallel resolves candidates in parallel
func (rd *RecursiveDiscovery) resolveParallel(ctx context.Context, candidates []string) []string {
var results []string
var resultsMu sync.Mutex
var wg sync.WaitGroup
sem := make(chan struct{}, rd.concurrency)
for _, candidate := range candidates {
select {
case <-ctx.Done():
break
default:
}
wg.Add(1)
go func(sub string) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
// Check context
select {
case <-ctx.Done():
return
default:
}
ips := dns.ResolveSubdomain(sub, rd.resolvers, rd.timeout)
if len(ips) > 0 {
rd.foundMu.Lock()
if !rd.found[sub] {
rd.found[sub] = true
resultsMu.Lock()
results = append(results, sub)
resultsMu.Unlock()
}
rd.foundMu.Unlock()
}
}(candidate)
}
wg.Wait()
return results
}
// isFound checks if subdomain was already found
func (rd *RecursiveDiscovery) isFound(sub string) bool {
rd.foundMu.RLock()
defer rd.foundMu.RUnlock()
return rd.found[sub]
}
// GetPatterns returns the learned patterns
func (rd *RecursiveDiscovery) GetPatterns() *PatternLearner {
return rd.patterns
}
// DiscoveryStats returns statistics about the discovery
type DiscoveryStats struct {
TotalFound int
ByDepth map[int]int
LearnedPatterns int
}
// GetStats returns discovery statistics
func (rd *RecursiveDiscovery) GetStats() DiscoveryStats {
rd.foundMu.RLock()
defer rd.foundMu.RUnlock()
return DiscoveryStats{
TotalFound: len(rd.found),
LearnedPatterns: len(rd.patterns.prefixes),
}
}