Codebase list feroxbuster / fb0470e
Update upstream source from tag 'upstream/2.7.0' Update to upstream version '2.7.0' with Debian dir 6e372ee1fb592ad1e2be2499d0821b4faa8dce45 Sophie Brun 2 years ago
31 changed file(s) with 579 addition(s) and 318 deletion(s). Raw diff Collapse all Expand all
389389 "name": "Jason Haddix",
390390 "avatar_url": "https://avatars.githubusercontent.com/u/3488554?v=4",
391391 "profile": "https://twitter.com/Jhaddix",
392 "contributions": [
393 "ideas"
394 ]
395 },
396 {
397 "login": "ThisLimn0",
398 "name": "Limn0",
399 "avatar_url": "https://avatars.githubusercontent.com/u/67125885?v=4",
400 "profile": "https://github.com/ThisLimn0",
401 "contributions": [
402 "bug"
403 ]
404 },
405 {
406 "login": "0xdf223",
407 "name": "0xdf",
408 "avatar_url": "https://avatars.githubusercontent.com/u/76954092?v=4",
409 "profile": "https://github.com/0xdf223",
410 "contributions": [
411 "bug",
412 "ideas"
413 ]
414 },
415 {
416 "login": "Flangyver",
417 "name": "Flangyver",
418 "avatar_url": "https://avatars.githubusercontent.com/u/59575870?v=4",
419 "profile": "https://github.com/Flangyver",
392420 "contributions": [
393421 "ideas"
394422 ]
7272 name: ${{ matrix.name }}.tar.gz
7373 path: ${{ matrix.name }}.tar.gz
7474
75 build-deb:
76 needs: [build-nix]
77 runs-on: ubuntu-latest
78 steps:
79 - uses: actions/checkout@master
80 - name: Deb Build
81 uses: ebbflow-io/[email protected]
82 - name: Upload Deb Artifact
83 uses: actions/upload-artifact@v2
84 with:
85 name: feroxbuster_amd64.deb
86 path: ./target/x86_64-unknown-linux-musl/debian/*
75 # build-deb:
76 # needs: [build-nix]
77 # runs-on: ubuntu-latest
78 # steps:
79 # - uses: actions/checkout@master
80 # - name: Install cargo-deb
81 # run: cargo install -f cargo-deb
82 # - name: Install musl toolchain
83 # run: rustup target add x86_64-unknown-linux-musl
84 # - name: Deb Build
85 # run: cargo deb --target=x86_64-unknown-linux-musl
86 # - name: Upload Deb Artifact
87 # uses: actions/upload-artifact@v2
88 # with:
89 # name: feroxbuster_amd64.deb
90 # path: ./target/x86_64-unknown-linux-musl/debian/*
8791
8892 build-macos:
8993 env:
77 runs-on: ubuntu-latest
88 steps:
99 - uses: actions/checkout@v2
10 - uses: actions-rs/toolchain@v1
11 with:
12 profile: minimal
13 toolchain: stable
14 override: true
1510 - uses: actions-rs/cargo@v1
1611 with:
1712 command: check
2116 runs-on: ubuntu-latest
2217 steps:
2318 - uses: actions/checkout@v2
24 - uses: actions-rs/toolchain@v1
19 - name: Install latest nextest release
20 uses: taiki-e/install-action@nextest
21 - name: Test with latest nextest release
22 uses: actions-rs/cargo@v1
2523 with:
26 profile: minimal
27 toolchain: stable
28 override: true
29 - uses: actions-rs/cargo@v1
30 with:
31 command: test
24 command: nextest
25 args: run --all-features --all-targets --retries 10
3226
3327 fmt:
3428 name: Rust fmt
3529 runs-on: ubuntu-latest
3630 steps:
3731 - uses: actions/checkout@v2
38 - uses: actions-rs/toolchain@v1
39 with:
40 profile: minimal
41 toolchain: stable
42 override: true
43 - run: rustup component add rustfmt
4432 - uses: actions-rs/cargo@v1
4533 with:
4634 command: fmt
5139 runs-on: ubuntu-latest
5240 steps:
5341 - uses: actions/checkout@v2
54 - uses: actions-rs/toolchain@v1
55 with:
56 profile: minimal
57 toolchain: stable
58 override: true
59 - run: rustup component add clippy
6042 - uses: actions-rs/cargo@v1
6143 with:
6244 command: clippy
63 args: --all-targets --all-features -- -D warnings -A clippy::deref_addrof -A clippy::mutex-atomic
45 args: --all-targets --all-features -- -D warnings
22 name: Code Coverage Pipeline
33
44 jobs:
5 upload-coverage:
5 coverage:
6 name: LLVM Coverage
67 runs-on: ubuntu-latest
78 steps:
8 - uses: actions/checkout@v1
9 - uses: actions-rs/toolchain@v1
10 with:
11 toolchain: nightly
12 override: true
13 - uses: actions-rs/cargo@v1
14 with:
15 command: clean
16 - uses: actions-rs/cargo@v1
17 with:
18 command: test
19 args: --all-features --no-fail-fast
20 env:
21 CARGO_INCREMENTAL: '0'
22 RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort'
23 RUSTDOCFLAGS: '-Cpanic=abort'
24 - uses: actions-rs/[email protected]
25 - uses: actions/upload-artifact@v2
26 with:
27 name: lcov.info
28 path: lcov.info
29 - name: Convert lcov to xml
30 run: |
31 curl -O https://raw.githubusercontent.com/epi052/lcov-to-cobertura-xml/master/lcov_cobertura/lcov_cobertura.py
32 chmod +x lcov_cobertura.py
33 ./lcov_cobertura.py ./lcov.info
34 - uses: codecov/codecov-action@v1
9 - uses: actions/checkout@v2
10 - name: Install llvm-tools-preview
11 run: rustup toolchain install stable --component llvm-tools-preview
12 - name: Install cargo-llvm-cov
13 uses: taiki-e/install-action@cargo-llvm-cov
14 - name: Install cargo-nextest
15 uses: taiki-e/install-action@nextest
16 - name: Generate code coverage
17 run: cargo llvm-cov nextest --all-features --no-fail-fast --lcov --output-path lcov.info -- --retries 10
18 - name: Upload coverage to Codecov
19 uses: codecov/codecov-action@v1
3520 with:
3621 token: ${{ secrets.CODECOV_TOKEN }}
37 file: ./coverage.xml
38 name: codecov-umbrella
22 files: lcov.info
3923 fail_ci_if_error: true
40 - uses: actions/upload-artifact@v2
41 with:
42 name: coverage.xml
43 path: ./coverage.xml
670670
671671 [[package]]
672672 name = "feroxbuster"
673 version = "2.6.4"
673 version = "2.7.0"
674674 dependencies = [
675675 "anyhow",
676676 "assert_cmd",
00 [package]
11 name = "feroxbuster"
2 version = "2.6.4"
2 version = "2.7.0"
33 authors = ["Ben 'epi' Risher (@epi052)"]
44 license = "MIT"
55 edition = "2021"
77 repository = "https://github.com/epi052/feroxbuster"
88 description = "A fast, simple, recursive content discovery tool."
99 categories = ["command-line-utilities"]
10 keywords = ["pentest", "enumeration", "url-bruteforce", "content-discovery", "web"]
10 keywords = [
11 "pentest",
12 "enumeration",
13 "url-bruteforce",
14 "content-discovery",
15 "web",
16 ]
1117 exclude = [".github/*", "img/*", "check-coverage.sh"]
1218 build = "build.rs"
1319
4854 ctrlc = "3.2.1"
4955 fuzzyhash = "0.2.1"
5056 anyhow = "1.0.56"
51 leaky-bucket = "0.10.0" # todo: upgrade, will take a little work/thought since api changed
57 leaky-bucket = "0.10.0" # todo: upgrade, will take a little work/thought since api changed
5258
5359 [dev-dependencies]
5460 tempfile = "3.3.0"
6672 license-file = ["LICENSE", "4"]
6773 conf-files = ["/etc/feroxbuster/ferox-config.toml"]
6874 assets = [
69 ["target/release/feroxbuster", "/usr/bin/", "755"],
70 ["ferox-config.toml.example", "/etc/feroxbuster/ferox-config.toml", "644"],
71 ["shell_completions/feroxbuster.bash", "/usr/share/bash-completion/completions/feroxbuster.bash", "644"],
72 ["shell_completions/feroxbuster.fish", "/usr/share/fish/completions/feroxbuster.fish", "644"],
73 ["shell_completions/_feroxbuster", "/usr/share/zsh/vendor-completions/_feroxbuster", "644"],
75 [
76 "target/release/feroxbuster",
77 "/usr/bin/",
78 "755",
79 ],
80 [
81 "ferox-config.toml.example",
82 "/etc/feroxbuster/ferox-config.toml",
83 "644",
84 ],
85 [
86 "shell_completions/feroxbuster.bash",
87 "/usr/share/bash-completion/completions/feroxbuster.bash",
88 "644",
89 ],
90 [
91 "shell_completions/feroxbuster.fish",
92 "/usr/share/fish/completions/feroxbuster.fish",
93 "644",
94 ],
95 [
96 "shell_completions/_feroxbuster",
97 "/usr/share/zsh/vendor-completions/_feroxbuster",
98 "644",
99 ],
74100 ]
200200 </tr>
201201 <tr>
202202 <td align="center"><a href="https://tib3rius.com"><img src="https://avatars.githubusercontent.com/u/48113936?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tib3rius</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ATib3rius" title="Bug reports">🐛</a></td>
203 <td align="center"><a href="https://github.com/0xdf"><img src="https://avatars.githubusercontent.com/u/1489045?v=4?s=100" width="100px;" alt=""/><br /><sub><b>0xdf</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3A0xdf" title="Bug reports">🐛</a></td>
203 <td align="center"><a href="https://github.com/Flangyver"><img src="https://avatars.githubusercontent.com/u/59575870?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Flangyver</b></sub></a><br /><a href="#ideas-Flangyver" title="Ideas, Planning, & Feedback">🤔</a></td>
204204 <td align="center"><a href="http://secure77.de"><img src="https://avatars.githubusercontent.com/u/31564517?v=4?s=100" width="100px;" alt=""/><br /><sub><b>secure-77</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asecure-77" title="Bug reports">🐛</a></td>
205205 <td align="center"><a href="https://github.com/sbrun"><img src="https://avatars.githubusercontent.com/u/7712154?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sophie Brun</b></sub></a><br /><a href="#infra-sbrun" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
206206 <td align="center"><a href="https://github.com/black-A"><img src="https://avatars.githubusercontent.com/u/30686803?v=4?s=100" width="100px;" alt=""/><br /><sub><b>black-A</b></sub></a><br /><a href="#ideas-black-A" title="Ideas, Planning, & Feedback">🤔</a></td>
234234 <td align="center"><a href="https://github.com/gtjamesa"><img src="https://avatars.githubusercontent.com/u/2078364?v=4?s=100" width="100px;" alt=""/><br /><sub><b>James</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Agtjamesa" title="Bug reports">🐛</a></td>
235235 <td align="center"><a href="https://twitter.com/Jhaddix"><img src="https://avatars.githubusercontent.com/u/3488554?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jason Haddix</b></sub></a><br /><a href="#ideas-jhaddix" title="Ideas, Planning, & Feedback">🤔</a></td>
236236 </tr>
237 <tr>
238 <td align="center"><a href="https://github.com/ThisLimn0"><img src="https://avatars.githubusercontent.com/u/67125885?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Limn0</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AThisLimn0" title="Bug reports">🐛</a></td>
239 <td align="center"><a href="https://github.com/0xdf223"><img src="https://avatars.githubusercontent.com/u/76954092?v=4?s=100" width="100px;" alt=""/><br /><sub><b>0xdf</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3A0xdf223" title="Bug reports">🐛</a> <a href="#ideas-0xdf223" title="Ideas, Planning, & Feedback">🤔</a></td>
240
241 </tr>
237242 </table>
238243
239244 <!-- markdownlint-restore -->
241246
242247 <!-- ALL-CONTRIBUTORS-LIST:END -->
243248
244 This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
249 This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
4444 # dont_filter = true
4545 # extract_links = true
4646 # depth = 1
47 # force_recursion = true
4748 # filter_size = [5174]
4849 # filter_regex = ["^ignore me$"]
4950 # filter_similar = ["https://somesite.com/soft404"]
2323 '--replay-proxy=[Send only unfiltered requests through a Replay Proxy, instead of all requests]:REPLAY_PROXY:_urls' \
2424 '*-R+[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \
2525 '*--replay-codes=[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \
26 '-a+[Sets the User-Agent (default: feroxbuster/2.6.4)]:USER_AGENT: ' \
27 '--user-agent=[Sets the User-Agent (default: feroxbuster/2.6.4)]:USER_AGENT: ' \
26 '-a+[Sets the User-Agent (default: feroxbuster/2.7.0)]:USER_AGENT: ' \
27 '--user-agent=[Sets the User-Agent (default: feroxbuster/2.7.0)]:USER_AGENT: ' \
2828 '*-x+[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \
2929 '*--extensions=[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \
3030 '*-m+[Which HTTP request method(s) should be sent (default: GET)]:HTTP_METHODS: ' \
4545 '*--filter-words=[Filter out messages of a particular word count (ex: -W 312 -W 91,82)]:WORDS: ' \
4646 '*-N+[Filter out messages of a particular line count (ex: -N 20 -N 31,30)]:LINES: ' \
4747 '*--filter-lines=[Filter out messages of a particular line count (ex: -N 20 -N 31,30)]:LINES: ' \
48 '*-C+[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \
49 '*--filter-status=[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \
48 '(-s --status-codes)*-C+[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \
49 '(-s --status-codes)*--filter-status=[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \
5050 '*--filter-similar-to=[Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)]:UNWANTED_PAGE:_urls' \
5151 '*-s+[Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)]:STATUS_CODE: ' \
5252 '*--status-codes=[Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)]:STATUS_CODE: ' \
8787 '--insecure[Disables TLS certificate validation in the client]' \
8888 '-n[Do not scan recursively]' \
8989 '--no-recursion[Do not scan recursively]' \
90 '(-n --no-recursion)--force-recursion[Force recursion attempts on all '\''found'\'' endpoints (still respects recursion depth)]' \
9091 '-e[Extract links from response body (html, javascript, etc...); make new requests based on findings]' \
9192 '--extract-links[Extract links from response body (html, javascript, etc...); make new requests based on findings]' \
9293 '(--auto-bail)--auto-tune[Automatically lower scan rate when an excessive amount of errors are encountered]' \
2929 [CompletionResult]::new('--replay-proxy', 'replay-proxy', [CompletionResultType]::ParameterName, 'Send only unfiltered requests through a Replay Proxy, instead of all requests')
3030 [CompletionResult]::new('-R', 'R', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)')
3131 [CompletionResult]::new('--replay-codes', 'replay-codes', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)')
32 [CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.6.4)')
33 [CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.6.4)')
32 [CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.7.0)')
33 [CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.7.0)')
3434 [CompletionResult]::new('-x', 'x', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)')
3535 [CompletionResult]::new('--extensions', 'extensions', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)')
3636 [CompletionResult]::new('-m', 'm', [CompletionResultType]::ParameterName, 'Which HTTP request method(s) should be sent (default: GET)')
9393 [CompletionResult]::new('--insecure', 'insecure', [CompletionResultType]::ParameterName, 'Disables TLS certificate validation in the client')
9494 [CompletionResult]::new('-n', 'n', [CompletionResultType]::ParameterName, 'Do not scan recursively')
9595 [CompletionResult]::new('--no-recursion', 'no-recursion', [CompletionResultType]::ParameterName, 'Do not scan recursively')
96 [CompletionResult]::new('--force-recursion', 'force-recursion', [CompletionResultType]::ParameterName, 'Force recursion attempts on all ''found'' endpoints (still respects recursion depth)')
9697 [CompletionResult]::new('-e', 'e', [CompletionResultType]::ParameterName, 'Extract links from response body (html, javascript, etc...); make new requests based on findings')
9798 [CompletionResult]::new('--extract-links', 'extract-links', [CompletionResultType]::ParameterName, 'Extract links from response body (html, javascript, etc...); make new requests based on findings')
9899 [CompletionResult]::new('--auto-tune', 'auto-tune', [CompletionResultType]::ParameterName, 'Automatically lower scan rate when an excessive amount of errors are encountered')
1818
1919 case "${cmd}" in
2020 feroxbuster)
21 opts="-h -V -u -p -P -R -a -A -x -m -H -b -Q -f -S -X -W -N -C -s -T -r -k -t -n -d -e -L -w -D -E -B -g -I -v -q -o --help --version --url --stdin --resume-from --burp --burp-replay --smart --thorough --proxy --replay-proxy --replay-codes --user-agent --random-agent --extensions --methods --data --headers --cookies --query --add-slash --dont-scan --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --status-codes --timeout --redirects --insecure --threads --no-recursion --depth --extract-links --scan-limit --parallel --rate-limit --time-limit --wordlist --auto-tune --auto-bail --dont-filter --collect-extensions --collect-backups --collect-words --dont-collect --verbosity --silent --quiet --json --output --debug-log --no-state"
21 opts="-h -V -u -p -P -R -a -A -x -m -H -b -Q -f -S -X -W -N -C -s -T -r -k -t -n -d -e -L -w -D -E -B -g -I -v -q -o --help --version --url --stdin --resume-from --burp --burp-replay --smart --thorough --proxy --replay-proxy --replay-codes --user-agent --random-agent --extensions --methods --data --headers --cookies --query --add-slash --dont-scan --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --status-codes --timeout --redirects --insecure --threads --no-recursion --depth --force-recursion --extract-links --scan-limit --parallel --rate-limit --time-limit --wordlist --auto-tune --auto-bail --dont-filter --collect-extensions --collect-backups --collect-words --dont-collect --verbosity --silent --quiet --json --output --debug-log --no-state"
2222 if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
2323 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
2424 return 0
2626 cand --replay-proxy 'Send only unfiltered requests through a Replay Proxy, instead of all requests'
2727 cand -R 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)'
2828 cand --replay-codes 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)'
29 cand -a 'Sets the User-Agent (default: feroxbuster/2.6.4)'
30 cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.6.4)'
29 cand -a 'Sets the User-Agent (default: feroxbuster/2.7.0)'
30 cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.7.0)'
3131 cand -x 'File extension(s) to search for (ex: -x php -x pdf js)'
3232 cand --extensions 'File extension(s) to search for (ex: -x php -x pdf js)'
3333 cand -m 'Which HTTP request method(s) should be sent (default: GET)'
9090 cand --insecure 'Disables TLS certificate validation in the client'
9191 cand -n 'Do not scan recursively'
9292 cand --no-recursion 'Do not scan recursively'
93 cand --force-recursion 'Force recursion attempts on all ''found'' endpoints (still respects recursion depth)'
9394 cand -e 'Extract links from response body (html, javascript, etc...); make new requests based on findings'
9495 cand --extract-links 'Extract links from response body (html, javascript, etc...); make new requests based on findings'
9596 cand --auto-tune 'Automatically lower scan rate when an excessive amount of errors are encountered'
162162
163163 /// represents Configuration.collect_words
164164 collect_words: BannerEntry,
165
166 /// represents Configuration.collect_words
167 force_recursion: BannerEntry,
165168 }
166169
167170 /// implementation of Banner
299302 &config.scan_limit.to_string(),
300303 );
301304
305 let force_recursion =
306 BannerEntry::new("🤘", "Force Recursion", &config.force_recursion.to_string());
302307 let replay_proxy = BannerEntry::new("🎥", "Replay Proxy", &config.replay_proxy);
303308 let auto_tune = BannerEntry::new("🎶", "Auto Tune", &config.auto_tune.to_string());
304309 let auto_bail = BannerEntry::new("🪣", "Auto Bail", &config.auto_bail.to_string());
408413 no_recursion,
409414 rate_limit,
410415 scan_limit,
416 force_recursion,
411417 time_limit,
412418 url_denylist,
413419 collect_extensions,
510516
511517 writeln!(&mut writer, "{}", self.threads)?;
512518 writeln!(&mut writer, "{}", self.wordlist)?;
513 writeln!(&mut writer, "{}", self.status_codes)?;
514
515 if !config.filter_status.is_empty() {
516 // exception here for an optional print in the middle of always printed values is due
517 // to me wanting the allows and denys to be printed one after the other
519
520 if config.filter_status.is_empty() {
521 // -C and -s are mutually exclusive, and -s meaning changes when -C is used
522 // so only print one or the other
523 writeln!(&mut writer, "{}", self.status_codes)?;
524 } else {
518525 writeln!(&mut writer, "{}", self.filter_status)?;
519526 }
520527
640647 }
641648
642649 writeln!(&mut writer, "{}", self.no_recursion)?;
650
651 if config.force_recursion {
652 writeln!(&mut writer, "{}", self.force_recursion)?;
653 }
643654
644655 if config.scan_limit > 0 {
645656 writeln!(&mut writer, "{}", self.scan_limit)?;
280280 /// Automatically discover important words from within responses and add them to the wordlist
281281 #[serde(default)]
282282 pub collect_words: bool,
283
284 /// override recursion logic to always attempt recursion, still respects --depth
285 #[serde(default)]
286 pub force_recursion: bool,
283287 }
284288
285289 impl Default for Configuration {
328332 collect_backups: false,
329333 collect_words: false,
330334 save_state: true,
335 force_recursion: false,
331336 proxy: String::new(),
332337 config: String::new(),
333338 output: String::new(),
404409 /// - **json**: `false`
405410 /// - **dont_filter**: `false` (auto filter wildcard responses)
406411 /// - **depth**: `4` (maximum recursion depth)
412 /// - **force_recursion**: `false` (still respects recursion depth)
407413 /// - **scan_limit**: `0` (no limit on concurrent scans imposed)
408414 /// - **parallel**: `0` (no limit on parallel scans imposed)
409415 /// - **rate_limit**: `0` (no limit on requests per second imposed)
773779 config.json = true;
774780 }
775781
782 if args.is_present("force_recursion") {
783 config.force_recursion = true;
784 }
785
776786 ////
777787 // organizational breakpoint; all options below alter the Client configuration
778788 ////
941951 update_if_not_default!(&mut conf.output, new.output, "");
942952 update_if_not_default!(&mut conf.redirects, new.redirects, false);
943953 update_if_not_default!(&mut conf.insecure, new.insecure, false);
954 update_if_not_default!(&mut conf.force_recursion, new.force_recursion, false);
944955 update_if_not_default!(&mut conf.extract_links, new.extract_links, false);
945956 update_if_not_default!(&mut conf.extensions, new.extensions, Vec::<String>::new());
946957 update_if_not_default!(&mut conf.methods, new.methods, Vec::<String>::new());
4848 json = true
4949 save_state = false
5050 depth = 1
51 force_recursion = true
5152 filter_size = [4120]
5253 filter_regex = ["^ignore me$"]
5354 filter_similar = ["https://somesite.com/soft404"]
9495 assert!(config.save_state);
9596 assert!(!config.stdin);
9697 assert!(!config.add_slash);
98 assert!(!config.force_recursion);
9799 assert!(!config.redirects);
98100 assert!(!config.extract_links);
99101 assert!(!config.insecure);
209211
210212 #[test]
211213 /// parse the test config and see that the value parsed is correct
214 fn config_reads_force_recursion() {
215 let config = setup_config_test();
216 assert!(config.force_recursion);
217 }
218
219 #[test]
220 /// parse the test config and see that the value parsed is correct
212221 fn config_reads_quiet() {
213222 let config = setup_config_test();
214223 assert!(config.quiet);
44
55 use crate::response::FeroxResponse;
66 use crate::{
7 event_handlers::Handles,
78 message::FeroxMessage,
89 statistics::{StatError, StatField},
910 traits::FeroxFilter,
7778
7879 /// Break out of the (infinite) mpsc receive loop
7980 Exit,
81
82 /// Give a handler access to an Arc<Handles> instance after the handler has
83 /// already been initialized
84 AddHandles(Arc<Handles>),
8085 }
44 use futures::future::{BoxFuture, FutureExt};
55 use tokio::sync::{mpsc, oneshot};
66
7 use crate::statistics::StatField::TotalExpected;
87 use crate::{
98 config::Configuration,
109 progress::PROGRESS_PRINTER,
1110 response::FeroxResponse,
1211 scanner::RESPONSES,
1312 send_command, skip_fail,
14 statistics::StatField::ResourcesDiscovered,
13 statistics::StatField::{ResourcesDiscovered, TotalExpected},
1514 traits::FeroxSerialize,
1615 utils::{ferox_print, fmt_err, make_request, open_file, write_to},
1716 CommandReceiver, CommandSender, Joiner,
143142
144143 /// pointer to "global" configuration struct
145144 config: Arc<Configuration>,
145
146 /// handles instance
147 handles: Option<Arc<Handles>>,
146148 }
147149
148150 /// implementation of TermOutHandler
160162 tx_file,
161163 file_task,
162164 config,
165 handles: None,
163166 }
164167 }
165168
210213 }
211214 Command::Sync(sender) => {
212215 sender.send(true).unwrap_or_default();
216 }
217 Command::AddHandles(handles) => {
218 self.handles = Some(handles);
213219 }
214220 Command::Exit => {
215221 if self.file_task.is_some() && self.tx_file.send(Command::Exit).is_ok() {
235241 log::trace!("enter: process_response({:?}, {:?})", resp, call_type);
236242
237243 async move {
238 let contains_sentry = self.config.status_codes.contains(&resp.status().as_u16());
244 let should_filter = self
245 .handles
246 .as_ref()
247 .unwrap()
248 .filters
249 .data
250 .should_filter_response(&resp, self.handles.as_ref().unwrap().stats.tx.clone());
251
252 let contains_sentry = if !self.config.filter_status.is_empty() {
253 // -C was used, meaning -s was not and we should ignore the defaults
254 // https://github.com/epi052/feroxbuster/issues/535
255 // -C indicates that we should filter that status code, but allow all others
256 !self.config.filter_status.contains(&resp.status().as_u16())
257 } else {
258 // -C wasn't used, so, we defer to checking the -s values
259 self.config.status_codes.contains(&resp.status().as_u16())
260 };
261
239262 let unknown_sentry = !RESPONSES.contains(&resp); // !contains == unknown
240 let should_process_response = contains_sentry && unknown_sentry;
263 let should_process_response = contains_sentry && unknown_sentry && !should_filter;
241264
242265 if should_process_response {
243266 // print to stdout
283306 && matches!(call_type, ProcessResponseCall::Recursive)
284307 {
285308 // --collect-backups was used; the response is one we care about, and the function
286 // call came from the loop in `.start` (i.e. recursive was specified
309 // call came from the loop in `.start` (i.e. recursive was specified)
287310 let backup_urls = self.generate_backup_urls(&resp).await;
288311
289312 // need to manually adjust stats
397420 #[cfg(test)]
398421 mod tests {
399422 use super::*;
423 use crate::event_handlers::Command;
400424
401425 #[test]
402426 /// try to hit struct field coverage of FileOutHandler
416440 let (tx, rx) = mpsc::unbounded_channel::<Command>();
417441 let (tx_file, _) = mpsc::unbounded_channel::<Command>();
418442 let config = Arc::new(Configuration::new().unwrap());
443 let handles = Arc::new(Handles::for_testing(None, None).0);
419444
420445 let toh = TermOutHandler {
421446 config,
422447 file_task: None,
423448 receiver: rx,
424449 tx_file,
450 handles: Some(handles),
425451 };
426452
427453 println!("{:?}", toh);
434460 let (tx, rx) = mpsc::unbounded_channel::<Command>();
435461 let (tx_file, _) = mpsc::unbounded_channel::<Command>();
436462 let config = Arc::new(Configuration::new().unwrap());
463 let handles = Arc::new(Handles::for_testing(None, None).0);
437464
438465 let toh = TermOutHandler {
439466 config,
440467 file_task: None,
441468 receiver: rx,
442469 tx_file,
470 handles: Some(handles),
443471 };
444472
445473 let expected: Vec<_> = vec![
477505 let (tx, rx) = mpsc::unbounded_channel::<Command>();
478506 let (tx_file, _) = mpsc::unbounded_channel::<Command>();
479507 let config = Arc::new(Configuration::new().unwrap());
508 let handles = Arc::new(Handles::for_testing(None, None).0);
480509
481510 let toh = TermOutHandler {
482511 config,
483512 file_task: None,
484513 receiver: rx,
485514 tx_file,
515 handles: Some(handles),
486516 };
487517
488518 let expected: Vec<_> = vec![
520550 let (tx, rx) = mpsc::unbounded_channel::<Command>();
521551 let (tx_file, _) = mpsc::unbounded_channel::<Command>();
522552 let config = Arc::new(Configuration::new().unwrap());
553 let handles = Arc::new(Handles::for_testing(None, None).0);
523554
524555 let toh = TermOutHandler {
525556 config,
526557 file_task: None,
527558 receiver: rx,
528559 tx_file,
560 handles: Some(handles),
529561 };
530562
531563 let expected: Vec<_> = vec![
367367 async fn try_recursion(&mut self, response: Box<FeroxResponse>) -> Result<()> {
368368 log::trace!("enter: try_recursion({:?})", response,);
369369
370 if !response.is_directory() {
371 // not a directory, quick exit
370 if !self.handles.config.force_recursion && !response.is_directory() {
371 // not a directory and --force-recursion wasn't used, quick exit
372372 return Ok(());
373373 }
374374
11 use crate::{
22 client,
33 event_handlers::{
4 Command,
54 Command::{AddError, AddToUsizeField},
65 Handles,
76 },
1110 StatField::{LinksExtracted, TotalExpected},
1211 },
1312 url::FeroxUrl,
14 utils::{logged_request, make_request, should_deny_url},
13 utils::{logged_request, make_request, send_try_recursion_command, should_deny_url},
1514 ExtractionResult, DEFAULT_METHOD,
1615 };
1716 use anyhow::{bail, Context, Result};
1817 use reqwest::{Client, StatusCode, Url};
1918 use scraper::{Html, Selector};
2019 use std::collections::HashSet;
21 use tokio::sync::oneshot;
2220
2321 /// Whether an active scan is recursive or not
2422 #[derive(Debug)]
185183 resp.set_url(&format!("{}/", resp.url()));
186184 }
187185
188 self.handles
189 .send_scan_command(Command::TryRecursion(Box::new(resp)))?;
190 let (tx, rx) = oneshot::channel::<bool>();
191 self.handles.send_scan_command(Command::Sync(tx))?;
192 rx.await?;
186 if self.handles.config.filter_status.is_empty() {
187 // -C wasn't used, so -s is the only 'filter' left to account for
188 if self
189 .handles
190 .config
191 .status_codes
192 .contains(&resp.status().as_u16())
193 {
194 send_try_recursion_command(self.handles.clone(), resp).await?;
195 }
196 } else {
197 // -C was used, that means the filters above would have removed
198 // those responses, and anything else should be let through
199 send_try_recursion_command(self.handles.clone(), resp).await?;
200 }
193201 }
194202 }
195203 log::trace!("exit: request_links");
6464 /// Default wordlist to use when `-w|--wordlist` isn't specified and not `wordlist` isn't set
6565 /// in a [ferox-config.toml](constant.DEFAULT_CONFIG_NAME.html) config file.
6666 ///
67 /// defaults to kali's default install location:
67 /// defaults to kali's default install location on linux:
6868 /// - `/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt`
69 ///
70 /// and to the current directory on windows
71 /// - `.\seclists\Discovery\Web-Content\raft-medium-directories.txt`
72 #[cfg(not(target_os = "windows"))]
6973 pub const DEFAULT_WORDLIST: &str =
7074 "/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt";
75 #[cfg(target_os = "windows")]
76 pub const DEFAULT_WORDLIST: &str =
77 ".\\SecLists\\Discovery\\Web-Content\\raft-medium-directories.txt";
7178
7279 /// Number of milliseconds to wait between polls of `PAUSE_SCAN` when user pauses a scan
7380 pub(crate) const SLEEP_DURATION: u64 = 500;
2121 banner::{Banner, UPDATE_URL},
2222 config::{Configuration, OutputLevel},
2323 event_handlers::{
24 Command::{CreateBar, Exit, JoinTasks, LoadStats, ScanInitialUrls, UpdateWordlist},
24 Command::{
25 AddHandles, CreateBar, Exit, JoinTasks, LoadStats, ScanInitialUrls, UpdateWordlist,
26 },
2527 FiltersHandler, Handles, ScanHandler, StatsHandler, Tasks, TermInputHandler,
2628 TermOutHandler, SCAN_COMPLETE,
2729 },
219221 let (scan_task, scan_handle) = ScanHandler::initialize(handles.clone());
220222
221223 handles.set_scan_handle(scan_handle); // must be done after Handles initialization
224 handles.output.send(AddHandles(handles.clone()))?;
222225
223226 filters::initialize(handles.clone()).await?; // send user-supplied filters to the handler
224227
2222 document.number_of_terms += processed.len();
2323
2424 for normalized in processed {
25 if normalized.len() > 2 {
25 if normalized.len() >= 2 {
2626 document.add_term(&normalized)
2727 }
2828 }
3737 }
3838 }
3939
40 /// remove ascii and some utf-8 punctuation characters from the given string
40 /// replace ascii and some utf-8 punctuation characters with ' ' (space) in the given string
4141 fn remove_punctuation(text: &str) -> String {
42 // non-separator type chars can be replaced with an empty string, while separators are replaced
43 // with a space. This attempts to keep things like
44 // 'aboutblogfaqcontactpresstermslexicondisclosure' from happening
4542 text.replace(
4643 [
4744 '!', '\\', '"', '#', '$', '%', '&', '(', ')', '*', '+', ':', ';', '<', '=', '>', '?',
48 '@', '[', ']', '^', '{', '}', '|', '~', ',', '\'', '“', '”', '’', '‘', '’', '‘',
45 '@', '[', ']', '^', '{', '}', '|', '~', ',', '\'', '“', '”', '’', '‘', '’', '‘', '/',
46 '–', '—', '.',
4947 ],
50 "",
48 " ",
5149 )
52 .replace(['/', '–', '—', '.'], " ")
5350 }
5451
5552 /// remove stop words from the given string
8582 fn test_remove_punctuation() {
8683 let tester = "!\\\"#$%&()*+/:;<=>?@[]^{}|~,.'“”’‘–—\n‘’";
8784 // the `" \n"` is because of the things like / getting replaced with a space
88 assert_eq!(remove_punctuation(tester), " \n");
85 assert_eq!(
86 remove_punctuation(tester),
87 " \n "
88 );
8989 }
9090
9191 #[test]
114114 /// ensure preprocess
115115 fn test_preprocess_results() {
116116 let tester = "WHY are Y'all YELLing?";
117 assert_eq!(&preprocess(tester), &["yall", "yelling"]);
117 assert_eq!(&preprocess(tester), &["y", "all", "yelling"]);
118118 }
119119
120120 #[test]
332332 .multiple_values(true)
333333 .multiple_occurrences(true)
334334 .use_value_delimiter(true)
335 .conflicts_with("status_codes")
335336 .help_heading("Response filters")
336337 .help(
337338 "Filter out status codes (deny list) (ex: -C 200 -C 401)",
425426 .takes_value(true)
426427 .help_heading("Scan settings")
427428 .help("Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)"),
429 ).arg(
430 Arg::new("force_recursion")
431 .long("force-recursion")
432 .conflicts_with("no_recursion")
433 .help_heading("Scan settings")
434 .help("Force recursion attempts on all 'found' endpoints (still respects recursion depth)"),
428435 ).arg(
429436 Arg::new("extract_links")
430437 .short('e')
667674 cat targets | ./feroxbuster --stdin --silent -s 200 301 302 --redirects -x js | fff -s 200 -o js-files
668675
669676 Proxy traffic through Burp
670 ./feroxbuster -u http://127.1 --insecure --proxy http://127.0.0.1:8080
677 ./feroxbuster -u http://127.1 --burp
671678
672679 Proxy traffic through a SOCKS proxy
673680 ./feroxbuster -u http://127.1 --proxy socks5://127.0.0.1:9050
278278 if handles
279279 .config
280280 .status_codes
281 .contains(&self.status().as_u16())
281 .contains(&self.status().as_u16()) // in -s list
282 // or -C was used, and -s should be all responses that aren't filtered
283 || !handles.config.filter_status.is_empty()
282284 {
283285 // only add extensions to those responses that pass our checks; filtered out
284286 // status codes are handled by should_filter, but we need to still check against
451451 r#""quiet":false"#,
452452 r#""auto_bail":false"#,
453453 r#""auto_tune":false"#,
454 r#""force_recursion":false"#,
454455 r#""json":false"#,
455456 r#""output":"""#,
456457 r#""debug_log":"""#,
77 use lazy_static::lazy_static;
88 use leaky_bucket::LeakyBucket;
99 use tokio::{
10 sync::{oneshot, RwLock},
10 sync::RwLock,
1111 time::{sleep, Duration},
1212 };
1313
1515 atomic_load, atomic_store,
1616 config::RequesterPolicy,
1717 event_handlers::{
18 Command::{self, AddError, SubtractFromUsizeField},
18 Command::{AddError, SubtractFromUsizeField},
1919 Handles,
2020 },
2121 extractor::{ExtractionTarget, ExtractorBuilder},
2424 scan_manager::{FeroxScan, ScanStatus},
2525 statistics::{StatError::Other, StatField::TotalExpected},
2626 url::FeroxUrl,
27 utils::{logged_request, should_deny_url},
27 utils::{logged_request, send_try_recursion_command, should_deny_url},
2828 HIGH_ERROR_RATIO,
2929 };
3030
378378 .await;
379379
380380 // do recursion if appropriate
381 if !self.handles.config.no_recursion {
382 self.handles
383 .send_scan_command(Command::TryRecursion(Box::new(
384 ferox_response.clone(),
385 )))?;
386 let (tx, rx) = oneshot::channel::<bool>();
387 self.handles.send_scan_command(Command::Sync(tx))?;
388 rx.await?;
381 if !self.handles.config.no_recursion && !self.handles.config.force_recursion {
382 // to support --force-recursion, we want to limit recursive calls to only
383 // 'found' assets. That means we need to either gate or delay the call.
384 //
385 // this branch will retain the 'old' behavior by checking that
386 // --force-recursion isn't turned on
387 send_try_recursion_command(self.handles.clone(), ferox_response.clone())
388 .await?;
389389 }
390390
391391 // purposefully doing recursion before filtering. the thought process is that
397397 .should_filter_response(&ferox_response, self.handles.stats.tx.clone())
398398 {
399399 continue;
400 }
401
402 if !self.handles.config.no_recursion && self.handles.config.force_recursion {
403 // in this branch, we're saying that both recursion AND force recursion
404 // are turned on. It comes after should_filter_response, so those cases
405 // are handled. Now we need to account for -s/-C options.
406
407 if self.handles.config.filter_status.is_empty() {
408 // -C wasn't used, so -s is the only 'filter' left to account for
409 if self
410 .handles
411 .config
412 .status_codes
413 .contains(&ferox_response.status().as_u16())
414 {
415 send_try_recursion_command(
416 self.handles.clone(),
417 ferox_response.clone(),
418 )
419 .await?;
420 }
421 } else {
422 // -C was used, that means the filters above would have removed
423 // those responses, and anything else should be let through
424 send_try_recursion_command(self.handles.clone(), ferox_response.clone())
425 .await?;
426 }
400427 }
401428
402429 if self.handles.config.collect_extensions {
468495 use crate::{
469496 config::Configuration,
470497 config::OutputLevel,
498 event_handlers::Command::AddStatus,
471499 event_handlers::{FiltersHandler, ScanHandler, StatsHandler, Tasks, TermOutHandler},
472500 filters,
473501 scan_manager::{ScanOrder, ScanType},
508536 /// helper to stay DRY
509537 async fn increment_errors(handles: Arc<Handles>, scan: Arc<FeroxScan>, num_errors: usize) {
510538 for _ in 0..num_errors {
511 handles
512 .stats
513 .send(Command::AddError(StatError::Other))
514 .unwrap();
539 handles.stats.send(AddError(StatError::Other)).unwrap();
515540 scan.add_error();
516541 }
517542
548573 code: StatusCode,
549574 ) {
550575 for _ in 0..num_codes {
551 handles.stats.send(Command::AddStatus(code)).unwrap();
576 handles.stats.send(AddStatus(code)).unwrap();
552577 if code == StatusCode::FORBIDDEN {
553578 scan.add_403();
554579 } else {
1111 time::Duration,
1212 time::{SystemTime, UNIX_EPOCH},
1313 };
14 use tokio::sync::mpsc::UnboundedSender;
15
16 use crate::config::Configuration;
14 use tokio::sync::{mpsc::UnboundedSender, oneshot};
15
1716 use crate::{
17 config::Configuration,
1818 config::OutputLevel,
1919 event_handlers::{
2020 Command::{self, AddError, AddStatus},
2121 Handles,
2222 },
2323 progress::PROGRESS_PRINTER,
24 response::FeroxResponse,
2425 send_command,
2526 statistics::StatError::{Connection, Other, Redirection, Request, Timeout},
2627 traits::FeroxSerialize,
6465 /// simple wrapper to stay DRY
6566 pub fn fmt_err(msg: &str) -> String {
6667 format!("{}: {}", status_colorizer("ERROR"), msg)
68 }
69
70 /// given a FeroxResponse, send a TryRecursion command
71 ///
72 /// moved to utils to allow for calls from extractor and scanner
73 pub(crate) async fn send_try_recursion_command(
74 handles: Arc<Handles>,
75 response: FeroxResponse,
76 ) -> Result<()> {
77 handles.send_scan_command(Command::TryRecursion(Box::new(response.clone())))?;
78 let (tx, rx) = oneshot::channel::<bool>();
79 handles.send_scan_command(Command::Sync(tx))?;
80 rx.await?;
81 Ok(())
6782 }
6883
6984 /// Takes in a string and colors it using console::style
783783 .and(predicate::str::contains("http://localhost"))
784784 .and(predicate::str::contains("Threads"))
785785 .and(predicate::str::contains("Wordlist"))
786 .and(predicate::str::contains("Status Codes"))
787786 .and(predicate::str::contains("Timeout (secs)"))
788787 .and(predicate::str::contains("User-Agent"))
789788 .and(predicate::str::contains("Status Code Filters"))
13931392 .and(predicate::str::contains("─┴─")),
13941393 );
13951394 }
1395
1396 #[test]
1397 /// test allows non-existent wordlist to trigger the banner printing to stderr
1398 /// expect to see all mandatory prints + force recursion
1399 fn banner_prints_force_recursion() {
1400 Command::cargo_bin("feroxbuster")
1401 .unwrap()
1402 .arg("--url")
1403 .arg("http://localhost")
1404 .arg("--force-recursion")
1405 .arg("--wordlist")
1406 .arg("/definitely/doesnt/exist/0cd7fed0-47f4-4b18-a1b0-ac39708c1676")
1407 .assert()
1408 .success()
1409 .stderr(
1410 predicate::str::contains("─┬─")
1411 .and(predicate::str::contains("Target Url"))
1412 .and(predicate::str::contains("http://localhost"))
1413 .and(predicate::str::contains("Threads"))
1414 .and(predicate::str::contains("Wordlist"))
1415 .and(predicate::str::contains("Status Codes"))
1416 .and(predicate::str::contains("Timeout (secs)"))
1417 .and(predicate::str::contains("User-Agent"))
1418 .and(predicate::str::contains("Force Recursion"))
1419 .and(predicate::str::contains("─┴─")),
1420 );
1421 }
268268 Ok(())
269269 }
270270
271 #[test]
272 /// test finds a static wildcard and reports as much to stdout
273 fn heuristics_wildcard_test_with_two_static_wildcards() {
274 let srv = MockServer::start();
275 let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
271 // #[test]
272 // /// test finds a static wildcard and reports as much to stdout
273 // fn heuristics_wildcard_test_with_two_static_wildcards() {
274 // let srv = MockServer::start();
275 // let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
276
277 // let mock = srv.mock(|when, then| {
278 // when.method(GET)
279 // .path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
280 // then.status(200)
281 // .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
282 // });
283
284 // let mock2 = srv.mock(|when, then| {
285 // when.method(GET)
286 // .path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
287 // then.status(200)
288 // .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
289 // });
290
291 // let cmd = Command::cargo_bin("feroxbuster")
292 // .unwrap()
293 // .arg("--url")
294 // .arg(srv.url("/"))
295 // .arg("--wordlist")
296 // .arg(file.as_os_str())
297 // .arg("--add-slash")
298 // .arg("--threads")
299 // .arg("1")
300 // .unwrap();
301
302 // teardown_tmp_directory(tmp_dir);
303
304 // cmd.assert().success().stdout(
305 // predicate::str::contains("WLD")
306 // .and(predicate::str::contains("Got"))
307 // .and(predicate::str::contains("200"))
308 // .and(predicate::str::contains("(url length: 32)"))
309 // .and(predicate::str::contains("(url length: 96)"))
310 // .and(predicate::str::contains(
311 // "Wildcard response is static; auto-filtering 46",
312 // )),
313 // );
314
315 // assert_eq!(mock.hits(), 1);
316 // assert_eq!(mock2.hits(), 1);
317 // }
318
319 #[test]
320 /// test finds a static wildcard and reports nothing to stdout
321 fn heuristics_wildcard_test_with_two_static_wildcards_with_silent_enabled(
322 ) -> Result<(), Box<dyn std::error::Error>> {
323 let srv = MockServer::start();
324 let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
276325
277326 let mock = srv.mock(|when, then| {
278327 when.method(GET)
295344 .arg("--wordlist")
296345 .arg(file.as_os_str())
297346 .arg("--add-slash")
347 .arg("--silent")
348 .arg("--threads")
349 .arg("1")
298350 .unwrap();
299351
300352 teardown_tmp_directory(tmp_dir);
301353
302 cmd.assert().success().stdout(
303 predicate::str::contains("WLD")
304 .and(predicate::str::contains("Got"))
305 .and(predicate::str::contains("200"))
306 .and(predicate::str::contains("(url length: 32)"))
307 .and(predicate::str::contains("(url length: 96)"))
308 .and(predicate::str::contains(
309 "Wildcard response is static; auto-filtering 46",
310 )),
311 );
354 cmd.assert().success().stdout(predicate::str::is_empty());
312355
313356 assert_eq!(mock.hits(), 1);
314357 assert_eq!(mock2.hits(), 1);
315 }
316
317 #[test]
318 /// test finds a static wildcard and reports nothing to stdout
319 fn heuristics_wildcard_test_with_two_static_wildcards_with_silent_enabled(
320 ) -> Result<(), Box<dyn std::error::Error>> {
321 let srv = MockServer::start();
322 let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
323
324 let mock = srv.mock(|when, then| {
325 when.method(GET)
326 .path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
327 then.status(200)
328 .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
329 });
330
331 let mock2 = srv.mock(|when, then| {
332 when.method(GET)
333 .path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
334 then.status(200)
335 .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
336 });
337
338 let cmd = Command::cargo_bin("feroxbuster")
339 .unwrap()
340 .arg("--url")
341 .arg(srv.url("/"))
342 .arg("--wordlist")
343 .arg(file.as_os_str())
344 .arg("--add-slash")
345 .arg("--silent")
346 .unwrap();
347
348 teardown_tmp_directory(tmp_dir);
349
350 cmd.assert().success().stdout(predicate::str::is_empty());
351
352 assert_eq!(mock.hits(), 1);
353 assert_eq!(mock2.hits(), 1);
354 Ok(())
355 }
356
357 #[test]
358 /// test finds a static wildcard and reports as much to stdout and a file
359 fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file() {
360 let srv = MockServer::start();
361 let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
362 let outfile = tmp_dir.path().join("outfile");
363
364 let mock = srv.mock(|when, then| {
365 when.method(GET)
366 .path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
367 then.status(200)
368 .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
369 });
370
371 let mock2 = srv.mock(|when, then| {
372 when.method(GET)
373 .path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
374 then.status(200)
375 .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
376 });
377
378 let cmd = Command::cargo_bin("feroxbuster")
379 .unwrap()
380 .arg("--url")
381 .arg(srv.url("/"))
382 .arg("--wordlist")
383 .arg(file.as_os_str())
384 .arg("--add-slash")
385 .arg("--output")
386 .arg(outfile.as_os_str())
387 .unwrap();
388
389 let contents = std::fs::read_to_string(outfile).unwrap();
390
391 teardown_tmp_directory(tmp_dir);
392
393 assert!(contents.contains("WLD"));
394 assert!(contents.contains("Got"));
395 assert!(contents.contains("200"));
396 assert!(contents.contains("(url length: 32)"));
397 assert!(contents.contains("(url length: 96)"));
398
399 cmd.assert().success().stdout(
400 predicate::str::contains("WLD")
401 .and(predicate::str::contains("Got"))
402 .and(predicate::str::contains("200"))
403 .and(predicate::str::contains("(url length: 32)"))
404 .and(predicate::str::contains("(url length: 96)"))
405 .and(predicate::str::contains(
406 "Wildcard response is static; auto-filtering 46",
407 )),
408 );
409
410 assert_eq!(mock.hits(), 1);
411 assert_eq!(mock2.hits(), 1);
412 }
413
414 #[test]
415 /// test finds a static wildcard that returns 3xx, expect redirects to => in response as well as
416 /// in the output file
417 fn heuristics_wildcard_test_with_redirect_as_response_code(
418 ) -> Result<(), Box<dyn std::error::Error>> {
419 let srv = MockServer::start();
420 let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
421 let outfile = tmp_dir.path().join("outfile");
422
423 let mock = srv.mock(|when, then| {
424 when.method(GET)
425 .path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
426 then.status(301)
427 .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
428 });
429
430 let mock2 = srv.mock(|when, then| {
431 when.method(GET)
432 .path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
433 then.status(301)
434 .header("Location", &srv.url("/some-redirect"))
435 .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
436 });
437
438 let cmd = Command::cargo_bin("feroxbuster")
439 .unwrap()
440 .arg("--url")
441 .arg(srv.url("/"))
442 .arg("--wordlist")
443 .arg(file.as_os_str())
444 .arg("--add-slash")
445 .arg("--output")
446 .arg(outfile.as_os_str())
447 .unwrap();
448
449 let contents = std::fs::read_to_string(outfile).unwrap();
450
451 teardown_tmp_directory(tmp_dir);
452
453 assert!(contents.contains("WLD"));
454 assert!(contents.contains("301"));
455 assert!(contents.contains("/some-redirect"));
456 assert!(contents.contains(" => "));
457 assert!(contents.contains(&srv.url("/")));
458 assert!(contents.contains("(url length: 32)"));
459
460 cmd.assert().success().stdout(
461 predicate::str::contains(" => ")
462 .and(predicate::str::contains("/some-redirect"))
463 .and(predicate::str::contains("301"))
464 .and(predicate::str::contains(srv.url("/")))
465 .and(predicate::str::contains("(url length: 32)"))
466 .and(predicate::str::contains("WLD")),
467 );
468
469 assert_eq!(mock.hits(), 1);
470 assert_eq!(mock2.hits(), 1);
471 Ok(())
472 }
358 Ok(())
359 }
360
361 // #[test]
362 // /// test finds a static wildcard and reports as much to stdout and a file
363 // fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file() {
364 // let srv = MockServer::start();
365 // let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
366 // let outfile = tmp_dir.path().join("outfile");
367
368 // let mock = srv.mock(|when, then| {
369 // when.method(GET)
370 // .path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
371 // then.status(200)
372 // .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
373 // });
374
375 // let mock2 = srv.mock(|when, then| {
376 // when.method(GET)
377 // .path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
378 // then.status(200)
379 // .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
380 // });
381
382 // let cmd = Command::cargo_bin("feroxbuster")
383 // .unwrap()
384 // .arg("--url")
385 // .arg(srv.url("/"))
386 // .arg("--wordlist")
387 // .arg(file.as_os_str())
388 // .arg("--add-slash")
389 // .arg("--output")
390 // .arg(outfile.as_os_str())
391 // .arg("--threads")
392 // .arg("1")
393 // .unwrap();
394
395 // let contents = std::fs::read_to_string(outfile).unwrap();
396
397 // teardown_tmp_directory(tmp_dir);
398
399 // assert!(contents.contains("WLD"));
400 // assert!(contents.contains("Got"));
401 // assert!(contents.contains("200"));
402 // assert!(contents.contains("(url length: 32)"));
403 // assert!(contents.contains("(url length: 96)"));
404
405 // cmd.assert().success().stdout(
406 // predicate::str::contains("WLD")
407 // .and(predicate::str::contains("Got"))
408 // .and(predicate::str::contains("200"))
409 // .and(predicate::str::contains("(url length: 32)"))
410 // .and(predicate::str::contains("(url length: 96)"))
411 // .and(predicate::str::contains(
412 // "Wildcard response is static; auto-filtering 46",
413 // )),
414 // );
415
416 // assert_eq!(mock.hits(), 1);
417 // assert_eq!(mock2.hits(), 1);
418 // }
419
420 // #[test]
421 // /// test finds a static wildcard that returns 3xx, expect redirects to => in response as well as
422 // /// in the output file
423 // fn heuristics_wildcard_test_with_redirect_as_response_code(
424 // ) -> Result<(), Box<dyn std::error::Error>> {
425 // let srv = MockServer::start();
426
427 // let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
428 // let outfile = tmp_dir.path().join("outfile");
429
430 // let mock = srv.mock(|when, then| {
431 // when.method(GET)
432 // .path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
433 // then.status(301)
434 // .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
435 // });
436
437 // let mock2 = srv.mock(|when, then| {
438 // when.method(GET)
439 // .path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
440 // then.status(301)
441 // .header("Location", &srv.url("/some-redirect"))
442 // .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
443 // });
444
445 // let cmd = Command::cargo_bin("feroxbuster")
446 // .unwrap()
447 // .arg("--url")
448 // .arg(srv.url("/"))
449 // .arg("--wordlist")
450 // .arg(file.as_os_str())
451 // .arg("--add-slash")
452 // .arg("--output")
453 // .arg(outfile.as_os_str())
454 // .arg("--threads")
455 // .arg("1")
456 // .unwrap();
457
458 // let contents = std::fs::read_to_string(outfile).unwrap();
459
460 // teardown_tmp_directory(tmp_dir);
461
462 // assert!(contents.contains("WLD"));
463 // assert!(contents.contains("301"));
464 // assert!(contents.contains("/some-redirect"));
465 // assert!(contents.contains(" => "));
466 // assert!(contents.contains(&srv.url("/")));
467 // assert!(contents.contains("(url length: 32)"));
468
469 // cmd.assert().success().stdout(
470 // predicate::str::contains(" => ")
471 // .and(predicate::str::contains("/some-redirect"))
472 // .and(predicate::str::contains("301"))
473 // .and(predicate::str::contains(srv.url("/")))
474 // .and(predicate::str::contains("(url length: 32)"))
475 // .and(predicate::str::contains("WLD")),
476 // );
477
478 // assert_eq!(mock.hits(), 1);
479 // assert_eq!(mock2.hits(), 1);
480 // Ok(())
481 // }
482
483 // todo figure out why ci hates these tests
851851
852852 teardown_tmp_directory(tmp_dir);
853853 }
854
855 #[test]
856 /// send a request to an endpoint that has abnormal redirect logic, ala fast-api
857 fn scanner_forced_recursion_ignores_normal_redirect_logic() -> Result<(), Box<dyn std::error::Error>>
858 {
859 let srv = MockServer::start();
860 let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
861
862 let mock1 = srv.mock(|when, then| {
863 when.method(GET).path("/LICENSE");
864 then.status(301)
865 .body("this is a test")
866 .header("Location", &srv.url("/LICENSE"));
867 });
868
869 let mock2 = srv.mock(|when, then| {
870 when.method(GET).path("/LICENSE/LICENSE");
871 then.status(404);
872 });
873
874 let mock3 = srv.mock(|when, then| {
875 when.method(GET).path("/LICENSE/LICENSE/LICENSE");
876 then.status(404);
877 });
878
879 let mock4 = srv.mock(|when, then| {
880 when.method(GET).path("/LICENSE/LICENSE/LICENSE/LICENSE");
881 then.status(404);
882 });
883
884 let outfile = tmp_dir.path().join("output");
885
886 Command::cargo_bin("feroxbuster")
887 .unwrap()
888 .arg("--url")
889 .arg(srv.url("/"))
890 .arg("--wordlist")
891 .arg(file.as_os_str())
892 .arg("--force-recursion")
893 .arg("-o")
894 .arg(outfile.as_os_str())
895 .unwrap();
896
897 let contents = std::fs::read_to_string(outfile)?;
898 println!("{}", contents);
899
900 assert!(contents.contains("/LICENSE"));
901 assert!(contents.contains("301"));
902 assert!(contents.contains("14"));
903
904 assert_eq!(mock1.hits(), 2);
905 assert_eq!(mock2.hits(), 1);
906 assert_eq!(mock3.hits(), 0);
907 assert_eq!(mock4.hits(), 0);
908
909 teardown_tmp_directory(tmp_dir);
910
911 Ok(())
912 }