Compare commits

..

33 Commits

Author SHA1 Message Date
epi
7b3540e13f Merge pull request #163 from epi052/137-extract-robots-txt
add robots.txt extraction to increase scan coverage
2020-12-19 10:58:53 -06:00
epi
4e492939c1 Merge branch 'master' into 137-extract-robots-txt 2020-12-19 10:57:20 -06:00
epi
d39692d1bd updated readme faq and added new robots.txt info 2020-12-19 10:49:43 -06:00
epi
086c9808a3 added integration test for robots.txt extraction 2020-12-19 09:20:06 -06:00
epi
f7ef202849 added robots.txt extraction 2020-12-19 07:30:24 -06:00
epi
77a450195c investigated suspected race condition and implemented fix 2020-12-19 06:35:54 -06:00
epi
b10c4caefb added connection closed before complete section to FAQ 2020-12-14 07:03:18 -06:00
epi
4ee374efb6 bumped version to 1.10.2 2020-12-13 21:20:44 -06:00
epi
183dc4cf14 added function to request robots.txt; fmt'd, clippy'd, and test'd #nbd 2020-12-13 21:20:10 -06:00
epi
81cd6c3a64 updated README ToC 2020-12-13 09:30:29 -06:00
epi
1f7ae68857 appeased clippy 2020-12-13 06:57:12 -06:00
epi
f175d759ca appeased clippy 2020-12-13 06:49:18 -06:00
epi
83f8a33413 fixed docs.rs build 2020-12-13 06:47:09 -06:00
epi
a22ca731b6 bumped to 1.10.1; cleaned up verbosity code 2020-12-13 06:29:30 -06:00
epi
e5934cef1f fixed response code in test_scanner 2020-12-12 18:01:46 -06:00
epi
1b49c5dfe9 Merge pull request #162 from epi052/emoji-fallback
added emoji fallback when terminals dont support; updated httpmock
2020-12-12 17:22:50 -06:00
epi
47c384e2ec added emoji fallback when terminals dont support; updated httpmock 2020-12-12 17:20:24 -06:00
epi
8d5a0c590e Merge pull request #158 from epi052/dependabot/cargo/console-0.13
Update console requirement from 0.12 to 0.13
2020-12-12 10:25:56 -06:00
epi
6b04bc6757 Merge pull request #159 from epi052/dependabot/cargo/httpmock-0.5.2
Update httpmock requirement from 0.4.5 to 0.5.2
2020-12-12 10:20:46 -06:00
dependabot-preview[bot]
baa996356c Update httpmock requirement from 0.4.5 to 0.5.2
Updates the requirements on [httpmock](https://github.com/alexliesenfeld/httpmock) to permit the latest version.
- [Release notes](https://github.com/alexliesenfeld/httpmock/releases)
- [Changelog](https://github.com/alexliesenfeld/httpmock/blob/master/CHANGELOG.md)
- [Commits](https://github.com/alexliesenfeld/httpmock/compare/v0.4.5...v0.5.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-12-12 12:55:10 +00:00
epi
ae5f7e5435 Merge pull request #157 from epi052/dependabot/add-v2-config-file
Create Dependabot config file
2020-12-12 06:55:09 -06:00
dependabot-preview[bot]
9241b3c748 Update console requirement from 0.12 to 0.13
Updates the requirements on [console](https://github.com/mitsuhiko/console) to permit the latest version.
- [Release notes](https://github.com/mitsuhiko/console/releases)
- [Changelog](https://github.com/mitsuhiko/console/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mitsuhiko/console/compare/v0.12.0...v0.13.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-12-12 12:54:54 +00:00
dependabot-preview[bot]
48b341db39 Create Dependabot config file 2020-12-12 12:54:28 +00:00
epi
b759e016bb added probot-stale config 2020-12-12 06:30:00 -06:00
epi
8dc7a86b2b Merge pull request #152 from epi052/138-max-local-runtime
add maximum runtime for scans, i.e. time limit
2020-12-12 06:21:17 -06:00
epi
0db0273513 added documentation for time-limit 2020-12-11 21:08:48 -06:00
epi
21254ad871 added extra-words for longer scans 2020-12-11 16:38:03 -06:00
epi
5bbf29859f added tests for time-limit 2020-12-11 16:28:09 -06:00
epi
730566fd05 added time limit banner test 2020-12-11 14:48:08 -06:00
epi
f05c5eca03 fixed failing test 2020-12-11 13:11:48 -06:00
epi
8c50d94f8e cleaned up todo; reduced memory usage; polished time limit code; updated example config; added banner entry 2020-12-11 11:40:35 -06:00
epi
91c42e137d poc for max time works 2020-12-09 19:43:46 -06:00
epi
a2a9ba289c bumped version to 1.10.0 2020-12-09 15:51:35 -06:00
27 changed files with 104152 additions and 803 deletions

7
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: cargo
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10

17
.github/stale.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 21
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View File

@@ -1,6 +1,6 @@
[package]
name = "feroxbuster"
version = "1.9.1"
version = "1.10.2"
authors = ["Ben 'epi' Risher <epibar052@gmail.com>"]
license = "MIT"
edition = "2018"
@@ -16,7 +16,9 @@ build = "build.rs"
maintenance = { status = "actively-developed" }
[build-dependencies]
clap = "2"
clap = "2.33"
regex = "1"
lazy_static = "1.4"
[dependencies]
futures = { version = "0.3"}
@@ -25,14 +27,14 @@ tokio-util = {version = "0.3", features = ["codec"]}
log = "0.4"
env_logger = "0.8"
reqwest = { version = "0.10", features = ["socks"] }
clap = "2"
clap = "2.33"
lazy_static = "1.4"
toml = "0.5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = { version = "0.8", features = ["v4"] }
indicatif = "0.15"
console = "0.12"
console = "0.13"
openssl = { version = "0.10", features = ["vendored"] }
dirs = "3.0"
regex = "1"
@@ -42,7 +44,7 @@ ctrlc = "3.1"
[dev-dependencies]
tempfile = "3.1"
httpmock = "0.4.5"
httpmock = "0.5.2"
assert_cmd = "1.0.1"
predicates = "1.0.5"

122
README.md
View File

@@ -85,12 +85,17 @@ This attack is also known as Predictable Resource Location, File Enumeration, Di
- [Filter Response by Status Code (new in `v1.3.0`)](#filter-response-by-status-code--new-in-v130)
- [Pause an Active Scan (new in `v1.4.0`)](#pause-an-active-scan-new-in-v140)
- [Replay Responses to a Proxy based on Status Code (new in `v1.5.0`)](#replay-responses-to-a-proxy-based-on-status-code-new-in-v150)
- [Filter Response by Word Count & Line Count (new in `v1.6.0`)](#filter-response-by-word-count--line-count--new-in-v160)
- [Filter Response Using a Regular Expression (new in `v1.8.0`)](#filter-response-using-a-regular-expression-new-in-v180)
- [Stop and Resume Scans (save scan's state to disk) (new in `v1.9.0`)](#stop-and-resume-scans---resume-from-file-new-in-v190)
- [Enforce a Time Limit on Your Scan (new in `v1.10.0`)](#enforce-a-time-limit-on-your-scan-new-in-v1100)
- [Comparison w/ Similar Tools](#-comparison-w-similar-tools)
- [Common Problems/Issues (FAQ)](#-common-problemsissues-faq)
- [No file descriptors available](#no-file-descriptors-available)
- [Progress bars print one line at a time](#progress-bars-print-one-line-at-a-time)
- [What do each of the numbers beside the URL mean?](#what-do-each-of-the-numbers-beside-the-url-mean)
- [Connection closed before message completed](#connection-closed-before-message-completed)
- [SSL Error routines:tls_process_server_certificate:certificate verify failed](#ssl-error-routinestls_process_server_certificatecertificate-verify-failed)
## 💿 Installation
@@ -351,6 +356,7 @@ A pre-made configuration file with examples of all available settings can be fou
# filter_line_count = [35, 36]
# queries = [["name","value"], ["rick", "astley"]]
# save_state = false
# time_limit = 10m
# headers can be specified on multiple lines or as an inline table
#
@@ -403,8 +409,8 @@ OPTIONS:
-o, --output <FILE> Output file to write results to (use w/ --json for JSON entries)
-p, --proxy <PROXY> Proxy to use for requests (ex: http(s)://host:port, socks5://host:port)
-Q, --query <QUERY>... Specify URL query parameters (ex: -Q token=stuff -Q secret=key)
-R, --replay-codes <REPLAY_CODE>... Status Codes to send through a Replay Proxy when found (default: --status
-codes value)
-R, --replay-codes <REPLAY_CODE>... Status Codes to send through a Replay Proxy when found (default: --status-
codes value)
-P, --replay-proxy <REPLAY_PROXY> Send only unfiltered requests through a Replay Proxy, instead of all
requests
--resume-from <STATE_FILE> State file from which to resume a partially complete scan (ex. --resume-from
@@ -413,6 +419,7 @@ OPTIONS:
-s, --status-codes <STATUS_CODE>... Status Codes to include (allow list) (default: 200 204 301 302 307 308 401
403 405)
-t, --threads <THREADS> Number of concurrent threads (default: 50)
--time-limit <TIME_SPEC> Limit total run time of all scans (ex: --time-limit 10m)
-T, --timeout <SECONDS> Number of seconds before a request times out (default: 7)
-u, --url <URL>... The target URL(s) (required, unless --stdin used)
-a, --user-agent <USER_AGENT> Sets the User-Agent (default: feroxbuster/VERSION)
@@ -541,6 +548,22 @@ Of note: this means that for every response that matches your replay criteria, y
![replay-proxy-demo](img/replay-proxy-demo.gif)
### Filter Response by Word Count & Line Count (new in `v1.6.0`)
In addition to filtering on the size of a response, version 1.6.0 added the ability to filter out responses based on the number of lines and/or words contained within the response body. This change drove a change to the information displayed to the user as well. This section will detail the new information and how to make use of it with the new filters provided.
Example output:
```
200 10l 212w 38437c https://example-site.com/index.html
```
There are five columns of output above:
- column 1: status code - can be filtered with `-C|--filter-status`
- column 2: number of lines - can be filtered with `-N|--filter-lines`
- column 3: number of words - can be filtered with `-W|--filter-words`
- column 4: number of bytes (overall size) - can be filtered with `-S|--filter-size`
- column 5: url to discovered resource
### Filter Response Using a Regular Expression (new in `v1.8.0`)
Version 1.3.0 included an overhaul to the filtering system which will allow for a wide array of filters to be added
@@ -615,6 +638,26 @@ In order to prevent state file creation when `Ctrl+C` is pressed, you can simply
save_state = false
```
### Enforce a Time Limit on Your Scan (new in `v1.10.0`)
Version 1.10.0 adds the ability to set a maximum runtime, or time limit, on your scan. The usage is pretty simple: a number followed directly by a single character representing seconds, minutes, hours, or days. `feroxbuster` refers to this combination as a time_spec.
Examples of possible time_specs:
- `30s` - 30 seconds
- `20m` - 20 minutes
- `1h` - 1 hour
- `1d` - 1 day (why??)
A valid time_spec can be passed to `--time-limit` in order to force a shutdown after the given time has elapsed.
![time-limit](img/time-limit.gif)
### Extract Links from robots.txt (New in `v1.10.2`)
In addition to [extracting links from the response body](#extract-links-from-response-body-new-in-v110), using
`--extract-links` makes a request to `/robots.txt` and examines all `Allow` and `Disallow` entries. Directory entries
are added to the scan queue, while file entries are requested and then reported if appropriate.
## 🧐 Comparison w/ Similar Tools
There are quite a few similar tools for forced browsing/content discovery. Burp Suite Pro, Dirb, Dirbuster, etc...
@@ -633,24 +676,33 @@ a few of the use-cases in which feroxbuster may be a better fit:
- You want **recursion** along with some other thing mentioned above (ffuf also does recursion)
- You want a **configuration file** option for overriding built-in default values for your scans
| | feroxbuster | gobuster | ffuf |
|------------------------------------------------------------------|---|---|---|
| fast | ✔ | ✔ | ✔ |
| easy to use | ✔ | ✔ | |
| filter out responses by status code (new in `v1.3.0`) | ✔ | | ✔ |
| allows recursion | ✔ | | ✔ |
| can specify query parameters | ✔ | | |
| SOCKS proxy support | ✔ | | |
| extracts links from response body to increase scan coverage | ✔ | | |
| multiple target scan (via stdin or multiple -u) | ✔ | | ✔ |
| configuration file for default value override | ✔ | | ✔ |
| can accept urls via STDIN as part of a pipeline | ✔ | | ✔ |
| can accept wordlists via STDIN | | | ✔ |
| filter based on response size, wordcount, and linecount | ✔ | | ✔ |
| auto-filter wildcard responses | | | ✔ |
| performs other scans (vhost, dns, etc) | | ✔ | ✔ |
| time delay / rate limiting | | | |
| **huge** number of other options | | | ✔ |
| | feroxbuster | gobuster | ffuf |
|------------------------------------------------------------------------------|---|---|---|
| fast | ✔ | ✔ | ✔ |
| easy to use | ✔ | ✔ | |
| allows recursion | ✔ | | ✔ |
| can specify query parameters | ✔ | | ✔ |
| SOCKS proxy support | ✔ | | |
| multiple target scan (via stdin or multiple -u) | ✔ | | |
| configuration file for default value override | ✔ | | |
| can accept urls via STDIN as part of a pipeline | ✔ | | ✔ |
| can accept wordlists via STDIN | | ✔ | ✔ |
| filter based on response size, wordcount, and linecount | ✔ | | ✔ |
| auto-filter wildcard responses | | | ✔ |
| performs other scans (vhost, dns, etc) | | ✔ | ✔ |
| time delay / rate limiting | | | ✔ |
| extracts links from response body to increase scan coverage (`v1.1.0`) | ✔ | | |
| limit number of concurrent recursive scans (`v1.2.0`) | | | |
| filter out responses by status code (`v1.3.0`) | | | ✔ |
| interactive pause and resume of active scan (`v1.4.0`) | ✔ | | |
| replay only matched requests to a proxy (`v1.5.0`) | ✔ | | ✔ |
| filter out responses by line & word count (`v1.6.0`) | ✔ | | ✔ |
| json output (ffuf supports other formats as well) (`v1.7.0`) | ✔ | | ✔ |
| filter out responses by regular expression (`v1.8.0`) | ✔ | | ✔ |
| save scan's state to disk (can pick up where it left off) (`v1.9.0`) | ✔ | | |
| maximum run time limit (`v1.10.0`) | ✔ | | ✔ |
| use robots.txt to increase scan coverage (`v1.10.2`) | ✔ | | |
| **huge** number of other options | | | ✔ |
Of note, there's another written-in-rust content discovery tool, [rustbuster](https://github.com/phra/rustbuster). I
came across rustbuster when I was naming my tool (😢). I don't have any experience using it, but it appears to
@@ -736,4 +788,32 @@ sudo sysctl net.ipv4.tcp_tw_reuse=1
![small-term](img/small-term.png)
If you can, simply make the terminal wider and rerun. If you're unable to make your terminal wider
consider using `-q` to suppress the progress bars.
consider using `-q` to suppress the progress bars.
### What do each of the numbers beside the URL mean?
Please refer to [this section](#filter-response-by-word-count--line-count--new-in-v160) where each number's meaning and how to use it to filter responses is discussed.
### Connection closed before message completed
The error in question can be boiled down to 'networking stuff'. `feroxbuster` uses [reqwest](https://docs.rs/reqwest/latest/) which uses [hyper](https://docs.rs/hyper/latest/hyper/) to make requests to the server. [This issue report](https://github.com/hyperium/hyper/issues/2136#issuecomment-589345238) to the hyper project explains what is happening (quoted below to save you a click). This isn't a bug so much as it's a target-specific tuning issue. When lowering the `-t` value, the error doesn't occur (or happens much less frequently).
This isn't a bug. Simply slow down the scan. A `-t` value of 50 was chosen as a sane default that's still quite fast out of the box. However, network related errors may occur when the client and/or server become over-saturated. The [Threads and Connection Limits At A High-Level](#threads-and-connection-limits-at-a-high-level) section details how to accomplish per-target tuning.
> This is just due to the racy nature of networking.
>
> hyper has a connection pool of idle connections, and it selected one to send your request. Most of the time, hyper will receive the server's FIN and drop the dead connection from its pool. But occasionally, a connection will be selected from the pool and written to at the same time the server is deciding to close the connection. Since hyper already wrote some of the request, it can't really retry it automatically on a new connection, since the server may have acted already.
### SSL Error routines:tls_process_server_certificate:certificate verify failed
In the event you see an error similar to
![self-signed](img/insecure.png)
```
error trying to connect: error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed:ssl/statem/statem_clnt.c:1913: (self signed certificate)
```
You just need to add the `-k|--insecure` flag to your command.
`feroxbuster` rejects self-signed certs and other "insecure" certificates/site configurations by default. You can choose to scan these services anyway by telling `feroxbuster` to ignore insecure server certs.

View File

@@ -5,6 +5,12 @@ use clap::Shell;
include!("src/parser.rs");
fn main() {
println!("cargo:rerun-if-env-changed=src/parser.rs");
if std::env::var("DOCS_RS").is_ok() {
return; // only build when we're not generating docs
}
let outdir = "shell_completions";
let mut app = initialize();

View File

@@ -37,6 +37,7 @@
# filter_line_count = [35, 36]
# queries = [["name","value"], ["rick", "astley"]]
# save_state = false
# time_limit = "10m"
# headers can be specified on multiple lines or as an inline table
#

BIN
img/time-limit.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

View File

@@ -1,53 +1,63 @@
#compdef feroxbuster
autoload -U is-at-least
_feroxbuster() {
typeset -A opt_args
typeset -a _arguments_options
local ret=1
if is-at-least 5.2; then
_arguments_options=(-s -S -C)
else
_arguments_options=(-s -C)
fi
local context curcontext="$curcontext" state line
_arguments -s -S -C \
_arguments "${_arguments_options[@]}" \
'-w+[Path to the wordlist]' \
'--wordlist+[Path to the wordlist]' \
'--wordlist=[Path to the wordlist]' \
'*-u+[The target URL(s) (required, unless --stdin used)]' \
'*--url+[The target URL(s) (required, unless --stdin used)]' \
'*--url=[The target URL(s) (required, unless --stdin used)]' \
'-t+[Number of concurrent threads (default: 50)]' \
'--threads+[Number of concurrent threads (default: 50)]' \
'--threads=[Number of concurrent threads (default: 50)]' \
'-d+[Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)]' \
'--depth+[Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)]' \
'--depth=[Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)]' \
'-T+[Number of seconds before a request times out (default: 7)]' \
'--timeout+[Number of seconds before a request times out (default: 7)]' \
'--timeout=[Number of seconds before a request times out (default: 7)]' \
'-p+[Proxy to use for requests (ex: http(s)://host:port, socks5://host:port)]' \
'--proxy+[Proxy to use for requests (ex: http(s)://host:port, socks5://host:port)]' \
'--proxy=[Proxy to use for requests (ex: http(s)://host:port, socks5://host:port)]' \
'-P+[Send only unfiltered requests through a Replay Proxy, instead of all requests]' \
'--replay-proxy+[Send only unfiltered requests through a Replay Proxy, instead of all requests]' \
'--replay-proxy=[Send only unfiltered requests through a Replay Proxy, instead of all requests]' \
'*-R+[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]' \
'*--replay-codes+[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]' \
'*--replay-codes=[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]' \
'*-s+[Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)]' \
'*--status-codes+[Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)]' \
'*--status-codes=[Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)]' \
'-o+[Output file to write results to (use w/ --json for JSON entries)]' \
'--output+[Output file to write results to (use w/ --json for JSON entries)]' \
'(-u --url)--resume-from+[State file from which to resume a partially complete scan (ex. --resume-from ferox-1606586780.state)]' \
'--debug-log+[Output file to write log entries (use w/ --json for JSON entries)]' \
'--output=[Output file to write results to (use w/ --json for JSON entries)]' \
'(-u --url)--resume-from=[State file from which to resume a partially complete scan (ex. --resume-from ferox-1606586780.state)]' \
'--debug-log=[Output file to write log entries (use w/ --json for JSON entries)]' \
'-a+[Sets the User-Agent (default: feroxbuster/VERSION)]' \
'--user-agent+[Sets the User-Agent (default: feroxbuster/VERSION)]' \
'--user-agent=[Sets the User-Agent (default: feroxbuster/VERSION)]' \
'*-x+[File extension(s) to search for (ex: -x php -x pdf js)]' \
'*--extensions+[File extension(s) to search for (ex: -x php -x pdf js)]' \
'*--extensions=[File extension(s) to search for (ex: -x php -x pdf js)]' \
'*-H+[Specify HTTP headers (ex: -H Header:val '\''stuff: things'\'')]' \
'*--headers+[Specify HTTP headers (ex: -H Header:val '\''stuff: things'\'')]' \
'*--headers=[Specify HTTP headers (ex: -H Header:val '\''stuff: things'\'')]' \
'*-Q+[Specify URL query parameters (ex: -Q token=stuff -Q secret=key)]' \
'*--query+[Specify URL query parameters (ex: -Q token=stuff -Q secret=key)]' \
'*--query=[Specify URL query parameters (ex: -Q token=stuff -Q secret=key)]' \
'*-S+[Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)]' \
'*--filter-size+[Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)]' \
'*--filter-size=[Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)]' \
'*-X+[Filter out messages via regular expression matching on the response'\''s body (ex: -X '\''^ignore me$'\'')]' \
'*--filter-regex+[Filter out messages via regular expression matching on the response'\''s body (ex: -X '\''^ignore me$'\'')]' \
'*--filter-regex=[Filter out messages via regular expression matching on the response'\''s body (ex: -X '\''^ignore me$'\'')]' \
'*-W+[Filter out messages of a particular word count (ex: -W 312 -W 91,82)]' \
'*--filter-words+[Filter out messages of a particular word count (ex: -W 312 -W 91,82)]' \
'*--filter-words=[Filter out messages of a particular word count (ex: -W 312 -W 91,82)]' \
'*-N+[Filter out messages of a particular line count (ex: -N 20 -N 31,30)]' \
'*--filter-lines+[Filter out messages of a particular line count (ex: -N 20 -N 31,30)]' \
'*--filter-lines=[Filter out messages of a particular line count (ex: -N 20 -N 31,30)]' \
'*-C+[Filter out status codes (deny list) (ex: -C 200 -C 401)]' \
'*--filter-status+[Filter out status codes (deny list) (ex: -C 200 -C 401)]' \
'*--filter-status=[Filter out status codes (deny list) (ex: -C 200 -C 401)]' \
'-L+[Limit total number of concurrent scans (default: 0, i.e. no limit)]' \
'--scan-limit+[Limit total number of concurrent scans (default: 0, i.e. no limit)]' \
'--scan-limit=[Limit total number of concurrent scans (default: 0, i.e. no limit)]' \
'--time-limit=[Limit total run time of all scans (ex: --time-limit 10m)]' \
'*-v[Increase verbosity level (use -vv or more for greater effect. \[CAUTION\] 4 -v'\''s is probably too much)]' \
'*--verbosity[Increase verbosity level (use -vv or more for greater effect. \[CAUTION\] 4 -v'\''s is probably too much)]' \
'-q[Only print URLs; Don'\''t print status codes, response size, running config, etc...]' \
@@ -74,13 +84,6 @@ _feroxbuster() {
}
(( $+functions[_feroxbuster_commands] )) ||
_feroxbuster_commands() {
local commands; commands=(
)
_describe -t commands 'feroxbuster commands' commands "$@"
}
(( $+functions[_feroxbuster_commands] )) ||
_feroxbuster_commands() {
local commands; commands=(

View File

@@ -1,38 +1,94 @@
@('feroxbuster', './feroxbuster') | %{
Register-ArgumentCompleter -Native -CommandName $_ -ScriptBlock {
param($wordToComplete, $commandAst, $cursorPosition)
using namespace System.Management.Automation
using namespace System.Management.Automation.Language
$command = '_feroxbuster'
$commandAst.CommandElements |
Select-Object -Skip 1 |
%{
switch ($_.ToString()) {
'feroxbuster' {
$command += '_feroxbuster'
break
}
default {
break
}
}
}
$completions = @()
switch ($command) {
'_feroxbuster' {
$completions = @('-v', '-q', '-D', '-r', '-k', '-n', '-f', '-e', '-h', '-V', '-w', '-u', '-t', '-d', '-T', '-p', '-P', '-R', '-s', '-o', '-a', '-x', '-H', '-Q', '-S', '-X', '-W', '-N', '-C', '-L', '--verbosity', '--quiet', '--json', '--dont-filter', '--redirects', '--insecure', '--no-recursion', '--add-slash', '--stdin', '--extract-links', '--help', '--version', '--wordlist', '--url', '--threads', '--depth', '--timeout', '--proxy', '--replay-proxy', '--replay-codes', '--status-codes', '--output', '--resume-from', '--debug-log', '--user-agent', '--extensions', '--headers', '--query', '--filter-size', '--filter-regex', '--filter-words', '--filter-lines', '--filter-status', '--scan-limit')
}
Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
param($wordToComplete, $commandAst, $cursorPosition)
$commandElements = $commandAst.CommandElements
$command = @(
'feroxbuster'
for ($i = 1; $i -lt $commandElements.Count; $i++) {
$element = $commandElements[$i]
if ($element -isnot [StringConstantExpressionAst] -or
$element.StringConstantType -ne [StringConstantType]::BareWord -or
$element.Value.StartsWith('-')) {
break
}
$element.Value
}) -join ';'
$completions |
?{ $_ -like "$wordToComplete*" } |
Sort-Object |
%{ New-Object System.Management.Automation.CompletionResult $_, $_, 'ParameterValue', $_ }
}
$completions = @(switch ($command) {
'feroxbuster' {
[CompletionResult]::new('-w', 'w', [CompletionResultType]::ParameterName, 'Path to the wordlist')
[CompletionResult]::new('--wordlist', 'wordlist', [CompletionResultType]::ParameterName, 'Path to the wordlist')
[CompletionResult]::new('-u', 'u', [CompletionResultType]::ParameterName, 'The target URL(s) (required, unless --stdin used)')
[CompletionResult]::new('--url', 'url', [CompletionResultType]::ParameterName, 'The target URL(s) (required, unless --stdin used)')
[CompletionResult]::new('-t', 't', [CompletionResultType]::ParameterName, 'Number of concurrent threads (default: 50)')
[CompletionResult]::new('--threads', 'threads', [CompletionResultType]::ParameterName, 'Number of concurrent threads (default: 50)')
[CompletionResult]::new('-d', 'd', [CompletionResultType]::ParameterName, 'Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)')
[CompletionResult]::new('--depth', 'depth', [CompletionResultType]::ParameterName, 'Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)')
[CompletionResult]::new('-T', 'T', [CompletionResultType]::ParameterName, 'Number of seconds before a request times out (default: 7)')
[CompletionResult]::new('--timeout', 'timeout', [CompletionResultType]::ParameterName, 'Number of seconds before a request times out (default: 7)')
[CompletionResult]::new('-p', 'p', [CompletionResultType]::ParameterName, 'Proxy to use for requests (ex: http(s)://host:port, socks5://host:port)')
[CompletionResult]::new('--proxy', 'proxy', [CompletionResultType]::ParameterName, 'Proxy to use for requests (ex: http(s)://host:port, socks5://host:port)')
[CompletionResult]::new('-P', 'P', [CompletionResultType]::ParameterName, 'Send only unfiltered requests through a Replay Proxy, instead of all requests')
[CompletionResult]::new('--replay-proxy', 'replay-proxy', [CompletionResultType]::ParameterName, 'Send only unfiltered requests through a Replay Proxy, instead of all requests')
[CompletionResult]::new('-R', 'R', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)')
[CompletionResult]::new('--replay-codes', 'replay-codes', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)')
[CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)')
[CompletionResult]::new('--status-codes', 'status-codes', [CompletionResultType]::ParameterName, 'Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)')
[CompletionResult]::new('-o', 'o', [CompletionResultType]::ParameterName, 'Output file to write results to (use w/ --json for JSON entries)')
[CompletionResult]::new('--output', 'output', [CompletionResultType]::ParameterName, 'Output file to write results to (use w/ --json for JSON entries)')
[CompletionResult]::new('--resume-from', 'resume-from', [CompletionResultType]::ParameterName, 'State file from which to resume a partially complete scan (ex. --resume-from ferox-1606586780.state)')
[CompletionResult]::new('--debug-log', 'debug-log', [CompletionResultType]::ParameterName, 'Output file to write log entries (use w/ --json for JSON entries)')
[CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/VERSION)')
[CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/VERSION)')
[CompletionResult]::new('-x', 'x', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)')
[CompletionResult]::new('--extensions', 'extensions', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)')
[CompletionResult]::new('-H', 'H', [CompletionResultType]::ParameterName, 'Specify HTTP headers (ex: -H Header:val ''stuff: things'')')
[CompletionResult]::new('--headers', 'headers', [CompletionResultType]::ParameterName, 'Specify HTTP headers (ex: -H Header:val ''stuff: things'')')
[CompletionResult]::new('-Q', 'Q', [CompletionResultType]::ParameterName, 'Specify URL query parameters (ex: -Q token=stuff -Q secret=key)')
[CompletionResult]::new('--query', 'query', [CompletionResultType]::ParameterName, 'Specify URL query parameters (ex: -Q token=stuff -Q secret=key)')
[CompletionResult]::new('-S', 'S', [CompletionResultType]::ParameterName, 'Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)')
[CompletionResult]::new('--filter-size', 'filter-size', [CompletionResultType]::ParameterName, 'Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)')
[CompletionResult]::new('-X', 'X', [CompletionResultType]::ParameterName, 'Filter out messages via regular expression matching on the response''s body (ex: -X ''^ignore me$'')')
[CompletionResult]::new('--filter-regex', 'filter-regex', [CompletionResultType]::ParameterName, 'Filter out messages via regular expression matching on the response''s body (ex: -X ''^ignore me$'')')
[CompletionResult]::new('-W', 'W', [CompletionResultType]::ParameterName, 'Filter out messages of a particular word count (ex: -W 312 -W 91,82)')
[CompletionResult]::new('--filter-words', 'filter-words', [CompletionResultType]::ParameterName, 'Filter out messages of a particular word count (ex: -W 312 -W 91,82)')
[CompletionResult]::new('-N', 'N', [CompletionResultType]::ParameterName, 'Filter out messages of a particular line count (ex: -N 20 -N 31,30)')
[CompletionResult]::new('--filter-lines', 'filter-lines', [CompletionResultType]::ParameterName, 'Filter out messages of a particular line count (ex: -N 20 -N 31,30)')
[CompletionResult]::new('-C', 'C', [CompletionResultType]::ParameterName, 'Filter out status codes (deny list) (ex: -C 200 -C 401)')
[CompletionResult]::new('--filter-status', 'filter-status', [CompletionResultType]::ParameterName, 'Filter out status codes (deny list) (ex: -C 200 -C 401)')
[CompletionResult]::new('-L', 'L', [CompletionResultType]::ParameterName, 'Limit total number of concurrent scans (default: 0, i.e. no limit)')
[CompletionResult]::new('--scan-limit', 'scan-limit', [CompletionResultType]::ParameterName, 'Limit total number of concurrent scans (default: 0, i.e. no limit)')
[CompletionResult]::new('--time-limit', 'time-limit', [CompletionResultType]::ParameterName, 'Limit total run time of all scans (ex: --time-limit 10m)')
[CompletionResult]::new('-v', 'v', [CompletionResultType]::ParameterName, 'Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v''s is probably too much)')
[CompletionResult]::new('--verbosity', 'verbosity', [CompletionResultType]::ParameterName, 'Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v''s is probably too much)')
[CompletionResult]::new('-q', 'q', [CompletionResultType]::ParameterName, 'Only print URLs; Don''t print status codes, response size, running config, etc...')
[CompletionResult]::new('--quiet', 'quiet', [CompletionResultType]::ParameterName, 'Only print URLs; Don''t print status codes, response size, running config, etc...')
[CompletionResult]::new('--json', 'json', [CompletionResultType]::ParameterName, 'Emit JSON logs to --output and --debug-log instead of normal text')
[CompletionResult]::new('-D', 'D', [CompletionResultType]::ParameterName, 'Don''t auto-filter wildcard responses')
[CompletionResult]::new('--dont-filter', 'dont-filter', [CompletionResultType]::ParameterName, 'Don''t auto-filter wildcard responses')
[CompletionResult]::new('-r', 'r', [CompletionResultType]::ParameterName, 'Follow redirects')
[CompletionResult]::new('--redirects', 'redirects', [CompletionResultType]::ParameterName, 'Follow redirects')
[CompletionResult]::new('-k', 'k', [CompletionResultType]::ParameterName, 'Disables TLS certificate validation')
[CompletionResult]::new('--insecure', 'insecure', [CompletionResultType]::ParameterName, 'Disables TLS certificate validation')
[CompletionResult]::new('-n', 'n', [CompletionResultType]::ParameterName, 'Do not scan recursively')
[CompletionResult]::new('--no-recursion', 'no-recursion', [CompletionResultType]::ParameterName, 'Do not scan recursively')
[CompletionResult]::new('-f', 'f', [CompletionResultType]::ParameterName, 'Append / to each request')
[CompletionResult]::new('--add-slash', 'add-slash', [CompletionResultType]::ParameterName, 'Append / to each request')
[CompletionResult]::new('--stdin', 'stdin', [CompletionResultType]::ParameterName, 'Read url(s) from STDIN')
[CompletionResult]::new('-e', 'e', [CompletionResultType]::ParameterName, 'Extract links from response body (html, javascript, etc...); make new requests based on findings (default: false)')
[CompletionResult]::new('--extract-links', 'extract-links', [CompletionResultType]::ParameterName, 'Extract links from response body (html, javascript, etc...); make new requests based on findings (default: false)')
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Prints help information')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Prints help information')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Prints version information')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Prints version information')
break
}
})
$completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
Sort-Object -Property ListItemText
}

View File

@@ -13,9 +13,6 @@ _feroxbuster() {
cmd="feroxbuster"
;;
feroxbuster)
cmd+="__feroxbuster"
;;
*)
;;
esac
@@ -23,186 +20,190 @@ _feroxbuster() {
case "${cmd}" in
feroxbuster)
opts=" -v -q -D -r -k -n -f -e -h -V -w -u -t -d -T -p -P -R -s -o -a -x -H -Q -S -X -W -N -C -L --verbosity --quiet --json --dont-filter --redirects --insecure --no-recursion --add-slash --stdin --extract-links --help --version --wordlist --url --threads --depth --timeout --proxy --replay-proxy --replay-codes --status-codes --output --resume-from --debug-log --user-agent --extensions --headers --query --filter-size --filter-regex --filter-words --filter-lines --filter-status --scan-limit "
opts=" -v -q -D -r -k -n -f -e -h -V -w -u -t -d -T -p -P -R -s -o -a -x -H -Q -S -X -W -N -C -L --verbosity --quiet --json --dont-filter --redirects --insecure --no-recursion --add-slash --stdin --extract-links --help --version --wordlist --url --threads --depth --timeout --proxy --replay-proxy --replay-codes --status-codes --output --resume-from --debug-log --user-agent --extensions --headers --query --filter-size --filter-regex --filter-words --filter-lines --filter-status --scan-limit --time-limit "
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
--wordlist)
COMPREPLY=("<FILE>")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-w)
COMPREPLY=("<FILE>")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--url)
COMPREPLY=("<URL>...")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-u)
COMPREPLY=("<URL>...")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--threads)
COMPREPLY=("<THREADS>")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-t)
COMPREPLY=("<THREADS>")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--depth)
COMPREPLY=("<RECURSION_DEPTH>")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-d)
COMPREPLY=("<RECURSION_DEPTH>")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--timeout)
COMPREPLY=("<SECONDS>")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-T)
COMPREPLY=("<SECONDS>")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--proxy)
COMPREPLY=("<PROXY>")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-p)
COMPREPLY=("<PROXY>")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--replay-proxy)
COMPREPLY=("<REPLAY_PROXY>")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-P)
COMPREPLY=("<REPLAY_PROXY>")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--replay-codes)
COMPREPLY=("<REPLAY_CODE>...")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-R)
COMPREPLY=("<REPLAY_CODE>...")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--status-codes)
COMPREPLY=("<STATUS_CODE>...")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-s)
COMPREPLY=("<STATUS_CODE>...")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--output)
COMPREPLY=("<FILE>")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-o)
COMPREPLY=("<FILE>")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--resume-from)
COMPREPLY=("<STATE_FILE>")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--debug-log)
COMPREPLY=("<FILE>")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--user-agent)
COMPREPLY=("<USER_AGENT>")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-a)
COMPREPLY=("<USER_AGENT>")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--extensions)
COMPREPLY=("<FILE_EXTENSION>...")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-x)
COMPREPLY=("<FILE_EXTENSION>...")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--headers)
COMPREPLY=("<HEADER>...")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-H)
COMPREPLY=("<HEADER>...")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--query)
COMPREPLY=("<QUERY>...")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-Q)
COMPREPLY=("<QUERY>...")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--filter-size)
COMPREPLY=("<SIZE>...")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-S)
COMPREPLY=("<SIZE>...")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--filter-regex)
COMPREPLY=("<REGEX>...")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-X)
COMPREPLY=("<REGEX>...")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--filter-words)
COMPREPLY=("<WORDS>...")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-W)
COMPREPLY=("<WORDS>...")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--filter-lines)
COMPREPLY=("<LINES>...")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-N)
COMPREPLY=("<LINES>...")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--filter-status)
COMPREPLY=("<STATUS_CODE>...")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-C)
COMPREPLY=("<STATUS_CODE>...")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--scan-limit)
COMPREPLY=("<SCAN_LIMIT>")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-L)
COMPREPLY=("<SCAN_LIMIT>")
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--time-limit)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;

View File

@@ -1,47 +1,35 @@
function __fish_using_command
set cmd (commandline -opc)
if [ (count $cmd) -eq (count $argv) ]
for i in (seq (count $argv))
if [ $cmd[$i] != $argv[$i] ]
return 1
end
end
return 0
end
return 1
end
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s w -l wordlist -d 'Path to the wordlist'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s u -l url -d 'The target URL(s) (required, unless --stdin used)'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s t -l threads -d 'Number of concurrent threads (default: 50)'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s d -l depth -d 'Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s T -l timeout -d 'Number of seconds before a request times out (default: 7)'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s p -l proxy -d 'Proxy to use for requests (ex: http(s)://host:port, socks5://host:port)'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s P -l replay-proxy -d 'Send only unfiltered requests through a Replay Proxy, instead of all requests'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s R -l replay-codes -d 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s s -l status-codes -d 'Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s o -l output -d 'Output file to write results to (use w/ --json for JSON entries)'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -l resume-from -d 'State file from which to resume a partially complete scan (ex. --resume-from ferox-1606586780.state)'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -l debug-log -d 'Output file to write log entries (use w/ --json for JSON entries)'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s a -l user-agent -d 'Sets the User-Agent (default: feroxbuster/VERSION)'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s x -l extensions -d 'File extension(s) to search for (ex: -x php -x pdf js)'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s H -l headers -d 'Specify HTTP headers (ex: -H Header:val \'stuff: things\')'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s Q -l query -d 'Specify URL query parameters (ex: -Q token=stuff -Q secret=key)'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s S -l filter-size -d 'Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s X -l filter-regex -d 'Filter out messages via regular expression matching on the response\'s body (ex: -X \'^ignore me$\')'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s W -l filter-words -d 'Filter out messages of a particular word count (ex: -W 312 -W 91,82)'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s N -l filter-lines -d 'Filter out messages of a particular line count (ex: -N 20 -N 31,30)'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s C -l filter-status -d 'Filter out status codes (deny list) (ex: -C 200 -C 401)'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s L -l scan-limit -d 'Limit total number of concurrent scans (default: 0, i.e. no limit)'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s v -l verbosity -d 'Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v\'s is probably too much)'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s q -l quiet -d 'Only print URLs; Don\'t print status codes, response size, running config, etc...'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -l json -d 'Emit JSON logs to --output and --debug-log instead of normal text'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s D -l dont-filter -d 'Don\'t auto-filter wildcard responses'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s r -l redirects -d 'Follow redirects'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s k -l insecure -d 'Disables TLS certificate validation'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s n -l no-recursion -d 'Do not scan recursively'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s f -l add-slash -d 'Append / to each request'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -l stdin -d 'Read url(s) from STDIN'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s e -l extract-links -d 'Extract links from response body (html, javascript, etc...); make new requests based on findings (default: false)'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s h -l help -d 'Prints help information'
complete -c feroxbuster -n "__fish_using_command feroxbuster" -s V -l version -d 'Prints version information'
complete -c feroxbuster -n "__fish_use_subcommand" -s w -l wordlist -d 'Path to the wordlist'
complete -c feroxbuster -n "__fish_use_subcommand" -s u -l url -d 'The target URL(s) (required, unless --stdin used)'
complete -c feroxbuster -n "__fish_use_subcommand" -s t -l threads -d 'Number of concurrent threads (default: 50)'
complete -c feroxbuster -n "__fish_use_subcommand" -s d -l depth -d 'Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)'
complete -c feroxbuster -n "__fish_use_subcommand" -s T -l timeout -d 'Number of seconds before a request times out (default: 7)'
complete -c feroxbuster -n "__fish_use_subcommand" -s p -l proxy -d 'Proxy to use for requests (ex: http(s)://host:port, socks5://host:port)'
complete -c feroxbuster -n "__fish_use_subcommand" -s P -l replay-proxy -d 'Send only unfiltered requests through a Replay Proxy, instead of all requests'
complete -c feroxbuster -n "__fish_use_subcommand" -s R -l replay-codes -d 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)'
complete -c feroxbuster -n "__fish_use_subcommand" -s s -l status-codes -d 'Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)'
complete -c feroxbuster -n "__fish_use_subcommand" -s o -l output -d 'Output file to write results to (use w/ --json for JSON entries)'
complete -c feroxbuster -n "__fish_use_subcommand" -l resume-from -d 'State file from which to resume a partially complete scan (ex. --resume-from ferox-1606586780.state)'
complete -c feroxbuster -n "__fish_use_subcommand" -l debug-log -d 'Output file to write log entries (use w/ --json for JSON entries)'
complete -c feroxbuster -n "__fish_use_subcommand" -s a -l user-agent -d 'Sets the User-Agent (default: feroxbuster/VERSION)'
complete -c feroxbuster -n "__fish_use_subcommand" -s x -l extensions -d 'File extension(s) to search for (ex: -x php -x pdf js)'
complete -c feroxbuster -n "__fish_use_subcommand" -s H -l headers -d 'Specify HTTP headers (ex: -H Header:val \'stuff: things\')'
complete -c feroxbuster -n "__fish_use_subcommand" -s Q -l query -d 'Specify URL query parameters (ex: -Q token=stuff -Q secret=key)'
complete -c feroxbuster -n "__fish_use_subcommand" -s S -l filter-size -d 'Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)'
complete -c feroxbuster -n "__fish_use_subcommand" -s X -l filter-regex -d 'Filter out messages via regular expression matching on the response\'s body (ex: -X \'^ignore me$\')'
complete -c feroxbuster -n "__fish_use_subcommand" -s W -l filter-words -d 'Filter out messages of a particular word count (ex: -W 312 -W 91,82)'
complete -c feroxbuster -n "__fish_use_subcommand" -s N -l filter-lines -d 'Filter out messages of a particular line count (ex: -N 20 -N 31,30)'
complete -c feroxbuster -n "__fish_use_subcommand" -s C -l filter-status -d 'Filter out status codes (deny list) (ex: -C 200 -C 401)'
complete -c feroxbuster -n "__fish_use_subcommand" -s L -l scan-limit -d 'Limit total number of concurrent scans (default: 0, i.e. no limit)'
complete -c feroxbuster -n "__fish_use_subcommand" -l time-limit -d 'Limit total run time of all scans (ex: --time-limit 10m)'
complete -c feroxbuster -n "__fish_use_subcommand" -s v -l verbosity -d 'Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v\'s is probably too much)'
complete -c feroxbuster -n "__fish_use_subcommand" -s q -l quiet -d 'Only print URLs; Don\'t print status codes, response size, running config, etc...'
complete -c feroxbuster -n "__fish_use_subcommand" -l json -d 'Emit JSON logs to --output and --debug-log instead of normal text'
complete -c feroxbuster -n "__fish_use_subcommand" -s D -l dont-filter -d 'Don\'t auto-filter wildcard responses'
complete -c feroxbuster -n "__fish_use_subcommand" -s r -l redirects -d 'Follow redirects'
complete -c feroxbuster -n "__fish_use_subcommand" -s k -l insecure -d 'Disables TLS certificate validation'
complete -c feroxbuster -n "__fish_use_subcommand" -s n -l no-recursion -d 'Do not scan recursively'
complete -c feroxbuster -n "__fish_use_subcommand" -s f -l add-slash -d 'Append / to each request'
complete -c feroxbuster -n "__fish_use_subcommand" -l stdin -d 'Read url(s) from STDIN'
complete -c feroxbuster -n "__fish_use_subcommand" -s e -l extract-links -d 'Extract links from response body (html, javascript, etc...); make new requests based on findings (default: false)'
complete -c feroxbuster -n "__fish_use_subcommand" -s h -l help -d 'Prints help information'
complete -c feroxbuster -n "__fish_use_subcommand" -s V -l version -d 'Prints version information'

View File

@@ -1,6 +1,6 @@
use crate::config::{Configuration, CONFIGURATION};
use crate::utils::{make_request, status_colorizer};
use console::style;
use console::{style, Emoji};
use reqwest::{Client, Url};
use serde_json::Value;
use std::io::Write;
@@ -126,6 +126,14 @@ async fn needs_update(client: &Client, url: &str, bin_version: &str) -> UpdateSt
unknown
}
/// Simple wrapper for emoji or fallback when terminal doesn't support emoji
fn format_emoji(emoji: &str) -> String {
let width = console::measure_text_width(emoji);
let pad_len = width * width;
let pad = format!("{:<pad_len$}", "\u{0020}", pad_len = pad_len);
Emoji(emoji, &pad).to_string()
}
/// Prints the banner to stdout.
///
/// Only prints those settings which are either always present, or passed in by the user.
@@ -138,10 +146,10 @@ where
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher {} ver: {}"#,
'\u{1F913}', version
by Ben "epi" Risher {} ver: {}"#,
Emoji("🤓", &format!("{:<2}", "\u{0020}")),
version
);
let status = needs_update(&CONFIGURATION.client, UPDATE_URL, version).await;
let top = "───────────────────────────┬──────────────────────";
@@ -156,7 +164,7 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1F3af}", "Target Url", target)
format_banner_entry!(format_emoji("🎯"), "Target Url", target)
)
.unwrap_or_default(); // 🎯
}
@@ -170,14 +178,14 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1F680}", "Threads", config.threads)
format_banner_entry!(format_emoji("🚀"), "Threads", config.threads)
)
.unwrap_or_default(); // 🚀
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f4d6}", "Wordlist", config.wordlist)
format_banner_entry!(format_emoji("📖"), "Wordlist", config.wordlist)
)
.unwrap_or_default(); // 📖
@@ -185,7 +193,7 @@ by Ben "epi" Risher {} ver: {}"#,
&mut writer,
"{}",
format_banner_entry!(
"\u{1F197}",
format_emoji("🆗"),
"Status Codes",
format!("[{}]", codes.join(", "))
)
@@ -205,7 +213,7 @@ by Ben "epi" Risher {} ver: {}"#,
&mut writer,
"{}",
format_banner_entry!(
"\u{1f5d1}",
format_emoji("🗑"),
"Status Code Filters",
format!("[{}]", code_filters.join(", "))
)
@@ -216,14 +224,14 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f4a5}", "Timeout (secs)", config.timeout)
format_banner_entry!(format_emoji("💥"), "Timeout (secs)", config.timeout)
)
.unwrap_or_default(); // 💥
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1F9a1}", "User-Agent", config.user_agent)
format_banner_entry!(format_emoji("🦡"), "User-Agent", config.user_agent)
)
.unwrap_or_default(); // 🦡
@@ -232,7 +240,7 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f489}", "Config File", config.config)
format_banner_entry!(format_emoji("💉"), "Config File", config.config)
)
.unwrap_or_default(); // 💉
}
@@ -241,7 +249,7 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f48e}", "Proxy", config.proxy)
format_banner_entry!(format_emoji("💎"), "Proxy", config.proxy)
)
.unwrap_or_default(); // 💎
}
@@ -255,7 +263,7 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f3a5}", "Replay Proxy", config.replay_proxy)
format_banner_entry!(format_emoji("🎥"), "Replay Proxy", config.replay_proxy)
)
.unwrap_or_default(); // 🎥
@@ -267,7 +275,7 @@ by Ben "epi" Risher {} ver: {}"#,
&mut writer,
"{}",
format_banner_entry!(
"\u{1f4fc}",
format_emoji("📼"),
"Replay Proxy Codes",
format!("[{}]", replay_codes.join(", "))
)
@@ -280,7 +288,7 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f92f}", "Header", name, value)
format_banner_entry!(format_emoji("🤯"), "Header", name, value)
)
.unwrap_or_default(); // 🤯
}
@@ -291,7 +299,7 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f4a2}", "Size Filter", filter)
format_banner_entry!(format_emoji("💢"), "Size Filter", filter)
)
.unwrap_or_default(); // 💢
}
@@ -301,7 +309,7 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f4a2}", "Word Count Filter", filter)
format_banner_entry!(format_emoji("💢"), "Word Count Filter", filter)
)
.unwrap_or_default(); // 💢
}
@@ -310,7 +318,7 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f4a2}", "Line Count Filter", filter)
format_banner_entry!(format_emoji("💢"), "Line Count Filter", filter)
)
.unwrap_or_default(); // 💢
}
@@ -319,7 +327,7 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f4a2}", "Regex Filter", filter)
format_banner_entry!(format_emoji("💢"), "Regex Filter", filter)
)
.unwrap_or_default(); // 💢
}
@@ -328,7 +336,7 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1F50E}", "Extract Links", config.extract_links)
format_banner_entry!(format_emoji("🔎"), "Extract Links", config.extract_links)
)
.unwrap_or_default(); // 🔎
}
@@ -337,7 +345,7 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1F9d4}", "JSON Output", config.json)
format_banner_entry!(format_emoji("🧔"), "JSON Output", config.json)
)
.unwrap_or_default(); // 🧔
}
@@ -348,7 +356,7 @@ by Ben "epi" Risher {} ver: {}"#,
&mut writer,
"{}",
format_banner_entry!(
"\u{1f914}",
format_emoji("🤔"),
"Query Parameter",
format!("{}={}", query.0, query.1)
)
@@ -361,7 +369,7 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f4be}", "Output File", config.output)
format_banner_entry!(format_emoji("💾"), "Output File", config.output)
)
.unwrap_or_default(); // 💾
}
@@ -370,7 +378,7 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1fab2}", "Debugging Log", config.debug_log)
format_banner_entry!(format_emoji("🪲"), "Debugging Log", config.debug_log)
)
.unwrap_or_default(); // 🪲
}
@@ -380,7 +388,7 @@ by Ben "epi" Risher {} ver: {}"#,
&mut writer,
"{}",
format_banner_entry!(
"\u{1f4b2}",
format_emoji("💲"),
"Extensions",
format!("[{}]", config.extensions.join(", "))
)
@@ -392,7 +400,7 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f513}", "Insecure", config.insecure)
format_banner_entry!(format_emoji("🔓"), "Insecure", config.insecure)
)
.unwrap_or_default(); // 🔓
}
@@ -401,7 +409,7 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f4cd}", "Follow Redirects", config.redirects)
format_banner_entry!(format_emoji("📍"), "Follow Redirects", config.redirects)
)
.unwrap_or_default(); // 📍
}
@@ -410,53 +418,31 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f92a}", "Filter Wildcards", !config.dont_filter)
format_banner_entry!(format_emoji("🤪"), "Filter Wildcards", !config.dont_filter)
)
.unwrap_or_default(); // 🤪
}
match config.verbosity {
let volume = ["🔈", "🔉", "🔊", "📢"];
if let 1..=4 = config.verbosity {
//speaker medium volume (increasing with verbosity to loudspeaker)
1 => {
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f508}", "Verbosity", config.verbosity)
writeln!(
&mut writer,
"{}",
format_banner_entry!(
format_emoji(volume[config.verbosity as usize - 1]),
"Verbosity",
config.verbosity
)
.unwrap_or_default(); // 🔈
}
2 => {
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f509}", "Verbosity", config.verbosity)
)
.unwrap_or_default(); // 🔉
}
3 => {
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f50a}", "Verbosity", config.verbosity)
)
.unwrap_or_default(); // 🔊
}
4 => {
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f4e2}", "Verbosity", config.verbosity)
)
.unwrap_or_default(); // 📢
}
_ => {}
)
.unwrap_or_default();
}
if config.add_slash {
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1fa93}", "Add Slash", config.add_slash)
format_banner_entry!(format_emoji("🪓"), "Add Slash", config.add_slash)
)
.unwrap_or_default(); // 🪓
}
@@ -466,14 +452,14 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f503}", "Recursion Depth", "INFINITE")
format_banner_entry!(format_emoji("🔃"), "Recursion Depth", "INFINITE")
)
.unwrap_or_default(); // 🔃
} else {
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f503}", "Recursion Depth", config.depth)
format_banner_entry!(format_emoji("🔃"), "Recursion Depth", config.depth)
)
.unwrap_or_default(); // 🔃
}
@@ -481,7 +467,7 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f6ab}", "Do Not Recurse", config.no_recursion)
format_banner_entry!(format_emoji("🚫"), "Do Not Recurse", config.no_recursion)
)
.unwrap_or_default(); // 🚫
}
@@ -490,17 +476,30 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f9a5}", "Concurrent Scan Limit", config.scan_limit)
format_banner_entry!(
format_emoji("🦥"),
"Concurrent Scan Limit",
config.scan_limit
)
)
.unwrap_or_default(); // 🦥
}
if !CONFIGURATION.time_limit.is_empty() {
writeln!(
&mut writer,
"{}",
format_banner_entry!(format_emoji("🕖"), "Time Limit", config.time_limit)
)
.unwrap_or_default(); // 🕖
}
if matches!(status, UpdateStatus::OutOfDate) {
writeln!(
&mut writer,
"{}",
format_banner_entry!(
"\u{1f389}",
format_emoji("🎉"),
"New Version Available",
"https://github.com/epi052/feroxbuster/releases/latest"
)
@@ -512,12 +511,14 @@ by Ben "epi" Risher {} ver: {}"#,
// ⏯
writeln!(
&mut writer,
" \u{23ef} Press [{}] to {}|{} your scan",
" {} Press [{}] to {}|{} your scan",
format_emoji(""),
style("ENTER").yellow(),
style("pause").red(),
style("resume").green()
)
.unwrap_or_default();
writeln!(&mut writer, "{}", addl_section).unwrap_or_default();
}
@@ -526,7 +527,7 @@ mod tests {
use super::*;
use crate::VERSION;
use httpmock::Method::GET;
use httpmock::{Mock, MockServer};
use httpmock::MockServer;
use std::fs::read_to_string;
use std::io::stderr;
use std::time::Duration;
@@ -611,16 +612,14 @@ mod tests {
async fn banner_needs_update_returns_up_to_date() {
let srv = MockServer::start();
let mock = Mock::new()
.expect_method(GET)
.expect_path("/latest")
.return_status(200)
.return_body("{\"tag_name\":\"v1.1.0\"}")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/latest");
then.status(200).body("{\"tag_name\":\"v1.1.0\"}");
});
let result = needs_update(&CONFIGURATION.client, &srv.url("/latest"), "1.1.0").await;
assert_eq!(mock.times_called(), 1);
assert_eq!(mock.hits(), 1);
assert!(matches!(result, UpdateStatus::UpToDate));
}
@@ -629,16 +628,14 @@ mod tests {
async fn banner_needs_update_returns_out_of_date() {
let srv = MockServer::start();
let mock = Mock::new()
.expect_method(GET)
.expect_path("/latest")
.return_status(200)
.return_body("{\"tag_name\":\"v1.1.0\"}")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/latest");
then.status(200).body("{\"tag_name\":\"v1.1.0\"}");
});
let result = needs_update(&CONFIGURATION.client, &srv.url("/latest"), "1.0.1").await;
assert_eq!(mock.times_called(), 1);
assert_eq!(mock.hits(), 1);
assert!(matches!(result, UpdateStatus::OutOfDate));
}
@@ -647,17 +644,16 @@ mod tests {
async fn banner_needs_update_returns_unknown_on_timeout() {
let srv = MockServer::start();
let mock = Mock::new()
.expect_method(GET)
.expect_path("/latest")
.return_status(200)
.return_body("{\"tag_name\":\"v1.1.0\"}")
.return_with_delay(Duration::from_secs(8))
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/latest");
then.status(200)
.body("{\"tag_name\":\"v1.1.0\"}")
.delay(Duration::from_secs(8));
});
let result = needs_update(&CONFIGURATION.client, &srv.url("/latest"), "1.0.1").await;
assert_eq!(mock.times_called(), 1);
assert_eq!(mock.hits(), 1);
assert!(matches!(result, UpdateStatus::Unknown));
}
@@ -666,16 +662,14 @@ mod tests {
async fn banner_needs_update_returns_unknown_on_bad_json_response() {
let srv = MockServer::start();
let mock = Mock::new()
.expect_method(GET)
.expect_path("/latest")
.return_status(200)
.return_body("not json")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/latest");
then.status(200).body("not json");
});
let result = needs_update(&CONFIGURATION.client, &srv.url("/latest"), "1.0.1").await;
assert_eq!(mock.times_called(), 1);
assert_eq!(mock.hits(), 1);
assert!(matches!(result, UpdateStatus::Unknown));
}
@@ -684,16 +678,15 @@ mod tests {
async fn banner_needs_update_returns_unknown_on_json_without_correct_tag() {
let srv = MockServer::start();
let mock = Mock::new()
.expect_method(GET)
.expect_path("/latest")
.return_status(200)
.return_body("{\"no tag_name\": \"doesn't exist\"}")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/latest");
then.status(200)
.body("{\"no tag_name\": \"doesn't exist\"}");
});
let result = needs_update(&CONFIGURATION.client, &srv.url("/latest"), "1.0.1").await;
assert_eq!(mock.times_called(), 1);
assert_eq!(mock.hits(), 1);
assert!(matches!(result, UpdateStatus::Unknown));
}
}

View File

@@ -228,6 +228,11 @@ pub struct Configuration {
/// Not configurable from CLI; can only be set from a config file
#[serde(default = "save_state")]
pub save_state: bool,
/// The maximum runtime for a scan, expressed as N[smdh] where N can be parsed into a
/// non-negative integer and the next character is either s, m, h, or d (case insensitive)
#[serde(default)]
pub time_limit: String,
}
// functions timeout, threads, status_codes, user_agent, wordlist, save_state, and depth are used to provide
@@ -314,6 +319,7 @@ impl Default for Configuration {
output: String::new(),
debug_log: String::new(),
target_url: String::new(),
time_limit: String::new(),
replay_proxy: String::new(),
queries: Vec::new(),
extensions: Vec::new(),
@@ -365,6 +371,7 @@ impl Configuration {
/// - **dont_filter**: `false` (auto filter wildcard responses)
/// - **depth**: `4` (maximum recursion depth)
/// - **scan_limit**: `0` (no limit on concurrent scans imposed)
/// - **time_limit**: `None` (no limit on length of scan imposed)
/// - **replay_proxy**: `None` (no limit on concurrent scans imposed)
/// - **replay_codes**: [`DEFAULT_RESPONSE_CODES`](constant.DEFAULT_RESPONSE_CODES.html)
///
@@ -388,7 +395,9 @@ impl Configuration {
pub fn new() -> Self {
// when compiling for test, we want to eliminate the runtime dependency of the parser
if cfg!(test) {
return Configuration::default();
let mut test_config = Configuration::default();
test_config.save_state = false; // don't clutter up junk when testing
return test_config;
}
let args = parser::initialize().get_matches();
@@ -499,6 +508,7 @@ impl Configuration {
update_config_if_present!(&mut config.wordlist, args, "wordlist", String);
update_config_if_present!(&mut config.output, args, "output", String);
update_config_if_present!(&mut config.debug_log, args, "debug_log", String);
update_config_if_present!(&mut config.time_limit, args, "time_limit", String);
if let Some(arg) = args.values_of("status_codes") {
config.status_codes = arg
@@ -730,6 +740,7 @@ impl Configuration {
// - resumed
// - config
update_if_not_default!(&mut conf.target_url, new.target_url, "");
update_if_not_default!(&mut conf.time_limit, new.time_limit, "");
update_if_not_default!(&mut conf.proxy, new.proxy, "");
update_if_not_default!(&mut conf.verbosity, new.verbosity, 0);
update_if_not_default!(&mut conf.quiet, new.quiet, false);
@@ -864,6 +875,7 @@ mod tests {
quiet = true
verbosity = 1
scan_limit = 6
time_limit = "10m"
output = "/some/otherpath"
debug_log = "/yet/anotherpath"
redirects = true
@@ -898,6 +910,7 @@ mod tests {
assert_eq!(config.wordlist, wordlist());
assert_eq!(config.proxy, String::new());
assert_eq!(config.target_url, String::new());
assert_eq!(config.time_limit, String::new());
assert_eq!(config.debug_log, String::new());
assert_eq!(config.config, String::new());
assert_eq!(config.replay_proxy, String::new());
@@ -1125,6 +1138,13 @@ mod tests {
assert_eq!(config.save_state, false);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_time_limit() {
let config = setup_config_test();
assert_eq!(config.time_limit, "10m");
}
#[test]
/// parse the test config and see that the values parsed are correct
fn config_reads_headers() {

View File

@@ -1,4 +1,10 @@
use crate::FeroxResponse;
use crate::{
client,
config::{Configuration, CONFIGURATION},
scanner::SCANNED_URLS,
utils::{format_url, make_request},
FeroxResponse,
};
use lazy_static::lazy_static;
use regex::Regex;
use reqwest::Url;
@@ -9,9 +15,18 @@ use std::collections::HashSet;
/// Incorporates change from this [Pull Request](https://github.com/GerbenJavado/LinkFinder/pull/66/files)
const LINKFINDER_REGEX: &str = r#"(?:"|')(((?:[a-zA-Z]{1,10}://|//)[^"'/]{1,}\.[a-zA-Z]{2,}[^"']{0,})|((?:/|\.\./|\./)[^"'><,;| *()(%%$^/\\\[\]][^"'><,;|()]{1,})|([a-zA-Z0-9_\-/]{1,}/[a-zA-Z0-9_\-/]{1,}\.(?:[a-zA-Z]{1,4}|action)(?:[\?|#][^"|']{0,}|))|([a-zA-Z0-9_\-/]{1,}/[a-zA-Z0-9_\-/]{3,}(?:[\?|#][^"|']{0,}|))|([a-zA-Z0-9_\-.]{1,}\.(?:php|asp|aspx|jsp|json|action|html|js|txt|xml)(?:[\?|#][^"|']{0,}|)))(?:"|')"#;
/// Regular expression to pull url paths from robots.txt
///
/// ref: https://developers.google.com/search/reference/robots_txt
const ROBOTS_TXT_REGEX: &str =
r#"(?m)^ *(Allow|Disallow): *(?P<url_path>[a-zA-Z0-9._/?#@!&'()+,;%=-]+?)$"#; // multi-line (?m)
lazy_static! {
/// `LINKFINDER_REGEX` as a regex::Regex type
static ref REGEX: Regex = Regex::new(LINKFINDER_REGEX).unwrap();
static ref LINKS_REGEX: Regex = Regex::new(LINKFINDER_REGEX).unwrap();
/// `ROBOTS_TXT_REGEX` as a regex::Regex type
static ref ROBOTS_REGEX: Regex = Regex::new(ROBOTS_TXT_REGEX).unwrap();
}
/// Iterate over a given path, return a list of every sub-path found
@@ -90,7 +105,7 @@ pub async fn get_links(response: &FeroxResponse) -> HashSet<String> {
let body = response.text();
for capture in REGEX.captures_iter(&body) {
for capture in LINKS_REGEX.captures_iter(&body) {
// remove single & double quotes from both ends of the capture
// capture[0] is the entire match, additional capture groups start at [1]
let link = capture[0].trim_matches(|c| c == '\'' || c == '"');
@@ -105,27 +120,14 @@ pub async fn get_links(response: &FeroxResponse) -> HashSet<String> {
continue;
}
for sub_path in get_sub_paths_from_path(absolute.path()) {
// take a url fragment like homepage/assets/img/icons/handshake.svg and
// incrementally add
// - homepage/assets/img/icons/
// - homepage/assets/img/
// - homepage/assets/
// - homepage/
log::debug!("Adding {} to {:?}", sub_path, links);
add_link_to_set_of_links(&sub_path, &response.url(), &mut links);
}
add_all_sub_paths(absolute.path(), &response, &mut links);
}
Err(e) => {
// this is the expected error that happens when we try to parse a url fragment
// ex: Url::parse("/login") -> Err("relative URL without a base")
// while this is technically an error, these are good results for us
if e.to_string().contains("relative URL without a base") {
for sub_path in get_sub_paths_from_path(link) {
// incrementally save all sub-paths that led to the relative url's resource
log::debug!("Adding {} to {:?}", sub_path, links);
add_link_to_set_of_links(&sub_path, &response.url(), &mut links);
}
add_all_sub_paths(link, &response, &mut links);
} else {
// unexpected error has occurred
log::error!("Could not parse given url: {}", e);
@@ -135,6 +137,152 @@ pub async fn get_links(response: &FeroxResponse) -> HashSet<String> {
}
log::trace!("exit: get_links -> {:?}", links);
links
}
/// take a url fragment like homepage/assets/img/icons/handshake.svg and
/// incrementally add
/// - homepage/assets/img/icons/
/// - homepage/assets/img/
/// - homepage/assets/
/// - homepage/
fn add_all_sub_paths(url_path: &str, response: &FeroxResponse, mut links: &mut HashSet<String>) {
log::trace!(
"enter: add_all_sub_paths({}, {}, {:?})",
url_path,
response,
links
);
for sub_path in get_sub_paths_from_path(url_path) {
log::debug!("Adding {} to {:?}", sub_path, links);
add_link_to_set_of_links(&sub_path, &response.url(), &mut links);
}
log::trace!("exit: add_all_sub_paths");
}
/// Wrapper around link extraction logic
/// currently used in two places:
/// - links from response bodys
/// - links from robots.txt responses
///
/// general steps taken:
/// - create a new Url object based on cli options/args
/// - check if the new Url has already been seen/scanned -> None
/// - make a request to the new Url ? -> Some(response) : None
pub async fn request_feroxresponse_from_new_link(url: &str) -> Option<FeroxResponse> {
log::trace!("enter: request_feroxresponse_from_new_link({})", url);
// create a url based on the given command line options, return None on error
let new_url = match format_url(
&url,
&"",
CONFIGURATION.add_slash,
&CONFIGURATION.queries,
None,
) {
Ok(url) => url,
Err(_) => {
log::trace!("exit: request_feroxresponse_from_new_link -> None");
return None;
}
};
if SCANNED_URLS.get_scan_by_url(&new_url.to_string()).is_some() {
//we've seen the url before and don't need to scan again
log::trace!("exit: request_feroxresponse_from_new_link -> None");
return None;
}
// make the request and store the response
let new_response = match make_request(&CONFIGURATION.client, &new_url).await {
Ok(resp) => resp,
Err(_) => {
log::trace!("exit: request_feroxresponse_from_new_link -> None");
return None;
}
};
let new_ferox_response = FeroxResponse::from(new_response, true).await;
log::trace!(
"exit: request_feroxresponse_from_new_link -> {:?}",
new_ferox_response
);
Some(new_ferox_response)
}
/// helper function that simply requests /robots.txt on the given url's base url
///
/// example:
/// http://localhost/api/users -> http://localhost/robots.txt
///
/// The length of the given path has no effect on what's requested; it's always
/// base url + /robots.txt
pub async fn request_robots_txt(base_url: &str, config: &Configuration) -> Option<FeroxResponse> {
log::trace!("enter: get_robots_file({})", base_url);
// more often than not, domain/robots.txt will redirect to www.domain/robots.txt or something
// similar; to account for that, create a client that will follow redirects, regardless of
// what the user specified for the scanning client. Other than redirects, it will respect
// all other user specified settings
let follow_redirects = true;
let proxy = if config.proxy.is_empty() {
None
} else {
Some(config.proxy.as_str())
};
let client = client::initialize(
config.timeout,
&config.user_agent,
follow_redirects,
config.insecure,
&config.headers,
proxy,
);
if let Ok(mut url) = Url::parse(base_url) {
url.set_path("/robots.txt"); // overwrite existing path with /robots.txt
if let Ok(response) = make_request(&client, &url).await {
let ferox_response = FeroxResponse::from(response, true).await;
log::trace!("exit: get_robots_file -> {}", ferox_response);
return Some(ferox_response);
}
}
None
}
/// Entry point to perform link extraction from robots.txt
///
/// `base_url` can have paths and subpaths, however robots.txt will be requested from the
/// root of the url
/// given the url:
/// http://localhost/stuff/things
/// this function requests:
/// http://localhost/robots.txt
pub async fn extract_robots_txt(base_url: &str, config: &Configuration) -> HashSet<String> {
log::trace!("enter: extract_robots_txt({}, CONFIGURATION)", base_url);
let mut links = HashSet::new();
if let Some(response) = request_robots_txt(&base_url, &config).await {
for capture in ROBOTS_REGEX.captures_iter(response.text.as_str()) {
if let Some(new_path) = capture.name("url_path") {
if let Ok(mut new_url) = Url::parse(base_url) {
new_url.set_path(new_path.as_str());
add_all_sub_paths(new_url.path(), &response, &mut links);
}
}
}
}
log::trace!("exit: extract_robots_txt -> {:?}", links);
links
}
@@ -143,7 +291,7 @@ mod tests {
use super::*;
use crate::utils::make_request;
use httpmock::Method::GET;
use httpmock::{Mock, MockServer};
use httpmock::MockServer;
use reqwest::Client;
#[test]
@@ -245,12 +393,12 @@ mod tests {
) -> Result<(), Box<dyn std::error::Error>> {
let srv = MockServer::start();
let mock = Mock::new()
.expect_method(GET)
.expect_path("/some-path")
.return_status(200)
.return_body("\"http://defintely.not.a.thing.probably.com/homepage/assets/img/icons/handshake.svg\"")
.create_on(&srv);
let mock = srv.mock(|when, then|{
when.method(GET)
.path("/some-path");
then.status(200)
.body("\"http://defintely.not.a.thing.probably.com/homepage/assets/img/icons/handshake.svg\"");
});
let client = Client::new();
let url = Url::parse(&srv.url("/some-path")).unwrap();
@@ -263,7 +411,30 @@ mod tests {
assert!(links.is_empty());
assert_eq!(mock.times_called(), 1);
assert_eq!(mock.hits(), 1);
Ok(())
}
#[tokio::test(core_threads = 1)]
/// test that /robots.txt is correctly requested given a base url (happy path)
async fn request_robots_txt_with_and_without_proxy() {
let srv = MockServer::start();
let mock = srv.mock(|when, then| {
when.method(GET).path("/robots.txt");
then.status(200).body("this is a test");
});
let mut config = Configuration::default();
request_robots_txt(&srv.url("/api/users/stuff/things"), &config).await;
// note: the proxy doesn't actually do anything other than hit a different code branch
// in this unit test; it would however have an effect on an integration test
config.proxy = srv.url("/ima-proxy");
request_robots_txt(&srv.url("/api/different/path"), &config).await;
assert_eq!(mock.hits(), 2);
}
}

View File

@@ -1,11 +1,13 @@
use crossterm::event::{self, Event, KeyCode};
use feroxbuster::progress::add_bar;
use feroxbuster::{
banner,
config::{CONFIGURATION, PROGRESS_BAR, PROGRESS_PRINTER},
heuristics, logger, reporter,
extractor::{extract_robots_txt, request_feroxresponse_from_new_link},
heuristics, logger,
progress::add_bar,
reporter,
scan_manager::{self, PAUSE_SCAN},
scanner::{self, scan_url, RESPONSES, SCANNED_URLS},
scanner::{self, scan_url, send_report, RESPONSES, SCANNED_URLS},
utils::{ferox_print, get_current_depth, module_colorizer, status_colorizer},
FeroxError, FeroxResponse, FeroxResult, FeroxSerialize, SLEEP_DURATION, VERSION,
};
@@ -97,7 +99,7 @@ fn get_unique_words_from_wordlist(path: &str) -> FeroxResult<Arc<HashSet<String>
/// Determine whether it's a single url scan or urls are coming from stdin, then scan as needed
async fn scan(
targets: Vec<String>,
mut targets: Vec<String>,
tx_term: UnboundedSender<FeroxResponse>,
tx_file: UnboundedSender<FeroxResponse>,
) -> FeroxResult<()> {
@@ -142,6 +144,37 @@ async fn scan(
}
}
if CONFIGURATION.extract_links {
for target in targets.clone() {
// modifying the targets vector, so we can't have a reference to it while we borrow
// it as mutable; thus the clone
let robots_links = extract_robots_txt(&target, &CONFIGURATION).await;
for robot_link in robots_links {
// create a url based on the given command line options, continue on error
let ferox_response = match request_feroxresponse_from_new_link(&robot_link).await {
Some(resp) => resp,
None => continue,
};
if ferox_response.is_file() {
SCANNED_URLS.add_file_scan(&robot_link);
send_report(tx_term.clone(), ferox_response);
} else {
let (unknown, _) = SCANNED_URLS.add_directory_scan(&robot_link);
if !unknown {
// known directory; can skip (unlikely)
continue;
}
// unknown directory; add to targets for scanning
targets.push(robot_link);
}
}
}
}
let mut tasks = vec![];
for target in targets {
@@ -220,6 +253,14 @@ async fn wrapped_main() {
PROGRESS_BAR.join().unwrap();
});
if !CONFIGURATION.time_limit.is_empty() {
// --time-limit value not an empty string, need to kick off the thread that enforces
// the limit
tokio::spawn(async move {
scan_manager::start_max_time_thread(&CONFIGURATION.time_limit).await
});
}
// can't trace main until after logger is initialized and the above task is started
log::trace!("enter: main");

View File

@@ -1,4 +1,18 @@
use clap::{App, Arg, ArgGroup};
use lazy_static::lazy_static;
use regex::Regex;
lazy_static! {
/// Regex used to validate values passed to --time-limit
///
/// Examples of expected values that will this regex will match:
/// - 30s
/// - 20m
/// - 1h
/// - 1d
pub static ref TIMESPEC_REGEX: Regex =
Regex::new(r"^(?i)(?P<n>\d+)(?P<m>[smdh])$").expect("Could not compile regex");
}
/// Create and return an instance of [clap::App](https://docs.rs/clap/latest/clap/struct.App.html), i.e. the Command Line Interface's configuration
pub fn initialize() -> App<'static, 'static> {
@@ -302,6 +316,14 @@ pub fn initialize() -> App<'static, 'static> {
.takes_value(true)
.help("Limit total number of concurrent scans (default: 0, i.e. no limit)")
)
.arg(
Arg::with_name("time_limit")
.long("time-limit")
.value_name("TIME_SPEC")
.takes_value(true)
.validator(valid_time_spec)
.help("Limit total run time of all scans (ex: --time-limit 10m)")
)
.group(ArgGroup::with_name("output_files")
.args(&["debug_log", "output"])
.multiple(true)
@@ -343,6 +365,20 @@ EXAMPLES:
"#)
}
/// Validate that a string is formatted as a number followed by s, m, h, or d (10d, 30s, etc...)
fn valid_time_spec(time_spec: String) -> Result<(), String> {
match TIMESPEC_REGEX.is_match(&time_spec) {
true => Ok(()),
false => {
let msg = format!(
"Expected a non-negative, whole number followed by s, m, h, or d (case insensitive); received {}",
time_spec
);
Err(msg)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -353,4 +389,37 @@ mod tests {
let app = initialize();
assert_eq!(app.get_name(), "feroxbuster");
}
#[test]
/// sanity checks that valid_time_spec correctly checks and rejects a given string
///
/// instead of having a bunch of single tests here, they're all quick and are mostly checking
/// that i didn't hose up the regex. Going to consolidate them into a single test
fn validate_valid_time_spec_validation() {
let float_rejected = "1.4m";
assert!(valid_time_spec(float_rejected.into()).is_err());
let negative_rejected = "-1m";
assert!(valid_time_spec(negative_rejected.into()).is_err());
let only_number_rejected = "1";
assert!(valid_time_spec(only_number_rejected.into()).is_err());
let only_measurement_rejected = "m";
assert!(valid_time_spec(only_measurement_rejected.into()).is_err());
for accepted_measurement in &["s", "m", "h", "d", "S", "M", "H", "D"] {
// all upper/lowercase should be good
assert!(valid_time_spec(format!("1{}", *accepted_measurement)).is_ok());
}
let leading_space_rejected = " 14m";
assert!(valid_time_spec(leading_space_rejected.into()).is_err());
let trailing_space_rejected = "14m ";
assert!(valid_time_spec(trailing_space_rejected.into()).is_err());
let space_between_rejected = "1 4m";
assert!(valid_time_spec(space_between_rejected.into()).is_err());
}
}

View File

@@ -94,7 +94,7 @@ async fn spawn_terminal_reporter(
save_output
);
while let Some(resp) = resp_chan.recv().await {
while let Some(mut resp) = resp_chan.recv().await {
log::trace!("received {} on reporting channel", resp.url());
let contains_sentry = CONFIGURATION.status_codes.contains(&resp.status().as_u16());
@@ -134,6 +134,11 @@ async fn spawn_terminal_reporter(
// add response to RESPONSES for serialization in case of ctrl+c
// placed all by its lonesome like this so that RESPONSES can take ownership
// of the FeroxResponse
// before ownership is transferred, there's no real reason to keep the body anymore
// so we can free that piece of data, reducing memory usage
resp.text = String::new();
RESPONSES.insert(resp);
}
}

View File

@@ -3,6 +3,7 @@ use crate::reporter::safe_file_write;
use crate::utils::open_file;
use crate::{
config::{CONFIGURATION, PROGRESS_PRINTER},
parser::TIMESPEC_REGEX,
progress,
scanner::{NUMBER_OF_REQUESTS, RESPONSES, SCANNED_URLS},
FeroxResponse, FeroxSerialize, SLEEP_DURATION,
@@ -589,51 +590,102 @@ impl FeroxSerialize for FeroxState {
}
}
/// Given a string representing some number of seconds, minutes, hours, or days, convert
/// that representation to seconds and then wait for those seconds to elapse. Once that period
/// of time has elapsed, kill all currently running scans and dump a state file to disk that can
/// be used to resume any unfinished scan.
pub async fn start_max_time_thread(time_spec: &str) {
log::trace!("enter: start_max_time_thread({})", time_spec);
// as this function has already made it through the parser, which calls is_match on
// the value passed to --time-limit using TIMESPEC_REGEX; we can safely assume that
// the capture groups are populated; can expect something like 10m, 30s, 1h, etc...
let captures = TIMESPEC_REGEX.captures(&time_spec).unwrap();
let length_match = captures.get(1).unwrap();
let measurement_match = captures.get(2).unwrap();
if let Ok(length) = length_match.as_str().parse::<u64>() {
let length_in_secs = match measurement_match.as_str().to_ascii_lowercase().as_str() {
"s" => length,
"m" => length * 60, // minutes
"h" => length * 60 * 60, // hours
"d" => length * 60 * 60 * 24, // days
_ => length,
};
log::debug!(
"max time limit as string: {} and as seconds: {}",
time_spec,
length_in_secs
);
time::delay_for(time::Duration::new(length_in_secs, 0)).await;
log::trace!("exit: start_max_time_thread");
#[cfg(test)]
panic!();
#[cfg(not(test))]
sigint_handler();
}
log::error!(
"Could not parse the value provided ({}), can't enforce time limit",
length_match.as_str()
);
}
/// Writes the current state of the program to disk (if save_state is true) and then exits
fn sigint_handler() {
log::trace!("enter: sigint_handler");
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let slug = if !CONFIGURATION.target_url.is_empty() {
// target url populated
CONFIGURATION
.target_url
.replace("://", "_")
.replace("/", "_")
.replace(".", "_")
} else {
// stdin used
"stdin".to_string()
};
let filename = format!("ferox-{}-{}.state", slug, ts);
let warning = format!(
"🚨 Caught {} 🚨 saving scan state to {} ...",
style("ctrl+c").yellow(),
filename
);
PROGRESS_PRINTER.println(warning);
let state = FeroxState {
config: &CONFIGURATION,
scans: &SCANNED_URLS,
responses: &RESPONSES,
};
let state_file = open_file(&filename);
if let Some(buffered_file) = state_file {
safe_file_write(&state, buffered_file, true);
}
log::trace!("exit: sigint_handler (end of program)");
std::process::exit(1);
}
/// Initialize the ctrl+c handler that saves scan state to disk
pub fn initialize() {
log::trace!("enter: initialize");
let result = ctrlc::set_handler(move || {
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let slug = if !CONFIGURATION.target_url.is_empty() {
// target url populated
CONFIGURATION
.target_url
.replace("://", "_")
.replace("/", "_")
.replace(".", "_")
} else {
// stdin used
"stdin".to_string()
};
let filename = format!("ferox-{}-{}.state", slug, ts);
let warning = format!(
"🚨 Caught {} 🚨 saving scan state to {} ...",
style("ctrl+c").yellow(),
filename
);
PROGRESS_PRINTER.println(warning);
let state = FeroxState {
config: &CONFIGURATION,
scans: &SCANNED_URLS,
responses: &RESPONSES,
};
let state_file = open_file(&filename);
if let Some(buffered_file) = state_file {
safe_file_write(&state, buffered_file, true);
}
std::process::exit(1);
});
let result = ctrlc::set_handler(sigint_handler);
if result.is_err() {
log::error!("Could not set Ctrl+c handler");
@@ -983,10 +1035,36 @@ mod tests {
let json_state = ferox_state.as_json();
let expected = format!(
r#"{{"scans":[{{"id":"{}","url":"https://spiritanimal.com","scan_type":"Directory","complete":false}}],"config":{{"type":"configuration","wordlist":"/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt","config":"","proxy":"","replay_proxy":"","target_url":"","status_codes":[200,204,301,302,307,308,401,403,405],"replay_codes":[200,204,301,302,307,308,401,403,405],"filter_status":[],"threads":50,"timeout":7,"verbosity":0,"quiet":false,"json":false,"output":"","debug_log":"","user_agent":"feroxbuster/{}","redirects":false,"insecure":false,"extensions":[],"headers":{{}},"queries":[],"no_recursion":false,"extract_links":false,"add_slash":false,"stdin":false,"depth":4,"scan_limit":0,"filter_size":[],"filter_line_count":[],"filter_word_count":[],"filter_regex":[],"dont_filter":false,"resumed":false,"save_state":true}},"responses":[{{"type":"response","url":"https://nerdcore.com/css","path":"/css","wildcard":true,"status":301,"content_length":173,"line_count":10,"word_count":16,"headers":{{"server":"nginx/1.16.1"}}}}]}}"#,
r#"{{"scans":[{{"id":"{}","url":"https://spiritanimal.com","scan_type":"Directory","complete":false}}],"config":{{"type":"configuration","wordlist":"/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt","config":"","proxy":"","replay_proxy":"","target_url":"","status_codes":[200,204,301,302,307,308,401,403,405],"replay_codes":[200,204,301,302,307,308,401,403,405],"filter_status":[],"threads":50,"timeout":7,"verbosity":0,"quiet":false,"json":false,"output":"","debug_log":"","user_agent":"feroxbuster/{}","redirects":false,"insecure":false,"extensions":[],"headers":{{}},"queries":[],"no_recursion":false,"extract_links":false,"add_slash":false,"stdin":false,"depth":4,"scan_limit":0,"filter_size":[],"filter_line_count":[],"filter_word_count":[],"filter_regex":[],"dont_filter":false,"resumed":false,"save_state":false,"time_limit":""}},"responses":[{{"type":"response","url":"https://nerdcore.com/css","path":"/css","wildcard":true,"status":301,"content_length":173,"line_count":10,"word_count":16,"headers":{{"server":"nginx/1.16.1"}}}}]}}"#,
saved_id, VERSION
);
assert!(predicates::str::similar(expected).eval(&json_state));
}
#[should_panic]
#[tokio::test(core_threads = 1)]
/// call start_max_time_thread with a valid timespec, expect a panic, but only after a certain
/// number of seconds
async fn start_max_time_thread_panics_after_delay() {
let now = time::Instant::now();
let delay = time::Duration::new(3, 0);
start_max_time_thread("3s").await;
assert!(now.elapsed() > delay);
}
#[tokio::test(core_threads = 1)]
/// call start_max_time_thread with a timespec that's too large to be parsed correctly, expect
/// immediate return and no panic, as the sigint handler is never called
async fn start_max_time_thread_returns_immediately_with_too_large_input() {
let now = time::Instant::now();
let delay = time::Duration::new(1, 0);
// pub const MAX: usize = usize::MAX; // 18_446_744_073_709_551_615usize
start_max_time_thread("18446744073709551616m").await; // can't fit in dest u64
assert!(now.elapsed() < delay); // assuming function call will take less than 1second
}
}

View File

@@ -1,6 +1,6 @@
use crate::{
config::{Configuration, CONFIGURATION},
extractor::get_links,
extractor::{get_links, request_feroxresponse_from_new_link},
filters::{
FeroxFilter, LinesFilter, RegexFilter, SizeFilter, StatusCodeFilter, WildcardFilter,
WordsFilter,
@@ -385,30 +385,11 @@ async fn make_requests(
let new_links = get_links(&ferox_response).await;
for new_link in new_links {
// create a url based on the given command line options, continue on error
let new_url = match format_url(
&new_link,
&"",
CONFIGURATION.add_slash,
&CONFIGURATION.queries,
None,
) {
Ok(url) => url,
Err(_) => continue,
};
if SCANNED_URLS.get_scan_by_url(&new_url.to_string()).is_some() {
//we've seen the url before and don't need to scan again
continue;
}
// make the request and store the response
let new_response = match make_request(&CONFIGURATION.client, &new_url).await {
Ok(resp) => resp,
Err(_) => continue,
};
let mut new_ferox_response = FeroxResponse::from(new_response, true).await;
let mut new_ferox_response =
match request_feroxresponse_from_new_link(&new_link).await {
Some(resp) => resp,
None => continue,
};
// filter if necessary
if should_filter_response(&new_ferox_response) {
@@ -419,7 +400,7 @@ async fn make_requests(
// very likely a file, simply request and report
log::debug!("Singular extraction: {}", new_ferox_response);
SCANNED_URLS.add_file_scan(&new_url.to_string());
SCANNED_URLS.add_file_scan(&new_ferox_response.url().to_string());
send_report(report_chan.clone(), new_ferox_response);
@@ -452,7 +433,7 @@ async fn make_requests(
}
/// Simple helper to send a `FeroxResponse` over the tx side of an `mpsc::unbounded_channel`
fn send_report(report_sender: UnboundedSender<FeroxResponse>, response: FeroxResponse) {
pub fn send_report(report_sender: UnboundedSender<FeroxResponse>, response: FeroxResponse) {
log::trace!("enter: send_report({:?}, {}", report_sender, response);
match report_sender.send(response) {

102774
tests/extra-words Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -783,3 +783,30 @@ fn banner_prints_filter_regex() {
.and(predicate::str::contains("─┴─")),
);
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + time limit
fn banner_prints_time_limit() {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("--time-limit")
.arg("10m")
.assert()
.success()
.stderr(
predicate::str::contains("─┬─")
.and(predicate::str::contains("Target Url"))
.and(predicate::str::contains("http://localhost"))
.and(predicate::str::contains("Threads"))
.and(predicate::str::contains("Wordlist"))
.and(predicate::str::contains("Status Codes"))
.and(predicate::str::contains("Timeout (secs)"))
.and(predicate::str::contains("User-Agent"))
.and(predicate::str::contains("Time Limit"))
.and(predicate::str::contains("│ 10m"))
.and(predicate::str::contains("─┴─")),
);
}

View File

@@ -1,7 +1,7 @@
mod utils;
use assert_cmd::prelude::*;
use httpmock::Method::GET;
use httpmock::{Mock, MockServer};
use httpmock::MockServer;
use predicates::prelude::*;
use std::process::Command;
use utils::{setup_tmp_directory, teardown_tmp_directory};
@@ -13,18 +13,17 @@ fn extractor_finds_absolute_url() -> Result<(), Box<dyn std::error::Error>> {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
let mock = Mock::new()
.expect_method(GET)
.expect_path("/LICENSE")
.return_status(200)
.return_body(&srv.url("'/homepage/assets/img/icons/handshake.svg'"))
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200)
.body(&srv.url("'/homepage/assets/img/icons/handshake.svg'"));
});
let mock_two = Mock::new()
.expect_method(GET)
.expect_path("/homepage/assets/img/icons/handshake.svg")
.return_status(200)
.create_on(&srv);
let mock_two = srv.mock(|when, then| {
when.method(GET)
.path("/homepage/assets/img/icons/handshake.svg");
then.status(200);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -43,8 +42,8 @@ fn extractor_finds_absolute_url() -> Result<(), Box<dyn std::error::Error>> {
)),
);
assert_eq!(mock.times_called(), 1);
assert_eq!(mock_two.times_called(), 1);
assert_eq!(mock.hits(), 1);
assert_eq!(mock_two.hits(), 1);
teardown_tmp_directory(tmp_dir);
Ok(())
}
@@ -56,12 +55,11 @@ fn extractor_finds_absolute_url_to_different_domain() -> Result<(), Box<dyn std:
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
let mock = Mock::new()
.expect_method(GET)
.expect_path("/LICENSE")
.return_status(200)
.return_body("\"http://localhost/homepage/assets/img/icons/handshake.svg\"")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200)
.body("\"http://localhost/homepage/assets/img/icons/handshake.svg\"");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -81,7 +79,7 @@ fn extractor_finds_absolute_url_to_different_domain() -> Result<(), Box<dyn std:
.not(),
);
assert_eq!(mock.times_called(), 1);
assert_eq!(mock.hits(), 1);
teardown_tmp_directory(tmp_dir);
Ok(())
}
@@ -92,18 +90,17 @@ fn extractor_finds_relative_url() -> Result<(), Box<dyn std::error::Error>> {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
let mock = Mock::new()
.expect_method(GET)
.expect_path("/LICENSE")
.return_status(200)
.return_body("\"/homepage/assets/img/icons/handshake.svg\"")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200)
.body("\"/homepage/assets/img/icons/handshake.svg\"");
});
let mock_two = Mock::new()
.expect_method(GET)
.expect_path("/homepage/assets/img/icons/handshake.svg")
.return_status(200)
.create_on(&srv);
let mock_two = srv.mock(|when, then| {
when.method(GET)
.path("/homepage/assets/img/icons/handshake.svg");
then.status(200);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -122,8 +119,8 @@ fn extractor_finds_relative_url() -> Result<(), Box<dyn std::error::Error>> {
)),
);
assert_eq!(mock.times_called(), 1);
assert_eq!(mock_two.times_called(), 1);
assert_eq!(mock.hits(), 1);
assert_eq!(mock_two.hits(), 1);
teardown_tmp_directory(tmp_dir);
Ok(())
}
@@ -136,25 +133,23 @@ fn extractor_finds_same_relative_url_twice() {
let (tmp_dir, file) =
setup_tmp_directory(&["LICENSE".to_string(), "README".to_string()], "wordlist").unwrap();
let mock = Mock::new()
.expect_method(GET)
.expect_path("/LICENSE")
.return_status(200)
.return_body(&srv.url("\"/homepage/assets/img/icons/handshake.svg\""))
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200)
.body(&srv.url("\"/homepage/assets/img/icons/handshake.svg\""));
});
let mock_two = Mock::new()
.expect_method(GET)
.expect_path("/README")
.return_body(&srv.url("\"/homepage/assets/img/icons/handshake.svg\""))
.return_status(200)
.create_on(&srv);
let mock_two = srv.mock(|when, then| {
when.method(GET).path("/README");
then.status(200)
.body(&srv.url("\"/homepage/assets/img/icons/handshake.svg\""));
});
let mock_three = Mock::new()
.expect_method(GET)
.expect_path("/homepage/assets/img/icons/handshake.svg")
.return_status(200)
.create_on(&srv);
let mock_three = srv.mock(|when, then| {
when.method(GET)
.path("/homepage/assets/img/icons/handshake.svg");
then.status(200);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -168,15 +163,14 @@ fn extractor_finds_same_relative_url_twice() {
cmd.assert().success().stdout(
predicate::str::contains("/LICENSE")
.and(predicate::str::contains("200"))
.and(predicate::str::contains(
"/homepage/assets/img/icons/handshake.svg",
)),
// .count(1) asserts that we only see the endpoint reported once, even though there
// is the potential to request the same url twice
.and(predicate::str::contains("/homepage/assets/img/icons/handshake.svg").count(1)),
);
assert_eq!(mock.times_called(), 1);
assert_eq!(mock_two.times_called(), 1);
assert!(mock_three.times_called() <= 2); // todo: sometimes this is 2 instead of 1
// the expectation is one, suggesting a race condition... investigate and fix
assert_eq!(mock.hits(), 1);
assert_eq!(mock_two.hits(), 1);
assert!(mock_three.hits() <= 2);
teardown_tmp_directory(tmp_dir);
}
@@ -188,19 +182,17 @@ fn extractor_finds_filtered_content() -> Result<(), Box<dyn std::error::Error>>
let (tmp_dir, file) =
setup_tmp_directory(&["LICENSE".to_string(), "README".to_string()], "wordlist")?;
let mock = Mock::new()
.expect_method(GET)
.expect_path("/LICENSE")
.return_status(200)
.return_body(&srv.url("\"/homepage/assets/img/icons/handshake.svg\""))
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200)
.body(&srv.url("\"/homepage/assets/img/icons/handshake.svg\""));
});
let mock_two = Mock::new()
.expect_method(GET)
.expect_path("/homepage/assets/img/icons/handshake.svg")
.return_body("im a little teapot")
.return_status(200)
.create_on(&srv);
let mock_two = srv.mock(|when, then| {
when.method(GET)
.path("/homepage/assets/img/icons/handshake.svg");
then.status(200).body("im a little teapot");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -222,8 +214,72 @@ fn extractor_finds_filtered_content() -> Result<(), Box<dyn std::error::Error>>
.not(),
);
assert_eq!(mock.times_called(), 1);
assert_eq!(mock_two.times_called(), 1);
assert_eq!(mock.hits(), 1);
assert_eq!(mock_two.hits(), 1);
teardown_tmp_directory(tmp_dir);
Ok(())
}
#[test]
/// serve a robots.txt with a file and and a folder link contained within it. ferox should
/// find both links and request each one. Additionally, a scan should start with the directory
/// link found, meaning the wordlist will be thrown at the sub directory
fn extractor_finds_robots_txt_links_and_displays_files_or_scans_directories() {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200).body("im a little teapot"); // 18
});
let mock_two = srv.mock(|when, then| {
when.method(GET).path("/robots.txt");
then.status(200).body(
r#"
User-agent: *
Crawl-delay: 10
# CSS, JS, Images
Allow: /misc/*.css$
Disallow: /misc/stupidfile.php
Disallow: /disallowed-subdir/
"#,
);
});
let mock_file = srv.mock(|when, then| {
when.method(GET).path("/misc/stupidfile.php");
then.status(200).body("im a little teapot too"); // 22
});
let mock_dir = srv.mock(|when, then| {
when.method(GET).path("/disallowed-subdir/LICENSE");
then.status(200).body("i too, am a container for tea"); // 29
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--extract-links")
.unwrap();
cmd.assert().success().stdout(
predicate::str::contains("/LICENSE") // 2 directories contain LICENSE
.count(2)
.and(predicate::str::contains("18c"))
.and(predicate::str::contains("/misc/stupidfile.php"))
.and(predicate::str::contains("22c"))
.and(predicate::str::contains("/disallowed-subdir/LICENSE"))
.and(predicate::str::contains("29c"))
.and(predicate::str::contains("200").count(3)),
);
assert_eq!(mock.hits(), 1);
assert_eq!(mock_dir.hits(), 1);
assert_eq!(mock_two.hits(), 1);
assert_eq!(mock_file.hits(), 1);
teardown_tmp_directory(tmp_dir);
}

View File

@@ -1,7 +1,7 @@
mod utils;
use assert_cmd::prelude::*;
use httpmock::Method::GET;
use httpmock::{Mock, MockServer};
use httpmock::MockServer;
use predicates::prelude::*;
use std::process::Command;
use utils::{setup_tmp_directory, teardown_tmp_directory};
@@ -14,19 +14,15 @@ fn filters_status_code_should_filter_response() {
let (tmp_dir, file) =
setup_tmp_directory(&["LICENSE".to_string(), "file.js".to_string()], "wordlist").unwrap();
let mock = Mock::new()
.expect_method(GET)
.expect_path("/LICENSE")
.return_status(302)
.return_body("this is a test")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(302).body("this is a test");
});
let mock_two = Mock::new()
.expect_method(GET)
.expect_path("/file.js")
.return_status(200)
.return_body("this is also a test of some import")
.create_on(&srv);
let mock_two = srv.mock(|when, then| {
when.method(GET).path("/file.js");
then.status(200).body("this is also a test of some import");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -51,8 +47,8 @@ fn filters_status_code_should_filter_response() {
.and(predicate::str::contains("34c")),
);
assert_eq!(mock.times_called(), 1);
assert_eq!(mock_two.times_called(), 1);
assert_eq!(mock.hits(), 1);
assert_eq!(mock_two.hits(), 1);
teardown_tmp_directory(tmp_dir);
}
@@ -64,19 +60,16 @@ fn filters_lines_should_filter_response() {
let (tmp_dir, file) =
setup_tmp_directory(&["LICENSE".to_string(), "file.js".to_string()], "wordlist").unwrap();
let mock = Mock::new()
.expect_method(GET)
.expect_path("/LICENSE")
.return_status(302)
.return_body("this is a test")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(302).body("this is a test");
});
let mock_two = Mock::new()
.expect_method(GET)
.expect_path("/file.js")
.return_status(200)
.return_body("this is also a test of some import\nwith 2 lines, no less")
.create_on(&srv);
let mock_two = srv.mock(|when, then| {
when.method(GET).path("/file.js");
then.status(200)
.body("this is also a test of some import\nwith 2 lines, no less");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -100,8 +93,8 @@ fn filters_lines_should_filter_response() {
.not(),
);
assert_eq!(mock.times_called(), 1);
assert_eq!(mock_two.times_called(), 1);
assert_eq!(mock.hits(), 1);
assert_eq!(mock_two.hits(), 1);
teardown_tmp_directory(tmp_dir);
}
@@ -113,19 +106,16 @@ fn filters_words_should_filter_response() {
let (tmp_dir, file) =
setup_tmp_directory(&["LICENSE".to_string(), "file.js".to_string()], "wordlist").unwrap();
let mock = Mock::new()
.expect_method(GET)
.expect_path("/LICENSE")
.return_status(302)
.return_body("this is a test")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(302).body("this is a test");
});
let mock_two = Mock::new()
.expect_method(GET)
.expect_path("/file.js")
.return_status(200)
.return_body("this is also a test of some import\nwith 2 lines, no less")
.create_on(&srv);
let mock_two = srv.mock(|when, then| {
when.method(GET).path("/file.js");
then.status(200)
.body("this is also a test of some import\nwith 2 lines, no less");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -149,8 +139,8 @@ fn filters_words_should_filter_response() {
.not(),
);
assert_eq!(mock.times_called(), 1);
assert_eq!(mock_two.times_called(), 1);
assert_eq!(mock.hits(), 1);
assert_eq!(mock_two.hits(), 1);
teardown_tmp_directory(tmp_dir);
}
@@ -162,19 +152,16 @@ fn filters_size_should_filter_response() {
let (tmp_dir, file) =
setup_tmp_directory(&["LICENSE".to_string(), "file.js".to_string()], "wordlist").unwrap();
let mock = Mock::new()
.expect_method(GET)
.expect_path("/LICENSE")
.return_status(302)
.return_body("this is a test")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(302).body("this is a test");
});
let mock_two = Mock::new()
.expect_method(GET)
.expect_path("/file.js")
.return_status(200)
.return_body("this is also a test of some import\nwith 2 lines, no less")
.create_on(&srv);
let mock_two = srv.mock(|when, then| {
when.method(GET).path("/file.js");
then.status(200)
.body("this is also a test of some import\nwith 2 lines, no less");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -198,7 +185,7 @@ fn filters_size_should_filter_response() {
.not(),
);
assert_eq!(mock.times_called(), 1);
assert_eq!(mock_two.times_called(), 1);
assert_eq!(mock.hits(), 1);
assert_eq!(mock_two.hits(), 1);
teardown_tmp_directory(tmp_dir);
}

View File

@@ -2,7 +2,7 @@ mod utils;
use assert_cmd::prelude::*;
use assert_cmd::Command;
use httpmock::Method::GET;
use httpmock::{Mock, MockServer, Regex};
use httpmock::{MockServer, Regex};
use predicates::prelude::*;
use utils::{setup_tmp_directory, teardown_tmp_directory};
@@ -65,12 +65,10 @@ fn test_one_good_and_one_bad_target_scan_succeeds() -> Result<(), Box<dyn std::e
let urls = vec![not_real, srv.url("/"), String::from("LICENSE")];
let (tmp_dir, file) = setup_tmp_directory(&urls, "wordlist")?;
let mock = Mock::new()
.expect_method(GET)
.expect_path("/LICENSE")
.return_status(200)
.return_body("this is a test")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200).body("this is a test");
});
let mut cmd = Command::cargo_bin("feroxbuster").unwrap();
@@ -86,7 +84,7 @@ fn test_one_good_and_one_bad_target_scan_succeeds() -> Result<(), Box<dyn std::e
.and(predicate::str::contains("200"))
.and(predicate::str::contains("14")),
);
assert_eq!(mock.times_called(), 1);
assert_eq!(mock.hits(), 1);
teardown_tmp_directory(tmp_dir);
Ok(())
@@ -98,12 +96,11 @@ fn test_static_wildcard_request_found() -> Result<(), Box<dyn std::error::Error>
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
let mock = Mock::new()
.expect_method(GET)
.expect_path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap())
.return_status(200)
.return_body("this is a test")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
then.status(200).body("this is a test");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -123,7 +120,7 @@ fn test_static_wildcard_request_found() -> Result<(), Box<dyn std::error::Error>
.and(predicate::str::contains("(url length: 32)")),
);
assert_eq!(mock.times_called(), 1);
assert_eq!(mock.hits(), 1);
Ok(())
}
@@ -134,19 +131,17 @@ fn test_dynamic_wildcard_request_found() {
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
let outfile = tmp_dir.path().join("outfile");
let mock = Mock::new()
.expect_method(GET)
.expect_path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap())
.return_status(200)
.return_body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
let mock2 = Mock::new()
.expect_method(GET)
.expect_path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap())
.return_status(200)
.return_body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
.create_on(&srv);
let mock2 = srv.mock(|when, then| {
when.method(GET).path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
then.status(200).body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -177,8 +172,8 @@ fn test_dynamic_wildcard_request_found() {
.and(predicate::str::contains("(url length: 96)")),
);
assert_eq!(mock.times_called(), 1);
assert_eq!(mock2.times_called(), 1);
assert_eq!(mock.hits(), 1);
assert_eq!(mock2.hits(), 1);
}
#[test]
@@ -187,12 +182,11 @@ fn heuristics_static_wildcard_request_with_dont_filter() -> Result<(), Box<dyn s
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
let mock = Mock::new()
.expect_method(GET)
.expect_path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap())
.return_status(200)
.return_body("this is a test")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
then.status(200).body("this is a test");
});
Command::cargo_bin("feroxbuster")
.unwrap()
@@ -205,7 +199,7 @@ fn heuristics_static_wildcard_request_with_dont_filter() -> Result<(), Box<dyn s
teardown_tmp_directory(tmp_dir);
assert_eq!(mock.times_called(), 0);
assert_eq!(mock.hits(), 0);
Ok(())
}
@@ -215,19 +209,19 @@ fn heuristics_wildcard_test_with_two_static_wildcards() {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
let mock = Mock::new()
.expect_method(GET)
.expect_path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap())
.return_status(200)
.return_body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
let mock2 = Mock::new()
.expect_method(GET)
.expect_path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap())
.return_status(200)
.return_body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
.create_on(&srv);
let mock2 = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -251,8 +245,8 @@ fn heuristics_wildcard_test_with_two_static_wildcards() {
)),
);
assert_eq!(mock.times_called(), 1);
assert_eq!(mock2.times_called(), 1);
assert_eq!(mock.hits(), 1);
assert_eq!(mock2.hits(), 1);
}
#[test]
@@ -262,19 +256,19 @@ fn heuristics_wildcard_test_with_two_static_wildcards_with_quiet_enabled(
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
let mock = Mock::new()
.expect_method(GET)
.expect_path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap())
.return_status(200)
.return_body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
let mock2 = Mock::new()
.expect_method(GET)
.expect_path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap())
.return_status(200)
.return_body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
.create_on(&srv);
let mock2 = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -290,8 +284,8 @@ fn heuristics_wildcard_test_with_two_static_wildcards_with_quiet_enabled(
cmd.assert().success().stdout(predicate::str::is_empty());
assert_eq!(mock.times_called(), 1);
assert_eq!(mock2.times_called(), 1);
assert_eq!(mock.hits(), 1);
assert_eq!(mock2.hits(), 1);
Ok(())
}
@@ -302,19 +296,19 @@ fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file() {
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
let outfile = tmp_dir.path().join("outfile");
let mock = Mock::new()
.expect_method(GET)
.expect_path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap())
.return_status(200)
.return_body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
let mock2 = Mock::new()
.expect_method(GET)
.expect_path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap())
.return_status(200)
.return_body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
.create_on(&srv);
let mock2 = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -348,8 +342,8 @@ fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file() {
)),
);
assert_eq!(mock.times_called(), 1);
assert_eq!(mock2.times_called(), 1);
assert_eq!(mock.hits(), 1);
assert_eq!(mock2.hits(), 1);
}
#[test]
@@ -361,20 +355,20 @@ fn heuristics_wildcard_test_with_redirect_as_response_code(
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
let outfile = tmp_dir.path().join("outfile");
let mock = Mock::new()
.expect_method(GET)
.expect_path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap())
.return_status(301)
.return_body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
then.status(301)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
let mock2 = Mock::new()
.expect_method(GET)
.expect_path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap())
.return_status(301)
.return_header("Location", &srv.url("/some-redirect"))
.return_body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
.create_on(&srv);
let mock2 = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
then.status(301)
.header("Location", &srv.url("/some-redirect"))
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -407,7 +401,7 @@ fn heuristics_wildcard_test_with_redirect_as_response_code(
.and(predicate::str::contains("WLD")),
);
assert_eq!(mock.times_called(), 1);
assert_eq!(mock2.times_called(), 1);
assert_eq!(mock.hits(), 1);
assert_eq!(mock2.hits(), 1);
Ok(())
}

View File

@@ -1,7 +1,7 @@
mod utils;
use assert_cmd::Command;
use httpmock::Method::GET;
use httpmock::{Mock, MockServer};
use httpmock::MockServer;
use predicates::prelude::*;
use utils::{setup_tmp_directory, teardown_tmp_directory};
@@ -10,12 +10,10 @@ use utils::{setup_tmp_directory, teardown_tmp_directory};
fn main_use_root_owned_file_as_wordlist() -> Result<(), Box<dyn std::error::Error>> {
let srv = MockServer::start();
let mock = Mock::new()
.expect_method(GET)
.expect_path("/")
.return_status(200)
.return_body("this is a test")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/");
then.status(200).body("this is a test");
});
Command::cargo_bin("feroxbuster")
.unwrap()
@@ -29,7 +27,7 @@ fn main_use_root_owned_file_as_wordlist() -> Result<(), Box<dyn std::error::Erro
.stdout(predicate::str::contains("Permission denied (os error 13)"));
// connectivity test hits it once
assert_eq!(mock.times_called(), 1);
assert_eq!(mock.hits(), 1);
Ok(())
}
@@ -39,12 +37,10 @@ fn main_use_empty_wordlist() -> Result<(), Box<dyn std::error::Error>> {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&[], "wordlist")?;
let mock = Mock::new()
.expect_method(GET)
.expect_path("/")
.return_status(200)
.return_body("this is a test")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/");
then.status(200).body("this is a test");
});
Command::cargo_bin("feroxbuster")
.unwrap()
@@ -57,7 +53,7 @@ fn main_use_empty_wordlist() -> Result<(), Box<dyn std::error::Error>> {
.failure()
.stdout(predicate::str::contains("Did not find any words in"));
assert_eq!(mock.times_called(), 1);
assert_eq!(mock.hits(), 1);
teardown_tmp_directory(tmp_dir);
Ok(())

View File

@@ -1,8 +1,11 @@
mod utils;
use assert_cmd::Command;
use httpmock::Method::GET;
use httpmock::{Mock, MockServer};
use httpmock::MockServer;
use predicates::prelude::*;
use std::fs::{read_to_string, write};
use std::path::Path;
use std::time;
use utils::{setup_tmp_directory, teardown_tmp_directory};
#[test]
@@ -40,28 +43,23 @@ fn resume_scan_works() {
let responses = format!(r#""responses":[{}]"#, response);
// not scanned because /js is not complete, and /js/stuff response is not known
let not_scanned_yet = Mock::new()
.expect_method(GET)
.expect_path("/js/stuff")
.return_status(200)
.return_body("i expect to be scanned")
.create_on(&srv);
let not_scanned_yet = srv.mock(|when, then| {
when.method(GET).path("/js/stuff");
then.status(200).body("i expect to be scanned");
});
// will get scanned because /js is not complete, but because response of /js/css is known, the
// response will not be in stdout
let already_scanned = Mock::new()
.expect_method(GET)
.expect_path("/js/css")
.return_status(200)
.create_on(&srv);
let already_scanned = srv.mock(|when, then| {
when.method(GET).path("/js/css");
then.status(200);
});
// already scanned because scan on / is complete
let also_already_scanned = Mock::new()
.expect_method(GET)
.expect_path("/css")
.return_status(200)
.return_body("two words")
.create_on(&srv);
let also_already_scanned = srv.mock(|when, then| {
when.method(GET).path("/css");
then.status(200).body("two words");
});
let state_file_contents = format!("{{{},{},{}}}", scans, config, responses);
let (tmp_dir2, state_file) = setup_tmp_directory(&[state_file_contents], "state-file").unwrap();
@@ -87,7 +85,46 @@ fn resume_scan_works() {
teardown_tmp_directory(tmp_dir);
teardown_tmp_directory(tmp_dir2);
assert_eq!(already_scanned.times_called(), 1);
assert_eq!(also_already_scanned.times_called(), 0);
assert_eq!(not_scanned_yet.times_called(), 1);
assert_eq!(already_scanned.hits(), 1);
assert_eq!(also_already_scanned.hits(), 0);
assert_eq!(not_scanned_yet.hits(), 1);
}
#[test]
/// kick off scan with a time limit;
fn time_limit_enforced_when_specified() {
let srv = MockServer::start();
let (tmp_dir, file) =
setup_tmp_directory(&["css".to_string(), "stuff".to_string()], "wordlist").unwrap();
// ensure the command will run long enough by adding crap to the wordlist
let more_words = read_to_string(Path::new("tests/extra-words")).unwrap();
write(&file, more_words).unwrap();
assert!(file.metadata().unwrap().len() > 100); // sanity check on wordlist size
let now = time::Instant::now();
let lower_bound = time::Duration::new(5, 0);
let upper_bound = time::Duration::new(6, 0);
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--time-limit")
.arg("5s")
.assert()
.failure();
// expected run time is somewhere in the 30 seconds ballpark (real 0m37.376s)
// so if the cmd returns in a significantly shorter amount of time, the test will have
// succeeded
// --time-limit is 5 seconds, so elapsed should be in a window that is greater than 5
// but significantly less than 30ish
assert!(now.elapsed() > lower_bound && now.elapsed() < upper_bound);
teardown_tmp_directory(tmp_dir);
}

View File

@@ -1,7 +1,7 @@
mod utils;
use assert_cmd::prelude::*;
use httpmock::Method::GET;
use httpmock::{Mock, MockServer};
use httpmock::MockServer;
use predicates::prelude::*;
use std::process::Command;
use utils::{setup_tmp_directory, teardown_tmp_directory};
@@ -12,12 +12,10 @@ fn scanner_single_request_scan() -> Result<(), Box<dyn std::error::Error>> {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
let mock = Mock::new()
.expect_method(GET)
.expect_path("/LICENSE")
.return_status(200)
.return_body("this is a test")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200).body("this is a test");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -34,7 +32,7 @@ fn scanner_single_request_scan() -> Result<(), Box<dyn std::error::Error>> {
.and(predicate::str::contains("14")),
);
assert_eq!(mock.times_called(), 1);
assert_eq!(mock.hits(), 1);
teardown_tmp_directory(tmp_dir);
Ok(())
}
@@ -51,33 +49,26 @@ fn scanner_recursive_request_scan() -> Result<(), Box<dyn std::error::Error>> {
];
let (tmp_dir, file) = setup_tmp_directory(&urls, "wordlist")?;
let js_mock = Mock::new()
.expect_method(GET)
.expect_path("/js")
.return_status(301)
.return_header("Location", &srv.url("/js/"))
.create_on(&srv);
let js_mock = srv.mock(|when, then| {
when.method(GET).path("/js");
then.status(301).header("Location", &srv.url("/js/"));
});
let js_prod_mock = Mock::new()
.expect_method(GET)
.expect_path("/js/prod")
.return_status(301)
.return_header("Location", &srv.url("/js/prod/"))
.create_on(&srv);
let js_prod_mock = srv.mock(|when, then| {
when.method(GET).path("/js/prod");
then.status(301).header("Location", &srv.url("/js/prod/"));
});
let js_dev_mock = Mock::new()
.expect_method(GET)
.expect_path("/js/dev")
.return_status(301)
.return_header("Location", &srv.url("/js/dev/"))
.create_on(&srv);
let js_dev_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev");
then.status(301).header("Location", &srv.url("/js/dev/"));
});
let js_dev_file_mock = Mock::new()
.expect_method(GET)
.expect_path("/js/dev/file.js")
.return_status(200)
.return_body("this is a test and is more bytes than other ones")
.create_on(&srv);
let js_dev_file_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev/file.js");
then.status(200)
.body("this is a test and is more bytes than other ones");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -98,10 +89,10 @@ fn scanner_recursive_request_scan() -> Result<(), Box<dyn std::error::Error>> {
.and(predicate::str::is_match("200.*js/dev/file.js").unwrap()),
);
assert_eq!(js_mock.times_called(), 1);
assert_eq!(js_prod_mock.times_called(), 1);
assert_eq!(js_dev_mock.times_called(), 1);
assert_eq!(js_dev_file_mock.times_called(), 1);
assert_eq!(js_mock.hits(), 1);
assert_eq!(js_prod_mock.hits(), 1);
assert_eq!(js_dev_mock.hits(), 1);
assert_eq!(js_dev_file_mock.hits(), 1);
teardown_tmp_directory(tmp_dir);
@@ -121,33 +112,26 @@ fn scanner_recursive_request_scan_using_only_success_responses(
];
let (tmp_dir, file) = setup_tmp_directory(&urls, "wordlist")?;
let js_mock = Mock::new()
.expect_method(GET)
.expect_path("/js/")
.return_status(200)
.return_header("Location", &srv.url("/js/"))
.create_on(&srv);
let js_mock = srv.mock(|when, then| {
when.method(GET).path("/js/");
then.status(200).header("Location", &srv.url("/js/"));
});
let js_prod_mock = Mock::new()
.expect_method(GET)
.expect_path("/js/prod/")
.return_status(200)
.return_header("Location", &srv.url("/js/prod/"))
.create_on(&srv);
let js_prod_mock = srv.mock(|when, then| {
when.method(GET).path("/js/prod/");
then.status(200).header("Location", &srv.url("/js/prod/"));
});
let js_dev_mock = Mock::new()
.expect_method(GET)
.expect_path("/js/dev/")
.return_status(200)
.return_header("Location", &srv.url("/js/dev/"))
.create_on(&srv);
let js_dev_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev/");
then.status(200).header("Location", &srv.url("/js/dev/"));
});
let js_dev_file_mock = Mock::new()
.expect_method(GET)
.expect_path("/js/dev/file.js")
.return_status(200)
.return_body("this is a test and is more bytes than other ones")
.create_on(&srv);
let js_dev_file_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev/file.js");
then.status(200)
.body("this is a test and is more bytes than other ones");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -169,10 +153,10 @@ fn scanner_recursive_request_scan_using_only_success_responses(
.and(predicate::str::is_match("200.*js/dev/file.js").unwrap()),
);
assert_eq!(js_mock.times_called(), 1);
assert_eq!(js_prod_mock.times_called(), 1);
assert_eq!(js_dev_mock.times_called(), 1);
assert_eq!(js_dev_file_mock.times_called(), 1);
assert_eq!(js_mock.hits(), 1);
assert_eq!(js_prod_mock.hits(), 1);
assert_eq!(js_dev_mock.hits(), 1);
assert_eq!(js_dev_file_mock.hits(), 1);
teardown_tmp_directory(tmp_dir);
@@ -185,12 +169,10 @@ fn scanner_single_request_scan_with_file_output() -> Result<(), Box<dyn std::err
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
let mock = Mock::new()
.expect_method(GET)
.expect_path("/LICENSE")
.return_status(200)
.return_body("this is a test")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200).body("this is a test");
});
let outfile = tmp_dir.path().join("output");
@@ -211,7 +193,7 @@ fn scanner_single_request_scan_with_file_output() -> Result<(), Box<dyn std::err
assert!(contents.contains("200"));
assert!(contents.contains("14"));
assert_eq!(mock.times_called(), 1);
assert_eq!(mock.hits(), 1);
teardown_tmp_directory(tmp_dir);
Ok(())
}
@@ -223,12 +205,10 @@ fn scanner_single_request_scan_with_file_output_and_tack_q(
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
let mock = Mock::new()
.expect_method(GET)
.expect_path("/LICENSE")
.return_status(200)
.return_body("this is a test")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200).body("this is a test");
});
let outfile = tmp_dir.path().join("output");
@@ -249,7 +229,7 @@ fn scanner_single_request_scan_with_file_output_and_tack_q(
let url = srv.url("/LICENSE");
assert!(contents.contains(&url));
assert_eq!(mock.times_called(), 1);
assert_eq!(mock.hits(), 1);
teardown_tmp_directory(tmp_dir);
Ok(())
}
@@ -261,12 +241,10 @@ fn scanner_single_request_scan_with_invalid_file_output() -> Result<(), Box<dyn
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
let mock = Mock::new()
.expect_method(GET)
.expect_path("/LICENSE")
.return_status(200)
.return_body("this is a test")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200).body("this is a test");
});
let outfile = tmp_dir.path(); // outfile is a directory
@@ -285,7 +263,7 @@ fn scanner_single_request_scan_with_invalid_file_output() -> Result<(), Box<dyn
let contents = std::fs::read_to_string(outfile);
assert!(contents.is_err());
assert_eq!(mock.times_called(), 1);
assert_eq!(mock.hits(), 1);
teardown_tmp_directory(tmp_dir);
Ok(())
}
@@ -296,12 +274,10 @@ fn scanner_single_request_quiet_scan() -> Result<(), Box<dyn std::error::Error>>
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
let mock = Mock::new()
.expect_method(GET)
.expect_path("/LICENSE")
.return_status(200)
.return_body("this is a test")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200).body("this is a test");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -321,7 +297,7 @@ fn scanner_single_request_quiet_scan() -> Result<(), Box<dyn std::error::Error>>
.not(),
);
assert_eq!(mock.times_called(), 1);
assert_eq!(mock.hits(), 1);
teardown_tmp_directory(tmp_dir);
Ok(())
}
@@ -334,12 +310,10 @@ fn scanner_single_request_returns_301_without_location_header(
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
let mock = Mock::new()
.expect_method(GET)
.expect_path("/LICENSE")
.return_body("this is a test")
.return_status(301)
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(301).body("this is a test");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -359,7 +333,7 @@ fn scanner_single_request_returns_301_without_location_header(
.and(predicate::str::contains("14")),
);
assert_eq!(mock.times_called(), 1);
assert_eq!(mock.hits(), 1);
teardown_tmp_directory(tmp_dir);
Ok(())
}
@@ -372,19 +346,15 @@ fn scanner_single_request_replayed_to_proxy() -> Result<(), Box<dyn std::error::
let proxy = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
let mock = Mock::new()
.expect_method(GET)
.expect_path("/LICENSE")
.return_status(200)
.return_body("this is a test")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200).body("this is a test");
});
let mock_two = Mock::new()
.expect_method(GET)
.expect_path("/LICENSE")
.return_status(200)
.return_body("this is a test")
.create_on(&proxy);
let mock_two = proxy.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200).body("this is a test");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -407,8 +377,8 @@ fn scanner_single_request_replayed_to_proxy() -> Result<(), Box<dyn std::error::
)
.stderr(predicate::str::contains("Replay Proxy Codes"));
assert_eq!(mock.times_called(), 1);
assert_eq!(mock_two.times_called(), 1);
assert_eq!(mock.hits(), 1);
assert_eq!(mock_two.hits(), 1);
teardown_tmp_directory(tmp_dir);
Ok(())
}
@@ -420,19 +390,15 @@ fn scanner_single_request_scan_with_filtered_result() -> Result<(), Box<dyn std:
let (tmp_dir, file) =
setup_tmp_directory(&["LICENSE".to_string(), "ignored".to_string()], "wordlist")?;
let mock = Mock::new()
.expect_method(GET)
.expect_path("/LICENSE")
.return_status(200)
.return_body("this is a not a test")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200).body("this is a not a test");
});
let filtered_mock = Mock::new()
.expect_method(GET)
.expect_path("/ignored")
.return_status(200)
.return_body("this is a test")
.create_on(&srv);
let filtered_mock = srv.mock(|when, then| {
when.method(GET).path("/ignored");
then.status(200).body("this is a test");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -455,8 +421,8 @@ fn scanner_single_request_scan_with_filtered_result() -> Result<(), Box<dyn std:
.not(),
);
assert_eq!(mock.times_called(), 1);
assert_eq!(filtered_mock.times_called(), 1);
assert_eq!(mock.hits(), 1);
assert_eq!(filtered_mock.hits(), 1);
teardown_tmp_directory(tmp_dir);
Ok(())
}
@@ -467,12 +433,10 @@ fn scanner_single_request_scan_with_debug_logging() {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
let mock = Mock::new()
.expect_method(GET)
.expect_path("/LICENSE")
.return_status(200)
.return_body("this is a test")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200).body("this is a test");
});
let outfile = tmp_dir.path().join("debug.log");
@@ -496,7 +460,7 @@ fn scanner_single_request_scan_with_debug_logging() {
assert!(contents.contains("feroxbuster All scans complete!"));
assert!(contents.contains("feroxbuster exit: terminal_input_handler"));
assert_eq!(mock.times_called(), 1);
assert_eq!(mock.hits(), 1);
teardown_tmp_directory(tmp_dir);
}
@@ -506,12 +470,10 @@ fn scanner_single_request_scan_with_debug_logging_as_json() {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
let mock = Mock::new()
.expect_method(GET)
.expect_path("/LICENSE")
.return_status(200)
.return_body("this is a test")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200).body("this is a test");
});
let outfile = tmp_dir.path().join("debug.log");
@@ -538,7 +500,7 @@ fn scanner_single_request_scan_with_debug_logging_as_json() {
assert!(contents.contains("All scans complete!"));
assert!(contents.contains("exit: terminal_input_handler"));
assert_eq!(mock.times_called(), 1);
assert_eq!(mock.hits(), 1);
teardown_tmp_directory(tmp_dir);
}
@@ -549,19 +511,16 @@ fn scanner_single_request_scan_with_regex_filtered_result() {
let (tmp_dir, file) =
setup_tmp_directory(&["LICENSE".to_string(), "ignored".to_string()], "wordlist").unwrap();
let mock = Mock::new()
.expect_method(GET)
.expect_path("/LICENSE")
.return_status(200)
.return_body("this is a not a test")
.create_on(&srv);
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200).body("this is a test");
});
let filtered_mock = Mock::new()
.expect_method(GET)
.expect_path("/ignored")
.return_status(200)
.return_body("this is a test\nThat rug really tied the room together")
.create_on(&srv);
let filtered_mock = srv.mock(|when, then| {
when.method(GET).path("/ignored");
then.status(200)
.body("this is a test\nThat rug really tied the room together");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
@@ -583,7 +542,7 @@ fn scanner_single_request_scan_with_regex_filtered_result() {
.not(),
);
assert_eq!(mock.times_called(), 1);
assert_eq!(filtered_mock.times_called(), 1);
assert_eq!(mock.hits(), 1);
assert_eq!(filtered_mock.hits(), 1);
teardown_tmp_directory(tmp_dir);
}