mirror of
https://github.com/epi052/feroxbuster.git
synced 2026-06-02 05:41:13 -03:00
Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c39944557 | ||
|
|
2be2da470f | ||
|
|
5d74b2bb2d | ||
|
|
9233bfc548 | ||
|
|
287120832d | ||
|
|
dc02f3bb9a | ||
|
|
2cb05ba17f | ||
|
|
6bb263462b | ||
|
|
563da57545 | ||
|
|
d43142575f | ||
|
|
f6d5739eea | ||
|
|
d10c7f0937 | ||
|
|
dc4cf6e5bf | ||
|
|
7e229a047f | ||
|
|
5845e7f286 | ||
|
|
3881789879 | ||
|
|
df19c63901 | ||
|
|
582ce9ed8d | ||
|
|
697a1cf715 | ||
|
|
8eec5ce1d9 | ||
|
|
c08180872e | ||
|
|
f8b18576aa | ||
|
|
46a471c8a7 | ||
|
|
1b1190582a | ||
|
|
addf867f59 | ||
|
|
4ef95ec246 | ||
|
|
b48445f714 | ||
|
|
dc10a56c79 | ||
|
|
b1b9ea71de | ||
|
|
3c41573db2 | ||
|
|
9929104adc | ||
|
|
eca26b73c5 | ||
|
|
5464ae4ddd | ||
|
|
1c9a42c9ea | ||
|
|
805f02ad2d | ||
|
|
880e884dea | ||
|
|
fd4a8d87a6 | ||
|
|
922014cb9b | ||
|
|
db88e168b2 | ||
|
|
85cba02b81 | ||
|
|
a93fe91459 | ||
|
|
4b811a42b9 | ||
|
|
678d371ca4 | ||
|
|
4f31ed1847 | ||
|
|
a7185f4262 | ||
|
|
a78f6b714d | ||
|
|
f9fe4d9874 | ||
|
|
0d365c034b | ||
|
|
49ee66f766 | ||
|
|
771a9556f1 | ||
|
|
48e53be244 | ||
|
|
57be47d30d | ||
|
|
dddbf916fa | ||
|
|
1267358017 | ||
|
|
46ff0120bc | ||
|
|
0333e48c65 | ||
|
|
23279eb1ed | ||
|
|
88260e0b04 | ||
|
|
e6f7a00ba0 | ||
|
|
2b7392735a | ||
|
|
d42806729d | ||
|
|
21f7a0715e | ||
|
|
0b36011ff5 | ||
|
|
22e936232d | ||
|
|
39040b2edf | ||
|
|
02de644f8c | ||
|
|
d71b77cb75 | ||
|
|
0dcdc2a496 | ||
|
|
2fff6bda4e | ||
|
|
d3e807c92f | ||
|
|
c3968e241f | ||
|
|
3cf056dac7 | ||
|
|
b00a47e5e5 | ||
|
|
171238b71d | ||
|
|
d0a6c61de2 | ||
|
|
729140bece | ||
|
|
416f34861b | ||
|
|
9f52731582 | ||
|
|
20938dd544 | ||
|
|
d63d7dc078 | ||
|
|
5e7be449d0 | ||
|
|
a2e13ea71a | ||
|
|
169d6c16fd | ||
|
|
c8775e3c8c | ||
|
|
427efdef3b | ||
|
|
45815ff796 | ||
|
|
0dbc3bee23 | ||
|
|
dd4f3e0aac | ||
|
|
260943f153 | ||
|
|
79d81da0f3 | ||
|
|
088b44bc72 | ||
|
|
6784e9428a |
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -11,7 +11,7 @@ Long form explanations of most of the items below can be found in the [CONTRIBUT
|
|||||||
|
|
||||||
## Static analysis checks
|
## Static analysis checks
|
||||||
- [ ] All rust files are formatted using `cargo fmt`
|
- [ ] All rust files are formatted using `cargo fmt`
|
||||||
- [ ] All `clippy` checks pass when running `cargo clippy --all-targets --all-features -- -D warnings -A clippy::unnecessary_unwrap`
|
- [ ] All `clippy` checks pass when running `cargo clippy --all-targets --all-features -- -D warnings -A clippy::deref_addrof`
|
||||||
- [ ] All existing tests pass
|
- [ ] All existing tests pass
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|||||||
4
.github/workflows/check.yml
vendored
4
.github/workflows/check.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: CI Pipeline
|
name: CI Pipeline
|
||||||
|
|
||||||
on: [push]
|
on: [push, pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
@@ -61,4 +61,4 @@ jobs:
|
|||||||
- uses: actions-rs/cargo@v1
|
- uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: clippy
|
command: clippy
|
||||||
args: --all-targets --all-features -- -D warnings -A clippy::unnecessary_unwrap
|
args: --all-targets --all-features -- -D warnings -A clippy::unnecessary_unwrap -A clippy::deref_addrof
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "feroxbuster"
|
name = "feroxbuster"
|
||||||
version = "1.5.1"
|
version = "1.7.0"
|
||||||
authors = ["Ben 'epi' Risher <epibar052@gmail.com>"]
|
authors = ["Ben 'epi' Risher <epibar052@gmail.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
@@ -33,6 +33,7 @@ openssl = { version = "0.10", features = ["vendored"] }
|
|||||||
dirs = "3.0"
|
dirs = "3.0"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
crossterm = "0.18"
|
crossterm = "0.18"
|
||||||
|
rlimit = "0.5"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.1"
|
tempfile = "3.1"
|
||||||
|
|||||||
63
README.md
63
README.md
@@ -61,6 +61,7 @@ This attack is also known as Predictable Resource Location, File Enumeration, Di
|
|||||||
-----------------
|
-----------------
|
||||||
- [Installation](#-installation)
|
- [Installation](#-installation)
|
||||||
- [Download a Release](#download-a-release)
|
- [Download a Release](#download-a-release)
|
||||||
|
- [Snap Install](#snap-install)
|
||||||
- [Homebrew on MacOS and Linux](#homebrew-on-macos-and-linux)
|
- [Homebrew on MacOS and Linux](#homebrew-on-macos-and-linux)
|
||||||
- [Cargo Install](#cargo-install)
|
- [Cargo Install](#cargo-install)
|
||||||
- [apt Install](#apt-install)
|
- [apt Install](#apt-install)
|
||||||
@@ -68,6 +69,7 @@ This attack is also known as Predictable Resource Location, File Enumeration, Di
|
|||||||
- [Docker Install](#docker-install)
|
- [Docker Install](#docker-install)
|
||||||
- [Configuration](#%EF%B8%8F-configuration)
|
- [Configuration](#%EF%B8%8F-configuration)
|
||||||
- [Default Values](#default-values)
|
- [Default Values](#default-values)
|
||||||
|
- [Threads and Connection Limits At A High-Level](#threads-and-connection-limits-at-a-high-level)
|
||||||
- [ferox-config.toml](#ferox-configtoml)
|
- [ferox-config.toml](#ferox-configtoml)
|
||||||
- [Command Line Parsing](#command-line-parsing)
|
- [Command Line Parsing](#command-line-parsing)
|
||||||
- [Example Usage](#-example-usage)
|
- [Example Usage](#-example-usage)
|
||||||
@@ -115,9 +117,34 @@ Expand-Archive .\feroxbuster.zip
|
|||||||
.\feroxbuster\feroxbuster.exe -V
|
.\feroxbuster\feroxbuster.exe -V
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Snap Install
|
||||||
|
|
||||||
|
Install using `snap`
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo snap install feroxbuster
|
||||||
|
```
|
||||||
|
|
||||||
|
The only gotcha here is that the snap package can only read wordlists from a few specific locations. There are a few
|
||||||
|
possible solutions, of which two are shown below.
|
||||||
|
|
||||||
|
If the wordlist is on the same partition as your home directory, it can be hard-linked into `~/snap/feroxbuster/common`
|
||||||
|
|
||||||
|
```
|
||||||
|
ln /path/to/the/wordlist ~/snap/feroxbuster/common
|
||||||
|
./feroxbuster -u http://localhost -w ~/snap/feroxbuster/common/wordlist
|
||||||
|
```
|
||||||
|
|
||||||
|
If the wordlist is on a separate partition, hard-linking won't work. You'll need to copy it into the snap directory.
|
||||||
|
|
||||||
|
```
|
||||||
|
cp /path/to/the/wordlist ~/snap/feroxbuster/common
|
||||||
|
./feroxbuster -u http://localhost -w ~/snap/feroxbuster/common/wordlist
|
||||||
|
```
|
||||||
|
|
||||||
### Homebrew on MacOS and Linux
|
### Homebrew on MacOS and Linux
|
||||||
|
|
||||||
Installable by Homebrew throughout own formulas:
|
Install using Homebrew via tap
|
||||||
|
|
||||||
🍏 [MacOS](https://github.com/TGotwig/homebrew-feroxbuster/blob/main/feroxbuster.rb)
|
🍏 [MacOS](https://github.com/TGotwig/homebrew-feroxbuster/blob/main/feroxbuster.rb)
|
||||||
|
|
||||||
@@ -230,6 +257,23 @@ Configuration begins with with the following built-in default values baked into
|
|||||||
- auto-filter wildcards - `true`
|
- auto-filter wildcards - `true`
|
||||||
- output: `stdout`
|
- output: `stdout`
|
||||||
|
|
||||||
|
### Threads and Connection Limits At A High-Level
|
||||||
|
|
||||||
|
This section explains how the `-t` and `-L` options work together to determine the overall aggressiveness of a scan. The combination of the two values set by these options determines how hard your target will get hit and to some extent also determines how many resources will be consumed on your local machine.
|
||||||
|
|
||||||
|
#### A Note on Green Threads
|
||||||
|
|
||||||
|
`feroxbuster` uses so-called [green threads](https://en.wikipedia.org/wiki/Green_threads) as opposed to traditional kernel/OS threads. This means (at a high-level) that the threads are implemented entirely in userspace, within a single running process. As a result, a scan with 30 green threads will appear to the OS to be a single process with no additional light-weight processes associated with it as far as the kernel is concerned. As such, there will not be any impact to process (`nproc`) limits when specifying larger values for `-t`. However, these threads will still consume file descriptors, so you will need to ensure that you have a suitable `nlimit` set when scaling up the amount of threads. More detailed documentation on setting appropriate `nlimit` values can be found in the [No File Descriptors Available](#no-file-descriptors-available) section of the FAQ
|
||||||
|
|
||||||
|
#### Threads and Connection Limits: The Implementation
|
||||||
|
|
||||||
|
* Threads: The `-t` option specifies the maximum amount of active threads *per-directory* during a scan
|
||||||
|
* Connection Limits: The `-L` option specifies the maximum amount of active connections per thread
|
||||||
|
|
||||||
|
#### Threads and Connection Limits: Examples
|
||||||
|
|
||||||
|
To truly have only 30 active requests to a site at any given time, `-t 30 -L 1` is necessary. Using `-t 30 -L 2` will result in a maximum of 60 total requests being processed at any given time for that site. And so on. For a conversation on this, please see [Issue #126](https://github.com/epi052/feroxbuster/issues/126) which may provide more (or less) clarity :wink:
|
||||||
|
|
||||||
### ferox-config.toml
|
### ferox-config.toml
|
||||||
After setting built-in default values, any values defined in a `ferox-config.toml` config file will override the
|
After setting built-in default values, any values defined in a `ferox-config.toml` config file will override the
|
||||||
built-in defaults.
|
built-in defaults.
|
||||||
@@ -277,15 +321,17 @@ A pre-made configuration file with examples of all available settings can be fou
|
|||||||
# wordlist = "/wordlists/jhaddix/all.txt"
|
# wordlist = "/wordlists/jhaddix/all.txt"
|
||||||
# status_codes = [200, 500]
|
# status_codes = [200, 500]
|
||||||
# filter_status = [301]
|
# filter_status = [301]
|
||||||
# replay_codes = [301]
|
|
||||||
# threads = 1
|
# threads = 1
|
||||||
# timeout = 5
|
# timeout = 5
|
||||||
# proxy = "http://127.0.0.1:8080"
|
# proxy = "http://127.0.0.1:8080"
|
||||||
# replay_proxy = "http://127.0.0.1:8081"
|
# replay_proxy = "http://127.0.0.1:8081"
|
||||||
|
# replay_codes = [200, 302]
|
||||||
# verbosity = 1
|
# verbosity = 1
|
||||||
# scan_limit = 6
|
# scan_limit = 6
|
||||||
# quiet = true
|
# quiet = true
|
||||||
|
# json = true
|
||||||
# output = "/targets/ellingson_mineral_company/gibson.txt"
|
# output = "/targets/ellingson_mineral_company/gibson.txt"
|
||||||
|
# debug_log = "/var/log/find-the-derp.log"
|
||||||
# user_agent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0"
|
# user_agent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0"
|
||||||
# redirects = true
|
# redirects = true
|
||||||
# insecure = true
|
# insecure = true
|
||||||
@@ -297,6 +343,8 @@ A pre-made configuration file with examples of all available settings can be fou
|
|||||||
# extract_links = true
|
# extract_links = true
|
||||||
# depth = 1
|
# depth = 1
|
||||||
# filter_size = [5174]
|
# filter_size = [5174]
|
||||||
|
# filter_word_count = [993]
|
||||||
|
# filter_line_count = [35, 36]
|
||||||
# queries = [["name","value"], ["rick", "astley"]]
|
# queries = [["name","value"], ["rick", "astley"]]
|
||||||
|
|
||||||
# headers can be specified on multiple lines or as an inline table
|
# headers can be specified on multiple lines or as an inline table
|
||||||
@@ -327,20 +375,25 @@ FLAGS:
|
|||||||
findings (default: false)
|
findings (default: false)
|
||||||
-h, --help Prints help information
|
-h, --help Prints help information
|
||||||
-k, --insecure Disables TLS certificate validation
|
-k, --insecure Disables TLS certificate validation
|
||||||
|
--json Emit JSON logs to --output and --debug-log instead of normal text
|
||||||
-n, --no-recursion Do not scan recursively
|
-n, --no-recursion Do not scan recursively
|
||||||
-q, --quiet Only print URLs; Don't print status codes, response size, running config, etc...
|
-q, --quiet Only print URLs; Don't print status codes, response size, running config, etc...
|
||||||
-r, --redirects Follow redirects
|
-r, --redirects Follow redirects
|
||||||
--stdin Read url(s) from STDIN
|
--stdin Read url(s) from STDIN
|
||||||
-V, --version Prints version information
|
-V, --version Prints version information
|
||||||
-v, --verbosity Increase verbosity level (use -vv or more for greater effect)
|
-v, --verbosity Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v's is probably
|
||||||
|
too much)
|
||||||
|
|
||||||
OPTIONS:
|
OPTIONS:
|
||||||
|
--debug-log <FILE> Output file to write log entries (use w/ --json for JSON entries)
|
||||||
-d, --depth <RECURSION_DEPTH> Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)
|
-d, --depth <RECURSION_DEPTH> Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)
|
||||||
-x, --extensions <FILE_EXTENSION>... File extension(s) to search for (ex: -x php -x pdf js)
|
-x, --extensions <FILE_EXTENSION>... File extension(s) to search for (ex: -x php -x pdf js)
|
||||||
|
-N, --filter-lines <LINES>... Filter out messages of a particular line count (ex: -N 20 -N 31,30)
|
||||||
-S, --filter-size <SIZE>... Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)
|
-S, --filter-size <SIZE>... Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)
|
||||||
-C, --filter-status <STATUS_CODE>... Filter out status codes (deny list) (ex: -C 200 -C 401)
|
-C, --filter-status <STATUS_CODE>... Filter out status codes (deny list) (ex: -C 200 -C 401)
|
||||||
|
-W, --filter-words <WORDS>... Filter out messages of a particular word count (ex: -W 312 -W 91,82)
|
||||||
-H, --headers <HEADER>... Specify HTTP headers (ex: -H Header:val 'stuff: things')
|
-H, --headers <HEADER>... Specify HTTP headers (ex: -H Header:val 'stuff: things')
|
||||||
-o, --output <FILE> Output file to write results to (default: stdout)
|
-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)
|
-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)
|
-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
|
-R, --replay-codes <REPLAY_CODE>... Status Codes to send through a Replay Proxy when found (default: --status
|
||||||
@@ -511,7 +564,7 @@ a few of the use-cases in which feroxbuster may be a better fit:
|
|||||||
| configuration file for default value override | ✔ | | ✔ |
|
| configuration file for default value override | ✔ | | ✔ |
|
||||||
| can accept urls via STDIN as part of a pipeline | ✔ | | ✔ |
|
| can accept urls via STDIN as part of a pipeline | ✔ | | ✔ |
|
||||||
| can accept wordlists via STDIN | | ✔ | ✔ |
|
| can accept wordlists via STDIN | | ✔ | ✔ |
|
||||||
| filter by response size | ✔ | | ✔ |
|
| filter based on response size, wordcount, and linecount | ✔ | | ✔ |
|
||||||
| auto-filter wildcard responses | ✔ | | ✔ |
|
| auto-filter wildcard responses | ✔ | | ✔ |
|
||||||
| performs other scans (vhost, dns, etc) | | ✔ | ✔ |
|
| performs other scans (vhost, dns, etc) | | ✔ | ✔ |
|
||||||
| time delay / rate limiting | | ✔ | ✔ |
|
| time delay / rate limiting | | ✔ | ✔ |
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
# verbosity = 1
|
# verbosity = 1
|
||||||
# scan_limit = 6
|
# scan_limit = 6
|
||||||
# quiet = true
|
# quiet = true
|
||||||
|
# json = true
|
||||||
# output = "/targets/ellingson_mineral_company/gibson.txt"
|
# output = "/targets/ellingson_mineral_company/gibson.txt"
|
||||||
|
# debug_log = "/var/log/find-the-derp.log"
|
||||||
# user_agent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0"
|
# user_agent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0"
|
||||||
# redirects = true
|
# redirects = true
|
||||||
# insecure = true
|
# insecure = true
|
||||||
@@ -30,6 +32,8 @@
|
|||||||
# extract_links = true
|
# extract_links = true
|
||||||
# depth = 1
|
# depth = 1
|
||||||
# filter_size = [5174]
|
# filter_size = [5174]
|
||||||
|
# filter_word_count = [993]
|
||||||
|
# filter_line_count = [35, 36]
|
||||||
# queries = [["name","value"], ["rick", "astley"]]
|
# queries = [["name","value"], ["rick", "astley"]]
|
||||||
|
|
||||||
# headers can be specified on multiple lines or as an inline table
|
# headers can be specified on multiple lines or as an inline table
|
||||||
|
|||||||
41
snapcraft.yaml
Normal file
41
snapcraft.yaml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
name: feroxbuster
|
||||||
|
version: git
|
||||||
|
summary: A simple, fast, recursive content discovery tool written in Rust
|
||||||
|
description: |
|
||||||
|
feroxbuster is a tool designed to perform Forced Browsing.
|
||||||
|
|
||||||
|
Forced browsing is an attack where the aim is to enumerate and access resources that are not referenced by the web application, but are still accessible by an attacker.
|
||||||
|
|
||||||
|
feroxbuster uses brute force combined with a wordlist to search for unlinked content in target directories. These resources may store sensitive information about web applications and operational systems, such as source code, credentials, internal network addressing, etc...
|
||||||
|
|
||||||
|
This attack is also known as Predictable Resource Location, File Enumeration, Directory Enumeration, and Resource Enumeration.
|
||||||
|
|
||||||
|
|
||||||
|
base: core18
|
||||||
|
|
||||||
|
plugs:
|
||||||
|
etc-feroxbuster:
|
||||||
|
interface: system-files
|
||||||
|
read:
|
||||||
|
- /etc/feroxbuster
|
||||||
|
dot-config-feroxbuster:
|
||||||
|
interface: personal-files
|
||||||
|
read:
|
||||||
|
- $HOME/.config/feroxbuster
|
||||||
|
|
||||||
|
architectures:
|
||||||
|
- build-on: amd64
|
||||||
|
- build-on: i386
|
||||||
|
|
||||||
|
parts:
|
||||||
|
feroxbuster:
|
||||||
|
plugin: rust
|
||||||
|
source: .
|
||||||
|
|
||||||
|
apps:
|
||||||
|
feroxbuster:
|
||||||
|
command: bin/feroxbuster
|
||||||
|
plugs:
|
||||||
|
- etc-feroxbuster
|
||||||
|
- dot-config-feroxbuster
|
||||||
|
- network
|
||||||
@@ -297,6 +297,24 @@ by Ben "epi" Risher {} ver: {}"#,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for filter in &config.filter_word_count {
|
||||||
|
writeln!(
|
||||||
|
&mut writer,
|
||||||
|
"{}",
|
||||||
|
format_banner_entry!("\u{1f4a2}", "Word Count Filter", filter)
|
||||||
|
)
|
||||||
|
.unwrap_or_default(); // 💢
|
||||||
|
}
|
||||||
|
|
||||||
|
for filter in &config.filter_line_count {
|
||||||
|
writeln!(
|
||||||
|
&mut writer,
|
||||||
|
"{}",
|
||||||
|
format_banner_entry!("\u{1f4a2}", "Line Count Filter", filter)
|
||||||
|
)
|
||||||
|
.unwrap_or_default(); // 💢
|
||||||
|
}
|
||||||
|
|
||||||
if config.extract_links {
|
if config.extract_links {
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut writer,
|
&mut writer,
|
||||||
@@ -306,6 +324,15 @@ by Ben "epi" Risher {} ver: {}"#,
|
|||||||
.unwrap_or_default(); // 🔎
|
.unwrap_or_default(); // 🔎
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.json {
|
||||||
|
writeln!(
|
||||||
|
&mut writer,
|
||||||
|
"{}",
|
||||||
|
format_banner_entry!("\u{1F9d4}", "JSON Output", config.json)
|
||||||
|
)
|
||||||
|
.unwrap_or_default(); // 🧔
|
||||||
|
}
|
||||||
|
|
||||||
if !config.queries.is_empty() {
|
if !config.queries.is_empty() {
|
||||||
for query in &config.queries {
|
for query in &config.queries {
|
||||||
writeln!(
|
writeln!(
|
||||||
@@ -330,6 +357,15 @@ by Ben "epi" Risher {} ver: {}"#,
|
|||||||
.unwrap_or_default(); // 💾
|
.unwrap_or_default(); // 💾
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !config.debug_log.is_empty() {
|
||||||
|
writeln!(
|
||||||
|
&mut writer,
|
||||||
|
"{}",
|
||||||
|
format_banner_entry!("\u{1fab2}", "Debugging Log", config.debug_log)
|
||||||
|
)
|
||||||
|
.unwrap_or_default(); // 🪲
|
||||||
|
}
|
||||||
|
|
||||||
if !config.extensions.is_empty() {
|
if !config.extensions.is_empty() {
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut writer,
|
&mut writer,
|
||||||
|
|||||||
@@ -32,31 +32,33 @@ pub fn initialize(
|
|||||||
.default_headers(header_map)
|
.default_headers(header_map)
|
||||||
.redirect(policy);
|
.redirect(policy);
|
||||||
|
|
||||||
let client = if proxy.is_some() && !proxy.unwrap().is_empty() {
|
let client = match proxy {
|
||||||
match Proxy::all(proxy.unwrap()) {
|
// a proxy is specified, need to add it to the client
|
||||||
Ok(proxy_obj) => client.proxy(proxy_obj),
|
Some(some_proxy) => {
|
||||||
Err(e) => {
|
if !some_proxy.is_empty() {
|
||||||
eprintln!(
|
// it's not an empty string
|
||||||
"{} {} Could not add proxy ({:?}) to Client configuration",
|
match Proxy::all(some_proxy) {
|
||||||
status_colorizer("ERROR"),
|
Ok(proxy_obj) => client.proxy(proxy_obj),
|
||||||
module_colorizer("Client::initialize"),
|
Err(e) => {
|
||||||
proxy
|
eprintln!(
|
||||||
);
|
"{} {} {}",
|
||||||
eprintln!(
|
status_colorizer("ERROR"),
|
||||||
"{} {} {}",
|
module_colorizer("Client::initialize"),
|
||||||
status_colorizer("ERROR"),
|
e
|
||||||
module_colorizer("Client::initialize"),
|
);
|
||||||
e
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
panic!();
|
panic!();
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
exit(1);
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
client // Some("") was used?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
// no proxy specified
|
||||||
client
|
None => client,
|
||||||
};
|
};
|
||||||
|
|
||||||
match client.build() {
|
match client.build() {
|
||||||
|
|||||||
294
src/config.rs
294
src/config.rs
@@ -1,11 +1,11 @@
|
|||||||
use crate::utils::{module_colorizer, status_colorizer};
|
use crate::utils::{module_colorizer, status_colorizer};
|
||||||
use crate::{client, parser, progress};
|
use crate::{client, parser, progress};
|
||||||
use crate::{DEFAULT_CONFIG_NAME, DEFAULT_STATUS_CODES, DEFAULT_WORDLIST, VERSION};
|
use crate::{FeroxSerialize, DEFAULT_CONFIG_NAME, DEFAULT_STATUS_CODES, DEFAULT_WORDLIST, VERSION};
|
||||||
use clap::value_t;
|
use clap::value_t;
|
||||||
use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget};
|
use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use reqwest::{Client, StatusCode};
|
use reqwest::{Client, StatusCode};
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env::{current_dir, current_exe};
|
use std::env::{current_dir, current_exe};
|
||||||
use std::fs::read_to_string;
|
use std::fs::read_to_string;
|
||||||
@@ -49,8 +49,12 @@ fn report_and_exit(err: &str) -> ! {
|
|||||||
/// In that order.
|
/// In that order.
|
||||||
///
|
///
|
||||||
/// Inspired by and derived from https://github.com/PhilipDaniels/rust-config-example
|
/// Inspired by and derived from https://github.com/PhilipDaniels/rust-config-example
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct Configuration {
|
pub struct Configuration {
|
||||||
|
#[serde(rename = "type", default = "serialized_type")]
|
||||||
|
/// Name of this type of struct, used for serialization, i.e. `{"type":"configuration"}`
|
||||||
|
kind: String,
|
||||||
|
|
||||||
/// Path to the wordlist
|
/// Path to the wordlist
|
||||||
#[serde(default = "wordlist")]
|
#[serde(default = "wordlist")]
|
||||||
pub wordlist: String,
|
pub wordlist: String,
|
||||||
@@ -107,10 +111,19 @@ pub struct Configuration {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub quiet: bool,
|
pub quiet: bool,
|
||||||
|
|
||||||
|
/// Store log output as NDJSON
|
||||||
|
#[serde(default)]
|
||||||
|
pub json: bool,
|
||||||
|
|
||||||
/// Output file to write results to (default: stdout)
|
/// Output file to write results to (default: stdout)
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub output: String,
|
pub output: String,
|
||||||
|
|
||||||
|
/// File in which to store debug output, used in conjunction with verbosity to dictate which
|
||||||
|
/// logs are written
|
||||||
|
#[serde(default)]
|
||||||
|
pub debug_log: String,
|
||||||
|
|
||||||
/// Sets the User-Agent (default: feroxbuster/VERSION)
|
/// Sets the User-Agent (default: feroxbuster/VERSION)
|
||||||
#[serde(default = "user_agent")]
|
#[serde(default = "user_agent")]
|
||||||
pub user_agent: String,
|
pub user_agent: String,
|
||||||
@@ -163,6 +176,14 @@ pub struct Configuration {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub filter_size: Vec<u64>,
|
pub filter_size: Vec<u64>,
|
||||||
|
|
||||||
|
/// Filter out messages of a particular line count
|
||||||
|
#[serde(default)]
|
||||||
|
pub filter_line_count: Vec<usize>,
|
||||||
|
|
||||||
|
/// Filter out messages of a particular word count
|
||||||
|
#[serde(default)]
|
||||||
|
pub filter_word_count: Vec<usize>,
|
||||||
|
|
||||||
/// Don't auto-filter wildcard responses
|
/// Don't auto-filter wildcard responses
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub dont_filter: bool,
|
pub dont_filter: bool,
|
||||||
@@ -172,6 +193,11 @@ pub struct Configuration {
|
|||||||
// defaults in the event that a ferox-config.toml is found but one or more of the values below
|
// 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
|
// aren't listed in the config. This way, we get the correct defaults upon Deserialization
|
||||||
|
|
||||||
|
/// default Configuration type for use in json output
|
||||||
|
fn serialized_type() -> String {
|
||||||
|
String::from("configuration")
|
||||||
|
}
|
||||||
|
|
||||||
/// default timeout value
|
/// default timeout value
|
||||||
fn timeout() -> u64 {
|
fn timeout() -> u64 {
|
||||||
7
|
7
|
||||||
@@ -214,8 +240,10 @@ impl Default for Configuration {
|
|||||||
let replay_client = None;
|
let replay_client = None;
|
||||||
let status_codes = status_codes();
|
let status_codes = status_codes();
|
||||||
let replay_codes = status_codes.clone();
|
let replay_codes = status_codes.clone();
|
||||||
|
let kind = serialized_type();
|
||||||
|
|
||||||
Configuration {
|
Configuration {
|
||||||
|
kind,
|
||||||
client,
|
client,
|
||||||
timeout,
|
timeout,
|
||||||
user_agent,
|
user_agent,
|
||||||
@@ -225,6 +253,7 @@ impl Default for Configuration {
|
|||||||
dont_filter: false,
|
dont_filter: false,
|
||||||
quiet: false,
|
quiet: false,
|
||||||
stdin: false,
|
stdin: false,
|
||||||
|
json: false,
|
||||||
verbosity: 0,
|
verbosity: 0,
|
||||||
scan_limit: 0,
|
scan_limit: 0,
|
||||||
add_slash: false,
|
add_slash: false,
|
||||||
@@ -235,11 +264,14 @@ impl Default for Configuration {
|
|||||||
proxy: String::new(),
|
proxy: String::new(),
|
||||||
config: String::new(),
|
config: String::new(),
|
||||||
output: String::new(),
|
output: String::new(),
|
||||||
|
debug_log: String::new(),
|
||||||
target_url: String::new(),
|
target_url: String::new(),
|
||||||
replay_proxy: String::new(),
|
replay_proxy: String::new(),
|
||||||
queries: Vec::new(),
|
queries: Vec::new(),
|
||||||
extensions: Vec::new(),
|
extensions: Vec::new(),
|
||||||
filter_size: Vec::new(),
|
filter_size: Vec::new(),
|
||||||
|
filter_line_count: Vec::new(),
|
||||||
|
filter_word_count: Vec::new(),
|
||||||
filter_status: Vec::new(),
|
filter_status: Vec::new(),
|
||||||
headers: HashMap::new(),
|
headers: HashMap::new(),
|
||||||
depth: depth(),
|
depth: depth(),
|
||||||
@@ -265,16 +297,20 @@ impl Configuration {
|
|||||||
/// - **status_codes**: [`DEFAULT_RESPONSE_CODES`](constant.DEFAULT_RESPONSE_CODES.html)
|
/// - **status_codes**: [`DEFAULT_RESPONSE_CODES`](constant.DEFAULT_RESPONSE_CODES.html)
|
||||||
/// - **filter_status**: `None`
|
/// - **filter_status**: `None`
|
||||||
/// - **output**: `None` (print to stdout)
|
/// - **output**: `None` (print to stdout)
|
||||||
|
/// - **debug_log**: `None`
|
||||||
/// - **quiet**: `false`
|
/// - **quiet**: `false`
|
||||||
/// - **user_agent**: `feroxer/VERSION`
|
/// - **user_agent**: `feroxbuster/VERSION`
|
||||||
/// - **insecure**: `false` (don't be insecure, i.e. don't allow invalid certs)
|
/// - **insecure**: `false` (don't be insecure, i.e. don't allow invalid certs)
|
||||||
/// - **extensions**: `None`
|
/// - **extensions**: `None`
|
||||||
/// - **filter_size**: `None`
|
/// - **filter_size**: `None`
|
||||||
|
/// - **filter_word_count**: `None`
|
||||||
|
/// - **filter_line_count**: `None`
|
||||||
/// - **headers**: `None`
|
/// - **headers**: `None`
|
||||||
/// - **queries**: `None`
|
/// - **queries**: `None`
|
||||||
/// - **no_recursion**: `false` (recursively scan enumerated sub-directories)
|
/// - **no_recursion**: `false` (recursively scan enumerated sub-directories)
|
||||||
/// - **add_slash**: `false`
|
/// - **add_slash**: `false`
|
||||||
/// - **stdin**: `false`
|
/// - **stdin**: `false`
|
||||||
|
/// - **json**: `false`
|
||||||
/// - **dont_filter**: `false` (auto filter wildcard responses)
|
/// - **dont_filter**: `false` (auto filter wildcard responses)
|
||||||
/// - **depth**: `4` (maximum recursion depth)
|
/// - **depth**: `4` (maximum recursion depth)
|
||||||
/// - **scan_limit**: `0` (no limit on concurrent scans imposed)
|
/// - **scan_limit**: `0` (no limit on concurrent scans imposed)
|
||||||
@@ -352,36 +388,31 @@ impl Configuration {
|
|||||||
|
|
||||||
let args = parser::initialize().get_matches();
|
let args = parser::initialize().get_matches();
|
||||||
|
|
||||||
// the .is_some appears clunky, but it allows default values to be incrementally
|
macro_rules! update_config_if_present {
|
||||||
// overwritten from Struct defaults, to file config, to command line args, soooo ¯\_(ツ)_/¯
|
($c:expr, $m:ident, $v:expr, $t:ty) => {
|
||||||
if args.value_of("threads").is_some() {
|
match value_t!($m, $v, $t) {
|
||||||
let threads = value_t!(args.value_of("threads"), usize).unwrap_or_else(|e| e.exit());
|
Ok(value) => *$c = value, // Update value
|
||||||
config.threads = threads;
|
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
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.value_of("depth").is_some() {
|
update_config_if_present!(&mut config.threads, args, "threads", usize);
|
||||||
let depth = value_t!(args.value_of("depth"), usize).unwrap_or_else(|e| e.exit());
|
update_config_if_present!(&mut config.depth, args, "depth", usize);
|
||||||
config.depth = depth;
|
update_config_if_present!(&mut config.scan_limit, args, "scan_limit", usize);
|
||||||
}
|
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);
|
||||||
|
|
||||||
if args.value_of("scan_limit").is_some() {
|
if let Some(arg) = args.values_of("status_codes") {
|
||||||
let scan_limit =
|
config.status_codes = arg
|
||||||
value_t!(args.value_of("scan_limit"), usize).unwrap_or_else(|e| e.exit());
|
|
||||||
config.scan_limit = scan_limit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.value_of("wordlist").is_some() {
|
|
||||||
config.wordlist = String::from(args.value_of("wordlist").unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.value_of("output").is_some() {
|
|
||||||
config.output = String::from(args.value_of("output").unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.values_of("status_codes").is_some() {
|
|
||||||
config.status_codes = args
|
|
||||||
.values_of("status_codes")
|
|
||||||
.unwrap() // already known good
|
|
||||||
.map(|code| {
|
.map(|code| {
|
||||||
StatusCode::from_bytes(code.as_bytes())
|
StatusCode::from_bytes(code.as_bytes())
|
||||||
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
|
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
|
||||||
@@ -390,11 +421,9 @@ impl Configuration {
|
|||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.values_of("replay_codes").is_some() {
|
if let Some(arg) = args.values_of("replay_codes") {
|
||||||
// replay codes passed in by the user
|
// replay codes passed in by the user
|
||||||
config.replay_codes = args
|
config.replay_codes = arg
|
||||||
.values_of("replay_codes")
|
|
||||||
.unwrap() // already known good
|
|
||||||
.map(|code| {
|
.map(|code| {
|
||||||
StatusCode::from_bytes(code.as_bytes())
|
StatusCode::from_bytes(code.as_bytes())
|
||||||
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
|
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
|
||||||
@@ -406,10 +435,8 @@ impl Configuration {
|
|||||||
config.replay_codes = config.status_codes.clone();
|
config.replay_codes = config.status_codes.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.values_of("filter_status").is_some() {
|
if let Some(arg) = args.values_of("filter_status") {
|
||||||
config.filter_status = args
|
config.filter_status = arg
|
||||||
.values_of("filter_status")
|
|
||||||
.unwrap() // already known good
|
|
||||||
.map(|code| {
|
.map(|code| {
|
||||||
StatusCode::from_bytes(code.as_bytes())
|
StatusCode::from_bytes(code.as_bytes())
|
||||||
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
|
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
|
||||||
@@ -418,20 +445,32 @@ impl Configuration {
|
|||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.values_of("extensions").is_some() {
|
if let Some(arg) = args.values_of("extensions") {
|
||||||
config.extensions = args
|
config.extensions = arg.map(|val| val.to_string()).collect();
|
||||||
.values_of("extensions")
|
}
|
||||||
.unwrap()
|
|
||||||
.map(|val| val.to_string())
|
if let Some(arg) = args.values_of("filter_size") {
|
||||||
|
config.filter_size = arg
|
||||||
|
.map(|size| {
|
||||||
|
size.parse::<u64>()
|
||||||
|
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.values_of("filter_size").is_some() {
|
if let Some(arg) = args.values_of("filter_words") {
|
||||||
config.filter_size = args
|
config.filter_word_count = arg
|
||||||
.values_of("filter_size")
|
|
||||||
.unwrap() // already known good
|
|
||||||
.map(|size| {
|
.map(|size| {
|
||||||
size.parse::<u64>()
|
size.parse::<usize>()
|
||||||
|
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(arg) = args.values_of("filter_lines") {
|
||||||
|
config.filter_line_count = arg
|
||||||
|
.map(|size| {
|
||||||
|
size.parse::<usize>()
|
||||||
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
|
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -442,11 +481,11 @@ impl Configuration {
|
|||||||
// consider a user specifying quiet = true in ferox-config.toml
|
// consider a user specifying quiet = true in ferox-config.toml
|
||||||
// if the line below is outside of the if, we'd overwrite true with
|
// if the line below is outside of the if, we'd overwrite true with
|
||||||
// false if no -q is used on the command line
|
// false if no -q is used on the command line
|
||||||
config.quiet = args.is_present("quiet");
|
config.quiet = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.is_present("dont_filter") {
|
if args.is_present("dont_filter") {
|
||||||
config.dont_filter = args.is_present("dont_filter");
|
config.dont_filter = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.occurrences_of("verbosity") > 0 {
|
if args.occurrences_of("verbosity") > 0 {
|
||||||
@@ -456,19 +495,23 @@ impl Configuration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if args.is_present("no_recursion") {
|
if args.is_present("no_recursion") {
|
||||||
config.no_recursion = args.is_present("no_recursion");
|
config.no_recursion = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.is_present("add_slash") {
|
if args.is_present("add_slash") {
|
||||||
config.add_slash = args.is_present("add_slash");
|
config.add_slash = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.is_present("extract_links") {
|
if args.is_present("extract_links") {
|
||||||
config.extract_links = args.is_present("extract_links");
|
config.extract_links = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.is_present("json") {
|
||||||
|
config.json = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.is_present("stdin") {
|
if args.is_present("stdin") {
|
||||||
config.stdin = args.is_present("stdin");
|
config.stdin = true;
|
||||||
} else {
|
} else {
|
||||||
config.target_url = String::from(args.value_of("url").unwrap());
|
config.target_url = String::from(args.value_of("url").unwrap());
|
||||||
}
|
}
|
||||||
@@ -476,33 +519,21 @@ impl Configuration {
|
|||||||
////
|
////
|
||||||
// organizational breakpoint; all options below alter the Client configuration
|
// organizational breakpoint; all options below alter the Client configuration
|
||||||
////
|
////
|
||||||
if args.value_of("proxy").is_some() {
|
update_config_if_present!(&mut config.proxy, args, "proxy", String);
|
||||||
config.proxy = String::from(args.value_of("proxy").unwrap());
|
update_config_if_present!(&mut config.replay_proxy, args, "replay_proxy", String);
|
||||||
}
|
update_config_if_present!(&mut config.user_agent, args, "user_agent", String);
|
||||||
|
update_config_if_present!(&mut config.timeout, args, "timeout", u64);
|
||||||
if args.value_of("replay_proxy").is_some() {
|
|
||||||
config.replay_proxy = String::from(args.value_of("replay_proxy").unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.value_of("user_agent").is_some() {
|
|
||||||
config.user_agent = String::from(args.value_of("user_agent").unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.value_of("timeout").is_some() {
|
|
||||||
let timeout = value_t!(args.value_of("timeout"), u64).unwrap_or_else(|e| e.exit());
|
|
||||||
config.timeout = timeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.is_present("redirects") {
|
if args.is_present("redirects") {
|
||||||
config.redirects = args.is_present("redirects");
|
config.redirects = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.is_present("insecure") {
|
if args.is_present("insecure") {
|
||||||
config.insecure = args.is_present("insecure");
|
config.insecure = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.values_of("headers").is_some() {
|
if let Some(headers) = args.values_of("headers") {
|
||||||
for val in args.values_of("headers").unwrap() {
|
for val in headers {
|
||||||
let mut split_val = val.split(':');
|
let mut split_val = val.split(':');
|
||||||
|
|
||||||
// explicitly take first split value as header's name
|
// explicitly take first split value as header's name
|
||||||
@@ -515,8 +546,8 @@ impl Configuration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.values_of("queries").is_some() {
|
if let Some(queries) = args.values_of("queries") {
|
||||||
for val in args.values_of("queries").unwrap() {
|
for val in queries {
|
||||||
// same basic logic used as reading in the headers HashMap above
|
// same basic logic used as reading in the headers HashMap above
|
||||||
let mut split_val = val.split('=');
|
let mut split_val = val.split('=');
|
||||||
|
|
||||||
@@ -616,11 +647,15 @@ impl Configuration {
|
|||||||
settings.stdin = settings_to_merge.stdin;
|
settings.stdin = settings_to_merge.stdin;
|
||||||
settings.depth = settings_to_merge.depth;
|
settings.depth = settings_to_merge.depth;
|
||||||
settings.filter_size = settings_to_merge.filter_size;
|
settings.filter_size = settings_to_merge.filter_size;
|
||||||
|
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.filter_status = settings_to_merge.filter_status;
|
||||||
settings.dont_filter = settings_to_merge.dont_filter;
|
settings.dont_filter = settings_to_merge.dont_filter;
|
||||||
settings.scan_limit = settings_to_merge.scan_limit;
|
settings.scan_limit = settings_to_merge.scan_limit;
|
||||||
settings.replay_proxy = settings_to_merge.replay_proxy;
|
settings.replay_proxy = settings_to_merge.replay_proxy;
|
||||||
settings.replay_codes = settings_to_merge.replay_codes;
|
settings.replay_codes = settings_to_merge.replay_codes;
|
||||||
|
settings.debug_log = settings_to_merge.debug_log;
|
||||||
|
settings.json = settings_to_merge.json;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If present, read in `DEFAULT_CONFIG_NAME` and deserialize the specified values
|
/// If present, read in `DEFAULT_CONFIG_NAME` and deserialize the specified values
|
||||||
@@ -646,6 +681,47 @@ impl Configuration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Implementation of FeroxMessage
|
||||||
|
impl FeroxSerialize for Configuration {
|
||||||
|
/// Simple wrapper around create_report_string
|
||||||
|
fn as_str(&self) -> String {
|
||||||
|
format!("{:#?}\n", *self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an NDJSON representation of the current scan's Configuration
|
||||||
|
///
|
||||||
|
/// (expanded for clarity)
|
||||||
|
/// ex:
|
||||||
|
/// {
|
||||||
|
/// "type":"configuration",
|
||||||
|
/// "wordlist":"test",
|
||||||
|
/// "config":"/home/epi/.config/feroxbuster/ferox-config.toml",
|
||||||
|
/// "proxy":"",
|
||||||
|
/// "replay_proxy":"",
|
||||||
|
/// "target_url":"https://localhost.com",
|
||||||
|
/// "status_codes":[
|
||||||
|
/// 200,
|
||||||
|
/// 204,
|
||||||
|
/// 301,
|
||||||
|
/// 302,
|
||||||
|
/// 307,
|
||||||
|
/// 308,
|
||||||
|
/// 401,
|
||||||
|
/// 403,
|
||||||
|
/// 405
|
||||||
|
/// ],
|
||||||
|
/// ...
|
||||||
|
/// }\n
|
||||||
|
fn as_json(&self) -> String {
|
||||||
|
if let Ok(mut json) = serde_json::to_string(&self) {
|
||||||
|
json.push('\n');
|
||||||
|
json
|
||||||
|
} else {
|
||||||
|
String::from("{\"error\":\"could not Configuration convert to json\"}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -666,6 +742,7 @@ mod tests {
|
|||||||
verbosity = 1
|
verbosity = 1
|
||||||
scan_limit = 6
|
scan_limit = 6
|
||||||
output = "/some/otherpath"
|
output = "/some/otherpath"
|
||||||
|
debug_log = "/yet/anotherpath"
|
||||||
redirects = true
|
redirects = true
|
||||||
insecure = true
|
insecure = true
|
||||||
extensions = ["html", "php", "js"]
|
extensions = ["html", "php", "js"]
|
||||||
@@ -676,8 +753,11 @@ mod tests {
|
|||||||
stdin = true
|
stdin = true
|
||||||
dont_filter = true
|
dont_filter = true
|
||||||
extract_links = true
|
extract_links = true
|
||||||
|
json = true
|
||||||
depth = 1
|
depth = 1
|
||||||
filter_size = [4120]
|
filter_size = [4120]
|
||||||
|
filter_word_count = [994, 992]
|
||||||
|
filter_line_count = [34]
|
||||||
filter_status = [201]
|
filter_status = [201]
|
||||||
"#;
|
"#;
|
||||||
let tmp_dir = TempDir::new().unwrap();
|
let tmp_dir = TempDir::new().unwrap();
|
||||||
@@ -693,6 +773,7 @@ mod tests {
|
|||||||
assert_eq!(config.wordlist, wordlist());
|
assert_eq!(config.wordlist, wordlist());
|
||||||
assert_eq!(config.proxy, String::new());
|
assert_eq!(config.proxy, String::new());
|
||||||
assert_eq!(config.target_url, String::new());
|
assert_eq!(config.target_url, String::new());
|
||||||
|
assert_eq!(config.debug_log, String::new());
|
||||||
assert_eq!(config.config, String::new());
|
assert_eq!(config.config, String::new());
|
||||||
assert_eq!(config.replay_proxy, String::new());
|
assert_eq!(config.replay_proxy, String::new());
|
||||||
assert_eq!(config.status_codes, status_codes());
|
assert_eq!(config.status_codes, status_codes());
|
||||||
@@ -706,6 +787,7 @@ mod tests {
|
|||||||
assert_eq!(config.quiet, false);
|
assert_eq!(config.quiet, false);
|
||||||
assert_eq!(config.dont_filter, false);
|
assert_eq!(config.dont_filter, false);
|
||||||
assert_eq!(config.no_recursion, false);
|
assert_eq!(config.no_recursion, false);
|
||||||
|
assert_eq!(config.json, false);
|
||||||
assert_eq!(config.stdin, false);
|
assert_eq!(config.stdin, false);
|
||||||
assert_eq!(config.add_slash, false);
|
assert_eq!(config.add_slash, false);
|
||||||
assert_eq!(config.redirects, false);
|
assert_eq!(config.redirects, false);
|
||||||
@@ -714,6 +796,8 @@ mod tests {
|
|||||||
assert_eq!(config.queries, Vec::new());
|
assert_eq!(config.queries, Vec::new());
|
||||||
assert_eq!(config.extensions, Vec::<String>::new());
|
assert_eq!(config.extensions, Vec::<String>::new());
|
||||||
assert_eq!(config.filter_size, Vec::<u64>::new());
|
assert_eq!(config.filter_size, Vec::<u64>::new());
|
||||||
|
assert_eq!(config.filter_word_count, Vec::<usize>::new());
|
||||||
|
assert_eq!(config.filter_line_count, Vec::<usize>::new());
|
||||||
assert_eq!(config.filter_status, Vec::<u16>::new());
|
assert_eq!(config.filter_status, Vec::<u16>::new());
|
||||||
assert_eq!(config.headers, HashMap::new());
|
assert_eq!(config.headers, HashMap::new());
|
||||||
}
|
}
|
||||||
@@ -725,6 +809,13 @@ mod tests {
|
|||||||
assert_eq!(config.wordlist, "/some/path");
|
assert_eq!(config.wordlist, "/some/path");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// parse the test config and see that the value parsed is correct
|
||||||
|
fn config_reads_debug_log() {
|
||||||
|
let config = setup_config_test();
|
||||||
|
assert_eq!(config.debug_log, "/yet/anotherpath");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// parse the test config and see that the value parsed is correct
|
/// parse the test config and see that the value parsed is correct
|
||||||
fn config_reads_status_codes() {
|
fn config_reads_status_codes() {
|
||||||
@@ -788,6 +879,13 @@ mod tests {
|
|||||||
assert_eq!(config.quiet, true);
|
assert_eq!(config.quiet, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// parse the test config and see that the value parsed is correct
|
||||||
|
fn config_reads_json() {
|
||||||
|
let config = setup_config_test();
|
||||||
|
assert_eq!(config.json, true);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// parse the test config and see that the value parsed is correct
|
/// parse the test config and see that the value parsed is correct
|
||||||
fn config_reads_verbosity() {
|
fn config_reads_verbosity() {
|
||||||
@@ -865,6 +963,20 @@ mod tests {
|
|||||||
assert_eq!(config.filter_size, vec![4120]);
|
assert_eq!(config.filter_size, vec![4120]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// parse the test config and see that the value parsed is correct
|
||||||
|
fn config_reads_filter_word_count() {
|
||||||
|
let config = setup_config_test();
|
||||||
|
assert_eq!(config.filter_word_count, vec![994, 992]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// parse the test config and see that the value parsed is correct
|
||||||
|
fn config_reads_filter_line_count() {
|
||||||
|
let config = setup_config_test();
|
||||||
|
assert_eq!(config.filter_line_count, vec![34]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// parse the test config and see that the value parsed is correct
|
/// parse the test config and see that the value parsed is correct
|
||||||
fn config_reads_filter_status() {
|
fn config_reads_filter_status() {
|
||||||
@@ -898,4 +1010,32 @@ mod tests {
|
|||||||
fn config_report_and_exit_works() {
|
fn config_report_and_exit_works() {
|
||||||
report_and_exit("some message");
|
report_and_exit("some message");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// test as_str method of Configuration
|
||||||
|
fn as_str_returns_string_with_newline() {
|
||||||
|
let config = Configuration::new();
|
||||||
|
let config_str = config.as_str();
|
||||||
|
println!("{}", config_str);
|
||||||
|
assert!(config_str.starts_with("Configuration {"));
|
||||||
|
assert!(config_str.ends_with("}\n"));
|
||||||
|
assert!(config_str.contains("replay_codes:"));
|
||||||
|
assert!(config_str.contains("client: Client {"));
|
||||||
|
assert!(config_str.contains("user_agent: \"feroxbuster"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// test as_json method of Configuration
|
||||||
|
fn as_json_returns_json_representation_of_configuration_with_newline() {
|
||||||
|
let mut config = Configuration::new();
|
||||||
|
config.timeout = 12;
|
||||||
|
config.depth = 2;
|
||||||
|
let config_str = config.as_json();
|
||||||
|
let json: Configuration = serde_json::from_str(&config_str).unwrap();
|
||||||
|
assert_eq!(json.config, config.config);
|
||||||
|
assert_eq!(json.wordlist, config.wordlist);
|
||||||
|
assert_eq!(json.replay_codes, config.replay_codes);
|
||||||
|
assert_eq!(json.timeout, config.timeout);
|
||||||
|
assert_eq!(json.depth, config.depth);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
153
src/filters.rs
153
src/filters.rs
@@ -53,7 +53,7 @@ impl FeroxFilter for WildcardFilter {
|
|||||||
/// Examine size, dynamic, and content_len to determine whether or not the response received
|
/// Examine size, dynamic, and content_len to determine whether or not the response received
|
||||||
/// is a wildcard response and therefore should be filtered out
|
/// is a wildcard response and therefore should be filtered out
|
||||||
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
|
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
|
||||||
log::trace!("enter: should_filter_response({:?} {:?})", self, response);
|
log::trace!("enter: should_filter_response({:?} {})", self, response);
|
||||||
|
|
||||||
// quick return if dont_filter is set
|
// quick return if dont_filter is set
|
||||||
if CONFIGURATION.dont_filter {
|
if CONFIGURATION.dont_filter {
|
||||||
@@ -114,7 +114,7 @@ pub struct StatusCodeFilter {
|
|||||||
impl FeroxFilter for StatusCodeFilter {
|
impl FeroxFilter for StatusCodeFilter {
|
||||||
/// Check `filter_code` against what was passed in via -C|--filter-status
|
/// Check `filter_code` against what was passed in via -C|--filter-status
|
||||||
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
|
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
|
||||||
log::trace!("enter: should_filter_response({:?} {:?})", self, response);
|
log::trace!("enter: should_filter_response({:?} {})", self, response);
|
||||||
|
|
||||||
if response.status().as_u16() == self.filter_code {
|
if response.status().as_u16() == self.filter_code {
|
||||||
log::debug!(
|
log::debug!(
|
||||||
@@ -140,3 +140,152 @@ impl FeroxFilter for StatusCodeFilter {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Simple implementor of FeroxFilter; used to filter out responses based on the number of lines
|
||||||
|
/// in a Response body; specified using -N|--filter-lines
|
||||||
|
#[derive(Default, Debug, PartialEq)]
|
||||||
|
pub struct LinesFilter {
|
||||||
|
/// Number of lines in a Response's body that should be filtered
|
||||||
|
pub line_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// implementation of FeroxFilter for LinesFilter
|
||||||
|
impl FeroxFilter for LinesFilter {
|
||||||
|
/// Check `line_count` against what was passed in via -N|--filter-lines
|
||||||
|
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
|
||||||
|
log::trace!("enter: should_filter_response({:?} {})", self, response);
|
||||||
|
|
||||||
|
let result = response.line_count() == self.line_count;
|
||||||
|
|
||||||
|
log::trace!("exit: should_filter_response -> {}", result);
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compare one LinesFilter to another
|
||||||
|
fn box_eq(&self, other: &dyn Any) -> bool {
|
||||||
|
other.downcast_ref::<Self>().map_or(false, |a| self == a)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return self as Any for dynamic dispatch purposes
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple implementor of FeroxFilter; used to filter out responses based on the number of words
|
||||||
|
/// in a Response body; specified using -W|--filter-words
|
||||||
|
#[derive(Default, Debug, PartialEq)]
|
||||||
|
pub struct WordsFilter {
|
||||||
|
/// Number of words in a Response's body that should be filtered
|
||||||
|
pub word_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// implementation of FeroxFilter for WordsFilter
|
||||||
|
impl FeroxFilter for WordsFilter {
|
||||||
|
/// Check `word_count` against what was passed in via -W|--filter-words
|
||||||
|
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
|
||||||
|
log::trace!("enter: should_filter_response({:?} {})", self, response);
|
||||||
|
|
||||||
|
let result = response.word_count() == self.word_count;
|
||||||
|
|
||||||
|
log::trace!("exit: should_filter_response -> {}", result);
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compare one WordsFilter to another
|
||||||
|
fn box_eq(&self, other: &dyn Any) -> bool {
|
||||||
|
other.downcast_ref::<Self>().map_or(false, |a| self == a)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return self as Any for dynamic dispatch purposes
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple implementor of FeroxFilter; used to filter out responses based on the length of a
|
||||||
|
/// Response body; specified using -S|--filter-size
|
||||||
|
#[derive(Default, Debug, PartialEq)]
|
||||||
|
pub struct SizeFilter {
|
||||||
|
/// Overall length of a Response's body that should be filtered
|
||||||
|
pub content_length: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// implementation of FeroxFilter for SizeFilter
|
||||||
|
impl FeroxFilter for SizeFilter {
|
||||||
|
/// Check `content_length` against what was passed in via -S|--filter-size
|
||||||
|
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
|
||||||
|
log::trace!("enter: should_filter_response({:?} {})", self, response);
|
||||||
|
|
||||||
|
let result = response.content_length() == self.content_length;
|
||||||
|
|
||||||
|
log::trace!("exit: should_filter_response -> {}", result);
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compare one SizeFilter to another
|
||||||
|
fn box_eq(&self, other: &dyn Any) -> bool {
|
||||||
|
other.downcast_ref::<Self>().map_or(false, |a| self == a)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return self as Any for dynamic dispatch purposes
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// just a simple test to increase code coverage by hitting as_any and the inner value
|
||||||
|
fn lines_filter_as_any() {
|
||||||
|
let filter = LinesFilter { line_count: 1 };
|
||||||
|
|
||||||
|
assert_eq!(filter.line_count, 1);
|
||||||
|
assert_eq!(
|
||||||
|
*filter.as_any().downcast_ref::<LinesFilter>().unwrap(),
|
||||||
|
filter
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// just a simple test to increase code coverage by hitting as_any and the inner value
|
||||||
|
fn words_filter_as_any() {
|
||||||
|
let filter = WordsFilter { word_count: 1 };
|
||||||
|
|
||||||
|
assert_eq!(filter.word_count, 1);
|
||||||
|
assert_eq!(
|
||||||
|
*filter.as_any().downcast_ref::<WordsFilter>().unwrap(),
|
||||||
|
filter
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// just a simple test to increase code coverage by hitting as_any and the inner value
|
||||||
|
fn size_filter_as_any() {
|
||||||
|
let filter = SizeFilter { content_length: 1 };
|
||||||
|
|
||||||
|
assert_eq!(filter.content_length, 1);
|
||||||
|
assert_eq!(
|
||||||
|
*filter.as_any().downcast_ref::<SizeFilter>().unwrap(),
|
||||||
|
filter
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// just a simple test to increase code coverage by hitting as_any and the inner value
|
||||||
|
fn status_code_filter_as_any() {
|
||||||
|
let filter = StatusCodeFilter { filter_code: 200 };
|
||||||
|
|
||||||
|
assert_eq!(filter.filter_code, 200);
|
||||||
|
assert_eq!(
|
||||||
|
*filter.as_any().downcast_ref::<StatusCodeFilter>().unwrap(),
|
||||||
|
filter
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ use crate::{
|
|||||||
config::{CONFIGURATION, PROGRESS_PRINTER},
|
config::{CONFIGURATION, PROGRESS_PRINTER},
|
||||||
filters::WildcardFilter,
|
filters::WildcardFilter,
|
||||||
scanner::should_filter_response,
|
scanner::should_filter_response,
|
||||||
utils::{
|
utils::{ferox_print, format_url, get_url_path_length, make_request, status_colorizer},
|
||||||
ferox_print, format_url, get_url_path_length, make_request, module_colorizer,
|
|
||||||
status_colorizer,
|
|
||||||
},
|
|
||||||
FeroxResponse,
|
FeroxResponse,
|
||||||
};
|
};
|
||||||
use console::style;
|
use console::style;
|
||||||
@@ -42,13 +39,13 @@ fn unique_string(length: usize) -> String {
|
|||||||
pub async fn wildcard_test(
|
pub async fn wildcard_test(
|
||||||
target_url: &str,
|
target_url: &str,
|
||||||
bar: ProgressBar,
|
bar: ProgressBar,
|
||||||
tx_file: UnboundedSender<String>,
|
tx_term: UnboundedSender<FeroxResponse>,
|
||||||
) -> Option<WildcardFilter> {
|
) -> Option<WildcardFilter> {
|
||||||
log::trace!(
|
log::trace!(
|
||||||
"enter: wildcard_test({:?}, {:?}, {:?})",
|
"enter: wildcard_test({:?}, {:?}, {:?})",
|
||||||
target_url,
|
target_url,
|
||||||
bar,
|
bar,
|
||||||
tx_file
|
tx_term
|
||||||
);
|
);
|
||||||
|
|
||||||
if CONFIGURATION.dont_filter {
|
if CONFIGURATION.dont_filter {
|
||||||
@@ -57,10 +54,10 @@ pub async fn wildcard_test(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let clone_req_one = tx_file.clone();
|
let tx_clone_one = tx_term.clone();
|
||||||
let clone_req_two = tx_file.clone();
|
let tx_clone_two = tx_term.clone();
|
||||||
|
|
||||||
if let Some(ferox_response) = make_wildcard_request(&target_url, 1, clone_req_one).await {
|
if let Some(ferox_response) = make_wildcard_request(&target_url, 1, tx_clone_one).await {
|
||||||
bar.inc(1);
|
bar.inc(1);
|
||||||
|
|
||||||
// found a wildcard response
|
// found a wildcard response
|
||||||
@@ -75,7 +72,7 @@ pub async fn wildcard_test(
|
|||||||
|
|
||||||
// content length of wildcard is non-zero, perform additional tests:
|
// content length of wildcard is non-zero, perform additional tests:
|
||||||
// make a second request, with a known-sized (64) longer request
|
// make a second request, with a known-sized (64) longer request
|
||||||
if let Some(resp_two) = make_wildcard_request(&target_url, 3, clone_req_two).await {
|
if let Some(resp_two) = make_wildcard_request(&target_url, 3, tx_clone_two).await {
|
||||||
bar.inc(1);
|
bar.inc(1);
|
||||||
|
|
||||||
let wc2_length = resp_two.content_length();
|
let wc2_length = resp_two.content_length();
|
||||||
@@ -89,42 +86,34 @@ pub async fn wildcard_test(
|
|||||||
|
|
||||||
if !CONFIGURATION.quiet {
|
if !CONFIGURATION.quiet {
|
||||||
let msg = format!(
|
let msg = format!(
|
||||||
"{} {:>10} Wildcard response is dynamic; {} ({} + url length) responses; toggle this behavior by using {}\n",
|
"{} {:>9} {:>9} {:>9} Wildcard response is dynamic; {} ({} + url length) responses; toggle this behavior by using {}\n",
|
||||||
status_colorizer("WLD"),
|
status_colorizer("WLD"),
|
||||||
wildcard.dynamic,
|
"-",
|
||||||
|
"-",
|
||||||
|
"-",
|
||||||
style("auto-filtering").yellow(),
|
style("auto-filtering").yellow(),
|
||||||
style(wc_length - url_len).cyan(),
|
style(wc_length - url_len).cyan(),
|
||||||
style("--dont-filter").yellow()
|
style("--dont-filter").yellow()
|
||||||
);
|
);
|
||||||
|
|
||||||
ferox_print(&msg, &PROGRESS_PRINTER);
|
ferox_print(&msg, &PROGRESS_PRINTER);
|
||||||
|
|
||||||
try_send_message_to_file(
|
|
||||||
&msg,
|
|
||||||
tx_file.clone(),
|
|
||||||
!CONFIGURATION.output.is_empty(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else if wc_length == wc2_length {
|
} else if wc_length == wc2_length {
|
||||||
wildcard.size = wc_length;
|
wildcard.size = wc_length;
|
||||||
|
|
||||||
if !CONFIGURATION.quiet {
|
if !CONFIGURATION.quiet {
|
||||||
let msg = format!(
|
let msg = format!(
|
||||||
"{} {:>10} Wildcard response is static; {} {} responses; toggle this behavior by using {}\n",
|
"{} {:>9} {:>9} {:>9} Wildcard response is static; {} {} responses; toggle this behavior by using {}\n",
|
||||||
status_colorizer("WLD"),
|
status_colorizer("WLD"),
|
||||||
wc_length,
|
"-",
|
||||||
|
"-",
|
||||||
|
"-",
|
||||||
style("auto-filtering").yellow(),
|
style("auto-filtering").yellow(),
|
||||||
style(wc_length).cyan(),
|
style(wc_length).cyan(),
|
||||||
style("--dont-filter").yellow()
|
style("--dont-filter").yellow()
|
||||||
);
|
);
|
||||||
|
|
||||||
ferox_print(&msg, &PROGRESS_PRINTER);
|
ferox_print(&msg, &PROGRESS_PRINTER);
|
||||||
|
|
||||||
try_send_message_to_file(
|
|
||||||
&msg,
|
|
||||||
tx_file.clone(),
|
|
||||||
!CONFIGURATION.output.is_empty(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -148,7 +137,7 @@ pub async fn wildcard_test(
|
|||||||
async fn make_wildcard_request(
|
async fn make_wildcard_request(
|
||||||
target_url: &str,
|
target_url: &str,
|
||||||
length: usize,
|
length: usize,
|
||||||
tx_file: UnboundedSender<String>,
|
tx_file: UnboundedSender<FeroxResponse>,
|
||||||
) -> Option<FeroxResponse> {
|
) -> Option<FeroxResponse> {
|
||||||
log::trace!(
|
log::trace!(
|
||||||
"enter: make_wildcard_request({}, {}, {:?})",
|
"enter: make_wildcard_request({}, {}, {:?})",
|
||||||
@@ -174,8 +163,6 @@ async fn make_wildcard_request(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let wildcard = status_colorizer("WLD");
|
|
||||||
|
|
||||||
match make_request(&CONFIGURATION.client, &nonexistent.to_owned()).await {
|
match make_request(&CONFIGURATION.client, &nonexistent.to_owned()).await {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
if CONFIGURATION
|
if CONFIGURATION
|
||||||
@@ -183,53 +170,17 @@ async fn make_wildcard_request(
|
|||||||
.contains(&response.status().as_u16())
|
.contains(&response.status().as_u16())
|
||||||
{
|
{
|
||||||
// found a wildcard response
|
// found a wildcard response
|
||||||
let ferox_response = FeroxResponse::from(response, false).await;
|
let mut ferox_response = FeroxResponse::from(response, true).await;
|
||||||
let url_len = get_url_path_length(&ferox_response.url());
|
ferox_response.wildcard = true;
|
||||||
let content_len = ferox_response.content_length();
|
|
||||||
|
|
||||||
if !CONFIGURATION.quiet && !should_filter_response(&ferox_response) {
|
if !CONFIGURATION.quiet
|
||||||
let msg = format!(
|
&& !should_filter_response(&ferox_response)
|
||||||
"{} {:>10} Got {} for {} (url length: {})\n",
|
&& tx_file.send(ferox_response.clone()).is_err()
|
||||||
wildcard,
|
{
|
||||||
content_len,
|
return None;
|
||||||
status_colorizer(&ferox_response.status().as_str()),
|
|
||||||
ferox_response.url(),
|
|
||||||
url_len
|
|
||||||
);
|
|
||||||
|
|
||||||
ferox_print(&msg, &PROGRESS_PRINTER);
|
|
||||||
|
|
||||||
try_send_message_to_file(
|
|
||||||
&msg,
|
|
||||||
tx_file.clone(),
|
|
||||||
!CONFIGURATION.output.is_empty(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ferox_response.status().is_redirection() {
|
log::trace!("exit: make_wildcard_request -> {}", ferox_response);
|
||||||
// show where it goes, if possible
|
|
||||||
if let Some(next_loc) = ferox_response.headers().get("Location") {
|
|
||||||
let next_loc_str = next_loc.to_str().unwrap_or("Unknown");
|
|
||||||
if !CONFIGURATION.quiet && !should_filter_response(&ferox_response) {
|
|
||||||
let msg = format!(
|
|
||||||
"{} {:>10} {} redirects to => {}\n",
|
|
||||||
wildcard,
|
|
||||||
content_len,
|
|
||||||
ferox_response.url(),
|
|
||||||
next_loc_str
|
|
||||||
);
|
|
||||||
|
|
||||||
ferox_print(&msg, &PROGRESS_PRINTER);
|
|
||||||
|
|
||||||
try_send_message_to_file(
|
|
||||||
&msg,
|
|
||||||
tx_file.clone(),
|
|
||||||
!CONFIGURATION.output.is_empty(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log::trace!("exit: make_wildcard_request -> {:?}", ferox_response);
|
|
||||||
return Some(ferox_response);
|
return Some(ferox_response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -293,35 +244,9 @@ pub async fn connectivity_test(target_urls: &[String]) -> Vec<String> {
|
|||||||
good_urls
|
good_urls
|
||||||
}
|
}
|
||||||
|
|
||||||
/// simple helper to keep DRY; sends a message using the transmitter side of the given mpsc channel
|
|
||||||
/// the receiver is expected to be the side that saves the message to CONFIGURATION.output.
|
|
||||||
fn try_send_message_to_file(msg: &str, tx_file: UnboundedSender<String>, save_output: bool) {
|
|
||||||
log::trace!("enter: try_send_message_to_file({}, {:?})", msg, tx_file);
|
|
||||||
|
|
||||||
if save_output {
|
|
||||||
match tx_file.send(msg.to_string()) {
|
|
||||||
Ok(_) => {
|
|
||||||
log::trace!(
|
|
||||||
"sent message from heuristics::try_send_message_to_file to file handler"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!(
|
|
||||||
"{} {}",
|
|
||||||
module_colorizer("heuristics::try_send_message_to_file"),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log::trace!("exit: try_send_message_to_file");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::FeroxChannel;
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// request a unique string of 32bytes * a value returns correct result
|
/// request a unique string of 32bytes * a value returns correct result
|
||||||
@@ -338,41 +263,4 @@ mod tests {
|
|||||||
assert_eq!(wcf.size, 0);
|
assert_eq!(wcf.size, 0);
|
||||||
assert_eq!(wcf.dynamic, 0);
|
assert_eq!(wcf.dynamic, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(core_threads = 1)]
|
|
||||||
/// tests that given a message and transmitter, the function sends the message across the
|
|
||||||
/// channel
|
|
||||||
async fn heuristics_try_send_message_to_file_sends_when_true() {
|
|
||||||
let (tx, mut rx): FeroxChannel<String> = mpsc::unbounded_channel();
|
|
||||||
let msg = "It really tied the room together.";
|
|
||||||
let should_save = true;
|
|
||||||
try_send_message_to_file(&msg, tx, should_save);
|
|
||||||
|
|
||||||
assert_eq!(rx.recv().await.unwrap(), msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(core_threads = 1)]
|
|
||||||
#[should_panic]
|
|
||||||
/// tests that when save_output is false, nothing is sent to the receiver
|
|
||||||
async fn heuristics_try_send_message_to_file_sends_when_false() {
|
|
||||||
let (tx, mut rx): FeroxChannel<String> = mpsc::unbounded_channel();
|
|
||||||
let msg = "I'm the Dude, so that's what you call me.";
|
|
||||||
let should_save = false;
|
|
||||||
try_send_message_to_file(&msg, tx, should_save);
|
|
||||||
|
|
||||||
assert_ne!(rx.recv().await.unwrap(), msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(core_threads = 1)]
|
|
||||||
/// tests that when save_output is true, but the receiver is closed, nothing is sent to the receiver
|
|
||||||
/// this test doesn't assert anything, but reaches the error block of the given function and
|
|
||||||
/// can be verified with --nocapture and RUST_LOG being set
|
|
||||||
async fn heuristics_try_send_message_to_file_sends_with_closed_receiver() {
|
|
||||||
env_logger::init();
|
|
||||||
let (tx, mut rx): FeroxChannel<String> = mpsc::unbounded_channel();
|
|
||||||
let msg = "Hey, nice marmot.";
|
|
||||||
let should_save = true;
|
|
||||||
rx.close();
|
|
||||||
try_send_message_to_file(&msg, tx, should_save);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
291
src/lib.rs
291
src/lib.rs
@@ -8,13 +8,15 @@ pub mod logger;
|
|||||||
pub mod parser;
|
pub mod parser;
|
||||||
pub mod progress;
|
pub mod progress;
|
||||||
pub mod reporter;
|
pub mod reporter;
|
||||||
|
pub mod scan_manager;
|
||||||
pub mod scanner;
|
pub mod scanner;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
use reqwest::{
|
use crate::utils::{get_url_path_length, status_colorizer};
|
||||||
header::HeaderMap,
|
use console::{style, Color};
|
||||||
{Response, StatusCode, Url},
|
use reqwest::{header::HeaderMap, Response, StatusCode, Url};
|
||||||
};
|
use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::{error, fmt};
|
use std::{error, fmt};
|
||||||
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
|
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||||
|
|
||||||
@@ -42,6 +44,9 @@ pub type FeroxChannel<T> = (UnboundedSender<T>, UnboundedReceiver<T>);
|
|||||||
/// Version pulled from Cargo.toml at compile time
|
/// Version pulled from Cargo.toml at compile time
|
||||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
/// Maximum number of file descriptors that can be opened during a scan
|
||||||
|
pub const DEFAULT_OPEN_FILE_LIMIT: usize = 8192;
|
||||||
|
|
||||||
/// Default wordlist to use when `-w|--wordlist` isn't specified and not `wordlist` isn't set
|
/// Default wordlist to use when `-w|--wordlist` isn't specified and not `wordlist` isn't set
|
||||||
/// in a [ferox-config.toml](constant.DEFAULT_CONFIG_NAME.html) config file.
|
/// in a [ferox-config.toml](constant.DEFAULT_CONFIG_NAME.html) config file.
|
||||||
///
|
///
|
||||||
@@ -81,6 +86,17 @@ pub const DEFAULT_STATUS_CODES: [StatusCode; 9] = [
|
|||||||
/// Expected location is in the same directory as the feroxbuster binary.
|
/// Expected location is in the same directory as the feroxbuster binary.
|
||||||
pub const DEFAULT_CONFIG_NAME: &str = "ferox-config.toml";
|
pub const DEFAULT_CONFIG_NAME: &str = "ferox-config.toml";
|
||||||
|
|
||||||
|
/// FeroxSerialize trait; represents different types that are Serialize and also implement
|
||||||
|
/// as_str / as_json methods
|
||||||
|
pub trait FeroxSerialize: Serialize {
|
||||||
|
/// Return a String representation of the object, generally the human readable version of the
|
||||||
|
/// implementor
|
||||||
|
fn as_str(&self) -> String;
|
||||||
|
|
||||||
|
/// Return an NDJSON representation of the object
|
||||||
|
fn as_json(&self) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
/// A `FeroxResponse`, derived from a `Response` to a submitted `Request`
|
/// A `FeroxResponse`, derived from a `Response` to a submitted `Request`
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FeroxResponse {
|
pub struct FeroxResponse {
|
||||||
@@ -98,6 +114,22 @@ pub struct FeroxResponse {
|
|||||||
|
|
||||||
/// The `Headers` of this `FeroxResponse`
|
/// The `Headers` of this `FeroxResponse`
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
|
|
||||||
|
/// Wildcard response status
|
||||||
|
wildcard: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implement Display for FeroxResponse
|
||||||
|
impl fmt::Display for FeroxResponse {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"FeroxResponse {{ url: {}, status: {}, content-length: {} }}",
|
||||||
|
self.url(),
|
||||||
|
self.status(),
|
||||||
|
self.content_length()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `FeroxResponse` implementation
|
/// `FeroxResponse` implementation
|
||||||
@@ -160,6 +192,19 @@ impl FeroxResponse {
|
|||||||
self.url.query_pairs().count() > 0 || has_extension
|
self.url.query_pairs().count() > 0 || has_extension
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns line count of the response text.
|
||||||
|
pub fn line_count(&self) -> usize {
|
||||||
|
self.text().lines().count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns word count of the response text.
|
||||||
|
pub fn word_count(&self) -> usize {
|
||||||
|
self.text()
|
||||||
|
.lines()
|
||||||
|
.map(|s| s.split_whitespace().count())
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new `FeroxResponse` from the given `Response`
|
/// Create a new `FeroxResponse` from the given `Response`
|
||||||
pub async fn from(response: Response, read_body: bool) -> Self {
|
pub async fn from(response: Response, read_body: bool) -> Self {
|
||||||
let url = response.url().clone();
|
let url = response.url().clone();
|
||||||
@@ -189,10 +234,206 @@ impl FeroxResponse {
|
|||||||
content_length,
|
content_length,
|
||||||
text,
|
text,
|
||||||
headers,
|
headers,
|
||||||
|
wildcard: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Implement FeroxSerialize for FeroxResponse
|
||||||
|
impl FeroxSerialize for FeroxResponse {
|
||||||
|
/// Simple wrapper around create_report_string
|
||||||
|
fn as_str(&self) -> String {
|
||||||
|
let lines = self.line_count().to_string();
|
||||||
|
let words = self.word_count().to_string();
|
||||||
|
let chars = self.content_length().to_string();
|
||||||
|
let status = self.status().as_str();
|
||||||
|
let wild_status = status_colorizer("WLD");
|
||||||
|
|
||||||
|
if self.wildcard {
|
||||||
|
// response is a wildcard, special messages abound when this is the case...
|
||||||
|
|
||||||
|
// create the base message
|
||||||
|
let mut message = format!(
|
||||||
|
"{} {:>8}l {:>8}w {:>8}c Got {} for {} (url length: {})\n",
|
||||||
|
wild_status,
|
||||||
|
lines,
|
||||||
|
words,
|
||||||
|
chars,
|
||||||
|
status_colorizer(&status),
|
||||||
|
self.url(),
|
||||||
|
get_url_path_length(&self.url())
|
||||||
|
);
|
||||||
|
|
||||||
|
if self.status().is_redirection() {
|
||||||
|
// when it's a redirect, show where it goes, if possible
|
||||||
|
if let Some(next_loc) = self.headers().get("Location") {
|
||||||
|
let next_loc_str = next_loc.to_str().unwrap_or("Unknown");
|
||||||
|
|
||||||
|
let redirect_msg = format!(
|
||||||
|
"{} {:>9} {:>9} {:>9} {} redirects to => {}\n",
|
||||||
|
wild_status,
|
||||||
|
"-",
|
||||||
|
"-",
|
||||||
|
"-",
|
||||||
|
self.url(),
|
||||||
|
next_loc_str
|
||||||
|
);
|
||||||
|
|
||||||
|
message.push_str(&redirect_msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// base message + redirection message (if appropriate)
|
||||||
|
message
|
||||||
|
} else {
|
||||||
|
// not a wildcard, just create a normal entry
|
||||||
|
utils::create_report_string(
|
||||||
|
self.status.as_str(),
|
||||||
|
&lines,
|
||||||
|
&words,
|
||||||
|
&chars,
|
||||||
|
self.url().as_str(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an NDJSON representation of the FeroxResponse
|
||||||
|
///
|
||||||
|
/// (expanded for clarity)
|
||||||
|
/// ex:
|
||||||
|
/// {
|
||||||
|
/// "type":"response",
|
||||||
|
/// "url":"https://localhost.com/images",
|
||||||
|
/// "path":"/images",
|
||||||
|
/// "status":301,
|
||||||
|
/// "content_length":179,
|
||||||
|
/// "line_count":10,
|
||||||
|
/// "word_count":16,
|
||||||
|
/// "headers":{
|
||||||
|
/// "x-content-type-options":"nosniff",
|
||||||
|
/// "strict-transport-security":"max-age=31536000; includeSubDomains",
|
||||||
|
/// "x-frame-options":"SAMEORIGIN",
|
||||||
|
/// "connection":"keep-alive",
|
||||||
|
/// "server":"nginx/1.16.1",
|
||||||
|
/// "content-type":"text/html; charset=UTF-8",
|
||||||
|
/// "referrer-policy":"origin-when-cross-origin",
|
||||||
|
/// "content-security-policy":"default-src 'none'",
|
||||||
|
/// "access-control-allow-headers":"X-Requested-With",
|
||||||
|
/// "x-xss-protection":"1; mode=block",
|
||||||
|
/// "content-length":"179",
|
||||||
|
/// "date":"Mon, 23 Nov 2020 15:33:24 GMT",
|
||||||
|
/// "location":"/images/",
|
||||||
|
/// "access-control-allow-origin":"https://localhost.com"
|
||||||
|
/// }
|
||||||
|
/// }\n
|
||||||
|
fn as_json(&self) -> String {
|
||||||
|
if let Ok(mut json) = serde_json::to_string(&self) {
|
||||||
|
json.push('\n');
|
||||||
|
json
|
||||||
|
} else {
|
||||||
|
format!("{{\"error\":\"could not convert {} to json\"}}", self.url())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize implementation for FeroxResponse
|
||||||
|
impl Serialize for FeroxResponse {
|
||||||
|
/// Function that handles serialization of a FeroxResponse to NDJSON
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let mut headers = HashMap::new();
|
||||||
|
let mut state = serializer.serialize_struct("FeroxResponse", 7)?;
|
||||||
|
|
||||||
|
// need to convert the HeaderMap to a HashMap in order to pass it to the serializer
|
||||||
|
for (key, value) in &self.headers {
|
||||||
|
let k = key.as_str().to_owned();
|
||||||
|
let v = String::from_utf8_lossy(value.as_bytes());
|
||||||
|
headers.insert(k, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.serialize_field("type", "response")?;
|
||||||
|
state.serialize_field("url", self.url.as_str())?;
|
||||||
|
state.serialize_field("path", self.url.path())?;
|
||||||
|
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("headers", &headers)?;
|
||||||
|
|
||||||
|
state.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
|
||||||
|
/// The log message
|
||||||
|
pub message: String,
|
||||||
|
|
||||||
|
/// The log level
|
||||||
|
pub level: String,
|
||||||
|
|
||||||
|
/// The number of seconds elapsed since the scan started
|
||||||
|
pub time_offset: f32,
|
||||||
|
|
||||||
|
/// The module from which log::* was called
|
||||||
|
pub module: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implementation of FeroxMessage
|
||||||
|
impl FeroxSerialize for FeroxMessage {
|
||||||
|
/// Create an NDJSON representation of the log message
|
||||||
|
///
|
||||||
|
/// (expanded for clarity)
|
||||||
|
/// ex:
|
||||||
|
/// {
|
||||||
|
/// "type": "log",
|
||||||
|
/// "message": "Sent https://localhost/api to file handler",
|
||||||
|
/// "level": "DEBUG",
|
||||||
|
/// "time_offset": 0.86333454,
|
||||||
|
/// "module": "feroxbuster::reporter"
|
||||||
|
/// }\n
|
||||||
|
fn as_json(&self) -> String {
|
||||||
|
if let Ok(mut json) = serde_json::to_string(&self) {
|
||||||
|
json.push('\n');
|
||||||
|
json
|
||||||
|
} else {
|
||||||
|
String::from("{\"error\":\"could not convert to json\"}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a string representation of the log message
|
||||||
|
///
|
||||||
|
/// ex: 301 10l 16w 173c https://localhost/api
|
||||||
|
fn as_str(&self) -> String {
|
||||||
|
let (level_name, level_color) = match self.level.as_str() {
|
||||||
|
"ERROR" => ("ERR", Color::Red),
|
||||||
|
"WARN" => ("WRN", Color::Red),
|
||||||
|
"INFO" => ("INF", Color::Cyan),
|
||||||
|
"DEBUG" => ("DBG", Color::Yellow),
|
||||||
|
"TRACE" => ("TRC", Color::Magenta),
|
||||||
|
"WILDCARD" => ("WLD", Color::Cyan),
|
||||||
|
_ => ("UNK", Color::White),
|
||||||
|
};
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"{} {:10.03} {} {}\n",
|
||||||
|
style(level_name).bg(level_color).black(),
|
||||||
|
style(self.time_offset).dim(),
|
||||||
|
self.module,
|
||||||
|
style(&self.message).dim(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -217,4 +458,46 @@ mod tests {
|
|||||||
fn default_version() {
|
fn default_version() {
|
||||||
assert_eq!(VERSION, env!("CARGO_PKG_VERSION"));
|
assert_eq!(VERSION, env!("CARGO_PKG_VERSION"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// test as_str method of FeroxMessage
|
||||||
|
fn ferox_message_as_str_returns_string_with_newline() {
|
||||||
|
let message = FeroxMessage {
|
||||||
|
message: "message".to_string(),
|
||||||
|
module: "utils".to_string(),
|
||||||
|
time_offset: 1.0,
|
||||||
|
level: "INFO".to_string(),
|
||||||
|
kind: "log".to_string(),
|
||||||
|
};
|
||||||
|
let message_str = message.as_str();
|
||||||
|
|
||||||
|
assert!(message_str.contains("INF"));
|
||||||
|
assert!(message_str.contains("1.000"));
|
||||||
|
assert!(message_str.contains("utils"));
|
||||||
|
assert!(message_str.contains("message"));
|
||||||
|
assert!(message_str.ends_with('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// test as_json method of FeroxMessage
|
||||||
|
fn ferox_message_as_json_returns_json_representation_of_ferox_message_with_newline() {
|
||||||
|
let message = FeroxMessage {
|
||||||
|
message: "message".to_string(),
|
||||||
|
module: "utils".to_string(),
|
||||||
|
time_offset: 1.0,
|
||||||
|
level: "INFO".to_string(),
|
||||||
|
kind: "log".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let message_str = message.as_json();
|
||||||
|
|
||||||
|
let error_margin = f32::EPSILON;
|
||||||
|
|
||||||
|
let json: FeroxMessage = serde_json::from_str(&message_str).unwrap();
|
||||||
|
assert_eq!(json.module, message.module);
|
||||||
|
assert_eq!(json.message, message.message);
|
||||||
|
assert!((json.time_offset - message.time_offset).abs() < error_margin);
|
||||||
|
assert_eq!(json.level, message.level);
|
||||||
|
assert_eq!(json.kind, message.kind);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
use crate::config::{CONFIGURATION, PROGRESS_PRINTER};
|
use crate::{
|
||||||
use crate::reporter::{get_cached_file_handle, safe_file_write};
|
config::{CONFIGURATION, PROGRESS_PRINTER},
|
||||||
use console::{style, Color};
|
reporter::safe_file_write,
|
||||||
|
utils::open_file,
|
||||||
|
FeroxMessage, FeroxSerialize,
|
||||||
|
};
|
||||||
use env_logger::Builder;
|
use env_logger::Builder;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
@@ -19,8 +22,8 @@ pub fn initialize(verbosity: u8) {
|
|||||||
0 => (),
|
0 => (),
|
||||||
1 => env::set_var("RUST_LOG", "warn"),
|
1 => env::set_var("RUST_LOG", "warn"),
|
||||||
2 => env::set_var("RUST_LOG", "info"),
|
2 => env::set_var("RUST_LOG", "info"),
|
||||||
3 => env::set_var("RUST_LOG", "debug,hyper=info,reqwest=info"),
|
3 => env::set_var("RUST_LOG", "feroxbuster=debug,info"),
|
||||||
_ => env::set_var("RUST_LOG", "trace,hyper=info,reqwest=info"),
|
_ => env::set_var("RUST_LOG", "feroxbuster=trace,info"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,43 +31,27 @@ pub fn initialize(verbosity: u8) {
|
|||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let mut builder = Builder::from_default_env();
|
let mut builder = Builder::from_default_env();
|
||||||
|
|
||||||
// I REALLY wanted the logger to also use the reporting channels found in the `reporter`
|
let debug_file = open_file(&CONFIGURATION.debug_log);
|
||||||
// module. However, in order to properly clean up the channels, all references to the
|
|
||||||
// transmitter side of a channel need to go out of scope, then you can await the future into
|
if let Some(buffered_file) = debug_file.clone() {
|
||||||
// which the receiver was moved.
|
// write out the configuration to the debug file if it exists
|
||||||
//
|
safe_file_write(&*CONFIGURATION, buffered_file, CONFIGURATION.json);
|
||||||
// The problem was that putting a transmitter reference in this closure, which gets initialized
|
}
|
||||||
// as part of the global logger, made it so that I couldn't destroy/leak/take/swap the last
|
|
||||||
// reference to allow the channels to gracefully close.
|
|
||||||
//
|
|
||||||
// The workaround was to have a RwLock around the file and allow both the logger and the
|
|
||||||
// file handler to both write independent of each other.
|
|
||||||
let locked_file = get_cached_file_handle(&CONFIGURATION.output);
|
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.format(move |_, record| {
|
.format(move |_, record| {
|
||||||
let t = start.elapsed().as_secs_f32();
|
let log_entry = FeroxMessage {
|
||||||
let level = record.level();
|
message: record.args().to_string(),
|
||||||
|
level: record.level().to_string(),
|
||||||
let (level_name, level_color) = match level {
|
time_offset: start.elapsed().as_secs_f32(),
|
||||||
log::Level::Error => ("ERR", Color::Red),
|
module: record.target().to_string(),
|
||||||
log::Level::Warn => ("WRN", Color::Red),
|
kind: "log".to_string(),
|
||||||
log::Level::Info => ("INF", Color::Cyan),
|
|
||||||
log::Level::Debug => ("DBG", Color::Yellow),
|
|
||||||
log::Level::Trace => ("TRC", Color::Magenta),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let msg = format!(
|
PROGRESS_PRINTER.println(&log_entry.as_str());
|
||||||
"{} {:10.03} {}\n",
|
|
||||||
style(level_name).bg(level_color).black(),
|
|
||||||
style(t).dim(),
|
|
||||||
style(record.args()).dim(),
|
|
||||||
);
|
|
||||||
|
|
||||||
PROGRESS_PRINTER.println(&msg);
|
if let Some(buffered_file) = debug_file.clone() {
|
||||||
|
safe_file_write(&log_entry, buffered_file, CONFIGURATION.json);
|
||||||
if let Some(buffered_file) = locked_file.clone() {
|
|
||||||
safe_file_write(&msg, buffered_file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
26
src/main.rs
26
src/main.rs
@@ -3,10 +3,13 @@ use feroxbuster::{
|
|||||||
banner,
|
banner,
|
||||||
config::{CONFIGURATION, PROGRESS_BAR, PROGRESS_PRINTER},
|
config::{CONFIGURATION, PROGRESS_BAR, PROGRESS_PRINTER},
|
||||||
heuristics, logger, reporter,
|
heuristics, logger, reporter,
|
||||||
scanner::{scan_url, PAUSE_SCAN},
|
scan_manager::PAUSE_SCAN,
|
||||||
|
scanner::{self, scan_url},
|
||||||
utils::{ferox_print, get_current_depth, module_colorizer, status_colorizer},
|
utils::{ferox_print, get_current_depth, module_colorizer, status_colorizer},
|
||||||
FeroxError, FeroxResponse, FeroxResult, SLEEP_DURATION, VERSION,
|
FeroxError, FeroxResponse, FeroxResult, SLEEP_DURATION, VERSION,
|
||||||
};
|
};
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
use feroxbuster::{utils::set_open_file_limit, DEFAULT_OPEN_FILE_LIMIT};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
@@ -94,7 +97,7 @@ fn get_unique_words_from_wordlist(path: &str) -> FeroxResult<Arc<HashSet<String>
|
|||||||
async fn scan(
|
async fn scan(
|
||||||
targets: Vec<String>,
|
targets: Vec<String>,
|
||||||
tx_term: UnboundedSender<FeroxResponse>,
|
tx_term: UnboundedSender<FeroxResponse>,
|
||||||
tx_file: UnboundedSender<String>,
|
tx_file: UnboundedSender<FeroxResponse>,
|
||||||
) -> FeroxResult<()> {
|
) -> FeroxResult<()> {
|
||||||
log::trace!("enter: scan({:?}, {:?}, {:?})", targets, tx_term, tx_file);
|
log::trace!("enter: scan({:?}, {:?}, {:?})", targets, tx_term, tx_file);
|
||||||
// cloning an Arc is cheap (it's basically a pointer into the heap)
|
// cloning an Arc is cheap (it's basically a pointer into the heap)
|
||||||
@@ -110,6 +113,16 @@ async fn scan(
|
|||||||
return Err(Box::new(err));
|
return Err(Box::new(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scanner::initialize(
|
||||||
|
words.len(),
|
||||||
|
CONFIGURATION.scan_limit,
|
||||||
|
&CONFIGURATION.extensions,
|
||||||
|
&CONFIGURATION.filter_status,
|
||||||
|
&CONFIGURATION.filter_line_count,
|
||||||
|
&CONFIGURATION.filter_word_count,
|
||||||
|
&CONFIGURATION.filter_size,
|
||||||
|
);
|
||||||
|
|
||||||
let mut tasks = vec![];
|
let mut tasks = vec![];
|
||||||
|
|
||||||
for target in targets {
|
for target in targets {
|
||||||
@@ -174,7 +187,6 @@ async fn wrapped_main() {
|
|||||||
|
|
||||||
// can't trace main until after logger is initialized and the above task is started
|
// can't trace main until after logger is initialized and the above task is started
|
||||||
log::trace!("enter: main");
|
log::trace!("enter: main");
|
||||||
log::debug!("{:#?}", *CONFIGURATION);
|
|
||||||
|
|
||||||
// spawn a thread that listens for keyboard input on stdin, when a user presses enter
|
// spawn a thread that listens for keyboard input on stdin, when a user presses enter
|
||||||
// the input handler will toggle PAUSE_SCAN, which in turn is used to pause and resume
|
// the input handler will toggle PAUSE_SCAN, which in turn is used to pause and resume
|
||||||
@@ -236,12 +248,12 @@ async fn wrapped_main() {
|
|||||||
async fn clean_up(
|
async fn clean_up(
|
||||||
tx_term: UnboundedSender<FeroxResponse>,
|
tx_term: UnboundedSender<FeroxResponse>,
|
||||||
term_handle: JoinHandle<()>,
|
term_handle: JoinHandle<()>,
|
||||||
tx_file: UnboundedSender<String>,
|
tx_file: UnboundedSender<FeroxResponse>,
|
||||||
file_handle: Option<JoinHandle<()>>,
|
file_handle: Option<JoinHandle<()>>,
|
||||||
save_output: bool,
|
save_output: bool,
|
||||||
) {
|
) {
|
||||||
log::trace!(
|
log::trace!(
|
||||||
"enter: clean_up({:?}, {:?}, {:?}, {:?}, {}",
|
"enter: clean_up({:?}, {:?}, {:?}, {:?}, {})",
|
||||||
tx_term,
|
tx_term,
|
||||||
term_handle,
|
term_handle,
|
||||||
tx_file,
|
tx_file,
|
||||||
@@ -294,6 +306,10 @@ fn main() {
|
|||||||
// setup logging based on the number of -v's used
|
// setup logging based on the number of -v's used
|
||||||
logger::initialize(CONFIGURATION.verbosity);
|
logger::initialize(CONFIGURATION.verbosity);
|
||||||
|
|
||||||
|
// this function uses rlimit, which is not supported on windows
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
set_open_file_limit(DEFAULT_OPEN_FILE_LIMIT);
|
||||||
|
|
||||||
if let Ok(mut runtime) = tokio::runtime::Runtime::new() {
|
if let Ok(mut runtime) = tokio::runtime::Runtime::new() {
|
||||||
let future = wrapped_main();
|
let future = wrapped_main();
|
||||||
runtime.block_on(future);
|
runtime.block_on(future);
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ pub fn initialize() -> App<'static, 'static> {
|
|||||||
.long("verbosity")
|
.long("verbosity")
|
||||||
.takes_value(false)
|
.takes_value(false)
|
||||||
.multiple(true)
|
.multiple(true)
|
||||||
.help("Increase verbosity level (use -vv or more for greater effect)"),
|
.help("Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v's is probably too much)"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("proxy")
|
Arg::with_name("proxy")
|
||||||
@@ -109,6 +109,12 @@ pub fn initialize() -> App<'static, 'static> {
|
|||||||
.takes_value(false)
|
.takes_value(false)
|
||||||
.help("Only print URLs; Don't print status codes, response size, running config, etc...")
|
.help("Only print URLs; Don't print status codes, response size, running config, etc...")
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("json")
|
||||||
|
.long("json")
|
||||||
|
.takes_value(false)
|
||||||
|
.help("Emit JSON logs to --output and --debug-log instead of normal text")
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("dont_filter")
|
Arg::with_name("dont_filter")
|
||||||
.short("D")
|
.short("D")
|
||||||
@@ -121,7 +127,14 @@ pub fn initialize() -> App<'static, 'static> {
|
|||||||
.short("o")
|
.short("o")
|
||||||
.long("output")
|
.long("output")
|
||||||
.value_name("FILE")
|
.value_name("FILE")
|
||||||
.help("Output file to write results to (default: stdout)")
|
.help("Output file to write results to (use w/ --json for JSON entries)")
|
||||||
|
.takes_value(true),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("debug_log")
|
||||||
|
.long("debug-log")
|
||||||
|
.value_name("FILE")
|
||||||
|
.help("Output file to write log entries (use w/ --json for JSON entries)")
|
||||||
.takes_value(true),
|
.takes_value(true),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
@@ -218,6 +231,30 @@ pub fn initialize() -> App<'static, 'static> {
|
|||||||
"Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)",
|
"Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("filter_words")
|
||||||
|
.short("W")
|
||||||
|
.long("filter-words")
|
||||||
|
.value_name("WORDS")
|
||||||
|
.takes_value(true)
|
||||||
|
.multiple(true)
|
||||||
|
.use_delimiter(true)
|
||||||
|
.help(
|
||||||
|
"Filter out messages of a particular word count (ex: -W 312 -W 91,82)",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("filter_lines")
|
||||||
|
.short("N")
|
||||||
|
.long("filter-lines")
|
||||||
|
.value_name("LINES")
|
||||||
|
.takes_value(true)
|
||||||
|
.multiple(true)
|
||||||
|
.use_delimiter(true)
|
||||||
|
.help(
|
||||||
|
"Filter out messages of a particular line count (ex: -N 20 -N 31,30)",
|
||||||
|
),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("filter_status")
|
Arg::with_name("filter_status")
|
||||||
.short("C")
|
.short("C")
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use crate::config::{CONFIGURATION, PROGRESS_PRINTER};
|
use crate::{
|
||||||
use crate::utils::{ferox_print, make_request, status_colorizer};
|
config::{CONFIGURATION, PROGRESS_PRINTER},
|
||||||
use crate::{FeroxChannel, FeroxResponse};
|
utils::{ferox_print, make_request, open_file},
|
||||||
|
FeroxChannel, FeroxResponse, FeroxSerialize,
|
||||||
|
};
|
||||||
use console::strip_ansi_codes;
|
use console::strip_ansi_codes;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::sync::{Arc, Once, RwLock};
|
use std::sync::{Arc, Once, RwLock};
|
||||||
@@ -41,14 +43,14 @@ pub fn initialize(
|
|||||||
save_output: bool,
|
save_output: bool,
|
||||||
) -> (
|
) -> (
|
||||||
UnboundedSender<FeroxResponse>,
|
UnboundedSender<FeroxResponse>,
|
||||||
UnboundedSender<String>,
|
UnboundedSender<FeroxResponse>,
|
||||||
JoinHandle<()>,
|
JoinHandle<()>,
|
||||||
Option<JoinHandle<()>>,
|
Option<JoinHandle<()>>,
|
||||||
) {
|
) {
|
||||||
log::trace!("enter: initialize({}, {})", output_file, save_output);
|
log::trace!("enter: initialize({}, {})", output_file, save_output);
|
||||||
|
|
||||||
let (tx_rpt, rx_rpt): FeroxChannel<FeroxResponse> = mpsc::unbounded_channel();
|
let (tx_rpt, rx_rpt): FeroxChannel<FeroxResponse> = mpsc::unbounded_channel();
|
||||||
let (tx_file, rx_file): FeroxChannel<String> = mpsc::unbounded_channel();
|
let (tx_file, rx_file): FeroxChannel<FeroxResponse> = mpsc::unbounded_channel();
|
||||||
|
|
||||||
let file_clone = tx_file.clone();
|
let file_clone = tx_file.clone();
|
||||||
|
|
||||||
@@ -81,7 +83,7 @@ pub fn initialize(
|
|||||||
/// reporting criteria
|
/// reporting criteria
|
||||||
async fn spawn_terminal_reporter(
|
async fn spawn_terminal_reporter(
|
||||||
mut resp_chan: UnboundedReceiver<FeroxResponse>,
|
mut resp_chan: UnboundedReceiver<FeroxResponse>,
|
||||||
file_chan: UnboundedSender<String>,
|
file_chan: UnboundedSender<FeroxResponse>,
|
||||||
save_output: bool,
|
save_output: bool,
|
||||||
) {
|
) {
|
||||||
log::trace!(
|
log::trace!(
|
||||||
@@ -95,28 +97,12 @@ async fn spawn_terminal_reporter(
|
|||||||
log::trace!("received {} on reporting channel", resp.url());
|
log::trace!("received {} on reporting channel", resp.url());
|
||||||
|
|
||||||
if CONFIGURATION.status_codes.contains(&resp.status().as_u16()) {
|
if CONFIGURATION.status_codes.contains(&resp.status().as_u16()) {
|
||||||
let report = if CONFIGURATION.quiet {
|
|
||||||
// -q used, just need the url
|
|
||||||
format!("{}\n", resp.url())
|
|
||||||
} else {
|
|
||||||
// normal printing with status and size
|
|
||||||
let status = status_colorizer(&resp.status().as_str());
|
|
||||||
format!(
|
|
||||||
// example output
|
|
||||||
// 200 3280 https://localhost.com/FAQ
|
|
||||||
"{} {:>10} {}\n",
|
|
||||||
status,
|
|
||||||
resp.content_length(),
|
|
||||||
resp.url()
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
// print to stdout
|
// print to stdout
|
||||||
ferox_print(&report, &PROGRESS_PRINTER);
|
ferox_print(&resp.as_str(), &PROGRESS_PRINTER);
|
||||||
|
|
||||||
if save_output {
|
if save_output {
|
||||||
// -o used, need to send the report to be written out to disk
|
// -o used, need to send the report to be written out to disk
|
||||||
match file_chan.send(report.to_string()) {
|
match file_chan.send(resp.clone()) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
log::debug!("Sent {} to file handler", resp.url());
|
log::debug!("Sent {} to file handler", resp.url());
|
||||||
}
|
}
|
||||||
@@ -148,7 +134,10 @@ async fn spawn_terminal_reporter(
|
|||||||
///
|
///
|
||||||
/// The consumer simply receives responses and writes them to the given output file if they meet
|
/// The consumer simply receives responses and writes them to the given output file if they meet
|
||||||
/// the given reporting criteria
|
/// the given reporting criteria
|
||||||
async fn spawn_file_reporter(mut report_channel: UnboundedReceiver<String>, output_file: &str) {
|
async fn spawn_file_reporter(
|
||||||
|
mut report_channel: UnboundedReceiver<FeroxResponse>,
|
||||||
|
output_file: &str,
|
||||||
|
) {
|
||||||
let buffered_file = match get_cached_file_handle(&CONFIGURATION.output) {
|
let buffered_file = match get_cached_file_handle(&CONFIGURATION.output) {
|
||||||
Some(file) => file,
|
Some(file) => file,
|
||||||
None => {
|
None => {
|
||||||
@@ -165,47 +154,33 @@ async fn spawn_file_reporter(mut report_channel: UnboundedReceiver<String>, outp
|
|||||||
|
|
||||||
log::info!("Writing scan results to {}", output_file);
|
log::info!("Writing scan results to {}", output_file);
|
||||||
|
|
||||||
while let Some(report) = report_channel.recv().await {
|
while let Some(response) = report_channel.recv().await {
|
||||||
safe_file_write(&report, buffered_file.clone());
|
safe_file_write(&response, buffered_file.clone(), CONFIGURATION.json);
|
||||||
}
|
}
|
||||||
|
|
||||||
log::trace!("exit: spawn_file_reporter");
|
log::trace!("exit: spawn_file_reporter");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Given the path to a file, open the file in append mode (create it if it doesn't exist) and
|
|
||||||
/// return a reference to the file that is buffered and locked
|
|
||||||
fn open_file(filename: &str) -> Option<Arc<RwLock<io::BufWriter<fs::File>>>> {
|
|
||||||
log::trace!("enter: open_file({})", filename);
|
|
||||||
|
|
||||||
match fs::OpenOptions::new() // std fs
|
|
||||||
.create(true)
|
|
||||||
.append(true)
|
|
||||||
.open(filename)
|
|
||||||
{
|
|
||||||
Ok(file) => {
|
|
||||||
let writer = io::BufWriter::new(file); // std io
|
|
||||||
|
|
||||||
let locked_file = Some(Arc::new(RwLock::new(writer)));
|
|
||||||
|
|
||||||
log::trace!("exit: open_file -> {:?}", locked_file);
|
|
||||||
locked_file
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("{}", e);
|
|
||||||
log::trace!("exit: open_file -> None");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Given a string and a reference to a locked buffered file, write the contents and flush
|
/// Given a string and a reference to a locked buffered file, write the contents and flush
|
||||||
/// the buffer to disk.
|
/// the buffer to disk.
|
||||||
pub fn safe_file_write(contents: &str, locked_file: Arc<RwLock<io::BufWriter<fs::File>>>) {
|
pub fn safe_file_write<T>(
|
||||||
|
value: &T,
|
||||||
|
locked_file: Arc<RwLock<io::BufWriter<fs::File>>>,
|
||||||
|
convert_to_json: bool,
|
||||||
|
) where
|
||||||
|
T: FeroxSerialize,
|
||||||
|
{
|
||||||
// note to future self: adding logging of anything other than error to this function
|
// note to future self: adding logging of anything other than error to this function
|
||||||
// is a bad idea. we call this function while processing records generated by the logger.
|
// is a bad idea. we call this function while processing records generated by the logger.
|
||||||
// If we then call log::... while already processing some logging output, it results in
|
// If we then call log::... while already processing some logging output, it results in
|
||||||
// the second log entry being injected into the first.
|
// the second log entry being injected into the first.
|
||||||
|
|
||||||
|
let contents = if convert_to_json {
|
||||||
|
value.as_json()
|
||||||
|
} else {
|
||||||
|
value.as_str()
|
||||||
|
};
|
||||||
|
|
||||||
let contents = strip_ansi_codes(&contents);
|
let contents = strip_ansi_codes(&contents);
|
||||||
|
|
||||||
if let Ok(mut handle) = locked_file.write() {
|
if let Ok(mut handle) = locked_file.write() {
|
||||||
|
|||||||
531
src/scan_manager.rs
Normal file
531
src/scan_manager.rs
Normal file
@@ -0,0 +1,531 @@
|
|||||||
|
use crate::{config::PROGRESS_PRINTER, progress, scanner::NUMBER_OF_REQUESTS, SLEEP_DURATION};
|
||||||
|
use console::style;
|
||||||
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use std::{
|
||||||
|
cmp::PartialEq,
|
||||||
|
fmt,
|
||||||
|
sync::{Arc, Mutex, RwLock},
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
io::{stderr, Write},
|
||||||
|
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||||
|
};
|
||||||
|
use tokio::{task::JoinHandle, time};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
/// A clock spinner protected with a RwLock to allow for a single thread to use at a time
|
||||||
|
// todo remove this when issue #107 is resolved
|
||||||
|
static ref SINGLE_SPINNER: RwLock<ProgressBar> = RwLock::new(get_single_spinner());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Single atomic number that gets incremented once, used to track first thread to interact with
|
||||||
|
/// when pausing a scan
|
||||||
|
static INTERACTIVE_BARRIER: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
|
/// Atomic boolean flag, used to determine whether or not a scan should pause or resume
|
||||||
|
pub static PAUSE_SCAN: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
/// Simple enum used to flag a `FeroxScan` as likely a directory or file
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ScanType {
|
||||||
|
File,
|
||||||
|
Directory,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Struct to hold scan-related state
|
||||||
|
///
|
||||||
|
/// The purpose of this container is to open up the pathway to aborting currently running tasks and
|
||||||
|
/// serialization of all scan state into a state file in order to resume scans that were cut short
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FeroxScan {
|
||||||
|
/// UUID that uniquely ID's the scan
|
||||||
|
pub id: String,
|
||||||
|
|
||||||
|
/// The URL that to be scanned
|
||||||
|
pub url: String,
|
||||||
|
|
||||||
|
/// The type of scan
|
||||||
|
pub scan_type: ScanType,
|
||||||
|
|
||||||
|
/// Whether or not this scan has completed
|
||||||
|
pub complete: bool,
|
||||||
|
|
||||||
|
/// The spawned tokio task performing this scan
|
||||||
|
pub task: Option<JoinHandle<()>>,
|
||||||
|
|
||||||
|
/// The progress bar associated with this scan
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a default FeroxScan, populates ID with a new UUID
|
||||||
|
fn default() -> Self {
|
||||||
|
let new_id = Uuid::new_v4().to_simple().to_string();
|
||||||
|
|
||||||
|
FeroxScan {
|
||||||
|
id: new_id,
|
||||||
|
task: None,
|
||||||
|
complete: false,
|
||||||
|
url: String::new(),
|
||||||
|
progress_bar: None,
|
||||||
|
scan_type: ScanType::File,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple helper to call .finish on the scan's progress bar
|
||||||
|
fn stop_progress_bar(&self) {
|
||||||
|
if let Some(pb) = &self.progress_bar {
|
||||||
|
pb.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple helper get a progress bar
|
||||||
|
pub fn progress_bar(&mut self) -> ProgressBar {
|
||||||
|
if let Some(pb) = &self.progress_bar {
|
||||||
|
pb.clone()
|
||||||
|
} else {
|
||||||
|
let num_requests = NUMBER_OF_REQUESTS.load(Ordering::Relaxed);
|
||||||
|
let pb = progress::add_bar(&self.url, num_requests, false);
|
||||||
|
|
||||||
|
pb.reset_elapsed();
|
||||||
|
|
||||||
|
self.progress_bar = Some(pb.clone());
|
||||||
|
|
||||||
|
pb
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a URL and ProgressBar, create a new FeroxScan, wrap it in an Arc and return it
|
||||||
|
pub fn new(url: &str, scan_type: ScanType, pb: Option<ProgressBar>) -> Arc<Mutex<Self>> {
|
||||||
|
let mut me = Self::default();
|
||||||
|
|
||||||
|
me.url = url.to_string();
|
||||||
|
me.scan_type = scan_type;
|
||||||
|
me.progress_bar = pb;
|
||||||
|
|
||||||
|
Arc::new(Mutex::new(me))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark the scan as complete and stop the scan's progress bar
|
||||||
|
pub fn finish(&mut self) {
|
||||||
|
self.complete = true;
|
||||||
|
self.stop_progress_bar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display implementation
|
||||||
|
impl fmt::Display for FeroxScan {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let complete = if self.complete {
|
||||||
|
style("complete").green()
|
||||||
|
} else {
|
||||||
|
style("incomplete").red()
|
||||||
|
};
|
||||||
|
|
||||||
|
write!(f, "{:10} {}", complete, self.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PartialEq implementation; uses FeroxScan.id for comparison
|
||||||
|
impl PartialEq for FeroxScan {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.id == other.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Container around a locked hashset of `FeroxScan`s, adds wrappers for insertion and searching
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct FeroxScans {
|
||||||
|
/// Internal structure: locked hashset of `FeroxScan`s
|
||||||
|
pub scans: Mutex<Vec<Arc<Mutex<FeroxScan>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implementation of `FeroxScans`
|
||||||
|
impl FeroxScans {
|
||||||
|
/// Add a `FeroxScan` to the internal container
|
||||||
|
///
|
||||||
|
/// If the internal container did NOT contain the scan, true is returned; else false
|
||||||
|
pub fn insert(&self, scan: Arc<Mutex<FeroxScan>>) -> bool {
|
||||||
|
let sentry = match scan.lock() {
|
||||||
|
Ok(locked_scan) => {
|
||||||
|
// If the container did contain the scan, set sentry to false
|
||||||
|
// If the container did not contain the scan, set sentry to true
|
||||||
|
!self.contains(&locked_scan.url)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// poisoned lock
|
||||||
|
log::error!("FeroxScan's ({:?}) mutex is poisoned: {}", self, e);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if sentry {
|
||||||
|
// can't update the internal container while the scan itself is locked, so first
|
||||||
|
// lock the scan and check the container for the scan's presence, then add if
|
||||||
|
// not found
|
||||||
|
match self.scans.lock() {
|
||||||
|
Ok(mut scans) => {
|
||||||
|
scans.push(scan);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("FeroxScans' container's mutex is poisoned: {}", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sentry
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple check for whether or not a FeroxScan is contained within the inner container based
|
||||||
|
/// on the given URL
|
||||||
|
pub fn contains(&self, url: &str) -> bool {
|
||||||
|
match self.scans.lock() {
|
||||||
|
Ok(scans) => {
|
||||||
|
for scan in scans.iter() {
|
||||||
|
if let Ok(locked_scan) = scan.lock() {
|
||||||
|
if locked_scan.url == url {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("FeroxScans' container's mutex is poisoned: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find and return a `FeroxScan` based on the given URL
|
||||||
|
pub fn get_scan_by_url(&self, url: &str) -> Option<Arc<Mutex<FeroxScan>>> {
|
||||||
|
if let Ok(scans) = self.scans.lock() {
|
||||||
|
for scan in scans.iter() {
|
||||||
|
if let Ok(locked_scan) = scan.lock() {
|
||||||
|
if locked_scan.url == url {
|
||||||
|
return Some(scan.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print all FeroxScans of type Directory
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// 0: complete https://10.129.45.20
|
||||||
|
/// 9: complete https://10.129.45.20/images
|
||||||
|
/// 10: complete https://10.129.45.20/assets
|
||||||
|
pub fn display_scans(&self) {
|
||||||
|
if let Ok(scans) = self.scans.lock() {
|
||||||
|
for (i, scan) in scans.iter().enumerate() {
|
||||||
|
if let Ok(unlocked_scan) = scan.lock() {
|
||||||
|
match unlocked_scan.scan_type {
|
||||||
|
ScanType::Directory => {
|
||||||
|
PROGRESS_PRINTER.println(format!("{:3}: {}", i, unlocked_scan));
|
||||||
|
}
|
||||||
|
ScanType::File => {
|
||||||
|
// we're only interested in displaying directory scans, as those are
|
||||||
|
// the only ones that make sense to be stopped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forced the calling thread into a busy loop
|
||||||
|
///
|
||||||
|
/// Every `SLEEP_DURATION` milliseconds, the function examines the result stored in `PAUSE_SCAN`
|
||||||
|
///
|
||||||
|
/// When the value stored in `PAUSE_SCAN` becomes `false`, the function returns, exiting the busy
|
||||||
|
/// loop
|
||||||
|
pub async fn pause(&self, get_user_input: bool) {
|
||||||
|
// function uses tokio::time, not std
|
||||||
|
|
||||||
|
// local testing showed a pretty slow increase (less than linear) in CPU usage as # of
|
||||||
|
// concurrent scans rose when SLEEP_DURATION was set to 500, using that as the default for now
|
||||||
|
let mut interval = time::interval(time::Duration::from_millis(SLEEP_DURATION));
|
||||||
|
|
||||||
|
// ignore any error returned
|
||||||
|
let _ = stderr().flush();
|
||||||
|
|
||||||
|
if INTERACTIVE_BARRIER.load(Ordering::Relaxed) == 0 {
|
||||||
|
INTERACTIVE_BARRIER.fetch_add(1, Ordering::Relaxed);
|
||||||
|
|
||||||
|
if get_user_input {
|
||||||
|
self.display_scans();
|
||||||
|
|
||||||
|
let mut user_input = String::new();
|
||||||
|
std::io::stdin().read_line(&mut user_input).unwrap();
|
||||||
|
// todo (issue #107) actual logic for parsing user input in a way that allows for
|
||||||
|
// calling .abort on the scan retrieved based on the input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if SINGLE_SPINNER.read().unwrap().is_finished() {
|
||||||
|
// todo remove this when issue #107 is resolved
|
||||||
|
|
||||||
|
// in order to not leave draw artifacts laying around in the terminal, we call
|
||||||
|
// finish_and_clear on the progress bar when resuming scans. For this reason, we need to
|
||||||
|
// check if the spinner is finished, and repopulate the RwLock with a new spinner if
|
||||||
|
// necessary
|
||||||
|
if let Ok(mut guard) = SINGLE_SPINNER.write() {
|
||||||
|
*guard = get_single_spinner();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(spinner) = SINGLE_SPINNER.write() {
|
||||||
|
spinner.enable_steady_tick(120);
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// first tick happens immediately, all others wait the specified duration
|
||||||
|
interval.tick().await;
|
||||||
|
|
||||||
|
if !PAUSE_SCAN.load(Ordering::Acquire) {
|
||||||
|
// PAUSE_SCAN is false, so we can exit the busy loop
|
||||||
|
|
||||||
|
if INTERACTIVE_BARRIER.load(Ordering::Relaxed) == 1 {
|
||||||
|
INTERACTIVE_BARRIER.fetch_sub(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(spinner) = SINGLE_SPINNER.write() {
|
||||||
|
// todo remove this when issue #107 is resolved
|
||||||
|
spinner.finish_and_clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = stderr().flush();
|
||||||
|
|
||||||
|
log::trace!("exit: pause_scan");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a url, create a new `FeroxScan` and add it to `FeroxScans`
|
||||||
|
///
|
||||||
|
/// If `FeroxScans` did not already contain the scan, return true; otherwise return false
|
||||||
|
///
|
||||||
|
/// Also return a reference to the new `FeroxScan`
|
||||||
|
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);
|
||||||
|
|
||||||
|
progress_bar.reset_elapsed();
|
||||||
|
|
||||||
|
Some(progress_bar)
|
||||||
|
}
|
||||||
|
ScanType::File => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let ferox_scan = FeroxScan::new(&url, scan_type, bar);
|
||||||
|
|
||||||
|
// If the set did not contain the scan, true is returned.
|
||||||
|
// If the set did contain the scan, false is returned.
|
||||||
|
let response = self.insert(ferox_scan.clone());
|
||||||
|
|
||||||
|
(response, ferox_scan)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a url, create a new `FeroxScan` and add it to `FeroxScans` as a Directory Scan
|
||||||
|
///
|
||||||
|
/// If `FeroxScans` did not already contain the scan, return true; otherwise return false
|
||||||
|
///
|
||||||
|
/// Also return a reference to the new `FeroxScan`
|
||||||
|
pub fn add_directory_scan(&self, url: &str) -> (bool, Arc<Mutex<FeroxScan>>) {
|
||||||
|
self.add_scan(&url, ScanType::Directory)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a url, create a new `FeroxScan` and add it to `FeroxScans` as a File Scan
|
||||||
|
///
|
||||||
|
/// If `FeroxScans` did not already contain the scan, return true; otherwise return false
|
||||||
|
///
|
||||||
|
/// Also return a reference to the new `FeroxScan`
|
||||||
|
pub fn add_file_scan(&self, url: &str) -> (bool, Arc<Mutex<FeroxScan>>) {
|
||||||
|
self.add_scan(&url, ScanType::File)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a clock spinner, used when scans are paused
|
||||||
|
// todo remove this when issue #107 is resolved
|
||||||
|
fn get_single_spinner() -> ProgressBar {
|
||||||
|
log::trace!("enter: get_single_spinner");
|
||||||
|
|
||||||
|
let spinner = ProgressBar::new_spinner().with_style(
|
||||||
|
ProgressStyle::default_spinner()
|
||||||
|
.tick_strings(&[
|
||||||
|
"🕛", "🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚",
|
||||||
|
])
|
||||||
|
.template(&format!(
|
||||||
|
"\t-= All Scans {{spinner}} {} =-",
|
||||||
|
style("Paused").red()
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
|
||||||
|
log::trace!("exit: get_single_spinner -> {:?}", spinner);
|
||||||
|
spinner
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// test that get_single_spinner returns the correct spinner
|
||||||
|
// todo remove this when issue #107 is resolved
|
||||||
|
fn scanner_get_single_spinner_returns_spinner() {
|
||||||
|
let spinner = get_single_spinner();
|
||||||
|
assert!(!spinner.is_finished());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(core_threads = 1)]
|
||||||
|
/// tests that pause_scan pauses execution and releases execution when PAUSE_SCAN is toggled
|
||||||
|
/// the spinner used during the test has had .finish_and_clear called on it, meaning that
|
||||||
|
/// a new one will be created, taking the if branch within the function
|
||||||
|
async fn scanner_pause_scan_with_finished_spinner() {
|
||||||
|
let now = time::Instant::now();
|
||||||
|
let urls = FeroxScans::default();
|
||||||
|
|
||||||
|
PAUSE_SCAN.store(true, Ordering::Relaxed);
|
||||||
|
|
||||||
|
let expected = time::Duration::from_secs(2);
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
time::delay_for(expected).await;
|
||||||
|
PAUSE_SCAN.store(false, Ordering::Relaxed);
|
||||||
|
});
|
||||||
|
|
||||||
|
urls.pause(false).await;
|
||||||
|
|
||||||
|
assert!(now.elapsed() > expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// add an unknown url to the hashset, expect true
|
||||||
|
fn add_url_to_list_of_scanned_urls_with_unknown_url() {
|
||||||
|
let urls = FeroxScans::default();
|
||||||
|
let url = "http://unknown_url";
|
||||||
|
let (result, _scan) = urls.add_scan(url, ScanType::Directory);
|
||||||
|
assert_eq!(result, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// add a known url to the hashset, with a trailing slash, expect false
|
||||||
|
fn add_url_to_list_of_scanned_urls_with_known_url() {
|
||||||
|
let urls = FeroxScans::default();
|
||||||
|
let pb = ProgressBar::new(1);
|
||||||
|
let url = "http://unknown_url/";
|
||||||
|
let scan = FeroxScan::new(url, ScanType::Directory, Some(pb));
|
||||||
|
|
||||||
|
assert_eq!(urls.insert(scan), true);
|
||||||
|
|
||||||
|
let (result, _scan) = urls.add_scan(url, ScanType::Directory);
|
||||||
|
|
||||||
|
assert_eq!(result, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// abort should call stop_progress_bar, marking it as finished
|
||||||
|
fn abort_stops_progress_bar() {
|
||||||
|
let pb = ProgressBar::new(1);
|
||||||
|
let url = "http://unknown_url/";
|
||||||
|
let scan = FeroxScan::new(url, ScanType::Directory, Some(pb));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
scan.lock()
|
||||||
|
.unwrap()
|
||||||
|
.progress_bar
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.is_finished(),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
scan.lock().unwrap().abort();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
scan.lock()
|
||||||
|
.unwrap()
|
||||||
|
.progress_bar
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.is_finished(),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// add a known url to the hashset, without a trailing slash, expect false
|
||||||
|
fn add_url_to_list_of_scanned_urls_with_known_url_without_slash() {
|
||||||
|
let urls = FeroxScans::default();
|
||||||
|
let url = "http://unknown_url";
|
||||||
|
let scan = FeroxScan::new(url, ScanType::File, None);
|
||||||
|
|
||||||
|
assert_eq!(urls.insert(scan), true);
|
||||||
|
|
||||||
|
let (result, _scan) = urls.add_scan(url, ScanType::File);
|
||||||
|
|
||||||
|
assert_eq!(result, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// just increasing coverage, no real expectations
|
||||||
|
fn call_display_scans() {
|
||||||
|
let urls = FeroxScans::default();
|
||||||
|
let pb = ProgressBar::new(1);
|
||||||
|
let pb_two = ProgressBar::new(2);
|
||||||
|
let url = "http://unknown_url/";
|
||||||
|
let url_two = "http://unknown_url/fa";
|
||||||
|
let scan = FeroxScan::new(url, ScanType::Directory, Some(pb));
|
||||||
|
let scan_two = FeroxScan::new(url_two, ScanType::Directory, Some(pb_two));
|
||||||
|
|
||||||
|
scan_two.lock().unwrap().finish(); // one complete, one incomplete
|
||||||
|
|
||||||
|
assert_eq!(urls.insert(scan), true);
|
||||||
|
|
||||||
|
urls.display_scans();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// ensure that PartialEq compares FeroxScan.id fields
|
||||||
|
fn partial_eq_compares_the_id_field() {
|
||||||
|
let url = "http://unknown_url/";
|
||||||
|
let scan = FeroxScan::new(url, ScanType::Directory, None);
|
||||||
|
let scan_two = FeroxScan::new(url, ScanType::Directory, None);
|
||||||
|
|
||||||
|
assert!(!scan.lock().unwrap().eq(&scan_two.lock().unwrap()));
|
||||||
|
|
||||||
|
scan_two.lock().unwrap().id = scan.lock().unwrap().id.clone();
|
||||||
|
|
||||||
|
assert!(scan.lock().unwrap().eq(&scan_two.lock().unwrap()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// show that a new progress bar is created if one doesn't exist
|
||||||
|
fn ferox_scan_get_progress_bar_when_none_is_set() {
|
||||||
|
let mut scan = FeroxScan::default();
|
||||||
|
|
||||||
|
assert!(scan.progress_bar.is_none()); // no pb exists
|
||||||
|
|
||||||
|
let pb = scan.progress_bar();
|
||||||
|
|
||||||
|
assert!(scan.progress_bar.is_some()); // new pb created
|
||||||
|
assert!(!pb.is_finished()) // not finished
|
||||||
|
}
|
||||||
|
}
|
||||||
392
src/scanner.rs
392
src/scanner.rs
@@ -1,25 +1,25 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::CONFIGURATION,
|
config::CONFIGURATION,
|
||||||
extractor::get_links,
|
extractor::get_links,
|
||||||
filters::{FeroxFilter, StatusCodeFilter, WildcardFilter},
|
filters::{
|
||||||
heuristics, progress,
|
FeroxFilter, LinesFilter, SizeFilter, StatusCodeFilter, WildcardFilter, WordsFilter,
|
||||||
|
},
|
||||||
|
heuristics,
|
||||||
|
scan_manager::{FeroxScans, PAUSE_SCAN},
|
||||||
utils::{format_url, get_current_depth, make_request},
|
utils::{format_url, get_current_depth, make_request},
|
||||||
FeroxChannel, FeroxResponse, SLEEP_DURATION,
|
FeroxChannel, FeroxResponse,
|
||||||
};
|
};
|
||||||
use console::style;
|
|
||||||
use futures::{
|
use futures::{
|
||||||
future::{BoxFuture, FutureExt},
|
future::{BoxFuture, FutureExt},
|
||||||
stream, StreamExt,
|
stream, StreamExt,
|
||||||
};
|
};
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
convert::TryInto,
|
convert::TryInto,
|
||||||
io::{stderr, Write},
|
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
|
sync::atomic::{AtomicU64, AtomicUsize, Ordering},
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
};
|
};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
@@ -28,21 +28,17 @@ use tokio::{
|
|||||||
Semaphore,
|
Semaphore,
|
||||||
},
|
},
|
||||||
task::JoinHandle,
|
task::JoinHandle,
|
||||||
time,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Single atomic number that gets incremented once, used to track first scan vs. all others
|
/// Single atomic number that gets incremented once, used to track first scan vs. all others
|
||||||
static CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
|
static CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
/// Atomic boolean flag, used to determine whether or not a scan should pause or resume
|
/// Single atomic number that gets holds the number of requests to be sent per directory scanned
|
||||||
pub static PAUSE_SCAN: AtomicBool = AtomicBool::new(false);
|
pub static NUMBER_OF_REQUESTS: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
/// Set of urls that have been sent to [scan_url](fn.scan_url.html), used for deduplication
|
/// Set of urls that have been sent to [scan_url](fn.scan_url.html), used for deduplication
|
||||||
static ref SCANNED_URLS: RwLock<HashSet<String>> = RwLock::new(HashSet::new());
|
pub static ref SCANNED_URLS: FeroxScans = FeroxScans::default();
|
||||||
|
|
||||||
/// A clock spinner protected with a RwLock to allow for a single thread to use at a time
|
|
||||||
static ref SINGLE_SPINNER: RwLock<ProgressBar> = RwLock::new(get_single_spinner());
|
|
||||||
|
|
||||||
/// Vector of implementors of the FeroxFilter trait
|
/// 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()));
|
static ref FILTERS: Arc<RwLock<Vec<Box<dyn FeroxFilter>>>> = Arc::new(RwLock::new(Vec::<Box<dyn FeroxFilter>>::new()));
|
||||||
@@ -51,109 +47,6 @@ lazy_static! {
|
|||||||
static ref SCAN_LIMITER: Semaphore = Semaphore::new(CONFIGURATION.scan_limit);
|
static ref SCAN_LIMITER: Semaphore = Semaphore::new(CONFIGURATION.scan_limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a clock spinner, used when scans are paused
|
|
||||||
fn get_single_spinner() -> ProgressBar {
|
|
||||||
log::trace!("enter: get_single_spinner");
|
|
||||||
|
|
||||||
let spinner = ProgressBar::new_spinner().with_style(
|
|
||||||
ProgressStyle::default_spinner()
|
|
||||||
.tick_strings(&[
|
|
||||||
"🕛", "🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚",
|
|
||||||
])
|
|
||||||
.template(&format!(
|
|
||||||
"\t-= All Scans {{spinner}} {} =-",
|
|
||||||
style("Paused").red()
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
|
|
||||||
log::trace!("exit: get_single_spinner -> {:?}", spinner);
|
|
||||||
spinner
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Forced the calling thread into a busy loop
|
|
||||||
///
|
|
||||||
/// Every `SLEEP_DURATION` milliseconds, the function examines the result stored in `PAUSE_SCAN`
|
|
||||||
///
|
|
||||||
/// When the value stored in `PAUSE_SCAN` becomes `false`, the function returns, exiting the busy
|
|
||||||
/// loop
|
|
||||||
async fn pause_scan() {
|
|
||||||
log::trace!("enter: pause_scan");
|
|
||||||
// function uses tokio::time, not std
|
|
||||||
|
|
||||||
// local testing showed a pretty slow increase (less than linear) in CPU usage as # of
|
|
||||||
// concurrent scans rose when SLEEP_DURATION was set to 500, using that as the default for now
|
|
||||||
let mut interval = time::interval(time::Duration::from_millis(SLEEP_DURATION));
|
|
||||||
|
|
||||||
// ignore any error returned
|
|
||||||
let _ = stderr().flush();
|
|
||||||
|
|
||||||
if SINGLE_SPINNER.read().unwrap().is_finished() {
|
|
||||||
// in order to not leave draw artifacts laying around in the terminal, we call
|
|
||||||
// finish_and_clear on the progress bar when resuming scans. For this reason, we need to
|
|
||||||
// check if the spinner is finished, and repopulate the RwLock with a new spinner if
|
|
||||||
// necessary
|
|
||||||
if let Ok(mut guard) = SINGLE_SPINNER.write() {
|
|
||||||
*guard = get_single_spinner();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(spinner) = SINGLE_SPINNER.write() {
|
|
||||||
spinner.enable_steady_tick(120);
|
|
||||||
}
|
|
||||||
|
|
||||||
loop {
|
|
||||||
// first tick happens immediately, all others wait the specified duration
|
|
||||||
interval.tick().await;
|
|
||||||
|
|
||||||
if !PAUSE_SCAN.load(Ordering::Acquire) {
|
|
||||||
// PAUSE_SCAN is false, so we can exit the busy loop
|
|
||||||
if let Ok(spinner) = SINGLE_SPINNER.write() {
|
|
||||||
spinner.finish_and_clear();
|
|
||||||
}
|
|
||||||
let _ = stderr().flush();
|
|
||||||
log::trace!("exit: pause_scan");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds the given url to `SCANNED_URLS`
|
|
||||||
///
|
|
||||||
/// If `SCANNED_URLS` did not already contain the url, return true; otherwise return false
|
|
||||||
fn add_url_to_list_of_scanned_urls(resp: &str, scanned_urls: &RwLock<HashSet<String>>) -> bool {
|
|
||||||
log::trace!(
|
|
||||||
"enter: add_url_to_list_of_scanned_urls({}, {:?})",
|
|
||||||
resp,
|
|
||||||
scanned_urls
|
|
||||||
);
|
|
||||||
|
|
||||||
match scanned_urls.write() {
|
|
||||||
// check new url against what's already been scanned
|
|
||||||
Ok(mut urls) => {
|
|
||||||
let normalized_url = if resp.ends_with('/') {
|
|
||||||
// append a / to the list of 'seen' urls, this is to prevent the case where
|
|
||||||
// 3xx and 2xx duplicate eachother
|
|
||||||
resp.to_string()
|
|
||||||
} else {
|
|
||||||
format!("{}/", resp)
|
|
||||||
};
|
|
||||||
|
|
||||||
// If the set did not contain resp, true is returned.
|
|
||||||
// If the set did contain resp, false is returned.
|
|
||||||
let response = urls.insert(normalized_url);
|
|
||||||
|
|
||||||
log::trace!("exit: add_url_to_list_of_scanned_urls -> {}", response);
|
|
||||||
response
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
// poisoned lock
|
|
||||||
log::error!("Set of scanned urls poisoned: {}", e);
|
|
||||||
log::trace!("exit: add_url_to_list_of_scanned_urls -> false");
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds the given FeroxFilter to the given list of FeroxFilter implementors
|
/// Adds the given FeroxFilter to the given list of FeroxFilter implementors
|
||||||
///
|
///
|
||||||
/// If the given list did not already contain the filter, return true; otherwise return false
|
/// If the given list did not already contain the filter, return true; otherwise return false
|
||||||
@@ -198,7 +91,7 @@ fn spawn_recursion_handler(
|
|||||||
wordlist: Arc<HashSet<String>>,
|
wordlist: Arc<HashSet<String>>,
|
||||||
base_depth: usize,
|
base_depth: usize,
|
||||||
tx_term: UnboundedSender<FeroxResponse>,
|
tx_term: UnboundedSender<FeroxResponse>,
|
||||||
tx_file: UnboundedSender<String>,
|
tx_file: UnboundedSender<FeroxResponse>,
|
||||||
) -> BoxFuture<'static, Vec<JoinHandle<()>>> {
|
) -> BoxFuture<'static, Vec<JoinHandle<()>>> {
|
||||||
log::trace!(
|
log::trace!(
|
||||||
"enter: spawn_recursion_handler({:?}, wordlist[{} words...], {}, {:?}, {:?})",
|
"enter: spawn_recursion_handler({:?}, wordlist[{} words...], {}, {:?}, {:?})",
|
||||||
@@ -213,7 +106,7 @@ fn spawn_recursion_handler(
|
|||||||
let mut scans = vec![];
|
let mut scans = vec![];
|
||||||
|
|
||||||
while let Some(resp) = recursion_channel.recv().await {
|
while let Some(resp) = recursion_channel.recv().await {
|
||||||
let unknown = add_url_to_list_of_scanned_urls(&resp, &SCANNED_URLS);
|
let (unknown, _) = SCANNED_URLS.add_directory_scan(&resp);
|
||||||
|
|
||||||
if !unknown {
|
if !unknown {
|
||||||
// not unknown, i.e. we've seen the url before and don't need to scan again
|
// not unknown, i.e. we've seen the url before and don't need to scan again
|
||||||
@@ -227,7 +120,7 @@ fn spawn_recursion_handler(
|
|||||||
let resp_clone = resp.clone();
|
let resp_clone = resp.clone();
|
||||||
let list_clone = wordlist.clone();
|
let list_clone = wordlist.clone();
|
||||||
|
|
||||||
scans.push(tokio::spawn(async move {
|
let future = tokio::spawn(async move {
|
||||||
scan_url(
|
scan_url(
|
||||||
resp_clone.to_owned().as_str(),
|
resp_clone.to_owned().as_str(),
|
||||||
list_clone,
|
list_clone,
|
||||||
@@ -236,7 +129,9 @@ fn spawn_recursion_handler(
|
|||||||
file_clone,
|
file_clone,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}));
|
});
|
||||||
|
|
||||||
|
scans.push(future);
|
||||||
}
|
}
|
||||||
scans
|
scans
|
||||||
}
|
}
|
||||||
@@ -293,7 +188,7 @@ fn create_urls(target_url: &str, word: &str, extensions: &[String]) -> Vec<Url>
|
|||||||
/// handles 2xx and 3xx responses by either checking if the url ends with a / (2xx)
|
/// handles 2xx and 3xx responses by either checking if the url ends with a / (2xx)
|
||||||
/// or if the Location header is present and matches the base url + / (3xx)
|
/// or if the Location header is present and matches the base url + / (3xx)
|
||||||
fn response_is_directory(response: &FeroxResponse) -> bool {
|
fn response_is_directory(response: &FeroxResponse) -> bool {
|
||||||
log::trace!("enter: is_directory({:?})", response);
|
log::trace!("enter: is_directory({})", response);
|
||||||
|
|
||||||
if response.status().is_redirection() {
|
if response.status().is_redirection() {
|
||||||
// status code is 3xx
|
// status code is 3xx
|
||||||
@@ -319,10 +214,7 @@ fn response_is_directory(response: &FeroxResponse) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
log::debug!(
|
log::debug!("expected Location header, but none was found: {}", response);
|
||||||
"expected Location header, but none was found: {:?}",
|
|
||||||
response
|
|
||||||
);
|
|
||||||
log::trace!("exit: is_directory -> false");
|
log::trace!("exit: is_directory -> false");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -378,7 +270,7 @@ async fn try_recursion(
|
|||||||
transmitter: UnboundedSender<String>,
|
transmitter: UnboundedSender<String>,
|
||||||
) {
|
) {
|
||||||
log::trace!(
|
log::trace!(
|
||||||
"enter: try_recursion({:?}, {}, {:?})",
|
"enter: try_recursion({}, {}, {:?})",
|
||||||
response,
|
response,
|
||||||
base_depth,
|
base_depth,
|
||||||
transmitter
|
transmitter
|
||||||
@@ -426,16 +318,6 @@ async fn try_recursion(
|
|||||||
/// Simple helper to stay DRY; determines whether or not a given `FeroxResponse` should be reported
|
/// Simple helper to stay DRY; determines whether or not a given `FeroxResponse` should be reported
|
||||||
/// to the user or not.
|
/// to the user or not.
|
||||||
pub fn should_filter_response(response: &FeroxResponse) -> bool {
|
pub fn should_filter_response(response: &FeroxResponse) -> bool {
|
||||||
if CONFIGURATION
|
|
||||||
.filter_size
|
|
||||||
.contains(&response.content_length())
|
|
||||||
{
|
|
||||||
// filtered value from --filter-size, size filters and wildcards are two separate filters
|
|
||||||
// and are applied independently
|
|
||||||
log::debug!("size filter: filtered out {}", response.url());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
match FILTERS.read() {
|
match FILTERS.read() {
|
||||||
Ok(filters) => {
|
Ok(filters) => {
|
||||||
for filter in filters.iter() {
|
for filter in filters.iter() {
|
||||||
@@ -478,7 +360,7 @@ async fn make_requests(
|
|||||||
for url in urls {
|
for url in urls {
|
||||||
if let Ok(response) = make_request(&CONFIGURATION.client, &url).await {
|
if let Ok(response) = make_request(&CONFIGURATION.client, &url).await {
|
||||||
// response came back without error, convert it to FeroxResponse
|
// response came back without error, convert it to FeroxResponse
|
||||||
let ferox_response = FeroxResponse::from(response, CONFIGURATION.extract_links).await;
|
let ferox_response = FeroxResponse::from(response, true).await;
|
||||||
|
|
||||||
// do recursion if appropriate
|
// do recursion if appropriate
|
||||||
if !CONFIGURATION.no_recursion {
|
if !CONFIGURATION.no_recursion {
|
||||||
@@ -496,13 +378,6 @@ async fn make_requests(
|
|||||||
let new_links = get_links(&ferox_response).await;
|
let new_links = get_links(&ferox_response).await;
|
||||||
|
|
||||||
for new_link in new_links {
|
for new_link in new_links {
|
||||||
let unknown = add_url_to_list_of_scanned_urls(&new_link, &SCANNED_URLS);
|
|
||||||
|
|
||||||
if !unknown {
|
|
||||||
// not unknown, i.e. we've seen the url before and don't need to scan again
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a url based on the given command line options, continue on error
|
// create a url based on the given command line options, continue on error
|
||||||
let new_url = match format_url(
|
let new_url = match format_url(
|
||||||
&new_link,
|
&new_link,
|
||||||
@@ -515,14 +390,18 @@ async fn make_requests(
|
|||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if SCANNED_URLS.get_scan_by_url(&new_url.to_string()).is_some() {
|
||||||
|
//we've seen the url before and don't need to scan again
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// make the request and store the response
|
// make the request and store the response
|
||||||
let new_response = match make_request(&CONFIGURATION.client, &new_url).await {
|
let new_response = match make_request(&CONFIGURATION.client, &new_url).await {
|
||||||
Ok(resp) => resp,
|
Ok(resp) => resp,
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut new_ferox_response =
|
let mut new_ferox_response = FeroxResponse::from(new_response, true).await;
|
||||||
FeroxResponse::from(new_response, CONFIGURATION.extract_links).await;
|
|
||||||
|
|
||||||
// filter if necessary
|
// filter if necessary
|
||||||
if should_filter_response(&new_ferox_response) {
|
if should_filter_response(&new_ferox_response) {
|
||||||
@@ -531,11 +410,9 @@ async fn make_requests(
|
|||||||
|
|
||||||
if new_ferox_response.is_file() {
|
if new_ferox_response.is_file() {
|
||||||
// very likely a file, simply request and report
|
// very likely a file, simply request and report
|
||||||
log::debug!(
|
log::debug!("Singular extraction: {}", new_ferox_response);
|
||||||
"Singular extraction: {} ({})",
|
|
||||||
new_ferox_response.url(),
|
SCANNED_URLS.add_file_scan(&new_url.to_string());
|
||||||
new_ferox_response.status().as_str(),
|
|
||||||
);
|
|
||||||
|
|
||||||
send_report(report_chan.clone(), new_ferox_response);
|
send_report(report_chan.clone(), new_ferox_response);
|
||||||
|
|
||||||
@@ -543,11 +420,7 @@ async fn make_requests(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !CONFIGURATION.no_recursion {
|
if !CONFIGURATION.no_recursion {
|
||||||
log::debug!(
|
log::debug!("Recursive extraction: {}", new_ferox_response);
|
||||||
"Recursive extraction: {} ({})",
|
|
||||||
new_ferox_response.url(),
|
|
||||||
new_ferox_response.status().as_str()
|
|
||||||
);
|
|
||||||
|
|
||||||
if new_ferox_response.status().is_success()
|
if new_ferox_response.status().is_success()
|
||||||
&& !new_ferox_response.url().as_str().ends_with('/')
|
&& !new_ferox_response.url().as_str().ends_with('/')
|
||||||
@@ -573,7 +446,7 @@ async fn make_requests(
|
|||||||
|
|
||||||
/// Simple helper to send a `FeroxResponse` over the tx side of an `mpsc::unbounded_channel`
|
/// Simple helper to send a `FeroxResponse` over the tx side of an `mpsc::unbounded_channel`
|
||||||
fn send_report(report_sender: UnboundedSender<FeroxResponse>, response: FeroxResponse) {
|
fn send_report(report_sender: UnboundedSender<FeroxResponse>, response: FeroxResponse) {
|
||||||
log::trace!("enter: send_report({:?}, {:?}", report_sender, response);
|
log::trace!("enter: send_report({:?}, {}", report_sender, response);
|
||||||
|
|
||||||
match report_sender.send(response) {
|
match report_sender.send(response) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
@@ -593,7 +466,7 @@ pub async fn scan_url(
|
|||||||
wordlist: Arc<HashSet<String>>,
|
wordlist: Arc<HashSet<String>>,
|
||||||
base_depth: usize,
|
base_depth: usize,
|
||||||
tx_term: UnboundedSender<FeroxResponse>,
|
tx_term: UnboundedSender<FeroxResponse>,
|
||||||
tx_file: UnboundedSender<String>,
|
tx_file: UnboundedSender<FeroxResponse>,
|
||||||
) {
|
) {
|
||||||
log::trace!(
|
log::trace!(
|
||||||
"enter: scan_url({:?}, wordlist[{} words...], {}, {:?}, {:?})",
|
"enter: scan_url({:?}, wordlist[{} words...], {}, {:?}, {:?})",
|
||||||
@@ -608,31 +481,33 @@ pub async fn scan_url(
|
|||||||
|
|
||||||
let (tx_dir, rx_dir): FeroxChannel<String> = mpsc::unbounded_channel();
|
let (tx_dir, rx_dir): FeroxChannel<String> = mpsc::unbounded_channel();
|
||||||
|
|
||||||
let num_reqs_expected: u64 = if CONFIGURATION.extensions.is_empty() {
|
|
||||||
wordlist.len().try_into().unwrap()
|
|
||||||
} else {
|
|
||||||
let total = wordlist.len() * (CONFIGURATION.extensions.len() + 1);
|
|
||||||
total.try_into().unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
let progress_bar = progress::add_bar(&target_url, num_reqs_expected, false);
|
|
||||||
progress_bar.reset_elapsed();
|
|
||||||
|
|
||||||
if CALL_COUNT.load(Ordering::Relaxed) == 0 {
|
if CALL_COUNT.load(Ordering::Relaxed) == 0 {
|
||||||
CALL_COUNT.fetch_add(1, Ordering::Relaxed);
|
CALL_COUNT.fetch_add(1, Ordering::Relaxed);
|
||||||
|
|
||||||
// this protection allows us to add the first scanned url to SCANNED_URLS
|
// this protection allows us to add the first scanned url to SCANNED_URLS
|
||||||
// from within the scan_url function instead of the recursion handler
|
// from within the scan_url function instead of the recursion handler
|
||||||
add_url_to_list_of_scanned_urls(&target_url, &SCANNED_URLS);
|
SCANNED_URLS.add_directory_scan(&target_url);
|
||||||
|
|
||||||
if CONFIGURATION.scan_limit == 0 {
|
|
||||||
// scan_limit == 0 means no limit should be imposed... however, scoping the Semaphore
|
|
||||||
// permit is tricky, so as a workaround, we'll add a ridiculous number of permits to
|
|
||||||
// the semaphore (1,152,921,504,606,846,975 to be exact) and call that 'unlimited'
|
|
||||||
SCAN_LIMITER.add_permits(usize::MAX >> 4);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let ferox_scan = match SCANNED_URLS.get_scan_by_url(&target_url) {
|
||||||
|
Some(scan) => scan,
|
||||||
|
None => {
|
||||||
|
log::error!(
|
||||||
|
"Could not find FeroxScan associated with {}; this shouldn't happen... exiting",
|
||||||
|
target_url
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let progress_bar = match ferox_scan.lock() {
|
||||||
|
Ok(mut scan) => scan.progress_bar(),
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("FeroxScan's ({:?}) mutex is poisoned: {}", ferox_scan, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// When acquire is called and the semaphore has remaining permits, the function immediately
|
// When acquire is called and the semaphore has remaining permits, the function immediately
|
||||||
// returns a permit. However, if no remaining permits are available, acquire (asynchronously)
|
// returns a permit. However, if no remaining permits are available, acquire (asynchronously)
|
||||||
// waits until an outstanding permit is dropped. At this point, the freed permit is assigned
|
// waits until an outstanding permit is dropped. At this point, the freed permit is assigned
|
||||||
@@ -641,7 +516,7 @@ pub async fn scan_url(
|
|||||||
|
|
||||||
// Arc clones to be passed around to the various scans
|
// Arc clones to be passed around to the various scans
|
||||||
let wildcard_bar = progress_bar.clone();
|
let wildcard_bar = progress_bar.clone();
|
||||||
let heuristics_file_clone = tx_file.clone();
|
let heuristics_term_clone = tx_term.clone();
|
||||||
let recurser_term_clone = tx_term.clone();
|
let recurser_term_clone = tx_term.clone();
|
||||||
let recurser_file_clone = tx_file.clone();
|
let recurser_file_clone = tx_file.clone();
|
||||||
let recurser_words = wordlist.clone();
|
let recurser_words = wordlist.clone();
|
||||||
@@ -660,22 +535,13 @@ pub async fn scan_url(
|
|||||||
|
|
||||||
// add any wildcard filters to `FILTERS`
|
// add any wildcard filters to `FILTERS`
|
||||||
let filter =
|
let filter =
|
||||||
match heuristics::wildcard_test(&target_url, wildcard_bar, heuristics_file_clone).await {
|
match heuristics::wildcard_test(&target_url, wildcard_bar, heuristics_term_clone).await {
|
||||||
Some(f) => Box::new(f),
|
Some(f) => Box::new(f),
|
||||||
None => Box::new(WildcardFilter::default()),
|
None => Box::new(WildcardFilter::default()),
|
||||||
};
|
};
|
||||||
|
|
||||||
add_filter_to_list_of_ferox_filters(filter, FILTERS.clone());
|
add_filter_to_list_of_ferox_filters(filter, FILTERS.clone());
|
||||||
|
|
||||||
// add any status code filters to `FILTERS`
|
|
||||||
for code_filter in &CONFIGURATION.filter_status {
|
|
||||||
let filter = StatusCodeFilter {
|
|
||||||
filter_code: *code_filter,
|
|
||||||
};
|
|
||||||
let boxed_filter = Box::new(filter);
|
|
||||||
add_filter_to_list_of_ferox_filters(boxed_filter, FILTERS.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// producer tasks (mp of mpsc); responsible for making requests
|
// producer tasks (mp of mpsc); responsible for making requests
|
||||||
let producers = stream::iter(looping_words.deref().to_owned())
|
let producers = stream::iter(looping_words.deref().to_owned())
|
||||||
.map(|word| {
|
.map(|word| {
|
||||||
@@ -689,7 +555,9 @@ pub async fn scan_url(
|
|||||||
// for every word in the wordlist, check to see if PAUSE_SCAN is set to true
|
// for every word in the wordlist, check to see if PAUSE_SCAN is set to true
|
||||||
// when true; enter a busy loop that only exits by setting PAUSE_SCAN back
|
// when true; enter a busy loop that only exits by setting PAUSE_SCAN back
|
||||||
// to false
|
// to false
|
||||||
pause_scan().await;
|
|
||||||
|
// todo change to true when issue #107 is resolved
|
||||||
|
SCANNED_URLS.pause(false).await;
|
||||||
}
|
}
|
||||||
make_requests(&tgt, &word, base_depth, txd, txr).await
|
make_requests(&tgt, &word, base_depth, txd, txr).await
|
||||||
}),
|
}),
|
||||||
@@ -715,7 +583,9 @@ pub async fn scan_url(
|
|||||||
// drop the current permit so the semaphore will allow another scan to proceed
|
// drop the current permit so the semaphore will allow another scan to proceed
|
||||||
drop(permit);
|
drop(permit);
|
||||||
|
|
||||||
progress_bar.finish();
|
if let Ok(mut scan) = ferox_scan.lock() {
|
||||||
|
scan.finish();
|
||||||
|
}
|
||||||
|
|
||||||
// manually drop tx in order for the rx task's while loops to eval to false
|
// manually drop tx in order for the rx task's while loops to eval to false
|
||||||
log::trace!("dropped recursion handler's transmitter");
|
log::trace!("dropped recursion handler's transmitter");
|
||||||
@@ -729,6 +599,84 @@ pub async fn scan_url(
|
|||||||
log::trace!("exit: scan_url");
|
log::trace!("exit: scan_url");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Perform steps necessary to run scans that only need to be performed once (warming up the
|
||||||
|
/// engine, as it were)
|
||||||
|
pub fn initialize(
|
||||||
|
num_words: usize,
|
||||||
|
scan_limit: usize,
|
||||||
|
extensions: &[String],
|
||||||
|
status_code_filters: &[u16],
|
||||||
|
lines_filters: &[usize],
|
||||||
|
words_filters: &[usize],
|
||||||
|
size_filters: &[u64],
|
||||||
|
) {
|
||||||
|
log::trace!(
|
||||||
|
"enter: initialize({}, {}, {:?}, {:?}, {:?}, {:?}, {:?})",
|
||||||
|
num_words,
|
||||||
|
scan_limit,
|
||||||
|
extensions,
|
||||||
|
status_code_filters,
|
||||||
|
lines_filters,
|
||||||
|
words_filters,
|
||||||
|
size_filters,
|
||||||
|
);
|
||||||
|
|
||||||
|
// number of requests only needs to be calculated once, and then can be reused
|
||||||
|
let num_reqs_expected: u64 = if extensions.is_empty() {
|
||||||
|
num_words.try_into().unwrap()
|
||||||
|
} else {
|
||||||
|
let total = num_words * (extensions.len() + 1);
|
||||||
|
total.try_into().unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
NUMBER_OF_REQUESTS.store(num_reqs_expected, Ordering::Relaxed);
|
||||||
|
|
||||||
|
// add any status code filters to `FILTERS` (-C|--filter-status)
|
||||||
|
for code_filter in status_code_filters {
|
||||||
|
let filter = StatusCodeFilter {
|
||||||
|
filter_code: *code_filter,
|
||||||
|
};
|
||||||
|
let boxed_filter = Box::new(filter);
|
||||||
|
add_filter_to_list_of_ferox_filters(boxed_filter, FILTERS.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// add any line count filters to `FILTERS` (-N|--filter-lines)
|
||||||
|
for lines_filter in lines_filters {
|
||||||
|
let filter = LinesFilter {
|
||||||
|
line_count: *lines_filter,
|
||||||
|
};
|
||||||
|
let boxed_filter = Box::new(filter);
|
||||||
|
add_filter_to_list_of_ferox_filters(boxed_filter, FILTERS.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// add any line count filters to `FILTERS` (-W|--filter-words)
|
||||||
|
for words_filter in words_filters {
|
||||||
|
let filter = WordsFilter {
|
||||||
|
word_count: *words_filter,
|
||||||
|
};
|
||||||
|
let boxed_filter = Box::new(filter);
|
||||||
|
add_filter_to_list_of_ferox_filters(boxed_filter, FILTERS.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// add any line count filters to `FILTERS` (-S|--filter-size)
|
||||||
|
for size_filter in size_filters {
|
||||||
|
let filter = SizeFilter {
|
||||||
|
content_length: *size_filter,
|
||||||
|
};
|
||||||
|
let boxed_filter = Box::new(filter);
|
||||||
|
add_filter_to_list_of_ferox_filters(boxed_filter, FILTERS.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if scan_limit == 0 {
|
||||||
|
// scan_limit == 0 means no limit should be imposed... however, scoping the Semaphore
|
||||||
|
// permit is tricky, so as a workaround, we'll add a ridiculous number of permits to
|
||||||
|
// the semaphore (1,152,921,504,606,846,975 to be exact) and call that 'unlimited'
|
||||||
|
SCAN_LIMITER.add_permits(usize::MAX >> 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
log::trace!("exit: initialize");
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -826,68 +774,4 @@ mod tests {
|
|||||||
let result = reached_max_depth(&url, 0, 2);
|
let result = reached_max_depth(&url, 0, 2);
|
||||||
assert!(result);
|
assert!(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// add an unknown url to the hashset, expect true
|
|
||||||
fn add_url_to_list_of_scanned_urls_with_unknown_url() {
|
|
||||||
let urls = RwLock::new(HashSet::<String>::new());
|
|
||||||
let url = "http://unknown_url";
|
|
||||||
assert_eq!(add_url_to_list_of_scanned_urls(url, &urls), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// add a known url to the hashset, with a trailing slash, expect false
|
|
||||||
fn add_url_to_list_of_scanned_urls_with_known_url() {
|
|
||||||
let urls = RwLock::new(HashSet::<String>::new());
|
|
||||||
let url = "http://unknown_url/";
|
|
||||||
|
|
||||||
assert_eq!(urls.write().unwrap().insert(url.to_string()), true);
|
|
||||||
|
|
||||||
assert_eq!(add_url_to_list_of_scanned_urls(url, &urls), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// add a known url to the hashset, without a trailing slash, expect false
|
|
||||||
fn add_url_to_list_of_scanned_urls_with_known_url_without_slash() {
|
|
||||||
let urls = RwLock::new(HashSet::<String>::new());
|
|
||||||
let url = "http://unknown_url";
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
urls.write()
|
|
||||||
.unwrap()
|
|
||||||
.insert("http://unknown_url/".to_string()),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(add_url_to_list_of_scanned_urls(url, &urls), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// test that get_single_spinner returns the correct spinner
|
|
||||||
fn scanner_get_single_spinner_returns_spinner() {
|
|
||||||
let spinner = get_single_spinner();
|
|
||||||
assert!(!spinner.is_finished());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(core_threads = 1)]
|
|
||||||
/// tests that pause_scan pauses execution and releases execution when PAUSE_SCAN is toggled
|
|
||||||
/// the spinner used during the test has had .finish_and_clear called on it, meaning that
|
|
||||||
/// a new one will be created, taking the if branch within the function
|
|
||||||
async fn scanner_pause_scan_with_finished_spinner() {
|
|
||||||
let now = time::Instant::now();
|
|
||||||
|
|
||||||
PAUSE_SCAN.store(true, Ordering::Relaxed);
|
|
||||||
SINGLE_SPINNER.write().unwrap().finish_and_clear();
|
|
||||||
|
|
||||||
let expected = time::Duration::from_secs(2);
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
time::delay_for(expected).await;
|
|
||||||
PAUSE_SCAN.store(false, Ordering::Relaxed);
|
|
||||||
});
|
|
||||||
|
|
||||||
pause_scan().await;
|
|
||||||
|
|
||||||
assert!(now.elapsed() > expected);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
183
src/utils.rs
183
src/utils.rs
@@ -1,9 +1,41 @@
|
|||||||
use crate::{FeroxError, FeroxResult};
|
use crate::{
|
||||||
|
config::{CONFIGURATION, PROGRESS_PRINTER},
|
||||||
|
FeroxError, FeroxResult,
|
||||||
|
};
|
||||||
use console::{strip_ansi_codes, style, user_attended};
|
use console::{strip_ansi_codes, style, user_attended};
|
||||||
use indicatif::ProgressBar;
|
use indicatif::ProgressBar;
|
||||||
use reqwest::Url;
|
use reqwest::{Client, Response, Url};
|
||||||
use reqwest::{Client, Response};
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
use rlimit::{getrlimit, setrlimit, Resource, Rlim};
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::{fs, io};
|
||||||
|
|
||||||
|
/// Given the path to a file, open the file in append mode (create it if it doesn't exist) and
|
||||||
|
/// return a reference to the file that is buffered and locked
|
||||||
|
pub fn open_file(filename: &str) -> Option<Arc<RwLock<io::BufWriter<fs::File>>>> {
|
||||||
|
log::trace!("enter: open_file({})", filename);
|
||||||
|
|
||||||
|
match fs::OpenOptions::new() // std fs
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(filename)
|
||||||
|
{
|
||||||
|
Ok(file) => {
|
||||||
|
let writer = io::BufWriter::new(file); // std io
|
||||||
|
|
||||||
|
let locked_file = Some(Arc::new(RwLock::new(writer)));
|
||||||
|
|
||||||
|
log::trace!("exit: open_file -> {:?}", locked_file);
|
||||||
|
locked_file
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("{}", e);
|
||||||
|
log::trace!("exit: open_file -> None");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Helper function that determines the current depth of a given url
|
/// Helper function that determines the current depth of a given url
|
||||||
///
|
///
|
||||||
@@ -19,13 +51,7 @@ use std::convert::TryInto;
|
|||||||
pub fn get_current_depth(target: &str) -> usize {
|
pub fn get_current_depth(target: &str) -> usize {
|
||||||
log::trace!("enter: get_current_depth({})", target);
|
log::trace!("enter: get_current_depth({})", target);
|
||||||
|
|
||||||
let target = if !target.ends_with('/') {
|
let target = normalize_url(target);
|
||||||
// target url doesn't end with a /, for the purposes of determining depth, we'll normalize
|
|
||||||
// all urls to end in a / and then calculate accordingly
|
|
||||||
format!("{}/", target)
|
|
||||||
} else {
|
|
||||||
String::from(target)
|
|
||||||
};
|
|
||||||
|
|
||||||
match Url::parse(&target) {
|
match Url::parse(&target) {
|
||||||
Ok(url) => {
|
Ok(url) => {
|
||||||
@@ -88,8 +114,8 @@ pub fn get_url_path_length(url: &Url) -> u64 {
|
|||||||
|
|
||||||
let path = url.path();
|
let path = url.path();
|
||||||
|
|
||||||
let segments = if path.starts_with('/') {
|
let segments = if let Some(split) = path.strip_prefix('/') {
|
||||||
path[1..].split_terminator('/')
|
split.split_terminator('/')
|
||||||
} else {
|
} else {
|
||||||
log::trace!("exit: get_url_path_length -> 0");
|
log::trace!("exit: get_url_path_length -> 0");
|
||||||
return 0;
|
return 0;
|
||||||
@@ -242,7 +268,6 @@ pub async fn make_request(client: &Client, url: &Url) -> FeroxResult<Response> {
|
|||||||
|
|
||||||
match client.get(url.to_owned()).send().await {
|
match client.get(url.to_owned()).send().await {
|
||||||
Ok(resp) => {
|
Ok(resp) => {
|
||||||
log::debug!("requested Url: {}", resp.url());
|
|
||||||
log::trace!("exit: make_request -> {:?}", resp);
|
log::trace!("exit: make_request -> {:?}", resp);
|
||||||
Ok(resp)
|
Ok(resp)
|
||||||
}
|
}
|
||||||
@@ -251,6 +276,19 @@ pub async fn make_request(client: &Client, url: &Url) -> FeroxResult<Response> {
|
|||||||
if e.to_string().contains("operation timed out") {
|
if e.to_string().contains("operation timed out") {
|
||||||
// only warn for timeouts, while actual errors are still left as errors
|
// only warn for timeouts, while actual errors are still left as errors
|
||||||
log::warn!("Error while making request: {}", e);
|
log::warn!("Error while making request: {}", e);
|
||||||
|
} else if e.is_redirect() {
|
||||||
|
if let Some(last_redirect) = e.url() {
|
||||||
|
// get where we were headed (last_redirect) and where we came from (url)
|
||||||
|
let fancy_message = format!("{} !=> {}", url, last_redirect);
|
||||||
|
|
||||||
|
let report = if let Some(msg_status) = e.status() {
|
||||||
|
create_report_string(msg_status.as_str(), "-1", "-1", "-1", &fancy_message)
|
||||||
|
} else {
|
||||||
|
create_report_string("UNK", "-1", "-1", "-1", &fancy_message)
|
||||||
|
};
|
||||||
|
|
||||||
|
ferox_print(&report, &PROGRESS_PRINTER)
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
log::error!("Error while making request: {}", e);
|
log::error!("Error while making request: {}", e);
|
||||||
}
|
}
|
||||||
@@ -259,10 +297,129 @@ pub async fn make_request(client: &Client, url: &Url) -> FeroxResult<Response> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper to create the standard line for output to file/terminal
|
||||||
|
///
|
||||||
|
/// example output:
|
||||||
|
/// 200 127l 283w 4134c http://localhost/faq
|
||||||
|
pub fn create_report_string(
|
||||||
|
status: &str,
|
||||||
|
line_count: &str,
|
||||||
|
word_count: &str,
|
||||||
|
content_length: &str,
|
||||||
|
url: &str,
|
||||||
|
) -> String {
|
||||||
|
if CONFIGURATION.quiet {
|
||||||
|
// -q used, just need the url
|
||||||
|
format!("{}\n", url)
|
||||||
|
} else {
|
||||||
|
// normal printing with status and sizes
|
||||||
|
let color_status = status_colorizer(status);
|
||||||
|
format!(
|
||||||
|
"{} {:>8}l {:>8}w {:>8}c {}\n",
|
||||||
|
color_status, line_count, word_count, content_length, url
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to set the soft limit for the RLIMIT_NOFILE resource
|
||||||
|
///
|
||||||
|
/// RLIMIT_NOFILE is the maximum number of file descriptors that can be opened by this process
|
||||||
|
///
|
||||||
|
/// The soft limit is the value that the kernel enforces for the corresponding resource.
|
||||||
|
/// The hard limit acts as a ceiling for the soft limit: an unprivileged process may set only its
|
||||||
|
/// soft limit to a value in the range from 0 up to the hard limit, and (irreversibly) lower its
|
||||||
|
/// hard limit.
|
||||||
|
///
|
||||||
|
/// A child process created via fork(2) inherits its parent's resource limits. Resource limits are
|
||||||
|
/// per-process attributes that are shared by all of the threads in a process.
|
||||||
|
///
|
||||||
|
/// Based on the above information, no attempt is made to restore the limit to its pre-scan value
|
||||||
|
/// as the adjustment made here is only valid for the scan itself (and any child processes, of which
|
||||||
|
/// there are none).
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
pub fn set_open_file_limit(limit: usize) -> bool {
|
||||||
|
log::trace!("enter: set_open_file_limit");
|
||||||
|
|
||||||
|
if let Ok((soft, hard)) = getrlimit(Resource::NOFILE) {
|
||||||
|
if hard.as_usize() > limit {
|
||||||
|
// our default open file limit is less than the current hard limit, this means we can
|
||||||
|
// set the soft limit to our default
|
||||||
|
let new_soft_limit = Rlim::from_usize(limit);
|
||||||
|
|
||||||
|
if setrlimit(Resource::NOFILE, new_soft_limit, hard).is_ok() {
|
||||||
|
log::debug!("set open file descriptor limit to {}", limit);
|
||||||
|
|
||||||
|
log::trace!("exit: set_open_file_limit -> {}", true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if soft != hard {
|
||||||
|
// hard limit is lower than our default, the next best option is to set the soft limit as
|
||||||
|
// high as the hard limit will allow
|
||||||
|
if setrlimit(Resource::NOFILE, hard, hard).is_ok() {
|
||||||
|
log::debug!("set open file descriptor limit to {}", limit);
|
||||||
|
|
||||||
|
log::trace!("exit: set_open_file_limit -> {}", true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// failed to set a new limit, as limit adjustments are a 'nice to have', we'll just log
|
||||||
|
// and move along
|
||||||
|
log::warn!("could not set open file descriptor limit to {}", limit);
|
||||||
|
|
||||||
|
log::trace!("exit: set_open_file_limit -> {}", false);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple helper to abstract away adding a forward-slash to a url if not present
|
||||||
|
///
|
||||||
|
/// used mostly for deduplication purposes and url state tracking
|
||||||
|
pub fn normalize_url(url: &str) -> String {
|
||||||
|
log::trace!("enter: normalize_url({})", url);
|
||||||
|
|
||||||
|
let normalized = if url.ends_with('/') {
|
||||||
|
url.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}/", url)
|
||||||
|
};
|
||||||
|
|
||||||
|
log::trace!("exit: normalize_url -> {}", normalized);
|
||||||
|
normalized
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// set_open_file_limit with a low requested limit succeeds
|
||||||
|
fn utils_set_open_file_limit_with_low_requested_limit() {
|
||||||
|
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
|
||||||
|
let lower_limit = hard.as_usize() - 1;
|
||||||
|
assert!(set_open_file_limit(lower_limit));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// set_open_file_limit with a high requested limit succeeds
|
||||||
|
fn utils_set_open_file_limit_with_high_requested_limit() {
|
||||||
|
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
|
||||||
|
let higher_limit = hard.as_usize() + 1;
|
||||||
|
// calculate a new soft to ensure soft != hard and hit that logic branch
|
||||||
|
let new_soft = Rlim::from_usize(hard.as_usize() - 1);
|
||||||
|
setrlimit(Resource::NOFILE, new_soft, hard).unwrap();
|
||||||
|
assert!(set_open_file_limit(higher_limit));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// set_open_file_limit should fail when hard == soft
|
||||||
|
fn utils_set_open_file_limit_with_fails_when_both_limits_are_equal() {
|
||||||
|
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
|
||||||
|
// calculate a new soft to ensure soft == hard and hit the failure logic branch
|
||||||
|
setrlimit(Resource::NOFILE, hard, hard).unwrap();
|
||||||
|
assert!(!set_open_file_limit(hard.as_usize())); // returns false
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// base url returns 1
|
/// base url returns 1
|
||||||
fn get_current_depth_base_url_returns_1() {
|
fn get_current_depth_base_url_returns_1() {
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ fn banner_prints_headers() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
#[test]
|
#[test]
|
||||||
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
||||||
/// expect to see all mandatory prints + multiple size filters
|
/// expect to see all mandatory prints + multiple size filters
|
||||||
fn banner_prints_filter_sizes() -> Result<(), Box<dyn std::error::Error>> {
|
fn banner_prints_filter_sizes() {
|
||||||
Command::cargo_bin("feroxbuster")
|
Command::cargo_bin("feroxbuster")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.arg("--url")
|
.arg("--url")
|
||||||
@@ -126,6 +126,14 @@ fn banner_prints_filter_sizes() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.arg("789456123")
|
.arg("789456123")
|
||||||
.arg("--filter-size")
|
.arg("--filter-size")
|
||||||
.arg("44444444")
|
.arg("44444444")
|
||||||
|
.arg("-N")
|
||||||
|
.arg("678")
|
||||||
|
.arg("--filter-lines")
|
||||||
|
.arg("679")
|
||||||
|
.arg("-W")
|
||||||
|
.arg("93")
|
||||||
|
.arg("--filter-words")
|
||||||
|
.arg("94")
|
||||||
.assert()
|
.assert()
|
||||||
.success()
|
.success()
|
||||||
.stderr(
|
.stderr(
|
||||||
@@ -138,11 +146,16 @@ fn banner_prints_filter_sizes() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.and(predicate::str::contains("Timeout (secs)"))
|
.and(predicate::str::contains("Timeout (secs)"))
|
||||||
.and(predicate::str::contains("User-Agent"))
|
.and(predicate::str::contains("User-Agent"))
|
||||||
.and(predicate::str::contains("Size Filter"))
|
.and(predicate::str::contains("Size Filter"))
|
||||||
|
.and(predicate::str::contains("Word Count Filter"))
|
||||||
|
.and(predicate::str::contains("Line Count Filter"))
|
||||||
.and(predicate::str::contains("789456123"))
|
.and(predicate::str::contains("789456123"))
|
||||||
.and(predicate::str::contains("44444444"))
|
.and(predicate::str::contains("44444444"))
|
||||||
|
.and(predicate::str::contains("93"))
|
||||||
|
.and(predicate::str::contains("94"))
|
||||||
|
.and(predicate::str::contains("678"))
|
||||||
|
.and(predicate::str::contains("679"))
|
||||||
.and(predicate::str::contains("─┴─")),
|
.and(predicate::str::contains("─┴─")),
|
||||||
);
|
);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -688,3 +701,56 @@ fn banner_prints_filter_status() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
||||||
|
/// expect to see all mandatory prints + json
|
||||||
|
fn banner_prints_json() {
|
||||||
|
Command::cargo_bin("feroxbuster")
|
||||||
|
.unwrap()
|
||||||
|
.arg("--url")
|
||||||
|
.arg("http://localhost")
|
||||||
|
.arg("--json")
|
||||||
|
.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("JSON Output"))
|
||||||
|
.and(predicate::str::contains("│ true"))
|
||||||
|
.and(predicate::str::contains("─┴─")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
||||||
|
/// expect to see all mandatory prints + json
|
||||||
|
fn banner_prints_debug_log() {
|
||||||
|
Command::cargo_bin("feroxbuster")
|
||||||
|
.unwrap()
|
||||||
|
.arg("--url")
|
||||||
|
.arg("http://localhost")
|
||||||
|
.arg("--debug-log")
|
||||||
|
.arg("im-a-debug-log.hurr-durr")
|
||||||
|
.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("Debugging Log"))
|
||||||
|
.and(predicate::str::contains("│ im-a-debug-log.hurr-durr"))
|
||||||
|
.and(predicate::str::contains("─┴─")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -131,10 +131,10 @@ fn extractor_finds_relative_url() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
#[test]
|
#[test]
|
||||||
/// send a request to a page that contains an relative link, follow it, and find the same link again
|
/// send a request to a page that contains an relative link, follow it, and find the same link again
|
||||||
/// should follow then filter
|
/// should follow then filter
|
||||||
fn extractor_finds_same_relative_url_twice() -> Result<(), Box<dyn std::error::Error>> {
|
fn extractor_finds_same_relative_url_twice() {
|
||||||
let srv = MockServer::start();
|
let srv = MockServer::start();
|
||||||
let (tmp_dir, file) =
|
let (tmp_dir, file) =
|
||||||
setup_tmp_directory(&["LICENSE".to_string(), "README".to_string()], "wordlist")?;
|
setup_tmp_directory(&["LICENSE".to_string(), "README".to_string()], "wordlist").unwrap();
|
||||||
|
|
||||||
let mock = Mock::new()
|
let mock = Mock::new()
|
||||||
.expect_method(GET)
|
.expect_method(GET)
|
||||||
@@ -177,7 +177,6 @@ fn extractor_finds_same_relative_url_twice() -> Result<(), Box<dyn std::error::E
|
|||||||
assert_eq!(mock_two.times_called(), 1);
|
assert_eq!(mock_two.times_called(), 1);
|
||||||
assert_eq!(mock_three.times_called(), 1);
|
assert_eq!(mock_three.times_called(), 1);
|
||||||
teardown_tmp_directory(tmp_dir);
|
teardown_tmp_directory(tmp_dir);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -44,11 +44,158 @@ fn filters_status_code_should_filter_response() {
|
|||||||
.not()
|
.not()
|
||||||
.and(predicate::str::contains("302"))
|
.and(predicate::str::contains("302"))
|
||||||
.not()
|
.not()
|
||||||
.and(predicate::str::contains("14"))
|
.and(predicate::str::contains("14c"))
|
||||||
.not()
|
.not()
|
||||||
.and(predicate::str::contains("/file.js"))
|
.and(predicate::str::contains("/file.js"))
|
||||||
.and(predicate::str::contains("200"))
|
.and(predicate::str::contains("200"))
|
||||||
.and(predicate::str::contains("34")),
|
.and(predicate::str::contains("34c")),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(mock.times_called(), 1);
|
||||||
|
assert_eq!(mock_two.times_called(), 1);
|
||||||
|
teardown_tmp_directory(tmp_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// create a FeroxResponse that should elicit a true from
|
||||||
|
/// LinesFilter::should_filter_response
|
||||||
|
fn filters_lines_should_filter_response() {
|
||||||
|
let srv = MockServer::start();
|
||||||
|
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_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 cmd = Command::cargo_bin("feroxbuster")
|
||||||
|
.unwrap()
|
||||||
|
.arg("--url")
|
||||||
|
.arg(srv.url("/"))
|
||||||
|
.arg("--wordlist")
|
||||||
|
.arg(file.as_os_str())
|
||||||
|
.arg("--filter-lines")
|
||||||
|
.arg("2")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cmd.assert().success().stdout(
|
||||||
|
predicate::str::contains("/LICENSE")
|
||||||
|
.and(predicate::str::contains("302"))
|
||||||
|
.and(predicate::str::contains("14"))
|
||||||
|
.and(predicate::str::contains("/file.js"))
|
||||||
|
.not()
|
||||||
|
.and(predicate::str::contains("200"))
|
||||||
|
.not()
|
||||||
|
.and(predicate::str::contains("2l"))
|
||||||
|
.not(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(mock.times_called(), 1);
|
||||||
|
assert_eq!(mock_two.times_called(), 1);
|
||||||
|
teardown_tmp_directory(tmp_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// create a FeroxResponse that should elicit a true from
|
||||||
|
/// WordsFilter::should_filter_response
|
||||||
|
fn filters_words_should_filter_response() {
|
||||||
|
let srv = MockServer::start();
|
||||||
|
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_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 cmd = Command::cargo_bin("feroxbuster")
|
||||||
|
.unwrap()
|
||||||
|
.arg("--url")
|
||||||
|
.arg(srv.url("/"))
|
||||||
|
.arg("--wordlist")
|
||||||
|
.arg(file.as_os_str())
|
||||||
|
.arg("--filter-words")
|
||||||
|
.arg("13")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cmd.assert().success().stdout(
|
||||||
|
predicate::str::contains("/LICENSE")
|
||||||
|
.and(predicate::str::contains("302"))
|
||||||
|
.and(predicate::str::contains("14"))
|
||||||
|
.and(predicate::str::contains("/file.js"))
|
||||||
|
.not()
|
||||||
|
.and(predicate::str::contains("200"))
|
||||||
|
.not()
|
||||||
|
.and(predicate::str::contains("13w"))
|
||||||
|
.not(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(mock.times_called(), 1);
|
||||||
|
assert_eq!(mock_two.times_called(), 1);
|
||||||
|
teardown_tmp_directory(tmp_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// create a FeroxResponse that should elicit a true from
|
||||||
|
/// SizeFilter::should_filter_response
|
||||||
|
fn filters_size_should_filter_response() {
|
||||||
|
let srv = MockServer::start();
|
||||||
|
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_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 cmd = Command::cargo_bin("feroxbuster")
|
||||||
|
.unwrap()
|
||||||
|
.arg("--url")
|
||||||
|
.arg(srv.url("/"))
|
||||||
|
.arg("--wordlist")
|
||||||
|
.arg(file.as_os_str())
|
||||||
|
.arg("--filter-size")
|
||||||
|
.arg("56")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cmd.assert().success().stdout(
|
||||||
|
predicate::str::contains("/LICENSE")
|
||||||
|
.and(predicate::str::contains("302"))
|
||||||
|
.and(predicate::str::contains("14"))
|
||||||
|
.and(predicate::str::contains("/file.js"))
|
||||||
|
.not()
|
||||||
|
.and(predicate::str::contains("200"))
|
||||||
|
.not()
|
||||||
|
.and(predicate::str::contains("56c"))
|
||||||
|
.not(),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(mock.times_called(), 1);
|
assert_eq!(mock.times_called(), 1);
|
||||||
|
|||||||
@@ -129,9 +129,9 @@ fn test_static_wildcard_request_found() -> Result<(), Box<dyn std::error::Error>
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// test finds a dynamic wildcard and reports as much to stdout and a file
|
/// test finds a dynamic wildcard and reports as much to stdout and a file
|
||||||
fn test_dynamic_wildcard_request_found() -> Result<(), Box<dyn std::error::Error>> {
|
fn test_dynamic_wildcard_request_found() {
|
||||||
let srv = MockServer::start();
|
let srv = MockServer::start();
|
||||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
|
||||||
let outfile = tmp_dir.path().join("outfile");
|
let outfile = tmp_dir.path().join("outfile");
|
||||||
|
|
||||||
let mock = Mock::new()
|
let mock = Mock::new()
|
||||||
@@ -166,31 +166,19 @@ fn test_dynamic_wildcard_request_found() -> Result<(), Box<dyn std::error::Error
|
|||||||
assert_eq!(contents.contains("WLD"), true);
|
assert_eq!(contents.contains("WLD"), true);
|
||||||
assert_eq!(contents.contains("Got"), true);
|
assert_eq!(contents.contains("Got"), true);
|
||||||
assert_eq!(contents.contains("200"), true);
|
assert_eq!(contents.contains("200"), true);
|
||||||
assert_eq!(contents.contains("auto-filtering"), true);
|
|
||||||
assert_eq!(contents.contains("(url length: 32)"), true);
|
assert_eq!(contents.contains("(url length: 32)"), true);
|
||||||
assert_eq!(contents.contains("(url length: 96)"), true);
|
assert_eq!(contents.contains("(url length: 96)"), true);
|
||||||
assert_eq!(contents.contains("Wildcard response is dynamic"), true);
|
|
||||||
assert_eq!(
|
|
||||||
contents.contains("(14 + url length) responses; toggle this behavior by using"),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
cmd.assert().success().stdout(
|
cmd.assert().success().stdout(
|
||||||
predicate::str::contains("WLD")
|
predicate::str::contains("WLD")
|
||||||
.and(predicate::str::contains("Got"))
|
.and(predicate::str::contains("Got"))
|
||||||
.and(predicate::str::contains("200"))
|
.and(predicate::str::contains("200"))
|
||||||
.and(predicate::str::contains("(url length: 32)"))
|
.and(predicate::str::contains("(url length: 32)"))
|
||||||
.and(predicate::str::contains("(url length: 96)"))
|
.and(predicate::str::contains("(url length: 96)")),
|
||||||
.and(predicate::str::contains("Wildcard response is dynamic;"))
|
|
||||||
.and(predicate::str::contains("auto-filtering"))
|
|
||||||
.and(predicate::str::contains(
|
|
||||||
"(14 + url length) responses; toggle this behavior by using",
|
|
||||||
)),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(mock.times_called(), 1);
|
assert_eq!(mock.times_called(), 1);
|
||||||
assert_eq!(mock2.times_called(), 1);
|
assert_eq!(mock2.times_called(), 1);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -223,9 +211,9 @@ fn heuristics_static_wildcard_request_with_dont_filter() -> Result<(), Box<dyn s
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// test finds a static wildcard and reports as much to stdout
|
/// test finds a static wildcard and reports as much to stdout
|
||||||
fn heuristics_wildcard_test_with_two_static_wildcards() -> Result<(), Box<dyn std::error::Error>> {
|
fn heuristics_wildcard_test_with_two_static_wildcards() {
|
||||||
let srv = MockServer::start();
|
let srv = MockServer::start();
|
||||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
|
||||||
|
|
||||||
let mock = Mock::new()
|
let mock = Mock::new()
|
||||||
.expect_method(GET)
|
.expect_method(GET)
|
||||||
@@ -265,7 +253,6 @@ fn heuristics_wildcard_test_with_two_static_wildcards() -> Result<(), Box<dyn st
|
|||||||
|
|
||||||
assert_eq!(mock.times_called(), 1);
|
assert_eq!(mock.times_called(), 1);
|
||||||
assert_eq!(mock2.times_called(), 1);
|
assert_eq!(mock2.times_called(), 1);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -310,10 +297,9 @@ fn heuristics_wildcard_test_with_two_static_wildcards_with_quiet_enabled(
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// test finds a static wildcard and reports as much to stdout and a file
|
/// test finds a static wildcard and reports as much to stdout and a file
|
||||||
fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file(
|
fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file() {
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let srv = MockServer::start();
|
let srv = MockServer::start();
|
||||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
|
||||||
let outfile = tmp_dir.path().join("outfile");
|
let outfile = tmp_dir.path().join("outfile");
|
||||||
|
|
||||||
let mock = Mock::new()
|
let mock = Mock::new()
|
||||||
@@ -350,10 +336,6 @@ fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file(
|
|||||||
assert_eq!(contents.contains("200"), true);
|
assert_eq!(contents.contains("200"), true);
|
||||||
assert_eq!(contents.contains("(url length: 32)"), true);
|
assert_eq!(contents.contains("(url length: 32)"), true);
|
||||||
assert_eq!(contents.contains("(url length: 96)"), true);
|
assert_eq!(contents.contains("(url length: 96)"), true);
|
||||||
assert_eq!(
|
|
||||||
contents.contains("Wildcard response is static; auto-filtering 46"),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
cmd.assert().success().stdout(
|
cmd.assert().success().stdout(
|
||||||
predicate::str::contains("WLD")
|
predicate::str::contains("WLD")
|
||||||
@@ -368,8 +350,6 @@ fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file(
|
|||||||
|
|
||||||
assert_eq!(mock.times_called(), 1);
|
assert_eq!(mock.times_called(), 1);
|
||||||
assert_eq!(mock2.times_called(), 1);
|
assert_eq!(mock2.times_called(), 1);
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -460,3 +460,84 @@ fn scanner_single_request_replayed_to_proxy() -> Result<(), Box<dyn std::error::
|
|||||||
teardown_tmp_directory(tmp_dir);
|
teardown_tmp_directory(tmp_dir);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// send a single valid request, get a response, and write the logging messages to disk
|
||||||
|
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 outfile = tmp_dir.path().join("debug.log");
|
||||||
|
|
||||||
|
Command::cargo_bin("feroxbuster")
|
||||||
|
.unwrap()
|
||||||
|
.arg("--url")
|
||||||
|
.arg(srv.url("/"))
|
||||||
|
.arg("--wordlist")
|
||||||
|
.arg(file.as_os_str())
|
||||||
|
.arg("-vvvv")
|
||||||
|
.arg("--debug-log")
|
||||||
|
.arg(outfile.as_os_str())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let contents = std::fs::read_to_string(outfile).unwrap();
|
||||||
|
println!("{}", contents);
|
||||||
|
assert!(contents.starts_with("Configuration {"));
|
||||||
|
assert!(contents.contains("TRC"));
|
||||||
|
assert!(contents.contains("DBG"));
|
||||||
|
assert!(contents.contains("INF"));
|
||||||
|
assert!(contents.contains("feroxbuster All scans complete!"));
|
||||||
|
assert!(contents.contains("feroxbuster exit: terminal_input_handler"));
|
||||||
|
|
||||||
|
assert_eq!(mock.times_called(), 1);
|
||||||
|
teardown_tmp_directory(tmp_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// send a single valid request, get a response, and write the logging messages to disk as NDJSON
|
||||||
|
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 outfile = tmp_dir.path().join("debug.log");
|
||||||
|
|
||||||
|
Command::cargo_bin("feroxbuster")
|
||||||
|
.unwrap()
|
||||||
|
.arg("--url")
|
||||||
|
.arg(srv.url("/"))
|
||||||
|
.arg("--wordlist")
|
||||||
|
.arg(file.as_os_str())
|
||||||
|
.arg("-vvvv")
|
||||||
|
.arg("--debug-log")
|
||||||
|
.arg(outfile.as_os_str())
|
||||||
|
.arg("--json")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let contents = std::fs::read_to_string(outfile).unwrap();
|
||||||
|
println!("{}", contents);
|
||||||
|
assert!(contents.starts_with("{\"type\":\"configuration\""));
|
||||||
|
assert!(contents.contains("\"level\":\"TRACE\""));
|
||||||
|
assert!(contents.contains("\"level\":\"DEBUG\""));
|
||||||
|
assert!(contents.contains("\"level\":\"INFO\""));
|
||||||
|
assert!(contents.contains("time_offset"));
|
||||||
|
assert!(contents.contains("\"module\":\"feroxbuster::scanner\""));
|
||||||
|
assert!(contents.contains("All scans complete!"));
|
||||||
|
assert!(contents.contains("exit: terminal_input_handler"));
|
||||||
|
|
||||||
|
assert_eq!(mock.times_called(), 1);
|
||||||
|
teardown_tmp_directory(tmp_dir);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user