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
389 | 389 | "name": "Jason Haddix", |
390 | 390 | "avatar_url": "https://avatars.githubusercontent.com/u/3488554?v=4", |
391 | 391 | "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", | |
392 | 420 | "contributions": [ |
393 | 421 | "ideas" |
394 | 422 | ] |
72 | 72 | name: ${{ matrix.name }}.tar.gz |
73 | 73 | path: ${{ matrix.name }}.tar.gz |
74 | 74 | |
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/* | |
87 | 91 | |
88 | 92 | build-macos: |
89 | 93 | env: |
7 | 7 | runs-on: ubuntu-latest |
8 | 8 | steps: |
9 | 9 | - uses: actions/checkout@v2 |
10 | - uses: actions-rs/toolchain@v1 | |
11 | with: | |
12 | profile: minimal | |
13 | toolchain: stable | |
14 | override: true | |
15 | 10 | - uses: actions-rs/cargo@v1 |
16 | 11 | with: |
17 | 12 | command: check |
21 | 16 | runs-on: ubuntu-latest |
22 | 17 | steps: |
23 | 18 | - 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 | |
25 | 23 | 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 | |
32 | 26 | |
33 | 27 | fmt: |
34 | 28 | name: Rust fmt |
35 | 29 | runs-on: ubuntu-latest |
36 | 30 | steps: |
37 | 31 | - 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 | |
44 | 32 | - uses: actions-rs/cargo@v1 |
45 | 33 | with: |
46 | 34 | command: fmt |
51 | 39 | runs-on: ubuntu-latest |
52 | 40 | steps: |
53 | 41 | - 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 | |
60 | 42 | - uses: actions-rs/cargo@v1 |
61 | 43 | with: |
62 | 44 | 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 |
2 | 2 | name: Code Coverage Pipeline |
3 | 3 | |
4 | 4 | jobs: |
5 | upload-coverage: | |
5 | coverage: | |
6 | name: LLVM Coverage | |
6 | 7 | runs-on: ubuntu-latest |
7 | 8 | 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 | |
35 | 20 | with: |
36 | 21 | token: ${{ secrets.CODECOV_TOKEN }} |
37 | file: ./coverage.xml | |
38 | name: codecov-umbrella | |
22 | files: lcov.info | |
39 | 23 | fail_ci_if_error: true |
40 | - uses: actions/upload-artifact@v2 | |
41 | with: | |
42 | name: coverage.xml | |
43 | path: ./coverage.xml |
670 | 670 | |
671 | 671 | [[package]] |
672 | 672 | name = "feroxbuster" |
673 | version = "2.6.4" | |
673 | version = "2.7.0" | |
674 | 674 | dependencies = [ |
675 | 675 | "anyhow", |
676 | 676 | "assert_cmd", |
0 | 0 | [package] |
1 | 1 | name = "feroxbuster" |
2 | version = "2.6.4" | |
2 | version = "2.7.0" | |
3 | 3 | authors = ["Ben 'epi' Risher (@epi052)"] |
4 | 4 | license = "MIT" |
5 | 5 | edition = "2021" |
7 | 7 | repository = "https://github.com/epi052/feroxbuster" |
8 | 8 | description = "A fast, simple, recursive content discovery tool." |
9 | 9 | 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 | ] | |
11 | 17 | exclude = [".github/*", "img/*", "check-coverage.sh"] |
12 | 18 | build = "build.rs" |
13 | 19 | |
48 | 54 | ctrlc = "3.2.1" |
49 | 55 | fuzzyhash = "0.2.1" |
50 | 56 | 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 | |
52 | 58 | |
53 | 59 | [dev-dependencies] |
54 | 60 | tempfile = "3.3.0" |
66 | 72 | license-file = ["LICENSE", "4"] |
67 | 73 | conf-files = ["/etc/feroxbuster/ferox-config.toml"] |
68 | 74 | 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 | ], | |
74 | 100 | ] |
200 | 200 | </tr> |
201 | 201 | <tr> |
202 | 202 | <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> | |
204 | 204 | <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> |
205 | 205 | <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> |
206 | 206 | <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> |
234 | 234 | <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> |
235 | 235 | <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> |
236 | 236 | </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> | |
237 | 242 | </table> |
238 | 243 | |
239 | 244 | <!-- markdownlint-restore --> |
241 | 246 | |
242 | 247 | <!-- ALL-CONTRIBUTORS-LIST:END --> |
243 | 248 | |
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! |
44 | 44 | # dont_filter = true |
45 | 45 | # extract_links = true |
46 | 46 | # depth = 1 |
47 | # force_recursion = true | |
47 | 48 | # filter_size = [5174] |
48 | 49 | # filter_regex = ["^ignore me$"] |
49 | 50 | # filter_similar = ["https://somesite.com/soft404"] |
23 | 23 | '--replay-proxy=[Send only unfiltered requests through a Replay Proxy, instead of all requests]:REPLAY_PROXY:_urls' \ |
24 | 24 | '*-R+[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \ |
25 | 25 | '*--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: ' \ | |
28 | 28 | '*-x+[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \ |
29 | 29 | '*--extensions=[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \ |
30 | 30 | '*-m+[Which HTTP request method(s) should be sent (default: GET)]:HTTP_METHODS: ' \ |
45 | 45 | '*--filter-words=[Filter out messages of a particular word count (ex: -W 312 -W 91,82)]:WORDS: ' \ |
46 | 46 | '*-N+[Filter out messages of a particular line count (ex: -N 20 -N 31,30)]:LINES: ' \ |
47 | 47 | '*--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: ' \ | |
50 | 50 | '*--filter-similar-to=[Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)]:UNWANTED_PAGE:_urls' \ |
51 | 51 | '*-s+[Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)]:STATUS_CODE: ' \ |
52 | 52 | '*--status-codes=[Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)]:STATUS_CODE: ' \ |
87 | 87 | '--insecure[Disables TLS certificate validation in the client]' \ |
88 | 88 | '-n[Do not scan recursively]' \ |
89 | 89 | '--no-recursion[Do not scan recursively]' \ |
90 | '(-n --no-recursion)--force-recursion[Force recursion attempts on all '\''found'\'' endpoints (still respects recursion depth)]' \ | |
90 | 91 | '-e[Extract links from response body (html, javascript, etc...); make new requests based on findings]' \ |
91 | 92 | '--extract-links[Extract links from response body (html, javascript, etc...); make new requests based on findings]' \ |
92 | 93 | '(--auto-bail)--auto-tune[Automatically lower scan rate when an excessive amount of errors are encountered]' \ |
29 | 29 | [CompletionResult]::new('--replay-proxy', 'replay-proxy', [CompletionResultType]::ParameterName, 'Send only unfiltered requests through a Replay Proxy, instead of all requests') |
30 | 30 | [CompletionResult]::new('-R', 'R', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)') |
31 | 31 | [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)') | |
34 | 34 | [CompletionResult]::new('-x', 'x', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)') |
35 | 35 | [CompletionResult]::new('--extensions', 'extensions', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)') |
36 | 36 | [CompletionResult]::new('-m', 'm', [CompletionResultType]::ParameterName, 'Which HTTP request method(s) should be sent (default: GET)') |
93 | 93 | [CompletionResult]::new('--insecure', 'insecure', [CompletionResultType]::ParameterName, 'Disables TLS certificate validation in the client') |
94 | 94 | [CompletionResult]::new('-n', 'n', [CompletionResultType]::ParameterName, 'Do not scan recursively') |
95 | 95 | [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)') | |
96 | 97 | [CompletionResult]::new('-e', 'e', [CompletionResultType]::ParameterName, 'Extract links from response body (html, javascript, etc...); make new requests based on findings') |
97 | 98 | [CompletionResult]::new('--extract-links', 'extract-links', [CompletionResultType]::ParameterName, 'Extract links from response body (html, javascript, etc...); make new requests based on findings') |
98 | 99 | [CompletionResult]::new('--auto-tune', 'auto-tune', [CompletionResultType]::ParameterName, 'Automatically lower scan rate when an excessive amount of errors are encountered') |
18 | 18 | |
19 | 19 | case "${cmd}" in |
20 | 20 | 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" | |
22 | 22 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then |
23 | 23 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) |
24 | 24 | return 0 |
26 | 26 | cand --replay-proxy 'Send only unfiltered requests through a Replay Proxy, instead of all requests' |
27 | 27 | cand -R 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)' |
28 | 28 | 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)' | |
31 | 31 | cand -x 'File extension(s) to search for (ex: -x php -x pdf js)' |
32 | 32 | cand --extensions 'File extension(s) to search for (ex: -x php -x pdf js)' |
33 | 33 | cand -m 'Which HTTP request method(s) should be sent (default: GET)' |
90 | 90 | cand --insecure 'Disables TLS certificate validation in the client' |
91 | 91 | cand -n 'Do not scan recursively' |
92 | 92 | cand --no-recursion 'Do not scan recursively' |
93 | cand --force-recursion 'Force recursion attempts on all ''found'' endpoints (still respects recursion depth)' | |
93 | 94 | cand -e 'Extract links from response body (html, javascript, etc...); make new requests based on findings' |
94 | 95 | cand --extract-links 'Extract links from response body (html, javascript, etc...); make new requests based on findings' |
95 | 96 | cand --auto-tune 'Automatically lower scan rate when an excessive amount of errors are encountered' |
162 | 162 | |
163 | 163 | /// represents Configuration.collect_words |
164 | 164 | collect_words: BannerEntry, |
165 | ||
166 | /// represents Configuration.collect_words | |
167 | force_recursion: BannerEntry, | |
165 | 168 | } |
166 | 169 | |
167 | 170 | /// implementation of Banner |
299 | 302 | &config.scan_limit.to_string(), |
300 | 303 | ); |
301 | 304 | |
305 | let force_recursion = | |
306 | BannerEntry::new("🤘", "Force Recursion", &config.force_recursion.to_string()); | |
302 | 307 | let replay_proxy = BannerEntry::new("🎥", "Replay Proxy", &config.replay_proxy); |
303 | 308 | let auto_tune = BannerEntry::new("🎶", "Auto Tune", &config.auto_tune.to_string()); |
304 | 309 | let auto_bail = BannerEntry::new("🪣", "Auto Bail", &config.auto_bail.to_string()); |
408 | 413 | no_recursion, |
409 | 414 | rate_limit, |
410 | 415 | scan_limit, |
416 | force_recursion, | |
411 | 417 | time_limit, |
412 | 418 | url_denylist, |
413 | 419 | collect_extensions, |
510 | 516 | |
511 | 517 | writeln!(&mut writer, "{}", self.threads)?; |
512 | 518 | 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 { | |
518 | 525 | writeln!(&mut writer, "{}", self.filter_status)?; |
519 | 526 | } |
520 | 527 | |
640 | 647 | } |
641 | 648 | |
642 | 649 | writeln!(&mut writer, "{}", self.no_recursion)?; |
650 | ||
651 | if config.force_recursion { | |
652 | writeln!(&mut writer, "{}", self.force_recursion)?; | |
653 | } | |
643 | 654 | |
644 | 655 | if config.scan_limit > 0 { |
645 | 656 | writeln!(&mut writer, "{}", self.scan_limit)?; |
280 | 280 | /// Automatically discover important words from within responses and add them to the wordlist |
281 | 281 | #[serde(default)] |
282 | 282 | pub collect_words: bool, |
283 | ||
284 | /// override recursion logic to always attempt recursion, still respects --depth | |
285 | #[serde(default)] | |
286 | pub force_recursion: bool, | |
283 | 287 | } |
284 | 288 | |
285 | 289 | impl Default for Configuration { |
328 | 332 | collect_backups: false, |
329 | 333 | collect_words: false, |
330 | 334 | save_state: true, |
335 | force_recursion: false, | |
331 | 336 | proxy: String::new(), |
332 | 337 | config: String::new(), |
333 | 338 | output: String::new(), |
404 | 409 | /// - **json**: `false` |
405 | 410 | /// - **dont_filter**: `false` (auto filter wildcard responses) |
406 | 411 | /// - **depth**: `4` (maximum recursion depth) |
412 | /// - **force_recursion**: `false` (still respects recursion depth) | |
407 | 413 | /// - **scan_limit**: `0` (no limit on concurrent scans imposed) |
408 | 414 | /// - **parallel**: `0` (no limit on parallel scans imposed) |
409 | 415 | /// - **rate_limit**: `0` (no limit on requests per second imposed) |
773 | 779 | config.json = true; |
774 | 780 | } |
775 | 781 | |
782 | if args.is_present("force_recursion") { | |
783 | config.force_recursion = true; | |
784 | } | |
785 | ||
776 | 786 | //// |
777 | 787 | // organizational breakpoint; all options below alter the Client configuration |
778 | 788 | //// |
941 | 951 | update_if_not_default!(&mut conf.output, new.output, ""); |
942 | 952 | update_if_not_default!(&mut conf.redirects, new.redirects, false); |
943 | 953 | update_if_not_default!(&mut conf.insecure, new.insecure, false); |
954 | update_if_not_default!(&mut conf.force_recursion, new.force_recursion, false); | |
944 | 955 | update_if_not_default!(&mut conf.extract_links, new.extract_links, false); |
945 | 956 | update_if_not_default!(&mut conf.extensions, new.extensions, Vec::<String>::new()); |
946 | 957 | update_if_not_default!(&mut conf.methods, new.methods, Vec::<String>::new()); |
48 | 48 | json = true |
49 | 49 | save_state = false |
50 | 50 | depth = 1 |
51 | force_recursion = true | |
51 | 52 | filter_size = [4120] |
52 | 53 | filter_regex = ["^ignore me$"] |
53 | 54 | filter_similar = ["https://somesite.com/soft404"] |
94 | 95 | assert!(config.save_state); |
95 | 96 | assert!(!config.stdin); |
96 | 97 | assert!(!config.add_slash); |
98 | assert!(!config.force_recursion); | |
97 | 99 | assert!(!config.redirects); |
98 | 100 | assert!(!config.extract_links); |
99 | 101 | assert!(!config.insecure); |
209 | 211 | |
210 | 212 | #[test] |
211 | 213 | /// 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 | |
212 | 221 | fn config_reads_quiet() { |
213 | 222 | let config = setup_config_test(); |
214 | 223 | assert!(config.quiet); |
4 | 4 | |
5 | 5 | use crate::response::FeroxResponse; |
6 | 6 | use crate::{ |
7 | event_handlers::Handles, | |
7 | 8 | message::FeroxMessage, |
8 | 9 | statistics::{StatError, StatField}, |
9 | 10 | traits::FeroxFilter, |
77 | 78 | |
78 | 79 | /// Break out of the (infinite) mpsc receive loop |
79 | 80 | Exit, |
81 | ||
82 | /// Give a handler access to an Arc<Handles> instance after the handler has | |
83 | /// already been initialized | |
84 | AddHandles(Arc<Handles>), | |
80 | 85 | } |
4 | 4 | use futures::future::{BoxFuture, FutureExt}; |
5 | 5 | use tokio::sync::{mpsc, oneshot}; |
6 | 6 | |
7 | use crate::statistics::StatField::TotalExpected; | |
8 | 7 | use crate::{ |
9 | 8 | config::Configuration, |
10 | 9 | progress::PROGRESS_PRINTER, |
11 | 10 | response::FeroxResponse, |
12 | 11 | scanner::RESPONSES, |
13 | 12 | send_command, skip_fail, |
14 | statistics::StatField::ResourcesDiscovered, | |
13 | statistics::StatField::{ResourcesDiscovered, TotalExpected}, | |
15 | 14 | traits::FeroxSerialize, |
16 | 15 | utils::{ferox_print, fmt_err, make_request, open_file, write_to}, |
17 | 16 | CommandReceiver, CommandSender, Joiner, |
143 | 142 | |
144 | 143 | /// pointer to "global" configuration struct |
145 | 144 | config: Arc<Configuration>, |
145 | ||
146 | /// handles instance | |
147 | handles: Option<Arc<Handles>>, | |
146 | 148 | } |
147 | 149 | |
148 | 150 | /// implementation of TermOutHandler |
160 | 162 | tx_file, |
161 | 163 | file_task, |
162 | 164 | config, |
165 | handles: None, | |
163 | 166 | } |
164 | 167 | } |
165 | 168 | |
210 | 213 | } |
211 | 214 | Command::Sync(sender) => { |
212 | 215 | sender.send(true).unwrap_or_default(); |
216 | } | |
217 | Command::AddHandles(handles) => { | |
218 | self.handles = Some(handles); | |
213 | 219 | } |
214 | 220 | Command::Exit => { |
215 | 221 | if self.file_task.is_some() && self.tx_file.send(Command::Exit).is_ok() { |
235 | 241 | log::trace!("enter: process_response({:?}, {:?})", resp, call_type); |
236 | 242 | |
237 | 243 | 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 | ||
239 | 262 | 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; | |
241 | 264 | |
242 | 265 | if should_process_response { |
243 | 266 | // print to stdout |
283 | 306 | && matches!(call_type, ProcessResponseCall::Recursive) |
284 | 307 | { |
285 | 308 | // --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) | |
287 | 310 | let backup_urls = self.generate_backup_urls(&resp).await; |
288 | 311 | |
289 | 312 | // need to manually adjust stats |
397 | 420 | #[cfg(test)] |
398 | 421 | mod tests { |
399 | 422 | use super::*; |
423 | use crate::event_handlers::Command; | |
400 | 424 | |
401 | 425 | #[test] |
402 | 426 | /// try to hit struct field coverage of FileOutHandler |
416 | 440 | let (tx, rx) = mpsc::unbounded_channel::<Command>(); |
417 | 441 | let (tx_file, _) = mpsc::unbounded_channel::<Command>(); |
418 | 442 | let config = Arc::new(Configuration::new().unwrap()); |
443 | let handles = Arc::new(Handles::for_testing(None, None).0); | |
419 | 444 | |
420 | 445 | let toh = TermOutHandler { |
421 | 446 | config, |
422 | 447 | file_task: None, |
423 | 448 | receiver: rx, |
424 | 449 | tx_file, |
450 | handles: Some(handles), | |
425 | 451 | }; |
426 | 452 | |
427 | 453 | println!("{:?}", toh); |
434 | 460 | let (tx, rx) = mpsc::unbounded_channel::<Command>(); |
435 | 461 | let (tx_file, _) = mpsc::unbounded_channel::<Command>(); |
436 | 462 | let config = Arc::new(Configuration::new().unwrap()); |
463 | let handles = Arc::new(Handles::for_testing(None, None).0); | |
437 | 464 | |
438 | 465 | let toh = TermOutHandler { |
439 | 466 | config, |
440 | 467 | file_task: None, |
441 | 468 | receiver: rx, |
442 | 469 | tx_file, |
470 | handles: Some(handles), | |
443 | 471 | }; |
444 | 472 | |
445 | 473 | let expected: Vec<_> = vec![ |
477 | 505 | let (tx, rx) = mpsc::unbounded_channel::<Command>(); |
478 | 506 | let (tx_file, _) = mpsc::unbounded_channel::<Command>(); |
479 | 507 | let config = Arc::new(Configuration::new().unwrap()); |
508 | let handles = Arc::new(Handles::for_testing(None, None).0); | |
480 | 509 | |
481 | 510 | let toh = TermOutHandler { |
482 | 511 | config, |
483 | 512 | file_task: None, |
484 | 513 | receiver: rx, |
485 | 514 | tx_file, |
515 | handles: Some(handles), | |
486 | 516 | }; |
487 | 517 | |
488 | 518 | let expected: Vec<_> = vec![ |
520 | 550 | let (tx, rx) = mpsc::unbounded_channel::<Command>(); |
521 | 551 | let (tx_file, _) = mpsc::unbounded_channel::<Command>(); |
522 | 552 | let config = Arc::new(Configuration::new().unwrap()); |
553 | let handles = Arc::new(Handles::for_testing(None, None).0); | |
523 | 554 | |
524 | 555 | let toh = TermOutHandler { |
525 | 556 | config, |
526 | 557 | file_task: None, |
527 | 558 | receiver: rx, |
528 | 559 | tx_file, |
560 | handles: Some(handles), | |
529 | 561 | }; |
530 | 562 | |
531 | 563 | let expected: Vec<_> = vec![ |
367 | 367 | async fn try_recursion(&mut self, response: Box<FeroxResponse>) -> Result<()> { |
368 | 368 | log::trace!("enter: try_recursion({:?})", response,); |
369 | 369 | |
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 | |
372 | 372 | return Ok(()); |
373 | 373 | } |
374 | 374 |
1 | 1 | use crate::{ |
2 | 2 | client, |
3 | 3 | event_handlers::{ |
4 | Command, | |
5 | 4 | Command::{AddError, AddToUsizeField}, |
6 | 5 | Handles, |
7 | 6 | }, |
11 | 10 | StatField::{LinksExtracted, TotalExpected}, |
12 | 11 | }, |
13 | 12 | url::FeroxUrl, |
14 | utils::{logged_request, make_request, should_deny_url}, | |
13 | utils::{logged_request, make_request, send_try_recursion_command, should_deny_url}, | |
15 | 14 | ExtractionResult, DEFAULT_METHOD, |
16 | 15 | }; |
17 | 16 | use anyhow::{bail, Context, Result}; |
18 | 17 | use reqwest::{Client, StatusCode, Url}; |
19 | 18 | use scraper::{Html, Selector}; |
20 | 19 | use std::collections::HashSet; |
21 | use tokio::sync::oneshot; | |
22 | 20 | |
23 | 21 | /// Whether an active scan is recursive or not |
24 | 22 | #[derive(Debug)] |
185 | 183 | resp.set_url(&format!("{}/", resp.url())); |
186 | 184 | } |
187 | 185 | |
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 | } | |
193 | 201 | } |
194 | 202 | } |
195 | 203 | log::trace!("exit: request_links"); |
64 | 64 | /// Default wordlist to use when `-w|--wordlist` isn't specified and not `wordlist` isn't set |
65 | 65 | /// in a [ferox-config.toml](constant.DEFAULT_CONFIG_NAME.html) config file. |
66 | 66 | /// |
67 | /// defaults to kali's default install location: | |
67 | /// defaults to kali's default install location on linux: | |
68 | 68 | /// - `/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"))] | |
69 | 73 | pub const DEFAULT_WORDLIST: &str = |
70 | 74 | "/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"; | |
71 | 78 | |
72 | 79 | /// Number of milliseconds to wait between polls of `PAUSE_SCAN` when user pauses a scan |
73 | 80 | pub(crate) const SLEEP_DURATION: u64 = 500; |
21 | 21 | banner::{Banner, UPDATE_URL}, |
22 | 22 | config::{Configuration, OutputLevel}, |
23 | 23 | event_handlers::{ |
24 | Command::{CreateBar, Exit, JoinTasks, LoadStats, ScanInitialUrls, UpdateWordlist}, | |
24 | Command::{ | |
25 | AddHandles, CreateBar, Exit, JoinTasks, LoadStats, ScanInitialUrls, UpdateWordlist, | |
26 | }, | |
25 | 27 | FiltersHandler, Handles, ScanHandler, StatsHandler, Tasks, TermInputHandler, |
26 | 28 | TermOutHandler, SCAN_COMPLETE, |
27 | 29 | }, |
219 | 221 | let (scan_task, scan_handle) = ScanHandler::initialize(handles.clone()); |
220 | 222 | |
221 | 223 | handles.set_scan_handle(scan_handle); // must be done after Handles initialization |
224 | handles.output.send(AddHandles(handles.clone()))?; | |
222 | 225 | |
223 | 226 | filters::initialize(handles.clone()).await?; // send user-supplied filters to the handler |
224 | 227 |
22 | 22 | document.number_of_terms += processed.len(); |
23 | 23 | |
24 | 24 | for normalized in processed { |
25 | if normalized.len() > 2 { | |
25 | if normalized.len() >= 2 { | |
26 | 26 | document.add_term(&normalized) |
27 | 27 | } |
28 | 28 | } |
37 | 37 | } |
38 | 38 | } |
39 | 39 | |
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 | |
41 | 41 | 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 | |
45 | 42 | text.replace( |
46 | 43 | [ |
47 | 44 | '!', '\\', '"', '#', '$', '%', '&', '(', ')', '*', '+', ':', ';', '<', '=', '>', '?', |
48 | '@', '[', ']', '^', '{', '}', '|', '~', ',', '\'', '“', '”', '’', '‘', '’', '‘', | |
45 | '@', '[', ']', '^', '{', '}', '|', '~', ',', '\'', '“', '”', '’', '‘', '’', '‘', '/', | |
46 | '–', '—', '.', | |
49 | 47 | ], |
50 | "", | |
48 | " ", | |
51 | 49 | ) |
52 | .replace(['/', '–', '—', '.'], " ") | |
53 | 50 | } |
54 | 51 | |
55 | 52 | /// remove stop words from the given string |
85 | 82 | fn test_remove_punctuation() { |
86 | 83 | let tester = "!\\\"#$%&()*+/:;<=>?@[]^{}|~,.'“”’‘–—\n‘’"; |
87 | 84 | // 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 | ); | |
89 | 89 | } |
90 | 90 | |
91 | 91 | #[test] |
114 | 114 | /// ensure preprocess |
115 | 115 | fn test_preprocess_results() { |
116 | 116 | let tester = "WHY are Y'all YELLing?"; |
117 | assert_eq!(&preprocess(tester), &["yall", "yelling"]); | |
117 | assert_eq!(&preprocess(tester), &["y", "all", "yelling"]); | |
118 | 118 | } |
119 | 119 | |
120 | 120 | #[test] |
332 | 332 | .multiple_values(true) |
333 | 333 | .multiple_occurrences(true) |
334 | 334 | .use_value_delimiter(true) |
335 | .conflicts_with("status_codes") | |
335 | 336 | .help_heading("Response filters") |
336 | 337 | .help( |
337 | 338 | "Filter out status codes (deny list) (ex: -C 200 -C 401)", |
425 | 426 | .takes_value(true) |
426 | 427 | .help_heading("Scan settings") |
427 | 428 | .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)"), | |
428 | 435 | ).arg( |
429 | 436 | Arg::new("extract_links") |
430 | 437 | .short('e') |
667 | 674 | cat targets | ./feroxbuster --stdin --silent -s 200 301 302 --redirects -x js | fff -s 200 -o js-files |
668 | 675 | |
669 | 676 | 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 | |
671 | 678 | |
672 | 679 | Proxy traffic through a SOCKS proxy |
673 | 680 | ./feroxbuster -u http://127.1 --proxy socks5://127.0.0.1:9050 |
278 | 278 | if handles |
279 | 279 | .config |
280 | 280 | .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() | |
282 | 284 | { |
283 | 285 | // only add extensions to those responses that pass our checks; filtered out |
284 | 286 | // status codes are handled by should_filter, but we need to still check against |
451 | 451 | r#""quiet":false"#, |
452 | 452 | r#""auto_bail":false"#, |
453 | 453 | r#""auto_tune":false"#, |
454 | r#""force_recursion":false"#, | |
454 | 455 | r#""json":false"#, |
455 | 456 | r#""output":"""#, |
456 | 457 | r#""debug_log":"""#, |
7 | 7 | use lazy_static::lazy_static; |
8 | 8 | use leaky_bucket::LeakyBucket; |
9 | 9 | use tokio::{ |
10 | sync::{oneshot, RwLock}, | |
10 | sync::RwLock, | |
11 | 11 | time::{sleep, Duration}, |
12 | 12 | }; |
13 | 13 | |
15 | 15 | atomic_load, atomic_store, |
16 | 16 | config::RequesterPolicy, |
17 | 17 | event_handlers::{ |
18 | Command::{self, AddError, SubtractFromUsizeField}, | |
18 | Command::{AddError, SubtractFromUsizeField}, | |
19 | 19 | Handles, |
20 | 20 | }, |
21 | 21 | extractor::{ExtractionTarget, ExtractorBuilder}, |
24 | 24 | scan_manager::{FeroxScan, ScanStatus}, |
25 | 25 | statistics::{StatError::Other, StatField::TotalExpected}, |
26 | 26 | url::FeroxUrl, |
27 | utils::{logged_request, should_deny_url}, | |
27 | utils::{logged_request, send_try_recursion_command, should_deny_url}, | |
28 | 28 | HIGH_ERROR_RATIO, |
29 | 29 | }; |
30 | 30 | |
378 | 378 | .await; |
379 | 379 | |
380 | 380 | // 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?; | |
389 | 389 | } |
390 | 390 | |
391 | 391 | // purposefully doing recursion before filtering. the thought process is that |
397 | 397 | .should_filter_response(&ferox_response, self.handles.stats.tx.clone()) |
398 | 398 | { |
399 | 399 | 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 | } | |
400 | 427 | } |
401 | 428 | |
402 | 429 | if self.handles.config.collect_extensions { |
468 | 495 | use crate::{ |
469 | 496 | config::Configuration, |
470 | 497 | config::OutputLevel, |
498 | event_handlers::Command::AddStatus, | |
471 | 499 | event_handlers::{FiltersHandler, ScanHandler, StatsHandler, Tasks, TermOutHandler}, |
472 | 500 | filters, |
473 | 501 | scan_manager::{ScanOrder, ScanType}, |
508 | 536 | /// helper to stay DRY |
509 | 537 | async fn increment_errors(handles: Arc<Handles>, scan: Arc<FeroxScan>, num_errors: usize) { |
510 | 538 | 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(); | |
515 | 540 | scan.add_error(); |
516 | 541 | } |
517 | 542 | |
548 | 573 | code: StatusCode, |
549 | 574 | ) { |
550 | 575 | for _ in 0..num_codes { |
551 | handles.stats.send(Command::AddStatus(code)).unwrap(); | |
576 | handles.stats.send(AddStatus(code)).unwrap(); | |
552 | 577 | if code == StatusCode::FORBIDDEN { |
553 | 578 | scan.add_403(); |
554 | 579 | } else { |
11 | 11 | time::Duration, |
12 | 12 | time::{SystemTime, UNIX_EPOCH}, |
13 | 13 | }; |
14 | use tokio::sync::mpsc::UnboundedSender; | |
15 | ||
16 | use crate::config::Configuration; | |
14 | use tokio::sync::{mpsc::UnboundedSender, oneshot}; | |
15 | ||
17 | 16 | use crate::{ |
17 | config::Configuration, | |
18 | 18 | config::OutputLevel, |
19 | 19 | event_handlers::{ |
20 | 20 | Command::{self, AddError, AddStatus}, |
21 | 21 | Handles, |
22 | 22 | }, |
23 | 23 | progress::PROGRESS_PRINTER, |
24 | response::FeroxResponse, | |
24 | 25 | send_command, |
25 | 26 | statistics::StatError::{Connection, Other, Redirection, Request, Timeout}, |
26 | 27 | traits::FeroxSerialize, |
64 | 65 | /// simple wrapper to stay DRY |
65 | 66 | pub fn fmt_err(msg: &str) -> String { |
66 | 67 | 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(()) | |
67 | 82 | } |
68 | 83 | |
69 | 84 | /// Takes in a string and colors it using console::style |
783 | 783 | .and(predicate::str::contains("http://localhost")) |
784 | 784 | .and(predicate::str::contains("Threads")) |
785 | 785 | .and(predicate::str::contains("Wordlist")) |
786 | .and(predicate::str::contains("Status Codes")) | |
787 | 786 | .and(predicate::str::contains("Timeout (secs)")) |
788 | 787 | .and(predicate::str::contains("User-Agent")) |
789 | 788 | .and(predicate::str::contains("Status Code Filters")) |
1393 | 1392 | .and(predicate::str::contains("─┴─")), |
1394 | 1393 | ); |
1395 | 1394 | } |
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 | } |
268 | 268 | Ok(()) |
269 | 269 | } |
270 | 270 | |
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")?; | |
276 | 325 | |
277 | 326 | let mock = srv.mock(|when, then| { |
278 | 327 | when.method(GET) |
295 | 344 | .arg("--wordlist") |
296 | 345 | .arg(file.as_os_str()) |
297 | 346 | .arg("--add-slash") |
347 | .arg("--silent") | |
348 | .arg("--threads") | |
349 | .arg("1") | |
298 | 350 | .unwrap(); |
299 | 351 | |
300 | 352 | teardown_tmp_directory(tmp_dir); |
301 | 353 | |
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()); | |
312 | 355 | |
313 | 356 | assert_eq!(mock.hits(), 1); |
314 | 357 | 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 |
851 | 851 | |
852 | 852 | teardown_tmp_directory(tmp_dir); |
853 | 853 | } |
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 | } |