feroxbuster/
parser.rs

1use clap::ArgAction;
2use clap::{
3    crate_authors, crate_description, crate_name, crate_version, Arg, ArgGroup, Command, ValueHint,
4};
5use lazy_static::lazy_static;
6use regex::Regex;
7use std::env;
8use std::process;
9
10lazy_static! {
11    /// Regex used to validate values passed to --time-limit
12    ///
13    /// Examples of expected values that will this regex will match:
14    /// - 30s
15    /// - 20m
16    /// - 1h
17    /// - 1d
18    pub static ref TIMESPEC_REGEX: Regex =
19        Regex::new(r"^(?i)(?P<n>\d+)(?P<m>[smdh])$").expect("Could not compile regex");
20
21    /// help string for user agent, your guess is as good as mine as to why this is required...
22    static ref DEFAULT_USER_AGENT: String = format!(
23        "Sets the User-Agent (default: feroxbuster/{})",
24        crate_version!()
25    );
26}
27
28/// Create and return an instance of [clap::App](https://docs.rs/clap/latest/clap/struct.App.html), i.e. the Command Line Interface's configuration
29pub fn initialize() -> Command {
30    let app = Command::new(crate_name!())
31        .version(crate_version!())
32        .author(crate_authors!())
33        .about(crate_description!());
34
35    /////////////////////////////////////////////////////////////////////
36    // group - target selection
37    /////////////////////////////////////////////////////////////////////
38    let app = app
39        .arg(
40            Arg::new("url")
41                .short('u')
42                .long("url")
43                .required_unless_present_any(["stdin", "resume_from", "update_app", "request_file"])
44                .help_heading("Target selection")
45                .value_name("URL")
46                .use_value_delimiter(true)
47                .value_hint(ValueHint::Url)
48                .help("The target URL (required, unless [--stdin || --resume-from || --request-file] used)"),
49        )
50        .arg(
51            Arg::new("stdin")
52                .long("stdin")
53                .help_heading("Target selection")
54                .num_args(0)
55                .help("Read url(s) from STDIN")
56                .conflicts_with("url")
57        )
58        .arg(
59            Arg::new("resume_from")
60                .long("resume-from")
61                .value_hint(ValueHint::FilePath)
62                .value_name("STATE_FILE")
63                .help_heading("Target selection")
64                .help("State file from which to resume a partially complete scan (ex. --resume-from ferox-1606586780.state)")
65                .conflicts_with("url")
66                .num_args(1),
67        ).arg(
68            Arg::new("request_file")
69                .long("request-file")
70                .help_heading("Target selection")
71                .value_hint(ValueHint::FilePath)
72                .conflicts_with("url")
73                .num_args(1)
74                .value_name("REQUEST_FILE")
75                .help("Raw HTTP request file to use as a template for all requests"),
76        );
77
78    /////////////////////////////////////////////////////////////////////
79    // group - composite settings
80    /////////////////////////////////////////////////////////////////////
81    let app = app
82        .arg(
83            Arg::new("burp")
84                .long("burp")
85                .num_args(0)
86                .help_heading("Composite settings")
87                .conflicts_with_all(["proxy", "insecure", "burp_replay"])
88                .help("Set --proxy to http://127.0.0.1:8080 and set --insecure to true"),
89        )
90        .arg(
91            Arg::new("burp_replay")
92                .long("burp-replay")
93                .num_args(0)
94                .help_heading("Composite settings")
95                .conflicts_with_all(["replay_proxy", "insecure"])
96                .help("Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true"),
97        )
98        .arg(
99            Arg::new("data-urlencoded")
100                .long("data-urlencoded")
101                .value_name("DATA")
102                .num_args(1)
103                .help_heading("Composite settings")
104                .conflicts_with_all(["data", "data-json"])
105                .help("Set -H 'Content-Type: application/x-www-form-urlencoded', --data to <data-urlencoded> (supports @file) and -m to POST"),
106        )
107        .arg(
108            Arg::new("data-json")
109                .long("data-json")
110                .value_name("DATA")
111                .num_args(1)
112                .help_heading("Composite settings")
113                .conflicts_with_all(["data", "data-urlencoded"])
114                .help("Set -H 'Content-Type: application/json', --data to <data-json> (supports @file) and -m to POST"),
115        )
116        .arg(
117            Arg::new("smart")
118                .long("smart")
119                .num_args(0)
120                .help_heading("Composite settings")
121                .conflicts_with_all(["rate_limit", "auto_bail"])
122                .help("Set --auto-tune, --collect-words, and --collect-backups to true"),
123        )
124        .arg(
125            Arg::new("thorough")
126                .long("thorough")
127                .num_args(0)
128                .help_heading("Composite settings")
129                .conflicts_with_all(["rate_limit", "auto_bail"])
130                .help("Use the same settings as --smart and set --collect-extensions and --scan-dir-listings to true"),
131        );
132
133    /////////////////////////////////////////////////////////////////////
134    // group - proxy settings
135    /////////////////////////////////////////////////////////////////////
136    let app = app
137        .arg(
138            Arg::new("proxy")
139                .short('p')
140                .long("proxy")
141                .num_args(1)
142                .value_name("PROXY")
143                .value_hint(ValueHint::Url)
144                .help_heading("Proxy settings")
145                .help(
146                    "Proxy to use for requests (ex: http(s)://host:port, socks5(h)://host:port)",
147                ),
148        )
149        .arg(
150            Arg::new("replay_proxy")
151                .short('P')
152                .long("replay-proxy")
153                .num_args(1)
154                .value_hint(ValueHint::Url)
155                .value_name("REPLAY_PROXY")
156                .help_heading("Proxy settings")
157                .help(
158                    "Send only unfiltered requests through a Replay Proxy, instead of all requests",
159                ),
160        )
161        .arg(
162            Arg::new("replay_codes")
163                .short('R')
164                .long("replay-codes")
165                .value_name("REPLAY_CODE")
166                .num_args(1..)
167                .action(ArgAction::Append)
168                .use_value_delimiter(true)
169                .requires("replay_proxy")
170                .help_heading("Proxy settings")
171                .help(
172                    "Status Codes to send through a Replay Proxy when found (default: --status-codes value)",
173                ),
174        );
175
176    /////////////////////////////////////////////////////////////////////
177    // group - request settings
178    /////////////////////////////////////////////////////////////////////
179    let app = app
180        .arg(
181            Arg::new("user_agent")
182                .short('a')
183                .long("user-agent")
184                .value_name("USER_AGENT")
185                .num_args(1)
186                .help_heading("Request settings")
187                .help(&**DEFAULT_USER_AGENT),
188        )
189        .arg(
190            Arg::new("random_agent")
191                .short('A')
192                .long("random-agent")
193                .num_args(0)
194                .help_heading("Request settings")
195                .help("Use a random User-Agent"),
196        )
197        .arg(
198            Arg::new("extensions")
199                .short('x')
200                .long("extensions")
201                .value_name("FILE_EXTENSION")
202                .num_args(1..)
203                .action(ArgAction::Append)
204                .use_value_delimiter(true)
205                .help_heading("Request settings")
206                .help(
207                    "File extension(s) to search for (ex: -x php -x pdf js); reads values (newline-separated) from file if input starts with an @ (ex: @ext.txt)",
208                ),
209        )
210        .arg(
211            Arg::new("methods")
212                .short('m')
213                .long("methods")
214                .value_name("HTTP_METHODS")
215                .num_args(1..)
216                .action(ArgAction::Append)
217                .use_value_delimiter(true)
218                .help_heading("Request settings")
219                .help(
220                    "Which HTTP request method(s) should be sent (default: GET)",
221                ),
222        )
223        .arg(
224            Arg::new("data")
225                .long("data")
226                .value_name("DATA")
227                .num_args(1)
228                .help_heading("Request settings")
229                .help(
230                    "Request's Body; can read data from a file if input starts with an @ (ex: @post.bin)",
231                ),
232        )
233        .arg(
234            Arg::new("headers")
235                .short('H')
236                .long("headers")
237                .value_name("HEADER")
238                .num_args(1..)
239                .action(ArgAction::Append)
240                .help_heading("Request settings")
241                .help(
242                    "Specify HTTP headers to be used in each request (ex: -H Header:val -H 'stuff: things')",
243                ),
244        )
245        .arg(
246            Arg::new("cookies")
247                .short('b')
248                .long("cookies")
249                .value_name("COOKIE")
250                .num_args(1..)
251                .action(ArgAction::Append)
252                .use_value_delimiter(true)
253                .help_heading("Request settings")
254                .help(
255                    "Specify HTTP cookies to be used in each request (ex: -b stuff=things)",
256                ),
257        )
258        .arg(
259            Arg::new("queries")
260                .short('Q')
261                .long("query")
262                .value_name("QUERY")
263                .num_args(1..)
264                .action(ArgAction::Append)
265                .use_value_delimiter(true)
266                .help_heading("Request settings")
267                .help(
268                    "Request's URL query parameters (ex: -Q token=stuff -Q secret=key)",
269                ),
270        )
271        .arg(
272            Arg::new("add_slash")
273                .short('f')
274                .long("add-slash")
275                .help_heading("Request settings")
276                .num_args(0)
277                .help("Append / to each request's URL")
278        ).arg(
279            Arg::new("protocol")
280                .long("protocol")
281                .value_name("PROTOCOL")
282                .num_args(1)
283                .help_heading("Request settings")
284                .help("Specify the protocol to use when targeting via --request-file or --url with domain only (default: https)"),
285        );
286
287    /////////////////////////////////////////////////////////////////////
288    // group - request filters
289    /////////////////////////////////////////////////////////////////////
290    let app = app.arg(
291        Arg::new("url_denylist")
292            .long("dont-scan")
293            .value_name("URL")
294            .num_args(1..)
295            .action(ArgAction::Append)
296            .use_value_delimiter(true)
297            .help_heading("Request filters")
298            .help("URL(s) or Regex Pattern(s) to exclude from recursion/scans"),
299    ).arg(
300        Arg::new("scope")
301            .long("scope")
302            .value_name("URL")
303            .num_args(1..)
304            .action(ArgAction::Append)
305            .use_value_delimiter(true)
306            .help_heading("Request filters")
307            .help("Additional domains/URLs to consider in-scope for scanning (in addition to current domain)"),
308    );
309
310    /////////////////////////////////////////////////////////////////////
311    // group - response filters
312    /////////////////////////////////////////////////////////////////////
313    let app = app
314        .arg(
315            Arg::new("filter_size")
316                .short('S')
317                .long("filter-size")
318                .value_name("SIZE")
319                .num_args(1..)
320                .action(ArgAction::Append)
321                .use_value_delimiter(true)
322                .help_heading("Response filters")
323                .help(
324                    "Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)",
325                ),
326        )
327        .arg(
328            Arg::new("filter_regex")
329                .short('X')
330                .long("filter-regex")
331                .value_name("REGEX")
332                .num_args(1..)
333                .action(ArgAction::Append)
334                .use_value_delimiter(true)
335                .help_heading("Response filters")
336                .help(
337                    "Filter out messages via regular expression matching on the response's body/headers (ex: -X '^ignore me$')",
338                ),
339        )
340        .arg(
341            Arg::new("filter_words")
342                .short('W')
343                .long("filter-words")
344                .value_name("WORDS")
345                .num_args(1..)
346                .action(ArgAction::Append)
347                .use_value_delimiter(true)
348                .help_heading("Response filters")
349                .help(
350                    "Filter out messages of a particular word count (ex: -W 312 -W 91,82)",
351                ),
352        )
353        .arg(
354            Arg::new("filter_lines")
355                .short('N')
356                .long("filter-lines")
357                .value_name("LINES")
358                .num_args(1..)
359                .action(ArgAction::Append)
360                .use_value_delimiter(true)
361                .help_heading("Response filters")
362                .help(
363                    "Filter out messages of a particular line count (ex: -N 20 -N 31,30)",
364                ),
365        )
366        .arg(
367            Arg::new("filter_status")
368                .short('C')
369                .long("filter-status")
370                .value_name("STATUS_CODE")
371                .num_args(1..)
372                .action(ArgAction::Append)
373                .use_value_delimiter(true)
374                .conflicts_with("status_codes")
375                .help_heading("Response filters")
376                .help(
377                    "Filter out status codes (deny list) (ex: -C 200 -C 401)",
378                ),
379        )
380        .arg(
381            Arg::new("filter_similar")
382                .long("filter-similar-to")
383                .value_name("UNWANTED_PAGE")
384                .num_args(1..)
385                .action(ArgAction::Append)
386                .value_hint(ValueHint::Url)
387                .use_value_delimiter(true)
388                .help_heading("Response filters")
389                .help(
390                    "Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)",
391                ),
392        )
393        .arg(
394            Arg::new("status_codes")
395                .short('s')
396                .long("status-codes")
397                .value_name("STATUS_CODE")
398                .num_args(1..)
399                .action(ArgAction::Append)
400                .use_value_delimiter(true)
401                .help_heading("Response filters")
402                .help(
403                    "Status Codes to include (allow list) (default: All Status Codes)",
404                ),
405        )
406        .arg(
407            Arg::new("unique")
408                .long("unique")
409                .num_args(0)
410                .help_heading("Response filters")
411                .help("Only show unique responses")
412        );
413
414    /////////////////////////////////////////////////////////////////////
415    // group - client settings
416    /////////////////////////////////////////////////////////////////////
417    let app = app
418        .arg(
419            Arg::new("timeout")
420                .short('T')
421                .long("timeout")
422                .value_name("SECONDS")
423                .num_args(1)
424                .help_heading("Client settings")
425                .help("Number of seconds before a client's request times out (default: 7)"),
426        )
427        .arg(
428            Arg::new("redirects")
429                .short('r')
430                .long("redirects")
431                .num_args(0)
432                .help_heading("Client settings")
433                .help("Allow client to follow redirects"),
434        )
435        .arg(
436            Arg::new("insecure")
437                .short('k')
438                .long("insecure")
439                .num_args(0)
440                .help_heading("Client settings")
441                .help("Disables TLS certificate validation in the client"),
442        )
443        .arg(
444            Arg::new("server_certs")
445                .long("server-certs")
446                .value_name("PEM|DER")
447                .value_hint(ValueHint::FilePath)
448                .num_args(1..)
449                .help_heading("Client settings")
450                .help("Add custom root certificate(s) for servers with unknown certificates"),
451        )
452        .arg(
453            Arg::new("client_cert")
454                .long("client-cert")
455                .value_name("PEM")
456                .value_hint(ValueHint::FilePath)
457                .num_args(1)
458                .requires("client_key")
459                .help_heading("Client settings")
460                .help("Add a PEM encoded certificate for mutual authentication (mTLS)"),
461        )
462        .arg(
463            Arg::new("client_key")
464                .long("client-key")
465                .value_name("PEM")
466                .value_hint(ValueHint::FilePath)
467                .num_args(1)
468                .requires("client_cert")
469                .help_heading("Client settings")
470                .help("Add a PEM encoded private key for mutual authentication (mTLS)"),
471        );
472
473    /////////////////////////////////////////////////////////////////////
474    // group - scan settings
475    /////////////////////////////////////////////////////////////////////
476    let app = app
477        .arg(
478            Arg::new("threads")
479                .short('t')
480                .long("threads")
481                .value_name("THREADS")
482                .num_args(1)
483                .help_heading("Scan settings")
484                .help("Number of concurrent threads (default: 50)"),
485        )
486        .arg(
487            Arg::new("no_recursion")
488                .short('n')
489                .long("no-recursion")
490                .num_args(0)
491                .help_heading("Scan settings")
492                .help("Do not scan recursively"),
493        )
494        .arg(
495            Arg::new("depth")
496                .short('d')
497                .long("depth")
498                .value_name("RECURSION_DEPTH")
499                .num_args(1)
500                .help_heading("Scan settings")
501                .help("Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)"),
502        ).arg(
503            Arg::new("force_recursion")
504                .long("force-recursion")
505                .num_args(0)
506                .conflicts_with("no_recursion")
507                .help_heading("Scan settings")
508                .help("Force recursion attempts on all 'found' endpoints (still respects recursion depth)"),
509        ).arg(
510            Arg::new("extract_links")
511                .short('e')
512                .long("extract-links")
513                .num_args(0)
514                .help_heading("Scan settings")
515                .hide(true)
516                .help("Extract links from response body (html, javascript, etc...); make new requests based on findings (default: true)")
517        )
518        .arg(
519            Arg::new("dont_extract_links")
520                .long("dont-extract-links")
521                .num_args(0)
522                .help_heading("Scan settings")
523                .help("Don't extract links from response body (html, javascript, etc...)")
524        )
525        .arg(
526            Arg::new("scan_limit")
527                .short('L')
528                .long("scan-limit")
529                .value_name("SCAN_LIMIT")
530                .num_args(1)
531                .help_heading("Scan settings")
532                .help("Limit total number of concurrent scans (default: 0, i.e. no limit)")
533        )
534        .arg(
535            Arg::new("parallel")
536                .long("parallel")
537                .value_name("PARALLEL_SCANS")
538                .conflicts_with("verbosity")
539                .conflicts_with("url")
540                .num_args(1)
541                .requires("stdin")
542                .help_heading("Scan settings")
543                .help("Run parallel feroxbuster instances (one child process per url passed via stdin)")
544        )
545        .arg(
546            Arg::new("rate_limit")
547                .long("rate-limit")
548                .value_name("RATE_LIMIT")
549                .num_args(1)
550                .help_heading("Scan settings")
551                .help("Limit number of requests per second (per directory) (default: 0, i.e. no limit)")
552        )
553        .arg(
554            Arg::new("response_size_limit")
555                .long("response-size-limit")
556                .value_name("BYTES")
557                .num_args(1)
558                .help_heading("Scan settings")
559                .help("Limit size of response body to read in bytes (default: 4MB)"),
560        )
561        .arg(
562            Arg::new("time_limit")
563                .long("time-limit")
564                .value_name("TIME_SPEC")
565                .num_args(1)
566                .value_parser(valid_time_spec)
567                .help_heading("Scan settings")
568                .help("Limit total run time of all scans (ex: --time-limit 10m)")
569        )
570        .arg(
571            Arg::new("wordlist")
572                .short('w')
573                .long("wordlist")
574                .value_hint(ValueHint::FilePath)
575                .value_name("FILE")
576                .help("Path or URL of the wordlist")
577                .help_heading("Scan settings")
578                .num_args(1),
579        ).arg(
580            Arg::new("auto_tune")
581                .long("auto-tune")
582                .num_args(0)
583                .conflicts_with("auto_bail")
584                .help_heading("Scan settings")
585                .help("Automatically lower scan rate when an excessive amount of errors are encountered")
586        )
587        .arg(
588            Arg::new("auto_bail")
589                .long("auto-bail")
590                .num_args(0)
591                .help_heading("Scan settings")
592                .help("Automatically stop scanning when an excessive amount of errors are encountered")
593        ).arg(
594            Arg::new("dont_filter")
595                .short('D')
596                .long("dont-filter")
597                .num_args(0)
598                .help_heading("Scan settings")
599                .help("Don't auto-filter wildcard responses")
600        ).arg(
601            Arg::new("collect_extensions")
602                .short('E')
603                .long("collect-extensions")
604                .num_args(0)
605                .help_heading("Dynamic collection settings")
606                .help("Automatically discover extensions and add them to --extensions (unless they're in --dont-collect)")
607        ).arg(
608            Arg::new("collect_backups")
609                .short('B')
610                .long("collect-backups")
611                .num_args(0..)
612                .help_heading("Dynamic collection settings")
613                .help("Automatically request likely backup extensions for \"found\" urls (default: ~, .bak, .bak2, .old, .1)")
614        )
615        .arg(
616            Arg::new("collect_words")
617                .short('g')
618                .long("collect-words")
619                .num_args(0)
620                .help_heading("Dynamic collection settings")
621                .help("Automatically discover important words from within responses and add them to the wordlist")
622        ).arg(
623            Arg::new("dont_collect")
624                .short('I')
625                .long("dont-collect")
626                .value_name("FILE_EXTENSION")
627                .num_args(1..)
628                .action(ArgAction::Append)
629                .use_value_delimiter(true)
630                .help_heading("Dynamic collection settings")
631                .help(
632                    "File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)",
633                ),
634        ).arg(
635            Arg::new("scan_dir_listings")
636                .long("scan-dir-listings")
637                .num_args(0)
638                .help_heading("Scan settings")
639                .help("Force scans to recurse into directory listings")
640        );
641
642    /////////////////////////////////////////////////////////////////////
643    // group - output settings
644    /////////////////////////////////////////////////////////////////////
645    let app = app
646        .arg(
647            Arg::new("verbosity")
648                .short('v')
649                .long("verbosity")
650                .num_args(0)
651                .action(ArgAction::Count)
652                .conflicts_with("silent")
653                .help_heading("Output settings")
654                .help("Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v's is probably too much)"),
655        ).arg(
656            Arg::new("silent")
657                .long("silent")
658                .num_args(0)
659                .conflicts_with("quiet")
660                .help_heading("Output settings")
661                .help("Only print URLs (or JSON w/ --json) + turn off logging (good for piping a list of urls to other commands)")
662        )
663        .arg(
664            Arg::new("quiet")
665                .short('q')
666                .long("quiet")
667                .num_args(0)
668                .help_heading("Output settings")
669                .help("Hide progress bars and banner (good for tmux windows w/ notifications)")
670        )
671
672        .arg(
673            Arg::new("json")
674                .long("json")
675                .num_args(0)
676                .requires("output_files")
677                .help_heading("Output settings")
678                .help("Emit JSON logs to --output and --debug-log instead of normal text")
679        ).arg(
680            Arg::new("output")
681                .short('o')
682                .long("output")
683                .value_hint(ValueHint::FilePath)
684                .value_name("FILE")
685                .help_heading("Output settings")
686                .help("Output file to write results to (use w/ --json for JSON entries)")
687                .num_args(1),
688        )
689        .arg(
690            Arg::new("debug_log")
691                .long("debug-log")
692                .value_name("FILE")
693                .value_hint(ValueHint::FilePath)
694                .help_heading("Output settings")
695                .help("Output file to write log entries (use w/ --json for JSON entries)")
696                .num_args(1),
697        )
698        .arg(
699            Arg::new("no_state")
700                .long("no-state")
701                .num_args(0)
702                .help_heading("Output settings")
703                .help("Disable state output file (*.state)")
704        ).arg(
705            Arg::new("limit_bars")
706                .long("limit-bars")
707                .value_name("NUM_BARS_TO_SHOW")
708                .num_args(1)
709                .help_heading("Output settings")
710                .help("Number of directory scan bars to show at any given time (default: no limit)"),
711        );
712
713    /////////////////////////////////////////////////////////////////////
714    // group - miscellaneous
715    /////////////////////////////////////////////////////////////////////
716    let mut app = app
717        .group(
718            ArgGroup::new("output_files")
719                .args(["debug_log", "output", "silent"])
720                .multiple(true),
721        )
722        .group(
723            ArgGroup::new("output_limiters")
724                .args(["quiet", "silent"])
725                .multiple(false),
726        )
727        .arg(
728            Arg::new("update_app")
729                .short('U')
730                .long("update")
731                .exclusive(true)
732                .num_args(0)
733                .help_heading("Update settings")
734                .help("Update feroxbuster to the latest version"),
735        )
736        .after_long_help(EPILOGUE);
737
738    /////////////////////////////////////////////////////////////////////
739    // end parser
740    /////////////////////////////////////////////////////////////////////
741    for arg in env::args() {
742        // secure-77 noticed that when an incorrect flag/option is used, the short help message is printed
743        // which is fine, but if you add -h|--help, it still errors out on the bad flag/option,
744        // never showing the full help message. This code addresses that behavior
745        if arg == "--help" {
746            app.print_long_help().unwrap();
747            println!(); // just a newline to mirror original --help output
748            process::exit(0);
749        } else if arg == "-h" {
750            // same for -h, just shorter
751            app.print_help().unwrap();
752            println!();
753            process::exit(0);
754        }
755    }
756
757    app
758}
759
760/// Validate that a string is formatted as a number followed by s, m, h, or d (10d, 30s, etc...)
761fn valid_time_spec(time_spec: &str) -> Result<String, String> {
762    match TIMESPEC_REGEX.is_match(time_spec) {
763        true => Ok(time_spec.to_string()),
764        false => {
765            let msg = format!(
766                "Expected a non-negative, whole number followed by s, m, h, or d (case insensitive); received {time_spec}"
767            );
768            Err(msg)
769        }
770    }
771}
772
773const EPILOGUE: &str = r#"NOTE:
774    Options that take multiple values are very flexible.  Consider the following ways of specifying
775    extensions:
776        ./feroxbuster -u http://127.1 -x pdf -x js,html -x php txt json,docx
777
778    The command above adds .pdf, .js, .html, .php, .txt, .json, and .docx to each url
779
780    All of the methods above (multiple flags, space separated, comma separated, etc...) are valid
781    and interchangeable.  The same goes for urls, headers, status codes, queries, and size filters.
782
783EXAMPLES:
784    Multiple headers:
785        ./feroxbuster -u http://127.1 -H Accept:application/json "Authorization: Bearer {token}"
786
787    IPv6, non-recursive scan with INFO-level logging enabled:
788        ./feroxbuster -u http://[::1] --no-recursion -vv
789
790    Read urls from STDIN; pipe only resulting urls out to another tool
791        cat targets | ./feroxbuster --stdin --silent -s 200 301 302 --redirects -x js | fff -s 200 -o js-files
792
793    Proxy traffic through Burp
794        ./feroxbuster -u http://127.1 --burp
795
796    Proxy traffic through a SOCKS proxy
797        ./feroxbuster -u http://127.1 --proxy socks5://127.0.0.1:9050
798
799    Pass auth token via query parameter
800        ./feroxbuster -u http://127.1 --query token=0123456789ABCDEF
801
802    Ludicrous speed... go!
803        ./feroxbuster -u http://127.1 --threads 200
804        
805    Limit to a total of 60 active requests at any given time (threads * scan limit)
806        ./feroxbuster -u http://127.1 --threads 30 --scan-limit 2
807    
808    Send all 200/302 responses to a proxy (only proxy requests/responses you care about)
809        ./feroxbuster -u http://127.1 --replay-proxy http://localhost:8080 --replay-codes 200 302 --insecure
810        
811    Abort or reduce scan speed to individual directory scans when too many errors have occurred
812        ./feroxbuster -u http://127.1 --auto-bail
813        ./feroxbuster -u http://127.1 --auto-tune
814        
815    Examples and demonstrations of all features
816        https://epi052.github.io/feroxbuster-docs/docs/examples/
817    "#;
818
819#[cfg(test)]
820mod tests {
821    use super::*;
822
823    #[test]
824    /// initialize parser, expect a clap::App returned
825    fn parser_initialize_gives_defaults() {
826        let app = initialize();
827        assert_eq!(app.get_name(), "feroxbuster");
828    }
829
830    #[test]
831    /// sanity checks that valid_time_spec correctly checks and rejects a given string
832    ///
833    /// instead of having a bunch of single tests here, they're all quick and are mostly checking
834    /// that i didn't hose up the regex.  Going to consolidate them into a single test
835    fn validate_valid_time_spec_validation() {
836        let float_rejected = "1.4m";
837        assert!(valid_time_spec(float_rejected).is_err());
838
839        let negative_rejected = "-1m";
840        assert!(valid_time_spec(negative_rejected).is_err());
841
842        let only_number_rejected = "1";
843        assert!(valid_time_spec(only_number_rejected).is_err());
844
845        let only_measurement_rejected = "m";
846        assert!(valid_time_spec(only_measurement_rejected).is_err());
847
848        for accepted_measurement in &["s", "m", "h", "d", "S", "M", "H", "D"] {
849            // all upper/lowercase should be good
850            assert!(valid_time_spec(&format!("1{}", *accepted_measurement)).is_ok());
851        }
852
853        let leading_space_rejected = " 14m";
854        assert!(valid_time_spec(leading_space_rejected).is_err());
855
856        let trailing_space_rejected = "14m ";
857        assert!(valid_time_spec(trailing_space_rejected).is_err());
858
859        let space_between_rejected = "1 4m";
860        assert!(valid_time_spec(space_between_rejected).is_err());
861    }
862}