mirror of
https://github.com/epi052/feroxbuster.git
synced 2026-05-25 23:21:12 -03:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5934cef1f | ||
|
|
1b49c5dfe9 | ||
|
|
47c384e2ec | ||
|
|
8d5a0c590e | ||
|
|
6b04bc6757 | ||
|
|
baa996356c | ||
|
|
ae5f7e5435 | ||
|
|
9241b3c748 | ||
|
|
48b341db39 | ||
|
|
b759e016bb | ||
|
|
8dc7a86b2b | ||
|
|
0db0273513 | ||
|
|
21254ad871 | ||
|
|
5bbf29859f | ||
|
|
730566fd05 | ||
|
|
f05c5eca03 | ||
|
|
8c50d94f8e | ||
|
|
91c42e137d | ||
|
|
a2a9ba289c | ||
|
|
0ea798e70e | ||
|
|
3caa8d2ceb | ||
|
|
bd836c8b55 | ||
|
|
f3bf05ab9b | ||
|
|
ef0b5d3780 | ||
|
|
ab5fbeb6ed | ||
|
|
6c779bd4c1 | ||
|
|
fbb964a893 | ||
|
|
4f1f63671e | ||
|
|
5578e8db5c | ||
|
|
5a93907d74 | ||
|
|
1d4403b497 | ||
|
|
6939884a95 | ||
|
|
509f09165a | ||
|
|
40d8e1b76a | ||
|
|
da1c085f4a | ||
|
|
53281c0921 | ||
|
|
b9cf9b5558 | ||
|
|
295500a746 | ||
|
|
b1f77d202d | ||
|
|
5a29f5fbb1 | ||
|
|
1d6e4374c0 | ||
|
|
eaa7d1c790 | ||
|
|
f29cd16616 | ||
|
|
1279ad6e68 | ||
|
|
8d4ba43cbe | ||
|
|
d2562a5e0a | ||
|
|
a1d67afb72 | ||
|
|
fd61b8506b | ||
|
|
75babad426 | ||
|
|
2b64030c0c | ||
|
|
26fcf457e6 | ||
|
|
26bf1e482d | ||
|
|
107eac7e25 | ||
|
|
e2b442ab0b | ||
|
|
b822a5d862 | ||
|
|
dc4e41305e | ||
|
|
fdfb4cff64 |
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
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -4,7 +4,7 @@ Long form explanations of most of the items below can be found in the [CONTRIBUT
|
||||
|
||||
## Branching checklist
|
||||
- [ ] There is an issue associated with your PR (bug, feature, etc.. if not, create one)
|
||||
- [ ] Your PR description references the associated issue (i.e. fixes #123)
|
||||
- [ ] Your PR description references the associated issue (i.e. fixes #123456)
|
||||
- [ ] Code is in its own branch
|
||||
- [ ] Branch name is related to the PR contents
|
||||
- [ ] PR targets master
|
||||
|
||||
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
|
||||
2
.github/workflows/check.yml
vendored
2
.github/workflows/check.yml
vendored
@@ -61,4 +61,4 @@ jobs:
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --all-targets --all-features -- -D warnings -A clippy::unnecessary_unwrap -A clippy::deref_addrof
|
||||
args: --all-targets --all-features -- -D warnings -A clippy::deref_addrof
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -22,3 +22,6 @@ img/**
|
||||
# scripts to check code coverage using nightly compiler
|
||||
check-coverage.sh
|
||||
lcov_cobertura.py
|
||||
|
||||
# dockerignore file that makes it so i can work on the docker config without copying a 4GB manifest or w/e it is
|
||||
.dockerignore
|
||||
|
||||
15
Cargo.toml
15
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "feroxbuster"
|
||||
version = "1.8.0"
|
||||
version = "1.10.0"
|
||||
authors = ["Ben 'epi' Risher <epibar052@gmail.com>"]
|
||||
license = "MIT"
|
||||
edition = "2018"
|
||||
@@ -10,10 +10,16 @@ description = "A fast, simple, recursive content discovery tool."
|
||||
categories = ["command-line-utilities"]
|
||||
keywords = ["pentest", "enumeration", "url-bruteforce", "content-discovery", "web"]
|
||||
exclude = [".github/*", "img/*", "check-coverage.sh"]
|
||||
build = "build.rs"
|
||||
|
||||
[badges]
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[build-dependencies]
|
||||
clap = "2.33"
|
||||
regex = "1"
|
||||
lazy_static = "1.4"
|
||||
|
||||
[dependencies]
|
||||
futures = { version = "0.3"}
|
||||
tokio = { version = "0.2", features = ["full"] }
|
||||
@@ -21,23 +27,24 @@ 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"
|
||||
crossterm = "0.18"
|
||||
rlimit = "0.5"
|
||||
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"
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
FROM alpine:latest
|
||||
LABEL maintainer="wfnintr@null.net"
|
||||
|
||||
RUN sed -i -e 's/v[[:digit:]]\..*\//edge\//g' /etc/apk/repositories && apk upgrade --update-cache --available
|
||||
|
||||
# download default wordlists
|
||||
RUN apk add --no-cache --virtual .depends subversion && \
|
||||
RUN apk add --no-cache --virtual .depends subversion font-noto-emoji && \
|
||||
svn export https://github.com/danielmiessler/SecLists/trunk/Discovery/Web-Content /usr/share/seclists/Discovery/Web-Content && \
|
||||
apk del .depends
|
||||
|
||||
|
||||
253
README.md
253
README.md
@@ -73,19 +73,22 @@ This attack is also known as Predictable Resource Location, File Enumeration, Di
|
||||
- [ferox-config.toml](#ferox-configtoml)
|
||||
- [Command Line Parsing](#command-line-parsing)
|
||||
- [Example Usage](#-example-usage)
|
||||
- [Pause and Resume Scans (new in `v1.4.0`)](#pause-and-resume-scans-new-in-v140)
|
||||
- [Multiple Values](#multiple-values)
|
||||
- [Extract Links from Response Body (new in `v1.1.0`)](#extract-links-from-response-body-new-in-v110)
|
||||
- [Include Headers](#include-headers)
|
||||
- [IPv6, Non-recursive scan with INFO logging enabled](#ipv6-non-recursive-scan-with-info-level-logging-enabled)
|
||||
- [Read urls from STDIN; pipe only resulting urls out to another tool](#read-urls-from-stdin-pipe-only-resulting-urls-out-to-another-tool)
|
||||
- [Proxy traffic through Burp](#proxy-traffic-through-burp)
|
||||
- [Proxy traffic through a SOCKS proxy](#proxy-traffic-through-a-socks-proxy)
|
||||
- [Pass auth token via query parameter](#pass-auth-token-via-query-parameter)
|
||||
- [Extract Links from Response Body (new in `v1.1.0`)](#extract-links-from-response-body-new-in-v110)
|
||||
- [Limit Total Number of Concurrent Scans (new in `v1.2.0`)](#limit-total-number-of-concurrent-scans-new-in-v120)
|
||||
- [Filter Response by Status Code (new in `v1.3.0`)](#filter-response-by-status-code--new-in-v130)
|
||||
- [Filter Response Using a Regular Expression (new in `v1.8.0`)](#filter-response-using-a-regular-expression-new-in-v180)
|
||||
- [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)
|
||||
@@ -257,6 +260,7 @@ Configuration begins with with the following built-in default values baked into
|
||||
- recursion depth: `4`
|
||||
- auto-filter wildcards - `true`
|
||||
- output: `stdout`
|
||||
- save_state: `true` (create a state file in cwd when `Ctrl+C` is received)
|
||||
|
||||
### Threads and Connection Limits At A High-Level
|
||||
|
||||
@@ -348,6 +352,8 @@ A pre-made configuration file with examples of all available settings can be fou
|
||||
# filter_word_count = [993]
|
||||
# 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
|
||||
#
|
||||
@@ -400,14 +406,17 @@ 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
|
||||
ferox-1606586780.state)
|
||||
-L, --scan-limit <SCAN_LIMIT> Limit total number of concurrent scans (default: 0, i.e. no limit)
|
||||
-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)
|
||||
@@ -416,12 +425,6 @@ OPTIONS:
|
||||
|
||||
## 🧰 Example Usage
|
||||
|
||||
### Pause and Resume Scans (new in `v1.4.0`)
|
||||
|
||||
Scans can be paused and resumed by pressing the ENTER key (shown below)
|
||||
|
||||

|
||||
|
||||
### Multiple Values
|
||||
|
||||
Options that take multiple values are very flexible. Consider the following ways of specifying extensions:
|
||||
@@ -440,6 +443,36 @@ All of the methods above (multiple flags, space separated, comma separated, etc.
|
||||
./feroxbuster -u http://127.1 -H Accept:application/json "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
### IPv6, non-recursive scan with INFO-level logging enabled
|
||||
|
||||
```
|
||||
./feroxbuster -u http://[::1] --no-recursion -vv
|
||||
```
|
||||
|
||||
### Read urls from STDIN; pipe only resulting urls out to another tool
|
||||
|
||||
```
|
||||
cat targets | ./feroxbuster --stdin --quiet -s 200 301 302 --redirects -x js | fff -s 200 -o js-files
|
||||
```
|
||||
|
||||
### Proxy traffic through Burp
|
||||
|
||||
```
|
||||
./feroxbuster -u http://127.1 --insecure --proxy http://127.0.0.1:8080
|
||||
```
|
||||
|
||||
### Proxy traffic through a SOCKS proxy
|
||||
|
||||
```
|
||||
./feroxbuster -u http://127.1 --proxy socks5://127.0.0.1:9050
|
||||
```
|
||||
|
||||
### Pass auth token via query parameter
|
||||
|
||||
```
|
||||
./feroxbuster -u http://127.1 --query token=0123456789ABCDEF
|
||||
```
|
||||
|
||||
### Extract Links from Response Body (New in `v1.1.0`)
|
||||
|
||||
Search through the body of valid responses (html, javascript, etc...) for additional endpoints to scan. This turns
|
||||
@@ -470,37 +503,6 @@ With `--extract-links`
|
||||
|
||||

|
||||
|
||||
|
||||
### IPv6, non-recursive scan with INFO-level logging enabled
|
||||
|
||||
```
|
||||
./feroxbuster -u http://[::1] --no-recursion -vv
|
||||
```
|
||||
|
||||
### Read urls from STDIN; pipe only resulting urls out to another tool
|
||||
|
||||
```
|
||||
cat targets | ./feroxbuster --stdin --quiet -s 200 301 302 --redirects -x js | fff -s 200 -o js-files
|
||||
```
|
||||
|
||||
### Proxy traffic through Burp
|
||||
|
||||
```
|
||||
./feroxbuster -u http://127.1 --insecure --proxy http://127.0.0.1:8080
|
||||
```
|
||||
|
||||
### Proxy traffic through a SOCKS proxy
|
||||
|
||||
```
|
||||
./feroxbuster -u http://127.1 --proxy socks5://127.0.0.1:9050
|
||||
```
|
||||
|
||||
### Pass auth token via query parameter
|
||||
|
||||
```
|
||||
./feroxbuster -u http://127.1 --query token=0123456789ABCDEF
|
||||
```
|
||||
|
||||
### Limit Total Number of Concurrent Scans (new in `v1.2.0`)
|
||||
|
||||
Limit the number of scans permitted to run at any given time. Recursion will still identify new directories, but newly
|
||||
@@ -523,18 +525,11 @@ each one is checked against a list of known filters and either displayed or not
|
||||
./feroxbuster -u http://127.1 --filter-status 301
|
||||
```
|
||||
|
||||
### Filter Response Using a Regular Expression (new in `v1.8.0`)
|
||||
### Pause an Active Scan (new in `v1.4.0`)
|
||||
|
||||
Version 1.3.0 included an overhaul to the filtering system which will allow for a wide array of filters to be added
|
||||
with minimal effort. The latest addition is a Regular Expression Filter. As responses come back from the scanned server,
|
||||
the **body** of the response is checked against the filter's regular expression. If the expression is found in the
|
||||
body, then that response is filtered out.
|
||||
Scans can be paused and resumed by pressing the ENTER key (shown below)
|
||||
|
||||
**NOTE: Using regular expressions to filter large responses or many regular expressions may negatively impact performance.**
|
||||
|
||||
```
|
||||
./feroxbuster -u http://127.1 --filter-regex '[aA]ccess [dD]enied.?' --output results.txt --json
|
||||
```
|
||||

|
||||
|
||||
### Replay Responses to a Proxy based on Status Code (new in `v1.5.0`)
|
||||
|
||||
@@ -550,6 +545,110 @@ 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
|
||||
with minimal effort. The latest addition is a Regular Expression Filter. As responses come back from the scanned server,
|
||||
the **body** of the response is checked against the filter's regular expression. If the expression is found in the
|
||||
body, then that response is filtered out.
|
||||
|
||||
**NOTE: Using regular expressions to filter large responses or many regular expressions may negatively impact performance.**
|
||||
|
||||
```
|
||||
./feroxbuster -u http://127.1 --filter-regex '[aA]ccess [dD]enied.?' --output results.txt --json
|
||||
```
|
||||
|
||||
### Stop and Resume Scans (`--resume-from FILE`) (new in `v1.9.0`)
|
||||
|
||||
Version 1.9.0 adds a few features that allow for completely stopping a scan, and resuming that same scan from a file on disk.
|
||||
|
||||
A simple `Ctrl+C` during a scan will create a file that contains information about the scan that was cancelled.
|
||||
|
||||

|
||||
|
||||
```json
|
||||
// example snippet of state file
|
||||
|
||||
{
|
||||
"scans":[
|
||||
{
|
||||
"id":"057016a14769414aac9a7a62707598cb",
|
||||
"url":"https://localhost.com",
|
||||
"scan_type":"Directory",
|
||||
"complete":true
|
||||
},
|
||||
{
|
||||
"id":"400b2323a16f43468a04ffcbbeba34c6",
|
||||
"url":"https://localhost.com/css",
|
||||
"scan_type":"Directory",
|
||||
"complete":false
|
||||
}
|
||||
],
|
||||
"config":{
|
||||
"wordlist":"/wordlists/seclists/Discovery/Web-Content/common.txt",
|
||||
"...":"..."
|
||||
},
|
||||
"responses":[
|
||||
{
|
||||
"type":"response",
|
||||
"url":"https://localhost.com/Login",
|
||||
"path":"/Login",
|
||||
"wildcard":false,
|
||||
"status":302,
|
||||
"content_length":0,
|
||||
"line_count":0,
|
||||
"word_count":0,
|
||||
"headers":{
|
||||
"content-length":"0",
|
||||
"server":"nginx/1.16.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
```
|
||||
|
||||
Based on the example image above, the same scan can be resumed by using `feroxbuster --resume-from ferox-http_localhost-1606947491.state`. Directories that were already complete are not rescanned, however partially complete scans are started from the beginning.
|
||||
|
||||

|
||||
|
||||
In order to prevent state file creation when `Ctrl+C` is pressed, you can simply add the entry below to your `ferox-config.toml`.
|
||||
|
||||
```toml
|
||||
# ferox-config.toml
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
## 🧐 Comparison w/ Similar Tools
|
||||
|
||||
There are quite a few similar tools for forced browsing/content discovery. Burp Suite Pro, Dirb, Dirbuster, etc...
|
||||
@@ -568,24 +667,32 @@ 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`) | ✔ | | ✔ |
|
||||
| **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
|
||||
@@ -671,4 +778,8 @@ 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.
|
||||
|
||||
17
build.rs
Normal file
17
build.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
extern crate clap;
|
||||
|
||||
use clap::Shell;
|
||||
|
||||
include!("src/parser.rs");
|
||||
|
||||
fn main() {
|
||||
let outdir = "shell_completions";
|
||||
|
||||
let mut app = initialize();
|
||||
|
||||
let shells: [Shell; 4] = [Shell::Bash, Shell::Fish, Shell::Zsh, Shell::PowerShell];
|
||||
|
||||
for shell in &shells {
|
||||
app.gen_completions("feroxbuster", *shell, outdir);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,8 @@
|
||||
# filter_word_count = [993]
|
||||
# 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/resumed-scan.gif
Normal file
BIN
img/resumed-scan.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 269 KiB |
BIN
img/save-state.png
Normal file
BIN
img/save-state.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
BIN
img/time-limit.gif
Normal file
BIN
img/time-limit.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 232 KiB |
95
shell_completions/_feroxbuster
Normal file
95
shell_completions/_feroxbuster
Normal file
@@ -0,0 +1,95 @@
|
||||
#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 "${_arguments_options[@]}" \
|
||||
'-w+[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)]' \
|
||||
'-t+[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)]' \
|
||||
'-T+[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)]' \
|
||||
'-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]' \
|
||||
'*-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)]' \
|
||||
'*-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)]' \
|
||||
'-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)]' \
|
||||
'-a+[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)]' \
|
||||
'*-H+[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)]' \
|
||||
'*-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)]' \
|
||||
'*-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$'\'')]' \
|
||||
'*-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)]' \
|
||||
'*-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)]' \
|
||||
'*-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)]' \
|
||||
'-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)]' \
|
||||
'--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...]' \
|
||||
'--quiet[Only print URLs; Don'\''t print status codes, response size, running config, etc...]' \
|
||||
'--json[Emit JSON logs to --output and --debug-log instead of normal text]' \
|
||||
'-D[Don'\''t auto-filter wildcard responses]' \
|
||||
'--dont-filter[Don'\''t auto-filter wildcard responses]' \
|
||||
'-r[Follow redirects]' \
|
||||
'--redirects[Follow redirects]' \
|
||||
'-k[Disables TLS certificate validation]' \
|
||||
'--insecure[Disables TLS certificate validation]' \
|
||||
'-n[Do not scan recursively]' \
|
||||
'--no-recursion[Do not scan recursively]' \
|
||||
'(-x --extensions)-f[Append / to each request]' \
|
||||
'(-x --extensions)--add-slash[Append / to each request]' \
|
||||
'(-u --url)--stdin[Read url(s) from STDIN]' \
|
||||
'-e[Extract links from response body (html, javascript, etc...); make new requests based on findings (default: false)]' \
|
||||
'--extract-links[Extract links from response body (html, javascript, etc...); make new requests based on findings (default: false)]' \
|
||||
'-h[Prints help information]' \
|
||||
'--help[Prints help information]' \
|
||||
'-V[Prints version information]' \
|
||||
'--version[Prints version information]' \
|
||||
&& ret=0
|
||||
|
||||
}
|
||||
|
||||
(( $+functions[_feroxbuster_commands] )) ||
|
||||
_feroxbuster_commands() {
|
||||
local commands; commands=(
|
||||
|
||||
)
|
||||
_describe -t commands 'feroxbuster commands' commands "$@"
|
||||
}
|
||||
|
||||
_feroxbuster "$@"
|
||||
94
shell_completions/_feroxbuster.ps1
Normal file
94
shell_completions/_feroxbuster.ps1
Normal file
@@ -0,0 +1,94 @@
|
||||
|
||||
using namespace System.Management.Automation
|
||||
using namespace System.Management.Automation.Language
|
||||
|
||||
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 = @(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
|
||||
}
|
||||
213
shell_completions/feroxbuster.bash
Normal file
213
shell_completions/feroxbuster.bash
Normal file
@@ -0,0 +1,213 @@
|
||||
_feroxbuster() {
|
||||
local i cur prev opts cmds
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
cmd=""
|
||||
opts=""
|
||||
|
||||
for i in ${COMP_WORDS[@]}
|
||||
do
|
||||
case "${i}" in
|
||||
feroxbuster)
|
||||
cmd="feroxbuster"
|
||||
;;
|
||||
|
||||
*)
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
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 --time-limit "
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
fi
|
||||
case "${prev}" in
|
||||
|
||||
--wordlist)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-w)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--url)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-u)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--threads)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-t)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--depth)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-d)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--timeout)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-T)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--proxy)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-p)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--replay-proxy)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-P)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--replay-codes)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-R)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--status-codes)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-s)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--output)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--resume-from)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--debug-log)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--user-agent)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-a)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--extensions)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-x)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--headers)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-H)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--query)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-Q)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--filter-size)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-S)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--filter-regex)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-X)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--filter-words)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-W)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--filter-lines)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-N)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--filter-status)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-C)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--scan-limit)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-L)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--time-limit)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
COMPREPLY=()
|
||||
;;
|
||||
esac
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
;;
|
||||
|
||||
esac
|
||||
}
|
||||
|
||||
complete -F _feroxbuster -o bashdefault -o default feroxbuster
|
||||
35
shell_completions/feroxbuster.fish
Normal file
35
shell_completions/feroxbuster.fish
Normal file
@@ -0,0 +1,35 @@
|
||||
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'
|
||||
169
src/banner.rs
169
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,7 +418,7 @@ 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(); // 🤪
|
||||
}
|
||||
@@ -421,7 +429,7 @@ by Ben "epi" Risher {} ver: {}"#,
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f508}", "Verbosity", config.verbosity)
|
||||
format_banner_entry!(format_emoji("🔈"), "Verbosity", config.verbosity)
|
||||
)
|
||||
.unwrap_or_default(); // 🔈
|
||||
}
|
||||
@@ -429,7 +437,7 @@ by Ben "epi" Risher {} ver: {}"#,
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f509}", "Verbosity", config.verbosity)
|
||||
format_banner_entry!(format_emoji("🔉"), "Verbosity", config.verbosity)
|
||||
)
|
||||
.unwrap_or_default(); // 🔉
|
||||
}
|
||||
@@ -437,7 +445,7 @@ by Ben "epi" Risher {} ver: {}"#,
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f50a}", "Verbosity", config.verbosity)
|
||||
format_banner_entry!(format_emoji("🔊"), "Verbosity", config.verbosity)
|
||||
)
|
||||
.unwrap_or_default(); // 🔊
|
||||
}
|
||||
@@ -445,7 +453,7 @@ by Ben "epi" Risher {} ver: {}"#,
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f4e2}", "Verbosity", config.verbosity)
|
||||
format_banner_entry!(format_emoji("📢"), "Verbosity", config.verbosity)
|
||||
)
|
||||
.unwrap_or_default(); // 📢
|
||||
}
|
||||
@@ -456,7 +464,7 @@ by Ben "epi" Risher {} ver: {}"#,
|
||||
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 +474,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 +489,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 +498,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 +533,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 +549,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 +634,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 +650,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 +666,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 +684,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 +700,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));
|
||||
}
|
||||
}
|
||||
|
||||
317
src/config.rs
317
src/config.rs
@@ -1,7 +1,8 @@
|
||||
use crate::scan_manager::resume_scan;
|
||||
use crate::utils::{module_colorizer, status_colorizer};
|
||||
use crate::{client, parser, progress};
|
||||
use crate::{FeroxSerialize, DEFAULT_CONFIG_NAME, DEFAULT_STATUS_CODES, DEFAULT_WORDLIST, VERSION};
|
||||
use clap::value_t;
|
||||
use clap::{value_t, ArgMatches};
|
||||
use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget};
|
||||
use lazy_static::lazy_static;
|
||||
use reqwest::{Client, StatusCode};
|
||||
@@ -21,7 +22,33 @@ lazy_static! {
|
||||
pub static ref PROGRESS_BAR: MultiProgress = MultiProgress::with_draw_target(ProgressDrawTarget::stdout());
|
||||
|
||||
/// Global progress bar that is only used for printing messages that don't jack up other bars
|
||||
pub static ref PROGRESS_PRINTER: ProgressBar = progress::add_bar("", 0, true);
|
||||
pub static ref PROGRESS_PRINTER: ProgressBar = progress::add_bar("", 0, true, false);
|
||||
}
|
||||
|
||||
/// macro helper to abstract away repetitive configuration updates
|
||||
macro_rules! update_config_if_present {
|
||||
($c:expr, $m:ident, $v:expr, $t:ty) => {
|
||||
match value_t!($m, $v, $t) {
|
||||
Ok(value) => *$c = value, // Update value
|
||||
Err(clap::Error {
|
||||
kind: clap::ErrorKind::ArgumentNotFound,
|
||||
message: _,
|
||||
info: _,
|
||||
}) => {
|
||||
// Do nothing if argument not found
|
||||
}
|
||||
Err(e) => e.exit(), // Exit with error on parse error
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// macro helper to abstract away repetitive if not default: update checks
|
||||
macro_rules! update_if_not_default {
|
||||
($old:expr, $new:expr, $default:expr) => {
|
||||
if $new != $default {
|
||||
*$old = $new;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// simple helper to clean up some code reuse below; panics under test / exits in prod
|
||||
@@ -80,7 +107,7 @@ pub struct Configuration {
|
||||
pub status_codes: Vec<u16>,
|
||||
|
||||
/// Status Codes to replay to the Replay Proxy (default: whatever is passed to --status-code)
|
||||
#[serde(default)]
|
||||
#[serde(default = "status_codes")]
|
||||
pub replay_codes: Vec<u16>,
|
||||
|
||||
/// Status Codes to filter out (deny list)
|
||||
@@ -191,9 +218,24 @@ pub struct Configuration {
|
||||
/// Don't auto-filter wildcard responses
|
||||
#[serde(default)]
|
||||
pub dont_filter: bool,
|
||||
|
||||
/// Scan started from a state file, not from CLI args
|
||||
#[serde(default)]
|
||||
pub resumed: bool,
|
||||
|
||||
/// Whether or not a scan's current state should be saved when user presses Ctrl+C
|
||||
///
|
||||
/// 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, and depth are used to provide
|
||||
// functions timeout, threads, status_codes, user_agent, wordlist, save_state, and depth are used to provide
|
||||
// defaults in the event that a ferox-config.toml is found but one or more of the values below
|
||||
// aren't listed in the config. This way, we get the correct defaults upon Deserialization
|
||||
|
||||
@@ -207,6 +249,11 @@ fn timeout() -> u64 {
|
||||
7
|
||||
}
|
||||
|
||||
/// default save_state value
|
||||
fn save_state() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// default threads value
|
||||
fn threads() -> usize {
|
||||
50
|
||||
@@ -256,6 +303,7 @@ impl Default for Configuration {
|
||||
replay_client,
|
||||
dont_filter: false,
|
||||
quiet: false,
|
||||
resumed: false,
|
||||
stdin: false,
|
||||
json: false,
|
||||
verbosity: 0,
|
||||
@@ -265,11 +313,13 @@ impl Default for Configuration {
|
||||
redirects: false,
|
||||
no_recursion: false,
|
||||
extract_links: false,
|
||||
save_state: true,
|
||||
proxy: String::new(),
|
||||
config: String::new(),
|
||||
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(),
|
||||
@@ -304,6 +354,7 @@ impl Configuration {
|
||||
/// - **output**: `None` (print to stdout)
|
||||
/// - **debug_log**: `None`
|
||||
/// - **quiet**: `false`
|
||||
/// - **save_state**: `true`
|
||||
/// - **user_agent**: `feroxbuster/VERSION`
|
||||
/// - **insecure**: `false` (don't be insecure, i.e. don't allow invalid certs)
|
||||
/// - **extensions**: `None`
|
||||
@@ -320,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)
|
||||
///
|
||||
@@ -343,13 +395,65 @@ 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();
|
||||
|
||||
// Get the default configuration, this is what will apply if nothing
|
||||
// else is specified.
|
||||
let mut config = Configuration::default();
|
||||
|
||||
// read in all config files
|
||||
Self::parse_config_files(&mut config);
|
||||
|
||||
// read in the user provided options, this produces a separate instance of Configuration
|
||||
// in order to allow for potentially merging into a --resume-from Configuration
|
||||
let cli_config = Self::parse_cli_args(&args);
|
||||
|
||||
// --resume-from used, need to first read the Configuration from disk, and then
|
||||
// merge the cli_config into the resumed config
|
||||
if let Some(filename) = args.value_of("resume_from") {
|
||||
// when resuming a scan, instead of normal configuration loading, we just
|
||||
// load the config from disk by calling resume_scan
|
||||
let mut previous_config = resume_scan(filename);
|
||||
|
||||
// if any other arguments were passed on the command line, the theory is that the
|
||||
// user meant to modify the previously cancelled/saved scan in some way that we
|
||||
// should take into account
|
||||
Self::merge_config(&mut previous_config, cli_config);
|
||||
|
||||
// the resumed flag isn't printed in the banner and really has no business being
|
||||
// serialized or included in much of the usual config logic; simply setting it to true
|
||||
// here and being done with it
|
||||
previous_config.resumed = true;
|
||||
|
||||
// if the user used --stdin, we already have all the scans started (or complete), we
|
||||
// need to flip stdin to false so that the 'read from stdin' logic doesn't fire (if
|
||||
// not flipped to false, the program hangs waiting for input from stdin again)
|
||||
previous_config.stdin = false;
|
||||
|
||||
// clients aren't serialized, have to remake them from the previous config
|
||||
Self::try_rebuild_clients(&mut previous_config);
|
||||
|
||||
return previous_config;
|
||||
}
|
||||
|
||||
// if we've gotten to this point in the code, --resume-from was not used, so we need to
|
||||
// merge the cli options into the config file options and return the result
|
||||
Self::merge_config(&mut config, cli_config);
|
||||
|
||||
// rebuild clients is the last step in either code branch
|
||||
Self::try_rebuild_clients(&mut config);
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
/// Parse all possible versions of the ferox-config.toml file, adhering to the order of
|
||||
/// precedence outlined above
|
||||
fn parse_config_files(mut config: &mut Self) {
|
||||
// Next, we parse the ferox-config.toml file, if present and set the values
|
||||
// therein to overwrite our default values. Deserialized defaults are specified
|
||||
// in the Configuration struct so that we don't change anything that isn't
|
||||
@@ -391,24 +495,12 @@ impl Configuration {
|
||||
let config_file = cwd.join(DEFAULT_CONFIG_NAME);
|
||||
Self::parse_and_merge_config(config_file, &mut config);
|
||||
}
|
||||
}
|
||||
|
||||
let args = parser::initialize().get_matches();
|
||||
|
||||
macro_rules! update_config_if_present {
|
||||
($c:expr, $m:ident, $v:expr, $t:ty) => {
|
||||
match value_t!($m, $v, $t) {
|
||||
Ok(value) => *$c = value, // Update value
|
||||
Err(clap::Error {
|
||||
kind: clap::ErrorKind::ArgumentNotFound,
|
||||
message: _,
|
||||
info: _,
|
||||
}) => {
|
||||
// Do nothing if argument not found
|
||||
}
|
||||
Err(e) => e.exit(), // Exit with error on parse error
|
||||
}
|
||||
};
|
||||
}
|
||||
/// Given a set of ArgMatches read from the CLI, update and return the default Configuration
|
||||
/// settings
|
||||
fn parse_cli_args(args: &ArgMatches) -> Self {
|
||||
let mut config = Configuration::default();
|
||||
|
||||
update_config_if_present!(&mut config.threads, args, "threads", usize);
|
||||
update_config_if_present!(&mut config.depth, args, "depth", usize);
|
||||
@@ -416,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
|
||||
@@ -522,8 +615,8 @@ impl Configuration {
|
||||
|
||||
if args.is_present("stdin") {
|
||||
config.stdin = true;
|
||||
} else {
|
||||
config.target_url = String::from(args.value_of("url").unwrap());
|
||||
} else if let Some(url) = args.value_of("url") {
|
||||
config.target_url = String::from(url);
|
||||
}
|
||||
|
||||
////
|
||||
@@ -569,50 +662,53 @@ impl Configuration {
|
||||
}
|
||||
}
|
||||
|
||||
// this if statement determines if we've gotten a Client configuration change from
|
||||
// either the config file or command line arguments; if we have, we need to rebuild
|
||||
// the client and store it in the config struct
|
||||
if !config.proxy.is_empty()
|
||||
|| config.timeout != timeout()
|
||||
|| config.user_agent != user_agent()
|
||||
|| config.redirects
|
||||
|| config.insecure
|
||||
|| !config.headers.is_empty()
|
||||
config
|
||||
}
|
||||
|
||||
/// this function determines if we've gotten a Client configuration change from
|
||||
/// either the config file or command line arguments; if we have, we need to rebuild
|
||||
/// the client and store it in the config struct
|
||||
fn try_rebuild_clients(configuration: &mut Configuration) {
|
||||
if !configuration.proxy.is_empty()
|
||||
|| configuration.timeout != timeout()
|
||||
|| configuration.user_agent != user_agent()
|
||||
|| configuration.redirects
|
||||
|| configuration.insecure
|
||||
|| !configuration.headers.is_empty()
|
||||
|| configuration.resumed
|
||||
{
|
||||
if config.proxy.is_empty() {
|
||||
config.client = client::initialize(
|
||||
config.timeout,
|
||||
&config.user_agent,
|
||||
config.redirects,
|
||||
config.insecure,
|
||||
&config.headers,
|
||||
if configuration.proxy.is_empty() {
|
||||
configuration.client = client::initialize(
|
||||
configuration.timeout,
|
||||
&configuration.user_agent,
|
||||
configuration.redirects,
|
||||
configuration.insecure,
|
||||
&configuration.headers,
|
||||
None,
|
||||
)
|
||||
} else {
|
||||
config.client = client::initialize(
|
||||
config.timeout,
|
||||
&config.user_agent,
|
||||
config.redirects,
|
||||
config.insecure,
|
||||
&config.headers,
|
||||
Some(&config.proxy),
|
||||
configuration.client = client::initialize(
|
||||
configuration.timeout,
|
||||
&configuration.user_agent,
|
||||
configuration.redirects,
|
||||
configuration.insecure,
|
||||
&configuration.headers,
|
||||
Some(&configuration.proxy),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if !config.replay_proxy.is_empty() {
|
||||
if !configuration.replay_proxy.is_empty() {
|
||||
// only set replay_client when replay_proxy is set
|
||||
config.replay_client = Some(client::initialize(
|
||||
config.timeout,
|
||||
&config.user_agent,
|
||||
config.redirects,
|
||||
config.insecure,
|
||||
&config.headers,
|
||||
Some(&config.replay_proxy),
|
||||
configuration.replay_client = Some(client::initialize(
|
||||
configuration.timeout,
|
||||
&configuration.user_agent,
|
||||
configuration.redirects,
|
||||
configuration.insecure,
|
||||
&configuration.headers,
|
||||
Some(&configuration.replay_proxy),
|
||||
));
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
/// Given a configuration file's location and an instance of `Configuration`, read in
|
||||
@@ -636,37 +732,64 @@ impl Configuration {
|
||||
}
|
||||
|
||||
/// Given two Configurations, overwrite `settings` with the fields found in `settings_to_merge`
|
||||
fn merge_config(settings: &mut Self, settings_to_merge: Self) {
|
||||
settings.threads = settings_to_merge.threads;
|
||||
settings.wordlist = settings_to_merge.wordlist;
|
||||
settings.status_codes = settings_to_merge.status_codes;
|
||||
settings.proxy = settings_to_merge.proxy;
|
||||
settings.timeout = settings_to_merge.timeout;
|
||||
settings.verbosity = settings_to_merge.verbosity;
|
||||
settings.quiet = settings_to_merge.quiet;
|
||||
settings.output = settings_to_merge.output;
|
||||
settings.user_agent = settings_to_merge.user_agent;
|
||||
settings.redirects = settings_to_merge.redirects;
|
||||
settings.insecure = settings_to_merge.insecure;
|
||||
settings.extract_links = settings_to_merge.extract_links;
|
||||
settings.extensions = settings_to_merge.extensions;
|
||||
settings.headers = settings_to_merge.headers;
|
||||
settings.queries = settings_to_merge.queries;
|
||||
settings.no_recursion = settings_to_merge.no_recursion;
|
||||
settings.add_slash = settings_to_merge.add_slash;
|
||||
settings.stdin = settings_to_merge.stdin;
|
||||
settings.depth = settings_to_merge.depth;
|
||||
settings.filter_size = settings_to_merge.filter_size;
|
||||
settings.filter_regex = settings_to_merge.filter_regex;
|
||||
settings.filter_word_count = settings_to_merge.filter_word_count;
|
||||
settings.filter_line_count = settings_to_merge.filter_line_count;
|
||||
settings.filter_status = settings_to_merge.filter_status;
|
||||
settings.dont_filter = settings_to_merge.dont_filter;
|
||||
settings.scan_limit = settings_to_merge.scan_limit;
|
||||
settings.replay_proxy = settings_to_merge.replay_proxy;
|
||||
settings.replay_codes = settings_to_merge.replay_codes;
|
||||
settings.debug_log = settings_to_merge.debug_log;
|
||||
settings.json = settings_to_merge.json;
|
||||
fn merge_config(conf: &mut Self, new: Self) {
|
||||
// does not include the following Configuration fields, as they don't make sense here
|
||||
// - kind
|
||||
// - client
|
||||
// - replay_client
|
||||
// - 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);
|
||||
update_if_not_default!(&mut conf.output, new.output, "");
|
||||
update_if_not_default!(&mut conf.redirects, new.redirects, false);
|
||||
update_if_not_default!(&mut conf.insecure, new.insecure, false);
|
||||
update_if_not_default!(&mut conf.extract_links, new.extract_links, false);
|
||||
update_if_not_default!(&mut conf.extensions, new.extensions, Vec::<String>::new());
|
||||
update_if_not_default!(&mut conf.headers, new.headers, HashMap::new());
|
||||
update_if_not_default!(&mut conf.queries, new.queries, Vec::new());
|
||||
update_if_not_default!(&mut conf.no_recursion, new.no_recursion, false);
|
||||
update_if_not_default!(&mut conf.add_slash, new.add_slash, false);
|
||||
update_if_not_default!(&mut conf.stdin, new.stdin, false);
|
||||
update_if_not_default!(&mut conf.filter_size, new.filter_size, Vec::<u64>::new());
|
||||
update_if_not_default!(
|
||||
&mut conf.filter_regex,
|
||||
new.filter_regex,
|
||||
Vec::<String>::new()
|
||||
);
|
||||
update_if_not_default!(
|
||||
&mut conf.filter_word_count,
|
||||
new.filter_word_count,
|
||||
Vec::<usize>::new()
|
||||
);
|
||||
update_if_not_default!(
|
||||
&mut conf.filter_line_count,
|
||||
new.filter_line_count,
|
||||
Vec::<usize>::new()
|
||||
);
|
||||
update_if_not_default!(
|
||||
&mut conf.filter_status,
|
||||
new.filter_status,
|
||||
Vec::<u16>::new()
|
||||
);
|
||||
update_if_not_default!(&mut conf.dont_filter, new.dont_filter, false);
|
||||
update_if_not_default!(&mut conf.scan_limit, new.scan_limit, 0);
|
||||
update_if_not_default!(&mut conf.replay_proxy, new.replay_proxy, "");
|
||||
update_if_not_default!(&mut conf.debug_log, new.debug_log, "");
|
||||
update_if_not_default!(&mut conf.json, new.json, false);
|
||||
|
||||
update_if_not_default!(&mut conf.timeout, new.timeout, timeout());
|
||||
update_if_not_default!(&mut conf.user_agent, new.user_agent, user_agent());
|
||||
update_if_not_default!(&mut conf.threads, new.threads, threads());
|
||||
update_if_not_default!(&mut conf.depth, new.depth, depth());
|
||||
update_if_not_default!(&mut conf.wordlist, new.wordlist, wordlist());
|
||||
update_if_not_default!(&mut conf.status_codes, new.status_codes, status_codes());
|
||||
// status_codes() is the default for replay_codes, if they're not provided
|
||||
update_if_not_default!(&mut conf.replay_codes, new.replay_codes, status_codes());
|
||||
update_if_not_default!(&mut conf.save_state, new.save_state, save_state());
|
||||
}
|
||||
|
||||
/// If present, read in `DEFAULT_CONFIG_NAME` and deserialize the specified values
|
||||
@@ -752,6 +875,7 @@ mod tests {
|
||||
quiet = true
|
||||
verbosity = 1
|
||||
scan_limit = 6
|
||||
time_limit = "10m"
|
||||
output = "/some/otherpath"
|
||||
debug_log = "/yet/anotherpath"
|
||||
redirects = true
|
||||
@@ -765,6 +889,7 @@ mod tests {
|
||||
dont_filter = true
|
||||
extract_links = true
|
||||
json = true
|
||||
save_state = false
|
||||
depth = 1
|
||||
filter_size = [4120]
|
||||
filter_regex = ["^ignore me$"]
|
||||
@@ -785,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());
|
||||
@@ -800,6 +926,7 @@ mod tests {
|
||||
assert_eq!(config.dont_filter, false);
|
||||
assert_eq!(config.no_recursion, false);
|
||||
assert_eq!(config.json, false);
|
||||
assert_eq!(config.save_state, true);
|
||||
assert_eq!(config.stdin, false);
|
||||
assert_eq!(config.add_slash, false);
|
||||
assert_eq!(config.redirects, false);
|
||||
@@ -1004,6 +1131,20 @@ mod tests {
|
||||
assert_eq!(config.filter_status, vec![201]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_save_state() {
|
||||
let config = setup_config_test();
|
||||
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() {
|
||||
|
||||
@@ -143,7 +143,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 +245,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 +263,7 @@ mod tests {
|
||||
|
||||
assert!(links.is_empty());
|
||||
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
assert_eq!(mock.hits(), 1);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,6 +360,8 @@ mod tests {
|
||||
wildcard: true,
|
||||
url: Url::parse("http://localhost").unwrap(),
|
||||
content_length: 100,
|
||||
word_count: 50,
|
||||
line_count: 25,
|
||||
headers: reqwest::header::HeaderMap::new(),
|
||||
status: reqwest::StatusCode::OK,
|
||||
};
|
||||
@@ -380,6 +382,8 @@ mod tests {
|
||||
wildcard: true,
|
||||
url: Url::parse("http://localhost/stuff").unwrap(),
|
||||
content_length: 100,
|
||||
word_count: 50,
|
||||
line_count: 25,
|
||||
headers: reqwest::header::HeaderMap::new(),
|
||||
status: reqwest::StatusCode::OK,
|
||||
};
|
||||
@@ -400,6 +404,8 @@ mod tests {
|
||||
wildcard: false,
|
||||
url: Url::parse("http://localhost/stuff").unwrap(),
|
||||
content_length: 100,
|
||||
word_count: 50,
|
||||
line_count: 25,
|
||||
headers: reqwest::header::HeaderMap::new(),
|
||||
status: reqwest::StatusCode::OK,
|
||||
};
|
||||
|
||||
113
src/lib.rs
113
src/lib.rs
@@ -14,9 +14,13 @@ pub mod utils;
|
||||
|
||||
use crate::utils::{get_url_path_length, status_colorizer};
|
||||
use console::{style, Color};
|
||||
use reqwest::header::{HeaderName, HeaderValue};
|
||||
use reqwest::{header::HeaderMap, Response, StatusCode, Url};
|
||||
use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer};
|
||||
use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::str::FromStr;
|
||||
use std::{error, fmt};
|
||||
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
|
||||
@@ -112,6 +116,12 @@ pub struct FeroxResponse {
|
||||
/// The content-length of this response, if known
|
||||
content_length: u64,
|
||||
|
||||
/// The number of lines contained in the body of this response, if known
|
||||
line_count: usize,
|
||||
|
||||
/// The number of words contained in the body of this response, if known
|
||||
word_count: usize,
|
||||
|
||||
/// The `Headers` of this `FeroxResponse`
|
||||
headers: HeaderMap,
|
||||
|
||||
@@ -194,15 +204,12 @@ impl FeroxResponse {
|
||||
|
||||
/// Returns line count of the response text.
|
||||
pub fn line_count(&self) -> usize {
|
||||
self.text().lines().count()
|
||||
self.line_count
|
||||
}
|
||||
|
||||
/// Returns word count of the response text.
|
||||
pub fn word_count(&self) -> usize {
|
||||
self.text()
|
||||
.lines()
|
||||
.map(|s| s.split_whitespace().count())
|
||||
.sum()
|
||||
self.word_count
|
||||
}
|
||||
|
||||
/// Create a new `FeroxResponse` from the given `Response`
|
||||
@@ -228,18 +235,23 @@ impl FeroxResponse {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let line_count = text.lines().count();
|
||||
let word_count = text.lines().map(|s| s.split_whitespace().count()).sum();
|
||||
|
||||
FeroxResponse {
|
||||
url,
|
||||
status,
|
||||
content_length,
|
||||
text,
|
||||
headers,
|
||||
line_count,
|
||||
word_count,
|
||||
wildcard: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement FeroxSerialize for FeroxResponse
|
||||
/// Implement FeroxSerialusize::from(ize for FeroxRespons)e
|
||||
impl FeroxSerialize for FeroxResponse {
|
||||
/// Simple wrapper around create_report_string
|
||||
fn as_str(&self) -> String {
|
||||
@@ -359,18 +371,99 @@ impl Serialize for FeroxResponse {
|
||||
state.serialize_field("wildcard", &self.wildcard)?;
|
||||
state.serialize_field("status", &self.status.as_u16())?;
|
||||
state.serialize_field("content_length", &self.content_length)?;
|
||||
state.serialize_field("line_count", &self.line_count())?;
|
||||
state.serialize_field("word_count", &self.word_count())?;
|
||||
state.serialize_field("line_count", &self.line_count)?;
|
||||
state.serialize_field("word_count", &self.word_count)?;
|
||||
state.serialize_field("headers", &headers)?;
|
||||
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialize implementation for FeroxResponse
|
||||
impl<'de> Deserialize<'de> for FeroxResponse {
|
||||
/// Deserialize a FeroxResponse from a serde_json::Value
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let mut response = Self {
|
||||
url: Url::parse("http://localhost").unwrap(),
|
||||
status: StatusCode::OK,
|
||||
text: String::new(),
|
||||
content_length: 0,
|
||||
headers: HeaderMap::new(),
|
||||
wildcard: false,
|
||||
line_count: 0,
|
||||
word_count: 0,
|
||||
};
|
||||
|
||||
let map: HashMap<String, Value> = HashMap::deserialize(deserializer)?;
|
||||
|
||||
for (key, value) in &map {
|
||||
match key.as_str() {
|
||||
"url" => {
|
||||
if let Some(url) = value.as_str() {
|
||||
if let Ok(parsed) = Url::parse(url) {
|
||||
response.url = parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
"status" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(smaller) = u16::try_from(num) {
|
||||
if let Ok(status) = StatusCode::from_u16(smaller) {
|
||||
response.status = status;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"content_length" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
response.content_length = num;
|
||||
}
|
||||
}
|
||||
"line_count" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
response.line_count = num.try_into().unwrap_or_default();
|
||||
}
|
||||
}
|
||||
"word_count" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
response.word_count = num.try_into().unwrap_or_default();
|
||||
}
|
||||
}
|
||||
"headers" => {
|
||||
let mut headers = HeaderMap::<HeaderValue>::default();
|
||||
|
||||
if let Some(map_headers) = value.as_object() {
|
||||
for (h_key, h_value) in map_headers {
|
||||
let h_value_str = h_value.as_str().unwrap_or("");
|
||||
let h_name = HeaderName::from_str(h_key)
|
||||
.unwrap_or_else(|_| HeaderName::from_str("Unknown").unwrap());
|
||||
let h_value_parsed = HeaderValue::from_str(h_value_str)
|
||||
.unwrap_or_else(|_| HeaderValue::from_str("Unknown").unwrap());
|
||||
headers.insert(h_name, h_value_parsed);
|
||||
}
|
||||
}
|
||||
|
||||
response.headers = headers;
|
||||
}
|
||||
"wildcard" => {
|
||||
if let Some(result) = value.as_bool() {
|
||||
response.wildcard = result;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
/// Representation of a log entry, can be represented as a human readable string or JSON
|
||||
pub struct FeroxMessage {
|
||||
// todo probably move to lib
|
||||
#[serde(rename = "type")]
|
||||
/// Name of this type of struct, used for serialization, i.e. `{"type":"log"}`
|
||||
kind: String,
|
||||
|
||||
62
src/main.rs
62
src/main.rs
@@ -1,16 +1,18 @@
|
||||
use crossterm::event::{self, Event, KeyCode};
|
||||
use feroxbuster::progress::add_bar;
|
||||
use feroxbuster::{
|
||||
banner,
|
||||
config::{CONFIGURATION, PROGRESS_BAR, PROGRESS_PRINTER},
|
||||
heuristics, logger, reporter,
|
||||
scan_manager::PAUSE_SCAN,
|
||||
scanner::{self, scan_url},
|
||||
scan_manager::{self, PAUSE_SCAN},
|
||||
scanner::{self, scan_url, RESPONSES, SCANNED_URLS},
|
||||
utils::{ferox_print, get_current_depth, module_colorizer, status_colorizer},
|
||||
FeroxError, FeroxResponse, FeroxResult, SLEEP_DURATION, VERSION,
|
||||
FeroxError, FeroxResponse, FeroxResult, FeroxSerialize, SLEEP_DURATION, VERSION,
|
||||
};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use feroxbuster::{utils::set_open_file_limit, DEFAULT_OPEN_FILE_LIMIT};
|
||||
use futures::StreamExt;
|
||||
use std::convert::TryInto;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs::File,
|
||||
@@ -115,6 +117,31 @@ async fn scan(
|
||||
|
||||
scanner::initialize(words.len(), &CONFIGURATION);
|
||||
|
||||
if CONFIGURATION.resumed {
|
||||
if let Ok(scans) = SCANNED_URLS.scans.lock() {
|
||||
for scan in scans.iter() {
|
||||
if let Ok(locked_scan) = scan.lock() {
|
||||
if locked_scan.complete {
|
||||
// these scans are complete, and just need to be shown to the user
|
||||
let pb = add_bar(
|
||||
&locked_scan.url,
|
||||
words.len().try_into().unwrap_or_default(),
|
||||
false,
|
||||
true,
|
||||
);
|
||||
pb.finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(responses) = RESPONSES.responses.read() {
|
||||
for response in responses.iter() {
|
||||
PROGRESS_PRINTER.println(response.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut tasks = vec![];
|
||||
|
||||
for target in targets {
|
||||
@@ -152,6 +179,22 @@ async fn get_targets() -> FeroxResult<Vec<String>> {
|
||||
while let Some(line) = reader.next().await {
|
||||
targets.push(line?);
|
||||
}
|
||||
} else if CONFIGURATION.resumed {
|
||||
// resume-from can't be used with --url, and --stdin is marked false for every resumed
|
||||
// scan, making it mutually exclusive from either of the other two options
|
||||
if let Ok(scans) = SCANNED_URLS.scans.lock() {
|
||||
for scan in scans.iter() {
|
||||
// SCANNED_URLS gets deserialized scans added to it at program start if --resume-from
|
||||
// is used, so scans that aren't marked complete still need to be scanned
|
||||
if let Ok(locked_scan) = scan.lock() {
|
||||
if locked_scan.complete {
|
||||
// this one's already done, ignore it
|
||||
continue;
|
||||
}
|
||||
targets.push(locked_scan.url.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
targets.push(CONFIGURATION.target_url.clone());
|
||||
}
|
||||
@@ -177,6 +220,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");
|
||||
|
||||
@@ -298,6 +349,11 @@ fn main() {
|
||||
// setup logging based on the number of -v's used
|
||||
logger::initialize(CONFIGURATION.verbosity);
|
||||
|
||||
if CONFIGURATION.save_state {
|
||||
// start the ctrl+c handler
|
||||
scan_manager::initialize();
|
||||
}
|
||||
|
||||
// this function uses rlimit, which is not supported on windows
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
set_open_file_limit(DEFAULT_OPEN_FILE_LIMIT);
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
use crate::VERSION;
|
||||
use clap::{App, Arg};
|
||||
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> {
|
||||
App::new("feroxbuster")
|
||||
.version(VERSION)
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.author("Ben 'epi' Risher (@epi052)")
|
||||
.about("A fast, simple, recursive content discovery tool written in Rust")
|
||||
.arg(
|
||||
@@ -19,7 +32,7 @@ pub fn initialize() -> App<'static, 'static> {
|
||||
Arg::with_name("url")
|
||||
.short("u")
|
||||
.long("url")
|
||||
.required_unless("stdin")
|
||||
.required_unless_one(&["stdin", "resume_from"])
|
||||
.value_name("URL")
|
||||
.multiple(true)
|
||||
.use_delimiter(true)
|
||||
@@ -113,6 +126,7 @@ pub fn initialize() -> App<'static, 'static> {
|
||||
Arg::with_name("json")
|
||||
.long("json")
|
||||
.takes_value(false)
|
||||
.requires("output_files")
|
||||
.help("Emit JSON logs to --output and --debug-log instead of normal text")
|
||||
)
|
||||
.arg(
|
||||
@@ -130,6 +144,14 @@ pub fn initialize() -> App<'static, 'static> {
|
||||
.help("Output file to write results to (use w/ --json for JSON entries)")
|
||||
.takes_value(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("resume_from")
|
||||
.long("resume-from")
|
||||
.value_name("STATE_FILE")
|
||||
.help("State file from which to resume a partially complete scan (ex. --resume-from ferox-1606586780.state)")
|
||||
.conflicts_with("url")
|
||||
.takes_value(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("debug_log")
|
||||
.long("debug-log")
|
||||
@@ -294,6 +316,18 @@ 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)
|
||||
)
|
||||
.after_help(r#"NOTE:
|
||||
Options that take multiple values are very flexible. Consider the following ways of specifying
|
||||
extensions:
|
||||
@@ -331,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::*;
|
||||
@@ -341,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,16 @@ use indicatif::{ProgressBar, ProgressStyle};
|
||||
|
||||
/// Add an [indicatif::ProgressBar](https://docs.rs/indicatif/latest/indicatif/struct.ProgressBar.html)
|
||||
/// to the global [PROGRESS_BAR](../config/struct.PROGRESS_BAR.html)
|
||||
pub fn add_bar(prefix: &str, length: u64, hidden: bool) -> ProgressBar {
|
||||
pub fn add_bar(prefix: &str, length: u64, hidden: bool, hide_per_sec: bool) -> ProgressBar {
|
||||
let style = if hidden || CONFIGURATION.quiet {
|
||||
ProgressStyle::default_bar().template("")
|
||||
} else if hide_per_sec {
|
||||
ProgressStyle::default_bar()
|
||||
.template(&format!(
|
||||
"[{{bar:.cyan/blue}}] - {{elapsed:<4}} {{pos:>7}}/{{len:7}} {:7} {{prefix}}",
|
||||
"-"
|
||||
))
|
||||
.progress_chars("#>-")
|
||||
} else {
|
||||
ProgressStyle::default_bar()
|
||||
.template("[{bar:.cyan/blue}] - {elapsed:<4} {pos:>7}/{len:7} {per_sec:7} {prefix}")
|
||||
@@ -20,3 +27,24 @@ pub fn add_bar(prefix: &str, length: u64, hidden: bool) -> ProgressBar {
|
||||
|
||||
progress_bar
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
/// hit all code branches for add_bar
|
||||
fn add_bar_with_all_configurations() {
|
||||
let p1 = add_bar("prefix", 2, true, false); // hidden
|
||||
let p2 = add_bar("prefix", 2, false, true); // no per second field
|
||||
let p3 = add_bar("prefix", 2, false, false); // normal bar
|
||||
|
||||
p1.finish();
|
||||
p2.finish();
|
||||
p3.finish();
|
||||
|
||||
assert!(p1.is_finished());
|
||||
assert!(p2.is_finished());
|
||||
assert!(p3.is_finished());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
config::{CONFIGURATION, PROGRESS_PRINTER},
|
||||
scanner::RESPONSES,
|
||||
utils::{ferox_print, make_request, open_file},
|
||||
FeroxChannel, FeroxResponse, FeroxSerialize,
|
||||
};
|
||||
@@ -93,10 +94,14 @@ 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());
|
||||
|
||||
if CONFIGURATION.status_codes.contains(&resp.status().as_u16()) {
|
||||
let contains_sentry = CONFIGURATION.status_codes.contains(&resp.status().as_u16());
|
||||
let unknown_sentry = !RESPONSES.contains(&resp); // !contains == unknown
|
||||
let should_process_response = contains_sentry && unknown_sentry;
|
||||
|
||||
if should_process_response {
|
||||
// print to stdout
|
||||
ferox_print(&resp.as_str(), &PROGRESS_PRINTER);
|
||||
|
||||
@@ -114,9 +119,7 @@ async fn spawn_terminal_reporter(
|
||||
}
|
||||
log::trace!("report complete: {}", resp.url());
|
||||
|
||||
if CONFIGURATION.replay_client.is_some()
|
||||
&& CONFIGURATION.replay_codes.contains(&resp.status().as_u16())
|
||||
{
|
||||
if CONFIGURATION.replay_client.is_some() && should_process_response {
|
||||
// replay proxy specified/client created and this response's status code is one that
|
||||
// should be replayed
|
||||
match make_request(CONFIGURATION.replay_client.as_ref().unwrap(), &resp.url()).await {
|
||||
@@ -126,6 +129,18 @@ async fn spawn_terminal_reporter(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if should_process_response {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
log::trace!("exit: spawn_terminal_reporter");
|
||||
}
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
use crate::{config::PROGRESS_PRINTER, progress, scanner::NUMBER_OF_REQUESTS, SLEEP_DURATION};
|
||||
use crate::config::Configuration;
|
||||
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,
|
||||
};
|
||||
use console::style;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{
|
||||
ser::{SerializeSeq, SerializeStruct},
|
||||
Deserialize, Deserializer, Serialize, Serializer,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::{
|
||||
cmp::PartialEq,
|
||||
fmt,
|
||||
fs::File,
|
||||
io::BufReader,
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use std::{
|
||||
io::{stderr, Write},
|
||||
@@ -28,12 +46,20 @@ static INTERACTIVE_BARRIER: AtomicUsize = AtomicUsize::new(0);
|
||||
pub static PAUSE_SCAN: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Simple enum used to flag a `FeroxScan` as likely a directory or file
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum ScanType {
|
||||
File,
|
||||
Directory,
|
||||
}
|
||||
|
||||
/// Default implementation for ScanType
|
||||
impl Default for ScanType {
|
||||
/// Return ScanType::File as default
|
||||
fn default() -> Self {
|
||||
Self::File
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct to hold scan-related state
|
||||
///
|
||||
/// The purpose of this container is to open up the pathway to aborting currently running tasks and
|
||||
@@ -59,17 +85,8 @@ pub struct FeroxScan {
|
||||
pub progress_bar: Option<ProgressBar>,
|
||||
}
|
||||
|
||||
/// Implementation of FeroxScan
|
||||
impl FeroxScan {
|
||||
/// Stop a currently running scan
|
||||
pub fn abort(&self) {
|
||||
self.stop_progress_bar();
|
||||
|
||||
if let Some(_task) = &self.task {
|
||||
// task.abort(); todo uncomment once upgraded to tokio 0.3 (issue #107)
|
||||
}
|
||||
}
|
||||
|
||||
/// Default implementation for FeroxScan
|
||||
impl Default for FeroxScan {
|
||||
/// Create a default FeroxScan, populates ID with a new UUID
|
||||
fn default() -> Self {
|
||||
let new_id = Uuid::new_v4().to_simple().to_string();
|
||||
@@ -83,6 +100,18 @@ impl FeroxScan {
|
||||
scan_type: ScanType::File,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation of FeroxScan
|
||||
impl FeroxScan {
|
||||
/// Stop a currently running scan
|
||||
pub fn abort(&self) {
|
||||
self.stop_progress_bar();
|
||||
|
||||
if let Some(_task) = &self.task {
|
||||
// task.abort(); todo uncomment once upgraded to tokio 0.3 (issue #107)
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple helper to call .finish on the scan's progress bar
|
||||
fn stop_progress_bar(&self) {
|
||||
@@ -97,7 +126,7 @@ impl FeroxScan {
|
||||
pb.clone()
|
||||
} else {
|
||||
let num_requests = NUMBER_OF_REQUESTS.load(Ordering::Relaxed);
|
||||
let pb = progress::add_bar(&self.url, num_requests, false);
|
||||
let pb = progress::add_bar(&self.url, num_requests, false, false);
|
||||
|
||||
pb.reset_elapsed();
|
||||
|
||||
@@ -145,6 +174,69 @@ impl PartialEq for FeroxScan {
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize implementation for FeroxScan
|
||||
impl Serialize for FeroxScan {
|
||||
/// Function that handles serialization of a FeroxScan
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut state = serializer.serialize_struct("FeroxScan", 4)?;
|
||||
|
||||
state.serialize_field("id", &self.id)?;
|
||||
state.serialize_field("url", &self.url)?;
|
||||
state.serialize_field("scan_type", &self.scan_type)?;
|
||||
state.serialize_field("complete", &self.complete)?;
|
||||
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialize implementation for FeroxScan
|
||||
impl<'de> Deserialize<'de> for FeroxScan {
|
||||
/// Deserialize a FeroxScan from a serde_json::Value
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let mut scan = Self::default();
|
||||
|
||||
let map: HashMap<String, Value> = HashMap::deserialize(deserializer)?;
|
||||
|
||||
for (key, value) in &map {
|
||||
match key.as_str() {
|
||||
"id" => {
|
||||
if let Some(id) = value.as_str() {
|
||||
scan.id = id.to_string();
|
||||
}
|
||||
}
|
||||
"scan_type" => {
|
||||
if let Some(scan_type) = value.as_str() {
|
||||
scan.scan_type = match scan_type {
|
||||
"File" => ScanType::File,
|
||||
"Directory" => ScanType::Directory,
|
||||
_ => ScanType::File,
|
||||
}
|
||||
}
|
||||
}
|
||||
"complete" => {
|
||||
if let Some(complete) = value.as_bool() {
|
||||
scan.complete = complete;
|
||||
}
|
||||
}
|
||||
"url" => {
|
||||
if let Some(url) = value.as_str() {
|
||||
scan.url = url.to_string();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(scan)
|
||||
}
|
||||
}
|
||||
|
||||
/// Container around a locked hashset of `FeroxScan`s, adds wrappers for insertion and searching
|
||||
#[derive(Debug, Default)]
|
||||
pub struct FeroxScans {
|
||||
@@ -152,6 +244,31 @@ pub struct FeroxScans {
|
||||
pub scans: Mutex<Vec<Arc<Mutex<FeroxScan>>>>,
|
||||
}
|
||||
|
||||
/// Serialize implementation for FeroxScans
|
||||
impl Serialize for FeroxScans {
|
||||
/// Function that handles serialization of FeroxScans
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
if let Ok(scans) = self.scans.lock() {
|
||||
let mut seq = serializer.serialize_seq(Some(scans.len()))?;
|
||||
|
||||
for scan in scans.iter() {
|
||||
if let Ok(unlocked) = scan.lock() {
|
||||
seq.serialize_element(&*unlocked)?;
|
||||
}
|
||||
}
|
||||
|
||||
seq.end()
|
||||
} else {
|
||||
// if for some reason we can't unlock the mutex, just write an empty list
|
||||
let seq = serializer.serialize_seq(Some(0))?;
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation of `FeroxScans`
|
||||
impl FeroxScans {
|
||||
/// Add a `FeroxScan` to the internal container
|
||||
@@ -324,8 +441,12 @@ impl FeroxScans {
|
||||
fn add_scan(&self, url: &str, scan_type: ScanType) -> (bool, Arc<Mutex<FeroxScan>>) {
|
||||
let bar = match scan_type {
|
||||
ScanType::Directory => {
|
||||
let progress_bar =
|
||||
progress::add_bar(&url, NUMBER_OF_REQUESTS.load(Ordering::Relaxed), false);
|
||||
let progress_bar = progress::add_bar(
|
||||
&url,
|
||||
NUMBER_OF_REQUESTS.load(Ordering::Relaxed),
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
progress_bar.reset_elapsed();
|
||||
|
||||
@@ -382,9 +503,263 @@ fn get_single_spinner() -> ProgressBar {
|
||||
spinner
|
||||
}
|
||||
|
||||
/// Container around a locked vector of `FeroxResponse`s, adds wrappers for insertion and search
|
||||
#[derive(Debug, Default)]
|
||||
pub struct FeroxResponses {
|
||||
/// Internal structure: locked hashset of `FeroxScan`s
|
||||
pub responses: Arc<RwLock<Vec<FeroxResponse>>>,
|
||||
}
|
||||
|
||||
/// Serialize implementation for FeroxResponses
|
||||
impl Serialize for FeroxResponses {
|
||||
/// Function that handles serialization of FeroxResponses
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
if let Ok(responses) = self.responses.read() {
|
||||
let mut seq = serializer.serialize_seq(Some(responses.len()))?;
|
||||
|
||||
for response in responses.iter() {
|
||||
seq.serialize_element(response)?;
|
||||
}
|
||||
|
||||
seq.end()
|
||||
} else {
|
||||
// if for some reason we can't unlock the mutex, just write an empty list
|
||||
let seq = serializer.serialize_seq(Some(0))?;
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation of `FeroxResponses`
|
||||
impl FeroxResponses {
|
||||
/// Add a `FeroxResponse` to the internal container
|
||||
pub fn insert(&self, response: FeroxResponse) {
|
||||
match self.responses.write() {
|
||||
Ok(mut responses) => {
|
||||
responses.push(response);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("FeroxResponses' container's mutex is poisoned: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple check for whether or not a FeroxResponse is contained within the inner container
|
||||
pub fn contains(&self, other: &FeroxResponse) -> bool {
|
||||
match self.responses.read() {
|
||||
Ok(responses) => {
|
||||
for response in responses.iter() {
|
||||
if response.url == other.url {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("FeroxResponses' container's mutex is poisoned: {}", e);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
/// Data container for (de)?serialization of multiple items
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct FeroxState {
|
||||
/// Known scans
|
||||
scans: &'static FeroxScans,
|
||||
|
||||
/// Current running config
|
||||
config: &'static Configuration,
|
||||
|
||||
/// Known responses
|
||||
responses: &'static FeroxResponses,
|
||||
}
|
||||
|
||||
/// FeroxSerialize implementation for FeroxState
|
||||
impl FeroxSerialize for FeroxState {
|
||||
/// Simply return debug format of FeroxState to satisfy as_str
|
||||
fn as_str(&self) -> String {
|
||||
format!("{:?}", self)
|
||||
}
|
||||
|
||||
/// Simple call to produce a JSON string using the given FeroxState
|
||||
fn as_json(&self) -> String {
|
||||
serde_json::to_string(&self).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(sigint_handler);
|
||||
|
||||
if result.is_err() {
|
||||
log::error!("Could not set Ctrl+c handler");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
log::trace!("exit: initialize");
|
||||
}
|
||||
|
||||
/// Primary logic used to load a Configuration from disk and populate the appropriate data
|
||||
/// structures
|
||||
pub fn resume_scan(filename: &str) -> Configuration {
|
||||
log::trace!("enter: resume_scan({})", filename);
|
||||
|
||||
let file = File::open(filename).unwrap_or_else(|e| {
|
||||
log::error!("{}", e);
|
||||
log::error!("Could not open state file, exiting");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
let reader = BufReader::new(file);
|
||||
let state: serde_json::Value = serde_json::from_reader(reader).unwrap();
|
||||
|
||||
let conf = state.get("config").unwrap_or_else(|| {
|
||||
log::error!("Could not load configuration from state file, exiting");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
let config = serde_json::from_value(conf.clone()).unwrap_or_else(|e| {
|
||||
log::error!("{}", e);
|
||||
log::error!("Could not deserialize configuration found in state file, exiting");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
if let Some(responses) = state.get("responses") {
|
||||
if let Some(arr_responses) = responses.as_array() {
|
||||
for response in arr_responses {
|
||||
if let Ok(deser_resp) = serde_json::from_value(response.clone()) {
|
||||
RESPONSES.insert(deser_resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(scans) = state.get("scans") {
|
||||
if let Some(arr_scans) = scans.as_array() {
|
||||
for scan in arr_scans {
|
||||
let deser_scan: FeroxScan =
|
||||
serde_json::from_value(scan.clone()).unwrap_or_default();
|
||||
// need to determine if it's complete and based on that create a progress bar
|
||||
// populate it accordingly based on completion
|
||||
SCANNED_URLS.insert(Arc::new(Mutex::new(deser_scan)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::trace!("exit: resume_scan -> {:?}", config);
|
||||
config
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::VERSION;
|
||||
use predicates::prelude::*;
|
||||
|
||||
#[test]
|
||||
/// test that ScanType's default is File
|
||||
fn default_scantype_is_file() {
|
||||
match ScanType::default() {
|
||||
ScanType::File => {}
|
||||
ScanType::Directory => panic!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test that get_single_spinner returns the correct spinner
|
||||
@@ -528,4 +903,168 @@ mod tests {
|
||||
assert!(scan.progress_bar.is_some()); // new pb created
|
||||
assert!(!pb.is_finished()) // not finished
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// given a JSON entry representing a FeroxScan, test that it deserializes into the proper type
|
||||
/// with the right attributes
|
||||
fn ferox_scan_deserialize() {
|
||||
let fs_json = r#"{"id":"057016a14769414aac9a7a62707598cb","url":"https://spiritanimal.com","scan_type":"Directory","complete":true}"#;
|
||||
let fs_json_two = r#"{"id":"057016a14769414aac9a7a62707598cb","url":"https://spiritanimal.com","scan_type":"Not Correct","complete":true}"#;
|
||||
|
||||
let fs: FeroxScan = serde_json::from_str(fs_json).unwrap();
|
||||
let fs_two: FeroxScan = serde_json::from_str(fs_json_two).unwrap();
|
||||
assert_eq!(fs.url, "https://spiritanimal.com");
|
||||
|
||||
match fs.scan_type {
|
||||
ScanType::Directory => {}
|
||||
ScanType::File => {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
match fs_two.scan_type {
|
||||
ScanType::Directory => {
|
||||
panic!();
|
||||
}
|
||||
ScanType::File => {}
|
||||
}
|
||||
|
||||
match fs.progress_bar {
|
||||
None => {}
|
||||
Some(_) => {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
assert_eq!(fs.complete, true);
|
||||
assert_eq!(fs.id, "057016a14769414aac9a7a62707598cb");
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// given a FeroxScan, test that it serializes into the proper JSON entry
|
||||
fn ferox_scan_serialize() {
|
||||
let fs = FeroxScan::new("https://spiritanimal.com", ScanType::Directory, None);
|
||||
let fs_json = format!(
|
||||
r#"{{"id":"{}","url":"https://spiritanimal.com","scan_type":"Directory","complete":false}}"#,
|
||||
fs.lock().unwrap().id
|
||||
);
|
||||
assert_eq!(
|
||||
fs_json,
|
||||
serde_json::to_string(&*fs.lock().unwrap()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// given a FeroxScans, test that it serializes into the proper JSON entry
|
||||
fn ferox_scans_serialize() {
|
||||
let ferox_scan = FeroxScan::new("https://spiritanimal.com", ScanType::Directory, None);
|
||||
let ferox_scans = FeroxScans::default();
|
||||
let ferox_scans_json = format!(
|
||||
r#"[{{"id":"{}","url":"https://spiritanimal.com","scan_type":"Directory","complete":false}}]"#,
|
||||
ferox_scan.lock().unwrap().id
|
||||
);
|
||||
ferox_scans.scans.lock().unwrap().push(ferox_scan);
|
||||
assert_eq!(
|
||||
ferox_scans_json,
|
||||
serde_json::to_string(&ferox_scans).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// given a FeroxResponses, test that it serializes into the proper JSON entry
|
||||
fn ferox_responses_serialize() {
|
||||
let json_response = r#"{"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"}}"#;
|
||||
let response: FeroxResponse = serde_json::from_str(json_response).unwrap();
|
||||
|
||||
let responses = FeroxResponses::default();
|
||||
responses.insert(response);
|
||||
// responses has a response now
|
||||
|
||||
// serialized should be a list of responses
|
||||
let expected = format!("[{}]", json_response);
|
||||
|
||||
let serialized = serde_json::to_string(&responses).unwrap();
|
||||
assert_eq!(expected, serialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// given a FeroxResponse, test that it serializes into the proper JSON entry
|
||||
fn ferox_response_serialize_and_deserialize() {
|
||||
// deserialize
|
||||
let json_response = r#"{"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"}}"#;
|
||||
let response: FeroxResponse = serde_json::from_str(json_response).unwrap();
|
||||
|
||||
assert_eq!(response.url.as_str(), "https://nerdcore.com/css");
|
||||
assert_eq!(response.url.path(), "/css");
|
||||
assert_eq!(response.wildcard, true);
|
||||
assert_eq!(response.status.as_u16(), 301);
|
||||
assert_eq!(response.content_length, 173);
|
||||
assert_eq!(response.line_count, 10);
|
||||
assert_eq!(response.word_count, 16);
|
||||
assert_eq!(response.headers.get("server").unwrap(), "nginx/1.16.1");
|
||||
|
||||
// serialize, however, this can fail when headers are out of order
|
||||
let new_json = serde_json::to_string(&response).unwrap();
|
||||
assert_eq!(json_response, new_json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test FeroxSerialize implementation of FeroxState
|
||||
fn feroxstates_feroxserialize_implementation() {
|
||||
let ferox_scan = FeroxScan::new("https://spiritanimal.com", ScanType::Directory, None);
|
||||
let saved_id = ferox_scan.lock().unwrap().id.clone();
|
||||
SCANNED_URLS.insert(ferox_scan);
|
||||
|
||||
let json_response = r#"{"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"}}"#;
|
||||
let response: FeroxResponse = serde_json::from_str(json_response).unwrap();
|
||||
RESPONSES.insert(response);
|
||||
|
||||
let ferox_state = FeroxState {
|
||||
scans: &SCANNED_URLS,
|
||||
responses: &RESPONSES,
|
||||
config: &CONFIGURATION,
|
||||
};
|
||||
|
||||
let expected_strs = predicates::str::contains("scans: FeroxScans").and(
|
||||
predicate::str::contains("config: Configuration")
|
||||
.and(predicate::str::contains("responses: FeroxResponses"))
|
||||
.and(predicate::str::contains("nerdcore.com"))
|
||||
.and(predicate::str::contains("/css"))
|
||||
.and(predicate::str::contains("https://spiritanimal.com")),
|
||||
);
|
||||
|
||||
assert!(expected_strs.eval(&ferox_state.as_str()));
|
||||
|
||||
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":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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
WordsFilter,
|
||||
},
|
||||
heuristics,
|
||||
scan_manager::{FeroxScans, PAUSE_SCAN},
|
||||
scan_manager::{FeroxResponses, FeroxScans, PAUSE_SCAN},
|
||||
utils::{format_url, get_current_depth, make_request},
|
||||
FeroxChannel, FeroxResponse,
|
||||
};
|
||||
@@ -47,6 +47,9 @@ lazy_static! {
|
||||
/// Vector of implementors of the FeroxFilter trait
|
||||
static ref FILTERS: Arc<RwLock<Vec<Box<dyn FeroxFilter>>>> = Arc::new(RwLock::new(Vec::<Box<dyn FeroxFilter>>::new()));
|
||||
|
||||
/// Vector of FeroxResponse objects
|
||||
pub static ref RESPONSES: FeroxResponses = FeroxResponses::default();
|
||||
|
||||
/// Bounded semaphore used as a barrier to limit concurrent scans
|
||||
static ref SCAN_LIMITER: Semaphore = Semaphore::new(CONFIGURATION.scan_limit);
|
||||
}
|
||||
|
||||
102774
tests/extra-words
Normal file
102774
tests/extra-words
Normal file
File diff suppressed because it is too large
Load Diff
@@ -711,6 +711,8 @@ fn banner_prints_json() {
|
||||
.arg("--url")
|
||||
.arg("http://localhost")
|
||||
.arg("--json")
|
||||
.arg("--output")
|
||||
.arg("/dev/null")
|
||||
.assert()
|
||||
.success()
|
||||
.stderr(
|
||||
@@ -781,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()
|
||||
@@ -173,10 +168,10 @@ fn extractor_finds_same_relative_url_twice() {
|
||||
)),
|
||||
);
|
||||
|
||||
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); // todo: sometimes this is 2 instead of 1
|
||||
// the expectation is one, suggesting a race condition... investigate and fix
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
}
|
||||
|
||||
@@ -188,19 +183,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 +215,8 @@ 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(())
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
|
||||
130
tests/test_scan_manager.rs
Normal file
130
tests/test_scan_manager.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
mod utils;
|
||||
use assert_cmd::Command;
|
||||
use httpmock::Method::GET;
|
||||
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]
|
||||
/// pass a known serialized scan with 1 scan complete and 1 not. expect the incomplete scan to
|
||||
/// start and the complete to not start. expect the responses, scans, and configuration structures
|
||||
/// to be populated based off the contents of the given state file
|
||||
fn resume_scan_works() {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) =
|
||||
setup_tmp_directory(&["css".to_string(), "stuff".to_string()], "wordlist").unwrap();
|
||||
|
||||
// localhost:PORT/ <- complete
|
||||
// localhost:PORT/js <- will get scanned with /css and /stuff
|
||||
let complete_scan = format!(
|
||||
r#"{{"id":"057016a14769414aac9a7a62707598cb","url":"{}","scan_type":"Directory","complete":true}}"#,
|
||||
srv.url("/")
|
||||
);
|
||||
let incomplete_scan = format!(
|
||||
r#"{{"id":"400b2323a16f43468a04ffcbbeba34c6","url":"{}","scan_type":"Directory","complete":false}}"#,
|
||||
srv.url("/js")
|
||||
);
|
||||
let scans = format!(r#""scans":[{},{}]"#, complete_scan, incomplete_scan);
|
||||
|
||||
let config = format!(
|
||||
r#""config": {{"type":"configuration","wordlist":"{}","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/1.9.0","redirects":false,"insecure":false,"extensions":[],"headers":{{}},"queries":[],"no_recursion":false,"extract_links":false,"add_slash":false,"stdin":false,"depth":2,"scan_limit":1,"filter_size":[],"filter_line_count":[],"filter_word_count":[],"filter_regex":[],"dont_filter":false}}"#,
|
||||
file.to_string_lossy(),
|
||||
srv.url("/")
|
||||
);
|
||||
|
||||
// // localhost:PORT/js/css has already been seen, expect not to be scanned
|
||||
let response = format!(
|
||||
r#"{{"type":"response","url":"{}","path":"/js/css","wildcard":true,"status":301,"content_length":173,"line_count":10,"word_count":16,"headers":{{"server":"nginx/1.16.1"}}}}"#,
|
||||
srv.url("/js/css")
|
||||
);
|
||||
let responses = format!(r#""responses":[{}]"#, response);
|
||||
|
||||
// not scanned because /js is not complete, and /js/stuff response is not known
|
||||
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 = srv.mock(|when, then| {
|
||||
when.method(GET).path("/js/css");
|
||||
then.status(200);
|
||||
});
|
||||
|
||||
// already scanned because scan on / is complete
|
||||
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();
|
||||
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--resume-from")
|
||||
.arg(state_file.as_os_str())
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(
|
||||
predicate::str::contains("/js/stuff")
|
||||
.and(predicate::str::contains("22c"))
|
||||
.and(predicate::str::contains("5w"))
|
||||
.and(predicate::str::contains("/js/css"))
|
||||
.not()
|
||||
.and(predicate::str::contains("2w"))
|
||||
.not()
|
||||
.and(predicate::str::contains("9c"))
|
||||
.not(),
|
||||
);
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
teardown_tmp_directory(tmp_dir2);
|
||||
|
||||
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,52 @@ 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(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// send a single valid request, expect a 200 response that then gets routed to the replay
|
||||
/// proxy
|
||||
fn scanner_single_request_replayed_to_proxy() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let srv = MockServer::start();
|
||||
let proxy = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||
|
||||
let mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/LICENSE");
|
||||
then.status(200).body("this is a test");
|
||||
});
|
||||
|
||||
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()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--replay-proxy")
|
||||
.arg(format!("http://{}", proxy.address().to_string()))
|
||||
.arg("--replay-codes")
|
||||
.arg("200")
|
||||
.unwrap();
|
||||
|
||||
cmd.assert()
|
||||
.success()
|
||||
.stdout(
|
||||
predicate::str::contains("/LICENSE")
|
||||
.and(predicate::str::contains("200"))
|
||||
.and(predicate::str::contains("14c")),
|
||||
)
|
||||
.stderr(predicate::str::contains("Replay Proxy Codes"));
|
||||
|
||||
assert_eq!(mock.hits(), 1);
|
||||
assert_eq!(mock_two.hits(), 1);
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
Ok(())
|
||||
}
|
||||
@@ -371,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()
|
||||
@@ -406,57 +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);
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// send a single valid request, expect a 200 response that then gets routed to the replay
|
||||
/// proxy
|
||||
fn scanner_single_request_replayed_to_proxy() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let srv = MockServer::start();
|
||||
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_two = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/LICENSE")
|
||||
.return_status(200)
|
||||
.return_body("this is a test")
|
||||
.create_on(&proxy);
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--replay-proxy")
|
||||
.arg(format!("http://{}", proxy.address().to_string()))
|
||||
.arg("--replay-codes")
|
||||
.arg("200")
|
||||
.unwrap();
|
||||
|
||||
cmd.assert()
|
||||
.success()
|
||||
.stdout(
|
||||
predicate::str::contains("/LICENSE")
|
||||
.and(predicate::str::contains("200"))
|
||||
.and(predicate::str::contains("14")),
|
||||
)
|
||||
.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!(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