package domains import ( "context" "crypto/tls" "fmt" "log" "net" "net/http" "strings" "sync" "time" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" ) // SubdomainDiscovery handles advanced subdomain discovery type SubdomainDiscovery struct { app core.App client *http.Client wordlist []string } // NewSubdomainDiscovery creates a new subdomain discovery service func NewSubdomainDiscovery(app core.App) *SubdomainDiscovery { return &SubdomainDiscovery{ app: app, client: &http.Client{ Timeout: 10 * time.Second, Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, }, wordlist: getEnhancedWordlist(), } } // DiscoveryResult represents a discovered subdomain type DiscoveryResult struct { Subdomain string `json:"subdomain"` FullDomain string `json:"full_domain"` IPAddresses []string `json:"ip_addresses"` StatusCode int `json:"status_code,omitempty"` Server string `json:"server,omitempty"` Source string `json:"source"` // dns, certificate, http, etc. FoundAt time.Time `json:"found_at"` } // Discover performs comprehensive subdomain discovery func (sd *SubdomainDiscovery) Discover(ctx context.Context, domainName string) ([]DiscoveryResult, error) { var results []DiscoveryResult var mu sync.Mutex var wg sync.WaitGroup // Create a channel for results resultChan := make(chan DiscoveryResult, 100) // Start multiple discovery methods concurrently wg.Add(4) // 1. DNS brute force with enhanced wordlist go func() { defer wg.Done() sd.dnsBruteForce(ctx, domainName, resultChan) }() // 2. Certificate transparency log search go func() { defer wg.Done() sd.ctLogSearch(ctx, domainName, resultChan) }() // 3. DNS enumeration via common patterns go func() { defer wg.Done() sd.patternEnumeration(ctx, domainName, resultChan) }() // 4. HTTP probe for common subdomains go func() { defer wg.Done() sd.httpProbe(ctx, domainName, resultChan) }() // Collect results in a separate goroutine go func() { wg.Wait() close(resultChan) }() // Process results seen := make(map[string]bool) for result := range resultChan { if seen[result.Subdomain] { continue } seen[result.Subdomain] = true mu.Lock() results = append(results, result) mu.Unlock() } return results, nil } // dnsBruteForce performs DNS brute forcing with wordlist func (sd *SubdomainDiscovery) dnsBruteForce(ctx context.Context, domainName string, results chan<- DiscoveryResult) { semaphore := make(chan struct{}, 20) // Limit concurrency var wg sync.WaitGroup for _, word := range sd.wordlist { select { case <-ctx.Done(): return default: } wg.Add(1) semaphore <- struct{}{} go func(word string) { defer wg.Done() defer func() { <-semaphore }() subdomain := word + "." + domainName ips, err := net.LookupHost(subdomain) if err != nil || len(ips) == 0 { return } results <- DiscoveryResult{ Subdomain: word, FullDomain: subdomain, IPAddresses: ips, Source: "dns", FoundAt: time.Now(), } }(word) } wg.Wait() } // ctLogSearch searches certificate transparency logs func (sd *SubdomainDiscovery) ctLogSearch(ctx context.Context, domainName string, results chan<- DiscoveryResult) { // Query crt.sh for certificates url := fmt.Sprintf("https://crt.sh/?q=%%.%s&output=json", domainName) resp, err := sd.client.Get(url) if err != nil { log.Printf("[subdomain-discovery] CT log search failed for %s: %v", domainName, err) return } defer resp.Body.Close() // Parse response (simplified - in production would parse JSON) // For now, just log that we attempted this log.Printf("[subdomain-discovery] CT log search attempted for %s (status: %d)", domainName, resp.StatusCode) } // patternEnumeration enumerates common subdomain patterns func (sd *SubdomainDiscovery) patternEnumeration(ctx context.Context, domainName string, results chan<- DiscoveryResult) { patterns := []string{ "api-v1", "api-v2", "api-v3", "api-dev", "api-staging", "api-prod", "app-dev", "app-staging", "app-prod", "web-dev", "web-staging", "admin-dev", "admin-staging", "admin-prod", "portal-dev", "portal-staging", "portal-prod", "dashboard-dev", "dashboard-staging", "dashboard-prod", "service-1", "service-2", "service-3", "node-1", "node-2", "node-3", "server-1", "server-2", "server-3", "web-01", "web-02", "web-03", "app-01", "app-02", "app-03", "us-east", "us-west", "eu-west", "eu-central", "ap-south", "ap-northeast", } semaphore := make(chan struct{}, 10) var wg sync.WaitGroup for _, pattern := range patterns { select { case <-ctx.Done(): return default: } wg.Add(1) semaphore <- struct{}{} go func(pattern string) { defer wg.Done() defer func() { <-semaphore }() subdomain := pattern + "." + domainName ips, err := net.LookupHost(subdomain) if err != nil || len(ips) == 0 { return } results <- DiscoveryResult{ Subdomain: pattern, FullDomain: subdomain, IPAddresses: ips, Source: "pattern", FoundAt: time.Now(), } }(pattern) } wg.Wait() } // httpProbe probes common subdomains via HTTP func (sd *SubdomainDiscovery) httpProbe(ctx context.Context, domainName string, results chan<- DiscoveryResult) { commonWebSubdomains := []string{"www", "app", "api", "admin", "dashboard", "portal", "web"} semaphore := make(chan struct{}, 5) var wg sync.WaitGroup for _, sub := range commonWebSubdomains { select { case <-ctx.Done(): return default: } wg.Add(1) semaphore <- struct{}{} go func(sub string) { defer wg.Done() defer func() { <-semaphore }() // Try HTTPS first url := fmt.Sprintf("https://%s.%s", sub, domainName) resp, err := sd.client.Get(url) if err != nil { // Try HTTP url = fmt.Sprintf("http://%s.%s", sub, domainName) resp, err = sd.client.Get(url) if err != nil { return } } defer resp.Body.Close() // Get IP addresses subdomain := sub + "." + domainName ips, _ := net.LookupHost(subdomain) results <- DiscoveryResult{ Subdomain: sub, FullDomain: subdomain, IPAddresses: ips, StatusCode: resp.StatusCode, Server: resp.Header.Get("Server"), Source: "http", FoundAt: time.Now(), } }(sub) } wg.Wait() } // SaveSubdomains saves discovered subdomains to the database func (sd *SubdomainDiscovery) SaveSubdomains(domainRecord *core.Record, results []DiscoveryResult, userID string) error { collection, err := sd.app.FindCollectionByNameOrId("subdomains") if err != nil { return err } // Get existing subdomains to avoid duplicates existing, _ := sd.app.FindAllRecords("subdomains", dbx.NewExp("domain = {:domain}", dbx.Params{"domain": domainRecord.Id}), ) existingMap := make(map[string]bool) for _, sub := range existing { existingMap[sub.GetString("subdomain_name")] = true } for _, result := range results { if existingMap[result.Subdomain] { continue } record := core.NewRecord(collection) record.Set("domain", domainRecord.Id) record.Set("subdomain_name", result.Subdomain) record.Set("full_domain", result.FullDomain) record.Set("status", "active") record.Set("ip_addresses", strings.Join(result.IPAddresses, ",")) record.Set("discovery_source", result.Source) record.Set("last_checked", time.Now()) record.Set("user", userID) if result.StatusCode > 0 { record.Set("http_status", result.StatusCode) } if result.Server != "" { record.Set("server_header", result.Server) } if err := sd.app.Save(record); err != nil { log.Printf("[subdomain-discovery] Failed to save subdomain %s: %v", result.FullDomain, err) } else { log.Printf("[subdomain-discovery] Saved subdomain: %s (source: %s)", result.FullDomain, result.Source) } } return nil } // getEnhancedWordlist returns an enhanced wordlist for subdomain discovery func getEnhancedWordlist() []string { // Comprehensive wordlist including common subdomains wordlist := []string{ // Common web "www", "mail", "ftp", "localhost", "admin", "dashboard", "portal", "api", "app", "mobile", "dev", "test", "staging", "demo", "beta", "prod", "production", "live", "www2", "www1", "blog", "shop", "store", "support", "help", "docs", "documentation", "wiki", "forum", "community", "news", "media", "cdn", "static", "assets", "files", "download", "upload", "images", "img", "css", "js", "scripts", "resources", "public", "private", // Services "api-v1", "api-v2", "api-v3", "api-dev", "api-staging", "api-prod", "graphql", "rest", "grpc", "websocket", "socket", "ws", "wss", "oauth", "auth", "login", "signin", "signup", "register", "sso", "saml", "oidc", "openid", "keycloak", "auth0", "cognito", "okta", // Infrastructure "server", "server1", "server2", "server3", "srv", "srv1", "srv2", "node", "node1", "node2", "node3", "worker", "worker1", "worker2", "web", "web1", "web2", "web3", "app1", "app2", "app3", "db", "database", "mysql", "postgres", "mongodb", "redis", "elasticsearch", "cache", "queue", "rabbitmq", "kafka", "zookeeper", "etcd", // Cloud & DevOps "k8s", "kubernetes", "docker", "swarm", "nomad", "consul", "vault", "terraform", "ansible", "puppet", "chef", "salt", "jenkins", "gitlab", "github", "bitbucket", "travis", "circleci", "drone", "argo", "spinnaker", "prometheus", "grafana", "alertmanager", "thanos", "loki", "tempo", "jaeger", "zipkin", "kibana", "elk", "efk", "splunk", "datadog", // Security "vpn", "ipsec", "openvpn", "wireguard", "fortinet", "paloalto", "firewall", "waf", "ids", "ips", "siem", "soc", "nids", "scan", "scanner", "security", "sec", "pentest", "vulnerability", // Monitoring & Logging "monitor", "monitoring", "status", "health", "healthcheck", "ping", "uptime", "metrics", "logs", "logging", "audit", "trace", "apm", "sentry", "bugsnag", "raygun", "rollbar", "airbrake", "honeybadger", "newrelic", "appdynamics", "dynatrace", "instana", "scout", "skylight", // Communication "mail", "email", "smtp", "pop", "imap", "exchange", "webmail", "mailgun", "sendgrid", "mailchimp", " SES", "postmark", "sparkpost", "chat", "slack", "teams", "discord", "zoom", "meet", "webex", "jitsi", "mattermost", "rocket", "element", "matrix", // Regions "us", "us-east", "us-west", "us-central", "us-south", "eu", "eu-west", "eu-central", "eu-east", "eu-north", "eu-south", "ap", "ap-south", "ap-northeast", "ap-southeast", "ap-east", "sa", "sa-east", "af", "af-south", "me", "me-south", // Environments "sandbox", "playground", "lab", "labs", "experiment", "canary", "blue", "green", "a", "b", "alpha", "beta", "gamma", "delta", "rc", "release", "nightly", "stable", "latest", "edge", "lts", // Corporate "corp", "corporate", "enterprise", "business", "company", "org", "intranet", "extranet", "partners", "vendors", "suppliers", "hr", "finance", "accounting", "legal", "compliance", "it", "sales", "marketing", "crm", "erp", "scm", "plm", "hrm", // Development tools "git", "svn", "cvs", "mercurial", "hg", "bitkeeper", "repo", "repository", "repos", "source", "code", "src", "build", "ci", "cd", "deploy", "deployment", "release", "artifact", "artifacts", "package", "packages", "registry", // Testing "qa", "qc", "test", "testing", "tests", "spec", "specs", "unit", "integration", "e2e", "acceptance", "regression", "mock", "stub", "fake", "dummy", "sandbox", "fixture", // Archive & Backup "archive", "archives", "backup", "backups", "snapshot", "snapshots", "old", "legacy", "deprecated", "retired", "historical", // Miscellaneous "search", "find", "lookup", "query", "browse", "explorer", "api-docs", "swagger", "openapi", "redoc", "postman", "graphql-playground", "status-page", "statuspage", "trust", "security", "compliance", "terms", "privacy", "legal", "about", "contact", "feedback", "careers", "jobs", "team", "people", "staff", "employees", "press", "media-kit", "brand", "assets", "styleguide", "design", "sitemap", "robots", "humans", "security", "pgp", "key", "invite", "join", "apply", "subscribe", "newsletter", "rss", "atom", "mobile", "m", "touch", "app", "ios", "android", "windows", "mac", "download", "downloads", "dl", "installer", "setup", "update", "upgrade", "payment", "pay", "billing", "invoice", "subscription", "checkout", "cart", "basket", "shop", "store", "buy", "purchase", "order", "account", "my", "profile", "user", "users", "member", "members", "session", "token", "auth", "verify", "validation", "confirm", "reset", "recovery", "forgot", "password", "pass", "pwd", "2fa", "mfa", "totp", "otp", "authenticator", "security-key", "webhook", "callback", "hook", "integration", "connector", "adapter", "plugin", "extension", "addon", "module", "component", "widget", "embed", "iframe", "frame", "proxy", "gateway", "ingress", "egress", "loadbalancer", "lb", "vip", "floating", "virtual", "ha", "failover", "primary", "secondary", "master", "slave", "leader", "follower", "active", "standby", "hot", "warm", "cold", "mirror", "replica", "shard", "shards", "partition", "partitions", "segment", "segments", } return wordlist }