package scanner import ( "bufio" "context" "fmt" "net" "net/http" "os" "path/filepath" "strings" "sync" "sync/atomic" "time" ) // DefaultSubdomains is a built-in fallback list used when no wordlist file // is available. The file at projectRoot/subdomains.txt (one name per line, // # for comments) is preferred and can contain thousands of entries. var DefaultSubdomains = []string{ "api", "www", "admin", "mail", "webmail", "dev", "staging", "app", "dashboard", "portal", "blog", "docs", "support", "git", "cdn", "static", "assets", "demo", "vpn", "remote", "secure", "login", "auth", "gateway", "console", "monitor", "status", "jenkins", "wiki", "forum", "corp", "internal", "partner", "help", "mx", "ns1", "ns2", "smtp", "imap", "pop3", } // hasWildcardDNS checks whether the domain uses wildcard DNS by resolving // a random non-existent subdomain. If it resolves, wildcard is active. func hasWildcardDNS(ctx context.Context, domain string) bool { probes := []string{ fmt.Sprintf("zzz%d%d.%s", time.Now().UnixNano()%1000, 9999, domain), fmt.Sprintf("x%sx.%s", fmt.Sprintf("%x", time.Now().UnixNano()%65535), domain), } for _, p := range probes { addrs, err := net.DefaultResolver.LookupHost(ctx, p) if err == nil && len(addrs) > 0 { return true } } return false } // ProbeSubdomains checks common subdomain names and returns those // confirmed to serve real HTTPS content (valid TLS certificate). // // TLS certificate validation is the definitive signal — a wildcard DNS // entry resolves for any name but has no valid certificate for arbitrary // subdomains. Only real web services pass. Any real API in 2026 runs HTTPS. // // Loads subdomain names from projectRoot/subdomains.txt (one per line, # for comments). // Falls back to a built-in list of ~40 names if the file doesn't exist. // // Health check timeout 3s, concurrency max 10, total probe < 15s for 40 names, // scales linearly with wordlist size (e.g. ~2min for 500 names). // // If onProgress is non-nil, it is called periodically with (checked, total) counts // so the caller can report progress to the user (e.g. "checking 142/5000"). func ProbeSubdomains(ctx context.Context, projectRoot, domain string, onProgress func(checked, total int)) []string { var ( alive []string mu sync.Mutex wg sync.WaitGroup ) if wildcard := hasWildcardDNS(ctx, domain); wildcard { fmt.Printf("probe: wildcard DNS for %s — filtering by TLS cert\n", domain) } // Load subdomain names from file or fall back to built-in list names := loadSubdomainList(projectRoot) fmt.Printf("probe: checking %d subdomain names against %s\n", len(names), domain) total := len(names) sem := make(chan struct{}, 10) client := &http.Client{Timeout: 3 * time.Second} // Atomically count completed probes for progress reporting var completed int32 for _, name := range names { wg.Add(1) go func(s string) { defer wg.Done() select { case sem <- struct{}{}: case <-ctx.Done(): return } defer func() { <-sem }() // HTTPS GET — does DNS + TLS in one shot. // Go's http.Client validates cert + hostname by default. resp, err := client.Get("https://" + s + "." + domain) if err == nil { resp.Body.Close() mu.Lock() alive = append(alive, s+"."+domain) mu.Unlock() } // Report progress c := int(atomic.AddInt32(&completed, 1)) if onProgress != nil { onProgress(c, total) } }(name) } wg.Wait() return alive } // loadSubdomainList reads subdomain names from projectRoot/subdomains.txt. // Each line is one name; lines starting with # and empty lines are ignored. // Falls back to DefaultSubdomains if the file doesn't exist. func loadSubdomainList(projectRoot string) []string { path := filepath.Join(projectRoot, "subdomains.txt") f, err := os.Open(path) if err != nil { fmt.Printf("probe: no subdomains.txt found, using default list (%d names)\n", len(DefaultSubdomains)) return DefaultSubdomains } defer f.Close() seen := make(map[string]bool) var names []string scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } name := strings.ToLower(line) if !seen[name] { seen[name] = true names = append(names, name) } } if err := scanner.Err(); err != nil { fmt.Printf("probe: error reading subdomains.txt: %v, using default list\n", err) return DefaultSubdomains } if len(names) == 0 { fmt.Printf("probe: subdomains.txt is empty, using default list\n") return DefaultSubdomains } fmt.Printf("probe: loaded %d names from subdomains.txt\n", len(names)) return names }