// Package recursive is a Fase 0.6 adapter for the v1 recursive discovery // engine (pattern learning from found subdomains). // // Unlike event-driven modules, recursive runs as a deferred second-pass: // after PhaseDiscovery completes it collects every host seen so far from // the store, runs the v1 engine, and emits SubdomainDiscovered for any // new hosts. It self-schedules in PhaseResolution to sit between discovery // and HTTP probing. package recursive import ( "time" "god-eye/internal/discovery" "god-eye/internal/eventbus" "god-eye/internal/module" "god-eye/internal/store" "strings" ) const ModuleName = "discovery.recursive" type recModule struct{} func Register() { module.Register(&recModule{}) } func (*recModule) Name() string { return ModuleName } func (*recModule) Phase() module.Phase { return module.PhaseResolution } // runs after discovery func (*recModule) Consumes() []eventbus.EventType { return []eventbus.EventType{eventbus.EventSubdomainDiscovered} } func (*recModule) Produces() []eventbus.EventType { return []eventbus.EventType{eventbus.EventSubdomainDiscovered} } // Recursive is opt-in by default — profiles enable it for bugbounty/pentest. func (*recModule) DefaultEnabled() bool { return false } func (*recModule) Run(mctx module.Context) error { if !mctx.Config.Bool("recursive", false) { return nil } target := mctx.Target depth := mctx.Config.Int("recursive.depth", 3) if depth < 1 { depth = 1 } else if depth > 5 { depth = 5 } timeout := mctx.Config.Int("timeout", 5) conc := mctx.Config.Int("concurrency", 500) if conc <= 0 { conc = 500 } resolvers := parseResolvers(mctx.Config.String("resolvers", "")) // Gather initial seeds from what's been discovered so far. hosts := mctx.Store.All(mctx.Ctx) seeds := make([]string, 0, len(hosts)) for _, h := range hosts { seeds = append(seeds, h.Subdomain) } if len(seeds) == 0 { return nil } rd := discovery.NewRecursiveDiscovery(discovery.RecursiveConfig{ Domain: target, Resolvers: resolvers, Timeout: timeout, MaxDepth: depth, Concurrency: conc, }) found := rd.Discover(mctx.Ctx, seeds) // Emit SubdomainDiscovered for any new hosts. seen := make(map[string]struct{}, len(seeds)) for _, s := range seeds { seen[s] = struct{}{} } for _, s := range found { if _, dup := seen[s]; dup { continue } seen[s] = struct{}{} _ = mctx.Store.Upsert(mctx.Ctx, s, func(h *store.Host) { store.AddDiscoveryMethod(h, "recursive") }) mctx.Bus.Publish(mctx.Ctx, eventbus.SubdomainDiscovered{ EventMeta: eventbus.EventMeta{At: time.Now(), Source: ModuleName, Target: s}, Subdomain: s, Method: "recursive", }) } return nil } func parseResolvers(s string) []string { s = strings.TrimSpace(s) if s == "" { return []string{"8.8.8.8:53", "1.1.1.1:53"} } 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) } return out }