// Package permutation generates candidate subdomains by mutating every // previously-discovered subdomain with a set of common prefixes/suffixes // and resolving them. This is the "alterx" pattern: you already found // api.example.com and dev.example.com, now try api-dev, dev-api, // api-staging, api.dev.example.com, etc. // // Pattern learning is intentionally lightweight in Fase 1: the core v1 // discovery.PatternLearner already extracts per-label frequencies. We // feed those back in via candidate generation. package permutation import ( "strings" "sync" "time" "god-eye/internal/config" godns "god-eye/internal/dns" "god-eye/internal/eventbus" "god-eye/internal/module" "god-eye/internal/store" ) const ModuleName = "discovery.permutation" type permModule struct{} func Register() { module.Register(&permModule{}) } func (*permModule) Name() string { return ModuleName } func (*permModule) Phase() module.Phase { return module.PhaseResolution } func (*permModule) Consumes() []eventbus.EventType { return []eventbus.EventType{eventbus.EventDNSResolved} } func (*permModule) Produces() []eventbus.EventType { return []eventbus.EventType{eventbus.EventSubdomainDiscovered} } func (*permModule) DefaultEnabled() bool { return false } // opt-in (burns a lot of DNS) // commonAffixes are applied to each label of discovered hostnames to // generate permutation candidates. Curated for bug-bounty signal. var commonAffixes = []string{ "dev", "stg", "staging", "prod", "qa", "test", "uat", "sandbox", "preview", "internal", "int", "private", "admin", "api", "api2", "apiv2", "gw", "new", "old", "legacy", "v2", "v3", "next", "beta", "alpha", "canary", "eu", "us", "apac", "emea", } var separators = []string{"-", "_", "."} func (*permModule) Run(mctx module.Context) error { if !mctx.Config.Bool("permutation", false) { return nil } target := mctx.Target timeout := mctx.Config.Int("timeout", 5) resolvers := parseResolvers(mctx.Config.String("resolvers", "")) conc := mctx.Config.Int("concurrency", 300) if conc <= 0 { conc = 300 } // Gather seeds from the store (all already-resolved hosts). seeds := mctx.Store.All(mctx.Ctx) if len(seeds) == 0 { return nil } candidates := make(map[string]struct{}) for _, h := range seeds { for _, c := range generateCandidates(h.Subdomain, target) { candidates[c] = struct{}{} } } // Resolve candidates in parallel. Only emit ones that resolve. sem := make(chan struct{}, conc) var wg sync.WaitGroup for cand := range candidates { if mctx.Ctx.Err() != nil { break } cand := cand wg.Add(1) sem <- struct{}{} go func() { defer wg.Done() defer func() { <-sem }() ips := godns.ResolveSubdomain(cand, resolvers, timeout) if len(ips) == 0 { return } _ = mctx.Store.Upsert(mctx.Ctx, cand, func(h *store.Host) { store.AddIPs(h, ips) store.AddDiscoveryMethod(h, "permutation") }) mctx.Bus.Publish(mctx.Ctx, eventbus.SubdomainDiscovered{ EventMeta: eventbus.EventMeta{At: time.Now(), Source: ModuleName, Target: cand}, Subdomain: cand, Method: "permutation", }) }() } wg.Wait() return nil } // generateCandidates produces permuted hostnames from a seed within the // target domain. The output is guaranteed to end in "."+target or ==target. func generateCandidates(seed, target string) []string { if !strings.HasSuffix(seed, target) { return nil } prefix := strings.TrimSuffix(seed, "."+target) if prefix == target || prefix == "" { return nil } labels := strings.Split(prefix, ".") if len(labels) == 0 { return nil } out := make(map[string]struct{}) // Leaf-label mutations: (affix)(sep)(label) and (label)(sep)(affix). leaf := labels[len(labels)-1] rest := strings.Join(labels[:len(labels)-1], ".") for _, aff := range commonAffixes { for _, sep := range separators { combos := []string{ aff + sep + leaf, leaf + sep + aff, } for _, c := range combos { parts := []string{c} if rest != "" { parts = []string{rest, c} } cand := strings.Join(parts, ".") + "." + target out[cand] = struct{}{} } } } // Prepend-an-affix mutation: aff. for _, aff := range commonAffixes { cand := aff + "." + prefix + "." + target out[cand] = struct{}{} } res := make([]string, 0, len(out)) for c := range out { res = append(res, c) } return res } func parseResolvers(s string) []string { s = strings.TrimSpace(s) if s == "" { return config.DefaultResolvers } var out []string for _, r := range strings.Split(s, ",") { r = strings.TrimSpace(r) if r == "" { continue } if !strings.Contains(r, ":") { r = r + ":53" } out = append(out, r) } if len(out) == 0 { return config.DefaultResolvers } return out }