@@ -85,33 +85,142 @@ func main() {
8585 os .Exit (run ())
8686}
8787
88- func run () int {
88+ // cliFlags holds all parsed command-line flag values.
89+ type cliFlags struct {
90+ wordlistFile string
91+ concurrency int
92+ timeoutMs int
93+ dnsServer string
94+ verbose bool
95+ showVersion bool
96+ showProgress bool
97+ testMode bool
98+ testHitRate int
99+ outputFile string
100+ attempts int
101+ retries int
102+ force bool
103+ }
104+
105+ func parseFlags () cliFlags {
106+ var f cliFlags
89107 flag .Bool ("tui" , false , "Launch the interactive terminal UI (all other flags are ignored)" )
90- wordlistFile := flag .String ( "w" , "" , "Path to the wordlist file" )
91- concurrency := flag .Int ( "t" , 100 , "Number of concurrent workers" )
92- timeoutMs := flag .Int ( "timeout" , 1000 , "DNS lookup timeout in milliseconds" )
93- dnsServer := flag .String ( "dns-server" , DefaultDNSServer , "DNS server to use (format: ip:port)" )
94- verbose := flag .Bool ( "v" , false , "Enable verbose output" )
95- showVersion := flag .Bool ( "version" , false , "Show version information" )
96- showProgress := flag .Bool ( "progress" , true , "Show progress during scanning" )
97- testMode := flag .Bool ( "simulate" , false , "Run in simulation mode without actual DNS queries (for testing)" )
98- testHitRate := flag .Int ( "hit-rate" , 15 , "In simulation mode, percentage of subdomains that will 'resolve' (1-100)" )
99- outputFile := flag .String ( "o" , "" , "Write results to file (in addition to stdout)" )
100- attempts := flag .Int ( "attempts" , 0 , "Total DNS resolution attempts per subdomain (1 = no retry)" )
101- retries := flag .Int ( "retries" , 0 , "Deprecated: use -attempts instead" )
102- force := flag .Bool ( "force" , false , "Continue scanning even if wildcard DNS is detected" )
108+ flag .StringVar ( & f . wordlistFile , "w" , "" , "Path to the wordlist file" )
109+ flag .IntVar ( & f . concurrency , "t" , 100 , "Number of concurrent workers" )
110+ flag .IntVar ( & f . timeoutMs , "timeout" , 1000 , "DNS lookup timeout in milliseconds" )
111+ flag .StringVar ( & f . dnsServer , "dns-server" , DefaultDNSServer , "DNS server to use (format: ip:port)" )
112+ flag .BoolVar ( & f . verbose , "v" , false , "Enable verbose output" )
113+ flag .BoolVar ( & f . showVersion , "version" , false , "Show version information" )
114+ flag .BoolVar ( & f . showProgress , "progress" , true , "Show progress during scanning" )
115+ flag .BoolVar ( & f . testMode , "simulate" , false , "Run in simulation mode without actual DNS queries (for testing)" )
116+ flag .IntVar ( & f . testHitRate , "hit-rate" , 15 , "In simulation mode, percentage of subdomains that will 'resolve' (1-100)" )
117+ flag .StringVar ( & f . outputFile , "o" , "" , "Write results to file (in addition to stdout)" )
118+ flag .IntVar ( & f . attempts , "attempts" , 0 , "Total DNS resolution attempts per subdomain (1 = no retry)" )
119+ flag .IntVar ( & f . retries , "retries" , 0 , "Deprecated: use -attempts instead" )
120+ flag .BoolVar ( & f . force , "force" , false , "Continue scanning even if wildcard DNS is detected" )
103121 flag .Parse ()
122+ return f
123+ }
104124
105- maxAttempts , err := resolveAttempts (* attempts , * retries )
125+ func validateFlags (f cliFlags , out * output.Writer , maxAttempts int ) (string , bool ) {
126+ if f .wordlistFile == "" || flag .NArg () == 0 {
127+ fmt .Fprintln (os .Stderr , "Usage: subenum -w <wordlist_file> [options] <domain>" )
128+ flag .PrintDefaults ()
129+ return "" , false
130+ }
131+ if f .concurrency <= 0 {
132+ out .Error ("Concurrency level (-t) must be greater than 0" )
133+ return "" , false
134+ }
135+ if f .timeoutMs <= 0 {
136+ out .Error ("Timeout (-timeout) must be greater than 0" )
137+ return "" , false
138+ }
139+ if f .testHitRate < 1 || f .testHitRate > 100 {
140+ out .Error ("Hit rate (-hit-rate) must be between 1 and 100" )
141+ return "" , false
142+ }
143+ if maxAttempts < 1 {
144+ out .Error ("Attempts (-attempts) must be at least 1" )
145+ return "" , false
146+ }
147+ if ! f .testMode {
148+ if err := validateDNSServer (f .dnsServer ); err != nil {
149+ out .Error ("DNS server %s: %v" , f .dnsServer , err )
150+ return "" , false
151+ }
152+ }
153+ domain := flag .Arg (0 )
154+ if err := validateDomain (domain ); err != nil {
155+ out .Error ("%v" , err )
156+ return "" , false
157+ }
158+ return domain , true
159+ }
160+
161+ func openOutputFile (path string , testMode bool , out * output.Writer ) (* output.Writer , * bufio.Writer , * os.File , bool ) {
162+ if path == "" {
163+ return out , nil , nil , true
164+ }
165+ f , err := os .Create (path )
166+ if err != nil {
167+ out .Error ("creating output file: %v" , err )
168+ return out , nil , nil , false
169+ }
170+ w := bufio .NewWriter (f )
171+ return output .New (w , testMode ), w , f , true
172+ }
106173
107- out := output .New (nil , * testMode )
174+ func logVerboseStart (f cliFlags , domain string , maxAttempts int , out * output.Writer ) {
175+ out .Info ("Starting %s v%s" , ProgramName , Version )
176+ if f .testMode {
177+ out .Info ("Mode: SIMULATION (no actual DNS queries)" )
178+ out .Info ("Simulated hit rate: %d%%" , f .testHitRate )
179+ } else {
180+ out .Info ("Mode: LIVE DNS RESOLUTION" )
181+ }
182+ out .Info ("Target domain: %s" , domain )
183+ out .Info ("Wordlist: %s" , f .wordlistFile )
184+ out .Info ("Concurrency: %d workers" , f .concurrency )
185+ out .Info ("Timeout: %d ms" , f .timeoutMs )
186+ out .Info ("Attempts: %d" , maxAttempts )
187+ if ! f .testMode {
188+ out .Info ("DNS Server: %s" , f .dnsServer )
189+ }
190+ if f .outputFile != "" {
191+ out .Info ("Output file: %s" , f .outputFile )
192+ }
193+ out .Info ("---" )
194+ }
195+
196+ func logVerboseDone (ev scan.Event , f cliFlags , outWriter * bufio.Writer , out * output.Writer ) {
197+ out .Info ("\n Scan completed for %s" , flag .Arg (0 ))
198+ out .Info ("Processed %d subdomain prefixes" , ev .Processed )
199+ if f .testMode {
200+ out .Info ("Found %d simulated subdomains" , ev .Found )
201+ } else {
202+ out .Info ("Found %d subdomains" , ev .Found )
203+ }
204+ if outWriter != nil {
205+ out .Info ("Results written to: %s" , f .outputFile )
206+ }
207+ if f .testMode {
208+ out .Info ("\n NOTE: Results were simulated and no actual DNS queries were performed." )
209+ out .Info ("This mode is intended for educational and testing purposes only." )
210+ }
211+ }
108212
213+ func run () int {
214+ f := parseFlags ()
215+
216+ maxAttempts , err := resolveAttempts (f .attempts , f .retries )
217+ out := output .New (nil , f .testMode )
109218 if err != nil {
110219 out .Error ("%v" , err )
111220 return 1
112221 }
113222
114- if * testMode {
223+ if f . testMode {
115224 out .Info ("" )
116225 out .Info ("╔════════════════════════════════════════════════════════════════════╗" )
117226 out .Info ("║ SIMULATION MODE ACTIVE - NO ACTUAL DNS QUERIES WILL BE PERFORMED ║" )
@@ -120,105 +229,46 @@ func run() int {
120229 out .Info ("" )
121230 }
122231
123- if * showVersion {
232+ if f . showVersion {
124233 fmt .Fprintf (os .Stderr , "%s v%s\n " , ProgramName , Version )
125- if * testMode {
234+ if f . testMode {
126235 fmt .Fprintln (os .Stderr , "Running in SIMULATION mode" )
127236 }
128237 return 0
129238 }
130239
131- if * wordlistFile == "" || flag .NArg () == 0 {
132- fmt .Fprintln (os .Stderr , "Usage: subenum -w <wordlist_file> [options] <domain>" )
133- flag .PrintDefaults ()
134- return 1
135- }
136-
137- if * concurrency <= 0 {
138- out .Error ("Concurrency level (-t) must be greater than 0" )
139- return 1
140- }
141-
142- if * timeoutMs <= 0 {
143- out .Error ("Timeout (-timeout) must be greater than 0" )
144- return 1
145- }
146-
147- if * testHitRate < 1 || * testHitRate > 100 {
148- out .Error ("Hit rate (-hit-rate) must be between 1 and 100" )
149- return 1
150- }
151-
152- if maxAttempts < 1 {
153- out .Error ("Attempts (-attempts) must be at least 1" )
240+ domain , ok := validateFlags (f , out , maxAttempts )
241+ if ! ok {
154242 return 1
155243 }
156244
157- if ! * testMode {
158- if err := validateDNSServer (* dnsServer ); err != nil {
159- out .Error ("DNS server %s: %v" , * dnsServer , err )
160- return 1
161- }
162- }
163-
164- domain := flag .Arg (0 )
165- if err := validateDomain (domain ); err != nil {
166- out .Error ("%v" , err )
245+ out , outWriter , outFile , ok := openOutputFile (f .outputFile , f .testMode , out )
246+ if ! ok {
167247 return 1
168248 }
169-
170- timeout := time .Duration (* timeoutMs ) * time .Millisecond
171-
172- var outWriter * bufio.Writer
173- if * outputFile != "" {
174- f , err := os .Create (* outputFile )
175- if err != nil {
176- out .Error ("creating output file: %v" , err )
177- return 1
178- }
179- outWriter = bufio .NewWriter (f )
180- out = output .New (outWriter , * testMode )
249+ if outFile != nil {
181250 defer func () {
182251 if flushErr := outWriter .Flush (); flushErr != nil {
183252 out .Error ("flushing output: %v" , flushErr )
184253 }
185- if closeErr := f .Close (); closeErr != nil {
254+ if closeErr := outFile .Close (); closeErr != nil {
186255 out .Error ("closing output file: %v" , closeErr )
187256 }
188257 }()
189258 }
190259
191- if * verbose {
192- out .Info ("Starting %s v%s" , ProgramName , Version )
193- if * testMode {
194- out .Info ("Mode: SIMULATION (no actual DNS queries)" )
195- out .Info ("Simulated hit rate: %d%%" , * testHitRate )
196- } else {
197- out .Info ("Mode: LIVE DNS RESOLUTION" )
198- }
199- out .Info ("Target domain: %s" , domain )
200- out .Info ("Wordlist: %s" , * wordlistFile )
201- out .Info ("Concurrency: %d workers" , * concurrency )
202- out .Info ("Timeout: %d ms" , * timeoutMs )
203- out .Info ("Attempts: %d" , maxAttempts )
204- if ! * testMode {
205- out .Info ("DNS Server: %s" , * dnsServer )
206- }
207- if * outputFile != "" {
208- out .Info ("Output file: %s" , * outputFile )
209- }
210- out .Info ("---" )
260+ if f .verbose {
261+ logVerboseStart (f , domain , maxAttempts , out )
211262 }
212263
213- entries , duplicates , err := wordlist .LoadWordlist (* wordlistFile )
264+ entries , duplicates , err := wordlist .LoadWordlist (f . wordlistFile )
214265 if err != nil {
215266 out .Error ("reading wordlist file: %v" , err )
216267 return 1
217268 }
218269
219270 totalWords := int64 (len (entries ))
220-
221- if * verbose {
271+ if f .verbose {
222272 out .Info ("Total wordlist entries: %d" , totalWords )
223273 if duplicates > 0 {
224274 out .Info ("Removed %d duplicate wordlist entries" , duplicates )
@@ -243,14 +293,14 @@ func run() int {
243293 cfg := scan.Config {
244294 Domain : domain ,
245295 Entries : entries ,
246- Concurrency : * concurrency ,
247- Timeout : timeout ,
248- DNSServer : * dnsServer ,
249- Simulate : * testMode ,
250- HitRate : * testHitRate ,
296+ Concurrency : f . concurrency ,
297+ Timeout : time . Duration ( f . timeoutMs ) * time . Millisecond ,
298+ DNSServer : f . dnsServer ,
299+ Simulate : f . testMode ,
300+ HitRate : f . testHitRate ,
251301 Attempts : maxAttempts ,
252- Force : * force ,
253- Verbose : * verbose ,
302+ Force : f . force ,
303+ Verbose : f . verbose ,
254304 }
255305
256306 events := make (chan scan.Event , 64 )
@@ -262,7 +312,7 @@ func run() int {
262312 case scan .EventResult :
263313 out .Result (ev .Domain )
264314 case scan .EventProgress :
265- if * showProgress && totalWords > 0 {
315+ if f . showProgress && totalWords > 0 {
266316 progressStarted = true
267317 pct := float64 (ev .Processed ) / float64 (ev .Total ) * 100
268318 out .Progress (pct , ev .Processed , ev .Total , ev .Found )
@@ -279,21 +329,8 @@ func run() int {
279329 if progressStarted {
280330 out .ProgressDone ()
281331 }
282- if * verbose {
283- out .Info ("\n Scan completed for %s" , domain )
284- out .Info ("Processed %d subdomain prefixes" , ev .Processed )
285- if * testMode {
286- out .Info ("Found %d simulated subdomains" , ev .Found )
287- } else {
288- out .Info ("Found %d subdomains" , ev .Found )
289- }
290- if outWriter != nil {
291- out .Info ("Results written to: %s" , * outputFile )
292- }
293- if * testMode {
294- out .Info ("\n NOTE: Results were simulated and no actual DNS queries were performed." )
295- out .Info ("This mode is intended for educational and testing purposes only." )
296- }
332+ if f .verbose {
333+ logVerboseDone (ev , f , outWriter , out )
297334 }
298335 }
299336 }
0 commit comments