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 pub static ref TIMESPEC_REGEX: Regex =
19 Regex::new(r"^(?i)(?P<n>\d+)(?P<m>[smdh])$").expect("Could not compile regex");
20
21 static ref DEFAULT_USER_AGENT: String = format!(
23 "Sets the User-Agent (default: feroxbuster/{})",
24 crate_version!()
25 );
26}
27
28pub fn initialize() -> Command {
30 let app = Command::new(crate_name!())
31 .version(crate_version!())
32 .author(crate_authors!())
33 .about(crate_description!());
34
35 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 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 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 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 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 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 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 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 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 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 for arg in env::args() {
742 if arg == "--help" {
746 app.print_long_help().unwrap();
747 println!(); process::exit(0);
749 } else if arg == "-h" {
750 app.print_help().unwrap();
752 println!();
753 process::exit(0);
754 }
755 }
756
757 app
758}
759
760fn 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 fn parser_initialize_gives_defaults() {
826 let app = initialize();
827 assert_eq!(app.get_name(), "feroxbuster");
828 }
829
830 #[test]
831 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 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}