mirror of
https://github.com/epi052/feroxbuster.git
synced 2026-05-24 22:21:12 -03:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b3540e13f | ||
|
|
4e492939c1 | ||
|
|
d39692d1bd | ||
|
|
086c9808a3 | ||
|
|
f7ef202849 | ||
|
|
77a450195c | ||
|
|
b10c4caefb | ||
|
|
4ee374efb6 | ||
|
|
183dc4cf14 | ||
|
|
81cd6c3a64 | ||
|
|
1f7ae68857 | ||
|
|
f175d759ca | ||
|
|
83f8a33413 | ||
|
|
a22ca731b6 | ||
|
|
e5934cef1f | ||
|
|
1b49c5dfe9 | ||
|
|
47c384e2ec | ||
|
|
8d5a0c590e | ||
|
|
6b04bc6757 | ||
|
|
baa996356c | ||
|
|
ae5f7e5435 | ||
|
|
9241b3c748 | ||
|
|
48b341db39 | ||
|
|
b759e016bb | ||
|
|
8dc7a86b2b | ||
|
|
0db0273513 | ||
|
|
21254ad871 | ||
|
|
5bbf29859f | ||
|
|
730566fd05 | ||
|
|
f05c5eca03 | ||
|
|
8c50d94f8e | ||
|
|
91c42e137d | ||
|
|
a2a9ba289c |
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal 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
17
.github/stale.yml
vendored
Normal 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
|
||||
12
Cargo.toml
12
Cargo.toml
@@ -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
122
README.md
@@ -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
|
||||
|
||||

|
||||
|
||||
### 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.
|
||||
|
||||

|
||||
|
||||
### 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
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
6
build.rs
6
build.rs
@@ -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();
|
||||
|
||||
@@ -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
BIN
img/time-limit.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 232 KiB |
@@ -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=(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
;;
|
||||
|
||||
@@ -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'
|
||||
|
||||
205
src/banner.rs
205
src/banner.rs
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
223
src/extractor.rs
223
src/extractor.rs
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
49
src/main.rs
49
src/main.rs
@@ -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");
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
102774
tests/extra-words
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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("─┴─")),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user