mirror of
https://github.com/epi052/feroxbuster.git
synced 2026-06-01 04:41:12 -03:00
Compare commits
99 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b3e9badbb | ||
|
|
c680be558a | ||
|
|
8cee7ce247 | ||
|
|
580aa19681 | ||
|
|
cd220fe471 | ||
|
|
15b4fd04e5 | ||
|
|
fceba0b68b | ||
|
|
eef4c9b5ed | ||
|
|
24da4e017c | ||
|
|
f3cedf01a5 | ||
|
|
08ee32595f | ||
|
|
4c4d1a2a61 | ||
|
|
64b54a6308 | ||
|
|
e27b3ee8da | ||
|
|
129725cedd | ||
|
|
17886da3df | ||
|
|
c8a46b7e5a | ||
|
|
f97d103fc6 | ||
|
|
aa2fecc5c1 | ||
|
|
6f2244e1ff | ||
|
|
a1dc90ba06 | ||
|
|
32f55ddfb7 | ||
|
|
9a65c7f1f5 | ||
|
|
0f6bc1c160 | ||
|
|
abef7a236b | ||
|
|
0cff62dbe2 | ||
|
|
a590188e44 | ||
|
|
dc3aa11966 | ||
|
|
57714d243a | ||
|
|
1d34a5e99f | ||
|
|
9ab3e5515e | ||
|
|
3abef25c8f | ||
|
|
454f3a4302 | ||
|
|
acb9c19f4d | ||
|
|
98f06951bd | ||
|
|
c9e1a7adbe | ||
|
|
c57cf82fce | ||
|
|
a3bcfaf95c | ||
|
|
c99afec740 | ||
|
|
fa9fd65c2f | ||
|
|
2af87971d5 | ||
|
|
e6753d9474 | ||
|
|
d23717dc6c | ||
|
|
4debe68ed6 | ||
|
|
e6b78e3986 | ||
|
|
7b268cf197 | ||
|
|
34ff884d52 | ||
|
|
7fef23f888 | ||
|
|
7a8d6d0d52 | ||
|
|
6d4f2a7ed9 | ||
|
|
329d04252f | ||
|
|
9b4092ea8c | ||
|
|
d942a7705a | ||
|
|
e3365b42a2 | ||
|
|
41689bd742 | ||
|
|
bc487475f0 | ||
|
|
393e775285 | ||
|
|
cf6c02307c | ||
|
|
88b9bc3a01 | ||
|
|
d1f90efb09 | ||
|
|
df4fad07a9 | ||
|
|
56d533117e | ||
|
|
9549e27f19 | ||
|
|
1677b51c2d | ||
|
|
d4f9442d38 | ||
|
|
8191fa1a5e | ||
|
|
4811b37aa4 | ||
|
|
941cad5844 | ||
|
|
d59af94f62 | ||
|
|
cf403c4d4a | ||
|
|
57a2b1cbab | ||
|
|
ef195bd653 | ||
|
|
9b1a24bca3 | ||
|
|
c6aefbfa97 | ||
|
|
42bad85208 | ||
|
|
f5709739fa | ||
|
|
248f56ed7a | ||
|
|
3de6ed9696 | ||
|
|
4bad39f4b9 | ||
|
|
9b303d8b5a | ||
|
|
7e0b003216 | ||
|
|
dc36a7bf4d | ||
|
|
d33632c421 | ||
|
|
7dc6a867a5 | ||
|
|
b937a0191e | ||
|
|
d57a83956c | ||
|
|
71efd78f03 | ||
|
|
139006d0a7 | ||
|
|
b5abb8b6e8 | ||
|
|
a076a333df | ||
|
|
461ed0a9ff | ||
|
|
4381569a0f | ||
|
|
a52bd10340 | ||
|
|
56a1144865 | ||
|
|
23ab009c08 | ||
|
|
fa4e3d5d88 | ||
|
|
ad7a1ffe44 | ||
|
|
0e4f8893f8 | ||
|
|
8e0b801ec5 |
5
.cargo/config
Normal file
5
.cargo/config
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[target.armv7-unknown-linux-gnueabihf]
|
||||||
|
linker = "arm-linux-gnueabihf-gcc"
|
||||||
|
|
||||||
|
[target.aarch64-unknown-linux-gnu]
|
||||||
|
linker = "aarch64-linux-gnu-gcc"
|
||||||
4
.github/FUNDING.yml
vendored
Normal file
4
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [epi052]
|
||||||
|
ko_fi: epi052
|
||||||
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::deref_addrof -A clippy::mutex-atomic`
|
- [ ] All `clippy` checks pass when running `cargo clippy --all-targets --all-features -- -D warnings -A clippy::mutex-atomic`
|
||||||
- [ ] All existing tests pass
|
- [ ] All existing tests pass
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|||||||
26
.github/workflows/build.yml
vendored
26
.github/workflows/build.yml
vendored
@@ -4,11 +4,13 @@ on: [push]
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-nix:
|
build-nix:
|
||||||
|
env:
|
||||||
|
IN_PIPELINE: true
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
type: [ubuntu-x64, ubuntu-x86]
|
type: [ubuntu-x64, ubuntu-x86, armv7, aarch64]
|
||||||
include:
|
include:
|
||||||
- type: ubuntu-x64
|
- type: ubuntu-x64
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
@@ -22,12 +24,25 @@ jobs:
|
|||||||
name: x86-linux-feroxbuster
|
name: x86-linux-feroxbuster
|
||||||
path: target/i686-unknown-linux-musl/release/feroxbuster
|
path: target/i686-unknown-linux-musl/release/feroxbuster
|
||||||
pkg_config_path: /usr/lib/i686-linux-gnu/pkgconfig
|
pkg_config_path: /usr/lib/i686-linux-gnu/pkgconfig
|
||||||
|
- type: armv7
|
||||||
|
os: ubuntu-latest
|
||||||
|
target: armv7-unknown-linux-gnueabihf
|
||||||
|
name: armv7-feroxbuster
|
||||||
|
path: target/armv7-unknown-linux-gnueabihf/release/feroxbuster
|
||||||
|
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
|
||||||
|
- type: aarch64
|
||||||
|
os: ubuntu-latest
|
||||||
|
target: aarch64-unknown-linux-gnu
|
||||||
|
name: aarch64-feroxbuster
|
||||||
|
path: target/aarch64-unknown-linux-gnu/release/feroxbuster
|
||||||
|
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Install System Dependencies
|
- name: Install System Dependencies
|
||||||
run: |
|
run: |
|
||||||
|
env
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y --no-install-recommends libssl-dev pkg-config
|
sudo apt-get install -y --no-install-recommends libssl-dev pkg-config gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
@@ -43,7 +58,7 @@ jobs:
|
|||||||
args: --release --target=${{ matrix.target }}
|
args: --release --target=${{ matrix.target }}
|
||||||
- name: Strip symbols from binary
|
- name: Strip symbols from binary
|
||||||
run: |
|
run: |
|
||||||
strip -s ${{ matrix.path }}
|
strip -s ${{ matrix.path }} || arm-linux-gnueabihf-strip -s ${{ matrix.path }} || aarch64-linux-gnu-strip -s ${{ matrix.path }}
|
||||||
- name: Build tar.gz for homebrew installs
|
- name: Build tar.gz for homebrew installs
|
||||||
if: matrix.type == 'ubuntu-x64'
|
if: matrix.type == 'ubuntu-x64'
|
||||||
run: |
|
run: |
|
||||||
@@ -72,6 +87,8 @@ jobs:
|
|||||||
path: ./target/x86_64-unknown-linux-musl/debian/*
|
path: ./target/x86_64-unknown-linux-musl/debian/*
|
||||||
|
|
||||||
build-macos:
|
build-macos:
|
||||||
|
env:
|
||||||
|
IN_PIPELINE: true
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
steps:
|
steps:
|
||||||
@@ -102,6 +119,8 @@ jobs:
|
|||||||
path: x86_64-macos-feroxbuster.tar.gz
|
path: x86_64-macos-feroxbuster.tar.gz
|
||||||
|
|
||||||
build-windows:
|
build-windows:
|
||||||
|
env:
|
||||||
|
IN_PIPELINE: true
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
strategy:
|
strategy:
|
||||||
@@ -134,4 +153,3 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: ${{ matrix.name }}
|
name: ${{ matrix.name }}
|
||||||
path: ${{ matrix.path }}
|
path: ${{ matrix.path }}
|
||||||
|
|
||||||
|
|||||||
837
Cargo.lock
generated
837
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
27
Cargo.toml
27
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "feroxbuster"
|
name = "feroxbuster"
|
||||||
version = "2.2.2"
|
version = "2.3.3"
|
||||||
authors = ["Ben 'epi' Risher <epibar052@gmail.com>"]
|
authors = ["Ben 'epi' Risher <epibar052@gmail.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
@@ -19,37 +19,38 @@ maintenance = { status = "actively-developed" }
|
|||||||
clap = "2.33"
|
clap = "2.33"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
|
dirs = "3.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
futures = { version = "0.3.13"}
|
futures = { version = "0.3"}
|
||||||
tokio = { version = "1.2.0", features = ["full"] }
|
tokio = { version = "1.10", features = ["full"] }
|
||||||
tokio-util = {version = "0.6.3", features = ["codec"]}
|
tokio-util = {version = "0.6.6", features = ["codec"]}
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.8.3"
|
env_logger = "0.9"
|
||||||
reqwest = { version = "0.11.1", features = ["socks"] }
|
reqwest = { version = "0.11", features = ["socks"] }
|
||||||
clap = "2.33"
|
clap = "2.33"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
toml = "0.5"
|
toml = "0.5"
|
||||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
serde_json = "1.0.64"
|
serde_json = "1.0"
|
||||||
uuid = { version = "0.8", features = ["v4"] }
|
uuid = { version = "0.8", features = ["v4"] }
|
||||||
indicatif = "0.15"
|
indicatif = "0.15"
|
||||||
console = "0.14"
|
console = "0.14"
|
||||||
openssl = { version = "0.10", features = ["vendored"] }
|
openssl = { version = "0.10", features = ["vendored"] }
|
||||||
dirs = "3.0"
|
dirs = "3.0"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
crossterm = "0.19"
|
crossterm = "0.20"
|
||||||
rlimit = "0.5.4"
|
rlimit = "0.6"
|
||||||
ctrlc = "3.1.8"
|
ctrlc = "3.2"
|
||||||
fuzzyhash = "0.2.1"
|
fuzzyhash = "0.2.1"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
leaky-bucket = "0.10.0"
|
leaky-bucket = "0.10.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.1"
|
tempfile = "3.1"
|
||||||
httpmock = "0.5.2"
|
httpmock = "0.6.2"
|
||||||
assert_cmd = "1.0.3"
|
assert_cmd = "2.0"
|
||||||
predicates = "1.0.7"
|
predicates = "2.0"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
|
|||||||
7
Makefile
7
Makefile
@@ -27,12 +27,17 @@ endif
|
|||||||
|
|
||||||
TARGET = target/$(RELEASE)
|
TARGET = target/$(RELEASE)
|
||||||
|
|
||||||
.PHONY: all clean install uninstall
|
.PHONY: all clean install uninstall test update
|
||||||
|
|
||||||
all: cli
|
all: cli
|
||||||
cli: $(TARGET)/$(BIN) $(TARGET)/$(BIN).1.gz $(SHR_SOURCES)
|
cli: $(TARGET)/$(BIN) $(TARGET)/$(BIN).1.gz $(SHR_SOURCES)
|
||||||
install: all install-cli
|
install: all install-cli
|
||||||
|
|
||||||
|
verify:
|
||||||
|
cargo fmt
|
||||||
|
cargo clippy --all-targets --all-features -- -D warnings -A clippy::mutex-atomic
|
||||||
|
cargo test
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
cargo clean
|
cargo clean
|
||||||
|
|
||||||
|
|||||||
200
README.md
200
README.md
@@ -107,6 +107,7 @@ Enumeration.
|
|||||||
- [Silence all Output or Be Kinda Quiet (new in `v2.0.0`)](#silence-all-output-or-be-kinda-quiet-new-in-v200)
|
- [Silence all Output or Be Kinda Quiet (new in `v2.0.0`)](#silence-all-output-or-be-kinda-quiet-new-in-v200)
|
||||||
- [Auto-tune or Auto-bail from Scans (new in `v2.1.0`)](#auto-tune-or-auto-bail-from-scans-new-in-v210)
|
- [Auto-tune or Auto-bail from Scans (new in `v2.1.0`)](#auto-tune-or-auto-bail-from-scans-new-in-v210)
|
||||||
- [Run Scans in Parallel (new in `v2.2.0`)](#run-scans-in-parallel-new-in-v220)
|
- [Run Scans in Parallel (new in `v2.2.0`)](#run-scans-in-parallel-new-in-v220)
|
||||||
|
- [Prevent Specific Domain/Directory Scans aka a Deny List (new in `v2.3.0`)](#prevent-specific-domaindirectory-scans-aka-a-deny-list-new-in-v230)
|
||||||
- [Comparison w/ Similar Tools](#-comparison-w-similar-tools)
|
- [Comparison w/ Similar Tools](#-comparison-w-similar-tools)
|
||||||
- [Common Problems/Issues (FAQ)](#-common-problemsissues-faq)
|
- [Common Problems/Issues (FAQ)](#-common-problemsissues-faq)
|
||||||
- [No file descriptors available](#no-file-descriptors-available)
|
- [No file descriptors available](#no-file-descriptors-available)
|
||||||
@@ -119,8 +120,9 @@ Enumeration.
|
|||||||
|
|
||||||
### Download a Release
|
### Download a Release
|
||||||
|
|
||||||
Releases for multiple architectures can be found in the [Releases](https://github.com/epi052/feroxbuster/releases)
|
Releases for `armv7`, `aarch64`, and an `x86_64 .deb` can be found in the [Releases](https://github.com/epi052/feroxbuster/releases) section.
|
||||||
section. The latest release for each of the following systems can be downloaded and executed as shown below.
|
|
||||||
|
All other OS/architecture combinations can be installed dynamically using one of the methods shown below.
|
||||||
|
|
||||||
#### Linux (32 and 64-bit) & MacOS
|
#### Linux (32 and 64-bit) & MacOS
|
||||||
|
|
||||||
@@ -224,6 +226,14 @@ Install `feroxbuster-git` on Arch Linux with your AUR helper of choice:
|
|||||||
yay -S feroxbuster-git
|
yay -S feroxbuster-git
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### BlackArch install
|
||||||
|
|
||||||
|
Install `feroxbuster` on BlackArch Linux:
|
||||||
|
|
||||||
|
```
|
||||||
|
pacman -S feroxbuster
|
||||||
|
```
|
||||||
|
|
||||||
### Docker Install
|
### Docker Install
|
||||||
|
|
||||||
> The following steps assume you have docker installed / setup
|
> The following steps assume you have docker installed / setup
|
||||||
@@ -294,7 +304,7 @@ Configuration begins with with the following built-in default values baked into
|
|||||||
- verbosity: `0` (no logging enabled)
|
- verbosity: `0` (no logging enabled)
|
||||||
- scan_limit: `0` (no limit imposed on concurrent scans)
|
- scan_limit: `0` (no limit imposed on concurrent scans)
|
||||||
- rate_limit: `0` (no limit imposed on requests per second)
|
- rate_limit: `0` (no limit imposed on requests per second)
|
||||||
- status_codes: `200 204 301 302 307 308 401 403 405`
|
- status_codes: `200 204 301 302 307 308 401 403 405 500`
|
||||||
- user_agent: `feroxbuster/VERSION`
|
- user_agent: `feroxbuster/VERSION`
|
||||||
- recursion depth: `4`
|
- recursion depth: `4`
|
||||||
- auto-filter wildcards - `true`
|
- auto-filter wildcards - `true`
|
||||||
@@ -407,6 +417,7 @@ A pre-made configuration file with examples of all available settings can be fou
|
|||||||
# dont_filter = true
|
# dont_filter = true
|
||||||
# extract_links = true
|
# extract_links = true
|
||||||
# depth = 1
|
# depth = 1
|
||||||
|
# url_denylist = ["https://dont-scan-me.com/"]
|
||||||
# filter_size = [5174]
|
# filter_size = [5174]
|
||||||
# filter_regex = ["^ignore me$"]
|
# filter_regex = ["^ignore me$"]
|
||||||
# filter_similar = ["https://somesite.com/soft404"]
|
# filter_similar = ["https://somesite.com/soft404"]
|
||||||
@@ -440,49 +451,95 @@ USAGE:
|
|||||||
feroxbuster [FLAGS] [OPTIONS] --url <URL>...
|
feroxbuster [FLAGS] [OPTIONS] --url <URL>...
|
||||||
|
|
||||||
FLAGS:
|
FLAGS:
|
||||||
-f, --add-slash Append / to each request
|
-f, --add-slash
|
||||||
--auto-bail Automatically stop scanning when an excessive amount of errors are encountered
|
Append / to each request
|
||||||
--auto-tune Automatically lower scan rate when an excessive amount of errors are encountered
|
|
||||||
-D, --dont-filter Don't auto-filter wildcard responses
|
--auto-bail
|
||||||
-e, --extract-links Extract links from response body (html, javascript, etc...); make new requests based on
|
Automatically stop scanning when an excessive amount of errors are encountered
|
||||||
findings (default: false)
|
|
||||||
-h, --help Prints help information
|
--auto-tune
|
||||||
-k, --insecure Disables TLS certificate validation
|
Automatically lower scan rate when an excessive amount of errors are encountered
|
||||||
--json Emit JSON logs to --output and --debug-log instead of normal text
|
|
||||||
-n, --no-recursion Do not scan recursively
|
-D, --dont-filter
|
||||||
-q, --quiet Hide progress bars and banner (good for tmux windows w/ notifications)
|
Don't auto-filter wildcard responses
|
||||||
-r, --redirects Follow redirects
|
|
||||||
--silent Only print URLs + turn off logging (good for piping a list of urls to other commands)
|
-e, --extract-links
|
||||||
--stdin Read url(s) from STDIN
|
Extract links from response body (html, javascript, etc...); make new requests based on findings (default:
|
||||||
-V, --version Prints version information
|
false)
|
||||||
-v, --verbosity Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v's is probably
|
-h, --help
|
||||||
too much)
|
Prints help information
|
||||||
|
|
||||||
|
-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
|
||||||
|
|
||||||
|
-q, --quiet
|
||||||
|
Hide progress bars and banner (good for tmux windows w/ notifications)
|
||||||
|
|
||||||
|
-r, --redirects
|
||||||
|
Follow redirects
|
||||||
|
|
||||||
|
--silent
|
||||||
|
Only print URLs + turn off logging (good for piping a list of urls to other commands)
|
||||||
|
|
||||||
|
--stdin
|
||||||
|
Read url(s) from STDIN
|
||||||
|
|
||||||
|
-V, --version
|
||||||
|
Prints version information
|
||||||
|
|
||||||
|
-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)
|
--debug-log <FILE>
|
||||||
|
Output file to write log entries (use w/ --json for JSON entries)
|
||||||
|
|
||||||
-d, --depth <RECURSION_DEPTH>
|
-d, --depth <RECURSION_DEPTH>
|
||||||
Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)
|
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>...
|
||||||
-N, --filter-lines <LINES>... Filter out messages of a particular line count (ex: -N 20 -N 31,30)
|
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)
|
||||||
|
|
||||||
-X, --filter-regex <REGEX>...
|
-X, --filter-regex <REGEX>...
|
||||||
Filter out messages via regular expression matching on the response's body (ex: -X '^ignore me$')
|
Filter out messages via regular expression matching on the response's body (ex: -X '^ignore me$')
|
||||||
|
|
||||||
--filter-similar-to <UNWANTED_PAGE>...
|
--filter-similar-to <UNWANTED_PAGE>...
|
||||||
Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)
|
Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)
|
||||||
|
|
||||||
-S, --filter-size <SIZE>... Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)
|
-S, --filter-size <SIZE>...
|
||||||
-C, --filter-status <STATUS_CODE>... Filter out status codes (deny list) (ex: -C 200 -C 401)
|
Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)
|
||||||
-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')
|
-C, --filter-status <STATUS_CODE>...
|
||||||
-o, --output <FILE> Output file to write results to (use w/ --json for JSON entries)
|
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')
|
||||||
|
|
||||||
|
-o, --output <FILE>
|
||||||
|
Output file to write results to (use w/ --json for JSON entries)
|
||||||
|
|
||||||
--parallel <PARALLEL_SCANS>
|
--parallel <PARALLEL_SCANS>
|
||||||
Run parallel feroxbuster instances (one child process per url passed via stdin)
|
Run parallel feroxbuster instances (one child process per url passed via stdin)
|
||||||
|
|
||||||
-p, --proxy <PROXY>
|
-p, --proxy <PROXY>
|
||||||
Proxy to use for requests (ex: http(s)://host:port, socks5(h)://host:port)
|
Proxy to use for requests (ex: http(s)://host:port, socks5(h)://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)
|
||||||
|
|
||||||
--rate-limit <RATE_LIMIT>
|
--rate-limit <RATE_LIMIT>
|
||||||
Limit number of requests per second (per directory) (default: 0, i.e. no limit)
|
Limit number of requests per second (per directory) (default: 0, i.e. no limit)
|
||||||
|
|
||||||
@@ -495,16 +552,32 @@ OPTIONS:
|
|||||||
--resume-from <STATE_FILE>
|
--resume-from <STATE_FILE>
|
||||||
State file from which to resume a partially complete scan (ex. --resume-from ferox-1606586780.state)
|
State file from which to resume a partially complete scan (ex. --resume-from ferox-1606586780.state)
|
||||||
|
|
||||||
-L, --scan-limit <SCAN_LIMIT> Limit total number of concurrent scans (default: 0, i.e. no limit)
|
-L, --scan-limit <SCAN_LIMIT>
|
||||||
-s, --status-codes <STATUS_CODE>...
|
Limit total number of concurrent scans (default: 0, i.e. no limit)
|
||||||
Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)
|
|
||||||
|
|
||||||
-t, --threads <THREADS> Number of concurrent threads (default: 50)
|
-s, --status-codes <STATUS_CODE>...
|
||||||
--time-limit <TIME_SPEC> Limit total run time of all scans (ex: --time-limit 10m)
|
Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405 500)
|
||||||
-T, --timeout <SECONDS> Number of seconds before a request times out (default: 7)
|
|
||||||
-u, --url <URL>... The target URL(s) (required, unless --stdin used)
|
-t, --threads <THREADS>
|
||||||
-a, --user-agent <USER_AGENT> Sets the User-Agent (default: feroxbuster/VERSION)
|
Number of concurrent threads (default: 50)
|
||||||
-w, --wordlist <FILE> Path to the wordlist
|
|
||||||
|
--time-limit <TIME_SPEC>
|
||||||
|
Limit total run time of all scans (ex: --time-limit 10m)
|
||||||
|
|
||||||
|
-T, --timeout <SECONDS>
|
||||||
|
Number of seconds before a request times out (default: 7)
|
||||||
|
|
||||||
|
-u, --url <URL>...
|
||||||
|
The target URL(s) (required, unless --stdin used)
|
||||||
|
|
||||||
|
--dont-scan <URL>...
|
||||||
|
URL(s) to exclude from recursion/scans
|
||||||
|
|
||||||
|
-a, --user-agent <USER_AGENT>
|
||||||
|
Sets the User-Agent (default: feroxbuster/VERSION)
|
||||||
|
|
||||||
|
-w, --wordlist <FILE>
|
||||||
|
Path to the wordlist
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📊 Scan's Display Explained
|
## 📊 Scan's Display Explained
|
||||||
@@ -827,10 +900,11 @@ Below is an example of the Scan Cancel Menu™.
|
|||||||
Using the menu is pretty simple:
|
Using the menu is pretty simple:
|
||||||
- Press `ENTER` to view the menu
|
- Press `ENTER` to view the menu
|
||||||
- Choose a scan to cancel by entering its scan index (`1`)
|
- Choose a scan to cancel by entering its scan index (`1`)
|
||||||
- more than one scan can be selected by using a comma-separated list (`1,2,3` ... etc)
|
- more than one scan can be selected by using a comma-separated list of indexes and/or ranges (`1-4,8,9-13` ... etc)
|
||||||
- Confirm selections, after which all non-cancelled scans will resume
|
- Confirm selections, after which all non-cancelled scans will resume
|
||||||
|
- To skip confirmation, simply add a `-f` somewhere in your input (`3-5 -f`)
|
||||||
|
|
||||||
Here is a short demonstration of cancelling two in-progress scans found via recursion.
|
Here is a short demonstration of force cancelling a range of scans followed by a single scan with interactive prompt.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -927,7 +1001,7 @@ Example Command:
|
|||||||
cat large-target-list | ./feroxbuster --stdin --parallel 10 --extract-links --auto-bail
|
cat large-target-list | ./feroxbuster --stdin --parallel 10 --extract-links --auto-bail
|
||||||
```
|
```
|
||||||
|
|
||||||
Resuling Process List (illustrative):
|
Resulting Process List (illustrative):
|
||||||
```
|
```
|
||||||
feroxbuster --stdin --parallel 10
|
feroxbuster --stdin --parallel 10
|
||||||
\_ feroxbuster --silent --extract-links --auto-bail -u https://target-one
|
\_ feroxbuster --silent --extract-links --auto-bail -u https://target-one
|
||||||
@@ -937,6 +1011,49 @@ feroxbuster --stdin --parallel 10
|
|||||||
\_ feroxbuster --silent --extract-links --auto-bail -u https://target-ten
|
\_ feroxbuster --silent --extract-links --auto-bail -u https://target-ten
|
||||||
```
|
```
|
||||||
|
|
||||||
|
As of `v2.3.2`, logging while using `--parallel` uses the value of `-o`|`--output` as a seed to create a directory named `OUTPUT_VALUE-TIMESTAMP.logs/`. Within the directory, an individual log file is created for each target passed over stdin.
|
||||||
|
|
||||||
|
Example Command:
|
||||||
|
```
|
||||||
|
cat large-target-list | ./feroxbuster --stdin --parallel 10 --output super-cool-mega-scan
|
||||||
|
```
|
||||||
|
|
||||||
|
Resulting directory structure (illustrative):
|
||||||
|
```
|
||||||
|
super-cool-mega-scan-1627865696.logs/
|
||||||
|
├── ferox-https_target_one_com-1627865696.log
|
||||||
|
├── ...
|
||||||
|
└── ferox-https_target_two_net-1627865696.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prevent Specific Domain/Directory Scans aka a Deny List (new in `v2.3.0`)
|
||||||
|
|
||||||
|
> This action is taken BEFORE a request is sent to the target, which differs from the filter-* options that are applied to responses
|
||||||
|
|
||||||
|
Version 2.3.0 introduces the `--dont-scan` option. The values passed to `--dont-scan` act as a deny-list. The values
|
||||||
|
can be an entire domain (`http://some.domain`), a specific folder (`http://some.domain/js`), or a specific file
|
||||||
|
(`http://some.domain/some-application/stupid-page.php`) If a folder/domain is used any sub-folder/sub-file of the
|
||||||
|
url passed to `--dont-scan` will be blocked before it can be requested.
|
||||||
|
|
||||||
|
For example, given the command
|
||||||
|
|
||||||
|
```
|
||||||
|
./feroxbuster -u http://some.domain --dont-scan http://some.domain/js
|
||||||
|
```
|
||||||
|
|
||||||
|
`http://some.domain` will be scanned recursively, but any url path that begins with `/js/` will not be requested at all.
|
||||||
|
|
||||||
|
A caveat to the sub-folder/sub-file rule is when the value passed to `--dont-scan` is a parent of the scan you want to
|
||||||
|
perform. When denying at a hierarchical level higher than your scan, only sub-files/sub-folders of your `-u|--stdin`
|
||||||
|
value(s) will be processed.
|
||||||
|
|
||||||
|
```
|
||||||
|
./feroxbuster -u http://some.domain/some-application --dont-scan http://some.domain/
|
||||||
|
```
|
||||||
|
|
||||||
|
In the command above, only `http://some.domain/some-application` and children of that directory found via recursion will
|
||||||
|
be scanned. Anything 'outside' of `/some-application` will not be scanned.
|
||||||
|
|
||||||
## 🧐 Comparison w/ Similar Tools
|
## 🧐 Comparison w/ Similar Tools
|
||||||
|
|
||||||
There are quite a few similar tools for forced browsing/content discovery. Burp Suite Pro, Dirb, Dirbuster, etc...
|
There are quite a few similar tools for forced browsing/content discovery. Burp Suite Pro, Dirb, Dirbuster, etc...
|
||||||
@@ -987,6 +1104,7 @@ few of the use-cases in which feroxbuster may be a better fit:
|
|||||||
| automatically tune scans based on errors/403s/429s (`v2.1.0`) | ✔ | | |
|
| automatically tune scans based on errors/403s/429s (`v2.1.0`) | ✔ | | |
|
||||||
| automatically stop scans based on errors/403s/429s (`v2.1.0`) | ✔ | | ✔ |
|
| automatically stop scans based on errors/403s/429s (`v2.1.0`) | ✔ | | ✔ |
|
||||||
| run scans in parallel (1 process per target) (`v2.2.0`) | ✔ | | |
|
| run scans in parallel (1 process per target) (`v2.2.0`) | ✔ | | |
|
||||||
|
| prevent requests to given domain/folder/file (`v2.3.0`) | ✔ | | |
|
||||||
| **huge** number of other options | | | ✔ |
|
| **huge** number of other options | | | ✔ |
|
||||||
|
|
||||||
Of note, there's another written-in-rust content discovery tool, [rustbuster](https://github.com/phra/rustbuster). I
|
Of note, there's another written-in-rust content discovery tool, [rustbuster](https://github.com/phra/rustbuster). I
|
||||||
|
|||||||
63
build.rs
63
build.rs
@@ -1,4 +1,7 @@
|
|||||||
|
use std::fs::{copy, create_dir_all, OpenOptions};
|
||||||
|
use std::io::{Read, Seek, SeekFrom, Write};
|
||||||
extern crate clap;
|
extern crate clap;
|
||||||
|
extern crate dirs;
|
||||||
|
|
||||||
use clap::Shell;
|
use clap::Shell;
|
||||||
|
|
||||||
@@ -20,4 +23,64 @@ fn main() {
|
|||||||
for shell in &shells {
|
for shell in &shells {
|
||||||
app.gen_completions("feroxbuster", *shell, outdir);
|
app.gen_completions("feroxbuster", *shell, outdir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 0xdf pointed out an oddity when tab-completing options that expect file paths, the fix we
|
||||||
|
// landed on was to add -o plusdirs to the bash completion script. The following code aims to
|
||||||
|
// automate that fix and have it present in all future builds
|
||||||
|
let mut contents = String::new();
|
||||||
|
|
||||||
|
let mut bash_file = OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.open(format!("{}/feroxbuster.bash", outdir))
|
||||||
|
.expect("Couldn't open bash completion script");
|
||||||
|
|
||||||
|
bash_file
|
||||||
|
.read_to_string(&mut contents)
|
||||||
|
.expect("Couldn't read bash completion script");
|
||||||
|
|
||||||
|
contents = contents.replace("default feroxbuster", "default -o plusdirs feroxbuster");
|
||||||
|
|
||||||
|
bash_file
|
||||||
|
.seek(SeekFrom::Start(0))
|
||||||
|
.expect("Couldn't seek to position 0 in bash completion script");
|
||||||
|
|
||||||
|
bash_file
|
||||||
|
.write_all(contents.as_bytes())
|
||||||
|
.expect("Couldn't write updated bash completion script to disk");
|
||||||
|
|
||||||
|
// hunter0x8 let me know that when installing via cargo, it would be nice if we dropped a
|
||||||
|
// config file during the build process. The following code will place an example config in
|
||||||
|
// the user's configuration directory
|
||||||
|
// - linux: $XDG_CONFIG_HOME or $HOME/.config
|
||||||
|
// - macOS: $HOME/Library/Application Support
|
||||||
|
// - windows: {FOLDERID_RoamingAppData}
|
||||||
|
let mut config_dir = dirs::config_dir().expect("Couldn't resolve user's config directory");
|
||||||
|
config_dir = config_dir.join("feroxbuster"); // $HOME/.config/feroxbuster
|
||||||
|
|
||||||
|
if !config_dir.exists() {
|
||||||
|
// recursively create the feroxbuster directory and all of its parent components if
|
||||||
|
// they are missing
|
||||||
|
if !config_dir.exists() {
|
||||||
|
// recursively create the feroxbuster directory and all of its parent components if
|
||||||
|
// they are missing
|
||||||
|
if create_dir_all(&config_dir).is_err() {
|
||||||
|
// only copy the config file when we're not running in the CI/CD pipeline
|
||||||
|
// which fails with permission denied
|
||||||
|
eprintln!("Couldn't create one or more directories needed to copy the config file");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hard-coding config name here to not rely on the crate we're building, if DEFAULT_CONFIG_NAME
|
||||||
|
// ever changes, this will need to be updated
|
||||||
|
let config_file = config_dir.join("ferox-config.toml");
|
||||||
|
|
||||||
|
if !config_file.exists() {
|
||||||
|
// config file doesn't exist, add it to the config directory
|
||||||
|
if copy("ferox-config.toml.example", config_file).is_err() {
|
||||||
|
eprintln!("Couldn't copy example config into config directory");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
# redirects = true
|
# redirects = true
|
||||||
# insecure = true
|
# insecure = true
|
||||||
# extensions = ["php", "html"]
|
# extensions = ["php", "html"]
|
||||||
|
# url_denylist = ["http://dont-scan.me", "https://also-not.me"]
|
||||||
# no_recursion = true
|
# no_recursion = true
|
||||||
# add_slash = true
|
# add_slash = true
|
||||||
# stdin = true
|
# stdin = true
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 46 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 313 KiB After Width: | Height: | Size: 670 KiB |
@@ -3,59 +3,63 @@
|
|||||||
BASE_URL=https://github.com/epi052/feroxbuster/releases/latest/download
|
BASE_URL=https://github.com/epi052/feroxbuster/releases/latest/download
|
||||||
|
|
||||||
MAC_ZIP=x86_64-macos-feroxbuster.zip
|
MAC_ZIP=x86_64-macos-feroxbuster.zip
|
||||||
MAC_URL="${BASE_URL}/${MAC_ZIP}"
|
MAC_URL="$BASE_URL/$MAC_ZIP"
|
||||||
|
|
||||||
LIN32_ZIP=x86-linux-feroxbuster.zip
|
LIN32_ZIP=x86-linux-feroxbuster.zip
|
||||||
LIN32_URL="${BASE_URL}/${LIN32_ZIP}"
|
LIN32_URL="$BASE_URL/$LIN32_ZIP"
|
||||||
|
|
||||||
LIN64_ZIP=x86_64-linux-feroxbuster.zip
|
LIN64_ZIP=x86_64-linux-feroxbuster.zip
|
||||||
LIN64_URL="${BASE_URL}/${LIN64_ZIP}"
|
LIN64_URL="$BASE_URL/$LIN64_ZIP"
|
||||||
|
|
||||||
EMOJI_URL=https://gist.github.com/epi052/8196b550ea51d0907ad4b93751b1b57d/raw/6112c9f32ae07922983fdc549c54fd3fb9a38e4c/NotoColorEmoji.ttf
|
EMOJI_URL=https://gist.github.com/epi052/8196b550ea51d0907ad4b93751b1b57d/raw/6112c9f32ae07922983fdc549c54fd3fb9a38e4c/NotoColorEmoji.ttf
|
||||||
|
|
||||||
echo "[+] Installing feroxbuster!"
|
echo "[+] Installing feroxbuster!"
|
||||||
|
|
||||||
if [[ "$(uname)" == "Darwin" ]]; then
|
which unzip &>/dev/null
|
||||||
echo "[=] Found MacOS, downloading from ${MAC_URL}"
|
if [ "$?" = "0" ]; then
|
||||||
|
echo "[+] unzip found"
|
||||||
curl -sLO "${MAC_URL}"
|
else
|
||||||
unzip -o "${MAC_ZIP}" > /dev/null
|
echo "[ ] unzip not found, exiting. "
|
||||||
rm "${MAC_ZIP}"
|
exit -1
|
||||||
elif [[ "$(expr substr $(uname -s) 1 5)" == "Linux" ]]; then
|
|
||||||
if [[ $(getconf LONG_BIT) == 32 ]]; then
|
|
||||||
echo "[=] Found 32-bit Linux, downloading from ${LIN32_URL}"
|
|
||||||
|
|
||||||
curl -sLO "${LIN32_URL}"
|
|
||||||
unzip -o "${LIN32_ZIP}" > /dev/null
|
|
||||||
rm "${LIN32_ZIP}"
|
|
||||||
else
|
|
||||||
echo "[=] Found 64-bit Linux, downloading from ${LIN64_URL}"
|
|
||||||
|
|
||||||
curl -sLO "${LIN64_URL}"
|
|
||||||
unzip -o "${LIN64_ZIP}" > /dev/null
|
|
||||||
rm "${LIN64_ZIP}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -e ~/.fonts/NotoColorEmoji.ttf ]]; then
|
|
||||||
echo "[=] Found Noto Emoji Font, skipping install"
|
|
||||||
else
|
|
||||||
echo "[=] Installing Noto Emoji Font"
|
|
||||||
mkdir -p ~/.fonts
|
|
||||||
pushd ~/.fonts 2>&1 >/dev/null
|
|
||||||
|
|
||||||
curl -sLO "${EMOJI_URL}"
|
|
||||||
|
|
||||||
fc-cache -f -v >/dev/null
|
|
||||||
|
|
||||||
popd 2>&1 >/dev/null
|
|
||||||
echo "[+] Noto Emoji Font installed"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "$(uname)" == "Darwin" ]]; then
|
||||||
|
echo "[=] Found MacOS, downloading from $MAC_URL"
|
||||||
|
|
||||||
|
curl -sLO "$MAC_URL"
|
||||||
|
unzip -o "$MAC_ZIP" >/dev/null
|
||||||
|
rm "$MAC_ZIP"
|
||||||
|
elif [[ "$(expr substr $(uname -s) 1 5)" == "Linux" ]]; then
|
||||||
|
if [[ $(getconf LONG_BIT) == 32 ]]; then
|
||||||
|
echo "[=] Found 32-bit Linux, downloading from $LIN32_URL"
|
||||||
|
|
||||||
|
curl -sLO "$LIN32_URL"
|
||||||
|
unzip -o "$LIN32_ZIP" >/dev/null
|
||||||
|
rm "$LIN32_ZIP"
|
||||||
|
else
|
||||||
|
echo "[=] Found 64-bit Linux, downloading from $LIN64_URL"
|
||||||
|
|
||||||
|
curl -sLO "$LIN64_URL"
|
||||||
|
unzip -o "$LIN64_ZIP" >/dev/null
|
||||||
|
rm "$LIN64_ZIP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -e ~/.fonts/NotoColorEmoji.ttf ]]; then
|
||||||
|
echo "[=] Found Noto Emoji Font, skipping install"
|
||||||
|
else
|
||||||
|
echo "[=] Installing Noto Emoji Font"
|
||||||
|
mkdir -p ~/.fonts
|
||||||
|
pushd ~/.fonts 2>&1 >/dev/null
|
||||||
|
|
||||||
|
curl -sLO "$EMOJI_URL"
|
||||||
|
|
||||||
|
fc-cache -f -v >/dev/null
|
||||||
|
|
||||||
|
popd 2>&1 >/dev/null
|
||||||
|
echo "[+] Noto Emoji Font installed"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
chmod +x ./feroxbuster
|
chmod +x ./feroxbuster
|
||||||
|
|
||||||
echo "[+] Installed feroxbuster version $(./feroxbuster -V)"
|
echo "[+] Installed feroxbuster version $(./feroxbuster -V)"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ _feroxbuster() {
|
|||||||
'--user-agent=[Sets the User-Agent (default: feroxbuster/VERSION)]' \
|
'--user-agent=[Sets the User-Agent (default: feroxbuster/VERSION)]' \
|
||||||
'*-x+[File extension(s) to search for (ex: -x php -x pdf js)]' \
|
'*-x+[File extension(s) to search for (ex: -x php -x pdf js)]' \
|
||||||
'*--extensions=[File extension(s) to search for (ex: -x php -x pdf js)]' \
|
'*--extensions=[File extension(s) to search for (ex: -x php -x pdf js)]' \
|
||||||
|
'*--dont-scan=[URL(s) to exclude from recursion/scans]' \
|
||||||
'*-H+[Specify HTTP headers (ex: -H Header:val '\''stuff: things'\'')]' \
|
'*-H+[Specify HTTP headers (ex: -H Header:val '\''stuff: things'\'')]' \
|
||||||
'*--headers=[Specify HTTP headers (ex: -H Header:val '\''stuff: things'\'')]' \
|
'*--headers=[Specify HTTP headers (ex: -H Header:val '\''stuff: things'\'')]' \
|
||||||
'*-Q+[Specify URL query parameters (ex: -Q token=stuff -Q secret=key)]' \
|
'*-Q+[Specify URL query parameters (ex: -Q token=stuff -Q secret=key)]' \
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
|
|||||||
[CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/VERSION)')
|
[CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/VERSION)')
|
||||||
[CompletionResult]::new('-x', 'x', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)')
|
[CompletionResult]::new('-x', 'x', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)')
|
||||||
[CompletionResult]::new('--extensions', 'extensions', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)')
|
[CompletionResult]::new('--extensions', 'extensions', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)')
|
||||||
|
[CompletionResult]::new('--dont-scan', 'dont-scan', [CompletionResultType]::ParameterName, 'URL(s) to exclude from recursion/scans')
|
||||||
[CompletionResult]::new('-H', 'H', [CompletionResultType]::ParameterName, 'Specify HTTP headers (ex: -H Header:val ''stuff: things'')')
|
[CompletionResult]::new('-H', 'H', [CompletionResultType]::ParameterName, 'Specify HTTP headers (ex: -H Header:val ''stuff: things'')')
|
||||||
[CompletionResult]::new('--headers', 'headers', [CompletionResultType]::ParameterName, 'Specify HTTP headers (ex: -H Header:val ''stuff: things'')')
|
[CompletionResult]::new('--headers', 'headers', [CompletionResultType]::ParameterName, 'Specify HTTP headers (ex: -H Header:val ''stuff: things'')')
|
||||||
[CompletionResult]::new('-Q', 'Q', [CompletionResultType]::ParameterName, 'Specify URL query parameters (ex: -Q token=stuff -Q secret=key)')
|
[CompletionResult]::new('-Q', 'Q', [CompletionResultType]::ParameterName, 'Specify URL query parameters (ex: -Q token=stuff -Q secret=key)')
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ _feroxbuster() {
|
|||||||
|
|
||||||
case "${cmd}" in
|
case "${cmd}" in
|
||||||
feroxbuster)
|
feroxbuster)
|
||||||
opts=" -v -q -D -r -k -n -f -e -h -V -w -u -t -d -T -p -P -R -s -o -a -x -H -Q -S -X -W -N -C -L --verbosity --silent --quiet --auto-tune --auto-bail --json --dont-filter --redirects --insecure --no-recursion --add-slash --stdin --extract-links --help --version --wordlist --url --threads --depth --timeout --proxy --replay-proxy --replay-codes --status-codes --output --resume-from --debug-log --user-agent --extensions --headers --query --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --scan-limit --parallel --rate-limit --time-limit "
|
opts=" -v -q -D -r -k -n -f -e -h -V -w -u -t -d -T -p -P -R -s -o -a -x -H -Q -S -X -W -N -C -L --verbosity --silent --quiet --auto-tune --auto-bail --json --dont-filter --redirects --insecure --no-recursion --add-slash --stdin --extract-links --help --version --wordlist --url --threads --depth --timeout --proxy --replay-proxy --replay-codes --status-codes --output --resume-from --debug-log --user-agent --extensions --dont-scan --headers --query --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --scan-limit --parallel --rate-limit --time-limit "
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
@@ -131,6 +131,10 @@ _feroxbuster() {
|
|||||||
COMPREPLY=($(compgen -f "${cur}"))
|
COMPREPLY=($(compgen -f "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--dont-scan)
|
||||||
|
COMPREPLY=($(compgen -f "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
--headers)
|
--headers)
|
||||||
COMPREPLY=($(compgen -f "${cur}"))
|
COMPREPLY=($(compgen -f "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
@@ -222,4 +226,4 @@ _feroxbuster() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
complete -F _feroxbuster -o bashdefault -o default feroxbuster
|
complete -F _feroxbuster -o bashdefault -o default -o plusdirs feroxbuster
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ complete -c feroxbuster -n "__fish_use_subcommand" -l resume-from -d 'State file
|
|||||||
complete -c feroxbuster -n "__fish_use_subcommand" -l debug-log -d 'Output file to write log entries (use w/ --json for JSON entries)'
|
complete -c feroxbuster -n "__fish_use_subcommand" -l debug-log -d 'Output file to write log entries (use w/ --json for JSON entries)'
|
||||||
complete -c feroxbuster -n "__fish_use_subcommand" -s a -l user-agent -d 'Sets the User-Agent (default: feroxbuster/VERSION)'
|
complete -c feroxbuster -n "__fish_use_subcommand" -s a -l user-agent -d 'Sets the User-Agent (default: feroxbuster/VERSION)'
|
||||||
complete -c feroxbuster -n "__fish_use_subcommand" -s x -l extensions -d 'File extension(s) to search for (ex: -x php -x pdf js)'
|
complete -c feroxbuster -n "__fish_use_subcommand" -s x -l extensions -d 'File extension(s) to search for (ex: -x php -x pdf js)'
|
||||||
|
complete -c feroxbuster -n "__fish_use_subcommand" -l dont-scan -d 'URL(s) to exclude from recursion/scans'
|
||||||
complete -c feroxbuster -n "__fish_use_subcommand" -s H -l headers -d 'Specify HTTP headers (ex: -H Header:val \'stuff: things\')'
|
complete -c feroxbuster -n "__fish_use_subcommand" -s H -l headers -d 'Specify HTTP headers (ex: -H Header:val \'stuff: things\')'
|
||||||
complete -c feroxbuster -n "__fish_use_subcommand" -s Q -l query -d 'Specify URL query parameters (ex: -Q token=stuff -Q secret=key)'
|
complete -c feroxbuster -n "__fish_use_subcommand" -s Q -l query -d 'Specify URL query parameters (ex: -Q token=stuff -Q secret=key)'
|
||||||
complete -c feroxbuster -n "__fish_use_subcommand" -s S -l filter-size -d 'Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)'
|
complete -c feroxbuster -n "__fish_use_subcommand" -s S -l filter-size -d 'Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)'
|
||||||
|
|||||||
@@ -134,6 +134,9 @@ pub struct Banner {
|
|||||||
/// represents Configuration.auto_bail
|
/// represents Configuration.auto_bail
|
||||||
auto_bail: BannerEntry,
|
auto_bail: BannerEntry,
|
||||||
|
|
||||||
|
/// represents Configuration.url_denylist
|
||||||
|
url_denylist: Vec<BannerEntry>,
|
||||||
|
|
||||||
/// current version of feroxbuster
|
/// current version of feroxbuster
|
||||||
pub(super) version: String,
|
pub(super) version: String,
|
||||||
|
|
||||||
@@ -146,6 +149,7 @@ impl Banner {
|
|||||||
/// Create a new Banner from a Configuration and live targets
|
/// Create a new Banner from a Configuration and live targets
|
||||||
pub fn new(tgts: &[String], config: &Configuration) -> Self {
|
pub fn new(tgts: &[String], config: &Configuration) -> Self {
|
||||||
let mut targets = Vec::new();
|
let mut targets = Vec::new();
|
||||||
|
let mut url_denylist = Vec::new();
|
||||||
let mut code_filters = Vec::new();
|
let mut code_filters = Vec::new();
|
||||||
let mut replay_codes = Vec::new();
|
let mut replay_codes = Vec::new();
|
||||||
let mut headers = Vec::new();
|
let mut headers = Vec::new();
|
||||||
@@ -160,6 +164,10 @@ impl Banner {
|
|||||||
targets.push(BannerEntry::new("🎯", "Target Url", target));
|
targets.push(BannerEntry::new("🎯", "Target Url", target));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for denied_url in &config.url_denylist {
|
||||||
|
url_denylist.push(BannerEntry::new("🚫", "Don't Scan", denied_url));
|
||||||
|
}
|
||||||
|
|
||||||
let mut codes = vec![];
|
let mut codes = vec![];
|
||||||
for code in &config.status_codes {
|
for code in &config.status_codes {
|
||||||
codes.push(status_colorizer(&code.to_string()))
|
codes.push(status_colorizer(&code.to_string()))
|
||||||
@@ -323,6 +331,7 @@ impl Banner {
|
|||||||
rate_limit,
|
rate_limit,
|
||||||
scan_limit,
|
scan_limit,
|
||||||
time_limit,
|
time_limit,
|
||||||
|
url_denylist,
|
||||||
config: cfg,
|
config: cfg,
|
||||||
version: VERSION.to_string(),
|
version: VERSION.to_string(),
|
||||||
update_status: UpdateStatus::Unknown,
|
update_status: UpdateStatus::Unknown,
|
||||||
@@ -413,6 +422,10 @@ by Ben "epi" Risher {} ver: {}"#,
|
|||||||
writeln!(&mut writer, "{}", target)?;
|
writeln!(&mut writer, "{}", target)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for denied_url in &self.url_denylist {
|
||||||
|
writeln!(&mut writer, "{}", denied_url)?;
|
||||||
|
}
|
||||||
|
|
||||||
writeln!(&mut writer, "{}", self.threads)?;
|
writeln!(&mut writer, "{}", self.threads)?;
|
||||||
writeln!(&mut writer, "{}", self.wordlist)?;
|
writeln!(&mut writer, "{}", self.wordlist)?;
|
||||||
writeln!(&mut writer, "{}", self.status_codes)?;
|
writeln!(&mut writer, "{}", self.status_codes)?;
|
||||||
|
|||||||
@@ -248,6 +248,10 @@ pub struct Configuration {
|
|||||||
/// Filter out response bodies that meet a certain threshold of similarity
|
/// Filter out response bodies that meet a certain threshold of similarity
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub filter_similar: Vec<String>,
|
pub filter_similar: Vec<String>,
|
||||||
|
|
||||||
|
/// URLs that should never be scanned/recursed into
|
||||||
|
#[serde(default)]
|
||||||
|
pub url_denylist: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Configuration {
|
impl Default for Configuration {
|
||||||
@@ -304,6 +308,7 @@ impl Default for Configuration {
|
|||||||
extensions: Vec::new(),
|
extensions: Vec::new(),
|
||||||
filter_size: Vec::new(),
|
filter_size: Vec::new(),
|
||||||
filter_regex: Vec::new(),
|
filter_regex: Vec::new(),
|
||||||
|
url_denylist: Vec::new(),
|
||||||
filter_line_count: Vec::new(),
|
filter_line_count: Vec::new(),
|
||||||
filter_word_count: Vec::new(),
|
filter_word_count: Vec::new(),
|
||||||
filter_status: Vec::new(),
|
filter_status: Vec::new(),
|
||||||
@@ -341,6 +346,7 @@ impl Configuration {
|
|||||||
/// - **user_agent**: `feroxbuster/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`
|
||||||
|
/// - **url_denylist**: `None`
|
||||||
/// - **filter_size**: `None`
|
/// - **filter_size**: `None`
|
||||||
/// - **filter_similar**: `None`
|
/// - **filter_similar**: `None`
|
||||||
/// - **filter_regex**: `None`
|
/// - **filter_regex**: `None`
|
||||||
@@ -538,6 +544,10 @@ impl Configuration {
|
|||||||
config.extensions = arg.map(|val| val.to_string()).collect();
|
config.extensions = arg.map(|val| val.to_string()).collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(arg) = args.values_of("url_denylist") {
|
||||||
|
config.url_denylist = arg.map(|val| val.to_string()).collect();
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(arg) = args.values_of("filter_regex") {
|
if let Some(arg) = args.values_of("filter_regex") {
|
||||||
config.filter_regex = arg.map(|val| val.to_string()).collect();
|
config.filter_regex = arg.map(|val| val.to_string()).collect();
|
||||||
}
|
}
|
||||||
@@ -767,6 +777,11 @@ impl Configuration {
|
|||||||
update_if_not_default!(&mut conf.insecure, new.insecure, false);
|
update_if_not_default!(&mut conf.insecure, new.insecure, false);
|
||||||
update_if_not_default!(&mut conf.extract_links, new.extract_links, false);
|
update_if_not_default!(&mut conf.extract_links, new.extract_links, false);
|
||||||
update_if_not_default!(&mut conf.extensions, new.extensions, Vec::<String>::new());
|
update_if_not_default!(&mut conf.extensions, new.extensions, Vec::<String>::new());
|
||||||
|
update_if_not_default!(
|
||||||
|
&mut conf.url_denylist,
|
||||||
|
new.url_denylist,
|
||||||
|
Vec::<String>::new()
|
||||||
|
);
|
||||||
update_if_not_default!(&mut conf.headers, new.headers, HashMap::new());
|
update_if_not_default!(&mut conf.headers, new.headers, HashMap::new());
|
||||||
update_if_not_default!(&mut conf.queries, new.queries, Vec::new());
|
update_if_not_default!(&mut conf.queries, new.queries, Vec::new());
|
||||||
update_if_not_default!(&mut conf.no_recursion, new.no_recursion, false);
|
update_if_not_default!(&mut conf.no_recursion, new.no_recursion, false);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ fn setup_config_test() -> Configuration {
|
|||||||
redirects = true
|
redirects = true
|
||||||
insecure = true
|
insecure = true
|
||||||
extensions = ["html", "php", "js"]
|
extensions = ["html", "php", "js"]
|
||||||
|
url_denylist = ["http://dont-scan.me", "https://also-not.me"]
|
||||||
headers = {stuff = "things", mostuff = "mothings"}
|
headers = {stuff = "things", mostuff = "mothings"}
|
||||||
queries = [["name","value"], ["rick", "astley"]]
|
queries = [["name","value"], ["rick", "astley"]]
|
||||||
no_recursion = true
|
no_recursion = true
|
||||||
@@ -72,24 +73,25 @@ fn default_configuration() {
|
|||||||
assert_eq!(config.timeout, timeout());
|
assert_eq!(config.timeout, timeout());
|
||||||
assert_eq!(config.verbosity, 0);
|
assert_eq!(config.verbosity, 0);
|
||||||
assert_eq!(config.scan_limit, 0);
|
assert_eq!(config.scan_limit, 0);
|
||||||
assert_eq!(config.silent, false);
|
assert!(!config.silent);
|
||||||
assert_eq!(config.quiet, false);
|
assert!(!config.quiet);
|
||||||
assert_eq!(config.output_level, OutputLevel::Default);
|
assert_eq!(config.output_level, OutputLevel::Default);
|
||||||
assert_eq!(config.dont_filter, false);
|
assert!(!config.dont_filter);
|
||||||
assert_eq!(config.auto_tune, false);
|
assert!(!config.auto_tune);
|
||||||
assert_eq!(config.auto_bail, false);
|
assert!(!config.auto_bail);
|
||||||
assert_eq!(config.requester_policy, RequesterPolicy::Default);
|
assert_eq!(config.requester_policy, RequesterPolicy::Default);
|
||||||
assert_eq!(config.no_recursion, false);
|
assert!(!config.no_recursion);
|
||||||
assert_eq!(config.json, false);
|
assert!(!config.json);
|
||||||
assert_eq!(config.save_state, true);
|
assert!(config.save_state);
|
||||||
assert_eq!(config.stdin, false);
|
assert!(!config.stdin);
|
||||||
assert_eq!(config.add_slash, false);
|
assert!(!config.add_slash);
|
||||||
assert_eq!(config.redirects, false);
|
assert!(!config.redirects);
|
||||||
assert_eq!(config.extract_links, false);
|
assert!(!config.extract_links);
|
||||||
assert_eq!(config.insecure, false);
|
assert!(!config.insecure);
|
||||||
assert_eq!(config.queries, Vec::new());
|
assert_eq!(config.queries, Vec::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.extensions, Vec::<String>::new());
|
||||||
|
assert_eq!(config.url_denylist, Vec::<String>::new());
|
||||||
assert_eq!(config.filter_regex, Vec::<String>::new());
|
assert_eq!(config.filter_regex, Vec::<String>::new());
|
||||||
assert_eq!(config.filter_similar, Vec::<String>::new());
|
assert_eq!(config.filter_similar, Vec::<String>::new());
|
||||||
assert_eq!(config.filter_word_count, Vec::<usize>::new());
|
assert_eq!(config.filter_word_count, Vec::<usize>::new());
|
||||||
@@ -186,35 +188,35 @@ fn config_reads_replay_proxy() {
|
|||||||
/// 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_silent() {
|
fn config_reads_silent() {
|
||||||
let config = setup_config_test();
|
let config = setup_config_test();
|
||||||
assert_eq!(config.silent, true);
|
assert!(config.silent);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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_quiet() {
|
fn config_reads_quiet() {
|
||||||
let config = setup_config_test();
|
let config = setup_config_test();
|
||||||
assert_eq!(config.quiet, true);
|
assert!(config.quiet);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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_json() {
|
fn config_reads_json() {
|
||||||
let config = setup_config_test();
|
let config = setup_config_test();
|
||||||
assert_eq!(config.json, true);
|
assert!(config.json);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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_auto_bail() {
|
fn config_reads_auto_bail() {
|
||||||
let config = setup_config_test();
|
let config = setup_config_test();
|
||||||
assert_eq!(config.auto_bail, true);
|
assert!(config.auto_bail);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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_auto_tune() {
|
fn config_reads_auto_tune() {
|
||||||
let config = setup_config_test();
|
let config = setup_config_test();
|
||||||
assert_eq!(config.auto_tune, true);
|
assert!(config.auto_tune);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -235,49 +237,49 @@ fn config_reads_output() {
|
|||||||
/// 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_redirects() {
|
fn config_reads_redirects() {
|
||||||
let config = setup_config_test();
|
let config = setup_config_test();
|
||||||
assert_eq!(config.redirects, true);
|
assert!(config.redirects);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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_insecure() {
|
fn config_reads_insecure() {
|
||||||
let config = setup_config_test();
|
let config = setup_config_test();
|
||||||
assert_eq!(config.insecure, true);
|
assert!(config.insecure);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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_no_recursion() {
|
fn config_reads_no_recursion() {
|
||||||
let config = setup_config_test();
|
let config = setup_config_test();
|
||||||
assert_eq!(config.no_recursion, true);
|
assert!(config.no_recursion);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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_stdin() {
|
fn config_reads_stdin() {
|
||||||
let config = setup_config_test();
|
let config = setup_config_test();
|
||||||
assert_eq!(config.stdin, true);
|
assert!(config.stdin);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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_dont_filter() {
|
fn config_reads_dont_filter() {
|
||||||
let config = setup_config_test();
|
let config = setup_config_test();
|
||||||
assert_eq!(config.dont_filter, true);
|
assert!(config.dont_filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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_add_slash() {
|
fn config_reads_add_slash() {
|
||||||
let config = setup_config_test();
|
let config = setup_config_test();
|
||||||
assert_eq!(config.add_slash, true);
|
assert!(config.add_slash);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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_extract_links() {
|
fn config_reads_extract_links() {
|
||||||
let config = setup_config_test();
|
let config = setup_config_test();
|
||||||
assert_eq!(config.extract_links, true);
|
assert!(config.extract_links);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -287,6 +289,16 @@ fn config_reads_extensions() {
|
|||||||
assert_eq!(config.extensions, vec!["html", "php", "js"]);
|
assert_eq!(config.extensions, vec!["html", "php", "js"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// parse the test config and see that the value parsed is correct
|
||||||
|
fn config_reads_url_denylist() {
|
||||||
|
let config = setup_config_test();
|
||||||
|
assert_eq!(
|
||||||
|
config.url_denylist,
|
||||||
|
vec!["http://dont-scan.me", "https://also-not.me"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[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_regex() {
|
fn config_reads_filter_regex() {
|
||||||
@@ -333,7 +345,7 @@ fn config_reads_filter_status() {
|
|||||||
/// 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_save_state() {
|
fn config_reads_save_state() {
|
||||||
let config = setup_config_test();
|
let config = setup_config_test();
|
||||||
assert_eq!(config.save_state, false);
|
assert!(!config.save_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -364,9 +376,10 @@ fn config_reads_headers() {
|
|||||||
/// parse the test config and see that the values parsed are correct
|
/// parse the test config and see that the values parsed are correct
|
||||||
fn config_reads_queries() {
|
fn config_reads_queries() {
|
||||||
let config = setup_config_test();
|
let config = setup_config_test();
|
||||||
let mut queries = vec![];
|
let queries = vec![
|
||||||
queries.push(("name".to_string(), "value".to_string()));
|
("name".to_string(), "value".to_string()),
|
||||||
queries.push(("rick".to_string(), "astley".to_string()));
|
("rick".to_string(), "astley".to_string()),
|
||||||
|
];
|
||||||
assert_eq!(config.queries, queries);
|
assert_eq!(config.queries, queries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::{
|
|||||||
scan_manager::{FeroxState, PAUSE_SCAN},
|
scan_manager::{FeroxState, PAUSE_SCAN},
|
||||||
scanner::RESPONSES,
|
scanner::RESPONSES,
|
||||||
statistics::StatError,
|
statistics::StatError,
|
||||||
|
utils::slugify_filename,
|
||||||
utils::{open_file, write_to},
|
utils::{open_file, write_to},
|
||||||
SLEEP_DURATION,
|
SLEEP_DURATION,
|
||||||
};
|
};
|
||||||
@@ -17,7 +18,6 @@ use std::{
|
|||||||
},
|
},
|
||||||
thread::sleep,
|
thread::sleep,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Atomic boolean flag, used to determine whether or not the terminal input handler should exit
|
/// Atomic boolean flag, used to determine whether or not the terminal input handler should exit
|
||||||
@@ -77,22 +77,14 @@ impl TermInputHandler {
|
|||||||
pub fn sigint_handler(handles: Arc<Handles>) -> Result<()> {
|
pub fn sigint_handler(handles: Arc<Handles>) -> Result<()> {
|
||||||
log::trace!("enter: sigint_handler({:?})", handles);
|
log::trace!("enter: sigint_handler({:?})", handles);
|
||||||
|
|
||||||
let ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
|
let filename = if !handles.config.target_url.is_empty() {
|
||||||
|
|
||||||
let slug = if !handles.config.target_url.is_empty() {
|
|
||||||
// target url populated
|
// target url populated
|
||||||
handles
|
slugify_filename(&handles.config.target_url, "ferox", "state")
|
||||||
.config
|
|
||||||
.target_url
|
|
||||||
.replace("://", "_")
|
|
||||||
.replace("/", "_")
|
|
||||||
.replace(".", "_")
|
|
||||||
} else {
|
} else {
|
||||||
// stdin used
|
// stdin used
|
||||||
"stdin".to_string()
|
slugify_filename("stdin", "ferox", "state")
|
||||||
};
|
};
|
||||||
|
|
||||||
let filename = format!("ferox-{}-{}.state", slug, ts);
|
|
||||||
let warning = format!(
|
let warning = format!(
|
||||||
"🚨 Caught {} 🚨 saving scan state to {} ...",
|
"🚨 Caught {} 🚨 saving scan state to {} ...",
|
||||||
style("ctrl+c").yellow(),
|
style("ctrl+c").yellow(),
|
||||||
|
|||||||
@@ -139,8 +139,8 @@ impl TermOutHandler {
|
|||||||
Self {
|
Self {
|
||||||
receiver,
|
receiver,
|
||||||
tx_file,
|
tx_file,
|
||||||
config,
|
|
||||||
file_task,
|
file_task,
|
||||||
|
config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +213,7 @@ impl TermOutHandler {
|
|||||||
// should be replayed; not using logged_request due to replay proxy client
|
// should be replayed; not using logged_request due to replay proxy client
|
||||||
make_request(
|
make_request(
|
||||||
self.config.replay_client.as_ref().unwrap(),
|
self.config.replay_client.as_ref().unwrap(),
|
||||||
&resp.url(),
|
resp.url(),
|
||||||
self.config.output_level,
|
self.config.output_level,
|
||||||
tx_stats.clone(),
|
tx_stats.clone(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,17 +3,19 @@ use std::sync::Arc;
|
|||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use tokio::sync::{mpsc, Semaphore};
|
use tokio::sync::{mpsc, Semaphore};
|
||||||
|
|
||||||
use crate::response::FeroxResponse;
|
|
||||||
use crate::url::FeroxUrl;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
response::FeroxResponse,
|
||||||
scan_manager::{FeroxScan, FeroxScans, ScanOrder},
|
scan_manager::{FeroxScan, FeroxScans, ScanOrder},
|
||||||
scanner::FeroxScanner,
|
scanner::FeroxScanner,
|
||||||
statistics::StatField::TotalScans,
|
statistics::StatField::TotalScans,
|
||||||
|
url::FeroxUrl,
|
||||||
|
utils::should_deny_url,
|
||||||
CommandReceiver, CommandSender, FeroxChannel, Joiner, SLEEP_DURATION,
|
CommandReceiver, CommandSender, FeroxChannel, Joiner, SLEEP_DURATION,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::command::Command::AddToUsizeField;
|
use super::command::Command::AddToUsizeField;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use reqwest::Url;
|
||||||
use tokio::time::Duration;
|
use tokio::time::Duration;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -187,6 +189,7 @@ impl ScanHandler {
|
|||||||
/// wrapper around scanning a url to stay DRY
|
/// wrapper around scanning a url to stay DRY
|
||||||
async fn ordered_scan_url(&mut self, targets: Vec<String>, order: ScanOrder) -> Result<()> {
|
async fn ordered_scan_url(&mut self, targets: Vec<String>, order: ScanOrder) -> Result<()> {
|
||||||
log::trace!("enter: ordered_scan_url({:?}, {:?})", targets, order);
|
log::trace!("enter: ordered_scan_url({:?}, {:?})", targets, order);
|
||||||
|
let should_test_deny = !self.handles.config.url_denylist.is_empty();
|
||||||
|
|
||||||
for target in targets {
|
for target in targets {
|
||||||
if self.data.contains(&target) && matches!(order, ScanOrder::Latest) {
|
if self.data.contains(&target) && matches!(order, ScanOrder::Latest) {
|
||||||
@@ -203,6 +206,13 @@ impl ScanHandler {
|
|||||||
self.data.add_directory_scan(&target, order).1 // add the new target; return FeroxScan
|
self.data.add_directory_scan(&target, order).1 // add the new target; return FeroxScan
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if should_test_deny && should_deny_url(&Url::parse(&target)?, self.handles.clone())? {
|
||||||
|
// response was caught by a user-provided deny list
|
||||||
|
// checking this last, since it's most susceptible to longer runtimes due to what
|
||||||
|
// input is received
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let list = self.get_wordlist()?;
|
let list = self.get_wordlist()?;
|
||||||
|
|
||||||
log::info!("scan handler received {} - beginning scan", target);
|
log::info!("scan handler received {} - beginning scan", target);
|
||||||
@@ -243,6 +253,11 @@ impl ScanHandler {
|
|||||||
async fn try_recursion(&mut self, response: Box<FeroxResponse>) -> Result<()> {
|
async fn try_recursion(&mut self, response: Box<FeroxResponse>) -> Result<()> {
|
||||||
log::trace!("enter: try_recursion({:?})", response,);
|
log::trace!("enter: try_recursion({:?})", response,);
|
||||||
|
|
||||||
|
if !response.is_directory() {
|
||||||
|
// not a directory, quick exit
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let mut base_depth = 1_usize;
|
let mut base_depth = 1_usize;
|
||||||
|
|
||||||
for (base_url, base_url_depth) in &self.depths {
|
for (base_url, base_url_depth) in &self.depths {
|
||||||
@@ -256,11 +271,6 @@ impl ScanHandler {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if !response.is_directory() {
|
|
||||||
// not a directory
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let targets = vec![response.url().to_string()];
|
let targets = vec![response.url().to_string()];
|
||||||
self.ordered_scan_url(targets, ScanOrder::Latest).await?;
|
self.ordered_scan_url(targets, ScanOrder::Latest).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::utils::should_deny_url;
|
||||||
use crate::{
|
use crate::{
|
||||||
client,
|
client,
|
||||||
event_handlers::{
|
event_handlers::{
|
||||||
@@ -53,13 +54,19 @@ pub struct Extractor<'a> {
|
|||||||
|
|
||||||
/// Extractor implementation
|
/// Extractor implementation
|
||||||
impl<'a> Extractor<'a> {
|
impl<'a> Extractor<'a> {
|
||||||
/// business logic that handles getting links from a normal http body response
|
/// perform extraction from the given target and return any links found
|
||||||
pub async fn extract(&self) -> Result<()> {
|
pub async fn extract(&self) -> Result<HashSet<String>> {
|
||||||
let links = match self.target {
|
log::trace!("enter: extract (this fn has associated trace exit msg)");
|
||||||
ExtractionTarget::ResponseBody => self.extract_from_body().await?,
|
match self.target {
|
||||||
ExtractionTarget::RobotsTxt => self.extract_from_robots().await?,
|
ExtractionTarget::ResponseBody => Ok(self.extract_from_body().await?),
|
||||||
};
|
ExtractionTarget::RobotsTxt => Ok(self.extract_from_robots().await?),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// given a set of links from a normal http body response, task the request handler to make
|
||||||
|
/// the requests
|
||||||
|
pub async fn request_links(&self, links: HashSet<String>) -> Result<()> {
|
||||||
|
log::trace!("enter: request_links({:?})", links);
|
||||||
let recursive = if self.handles.config.no_recursion {
|
let recursive = if self.handles.config.no_recursion {
|
||||||
RecursionStatus::NotRecursive
|
RecursionStatus::NotRecursive
|
||||||
} else {
|
} else {
|
||||||
@@ -121,6 +128,7 @@ impl<'a> Extractor<'a> {
|
|||||||
rx.await?;
|
rx.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
log::trace!("exit: request_links");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +149,7 @@ impl<'a> Extractor<'a> {
|
|||||||
|
|
||||||
let body = self.response.unwrap().text();
|
let body = self.response.unwrap().text();
|
||||||
|
|
||||||
for capture in self.links_regex.captures_iter(&body) {
|
for capture in self.links_regex.captures_iter(body) {
|
||||||
// remove single & double quotes from both ends of the capture
|
// remove single & double quotes from both ends of the capture
|
||||||
// capture[0] is the entire match, additional capture groups start at [1]
|
// capture[0] is the entire match, additional capture groups start at [1]
|
||||||
let link = capture[0].trim_matches(|c| c == '\'' || c == '"');
|
let link = capture[0].trim_matches(|c| c == '\'' || c == '"');
|
||||||
@@ -267,7 +275,7 @@ impl<'a> Extractor<'a> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let new_url = old_url
|
let new_url = old_url
|
||||||
.join(&link)
|
.join(link)
|
||||||
.with_context(|| format!("Could not join {} with {}", old_url, link))?;
|
.with_context(|| format!("Could not join {} with {}", old_url, link))?;
|
||||||
|
|
||||||
links.insert(new_url.to_string());
|
links.insert(new_url.to_string());
|
||||||
@@ -289,10 +297,10 @@ impl<'a> Extractor<'a> {
|
|||||||
pub(super) async fn request_link(&self, url: &str) -> Result<FeroxResponse> {
|
pub(super) async fn request_link(&self, url: &str) -> Result<FeroxResponse> {
|
||||||
log::trace!("enter: request_link({})", url);
|
log::trace!("enter: request_link({})", url);
|
||||||
|
|
||||||
let ferox_url = FeroxUrl::from_string(&url, self.handles.clone());
|
let ferox_url = FeroxUrl::from_string(url, self.handles.clone());
|
||||||
|
|
||||||
// create a url based on the given command line options
|
// create a url based on the given command line options
|
||||||
let new_url = ferox_url.format(&"", None)?;
|
let new_url = ferox_url.format("", None)?;
|
||||||
|
|
||||||
let scanned_urls = self.handles.ferox_scans()?;
|
let scanned_urls = self.handles.ferox_scans()?;
|
||||||
|
|
||||||
@@ -302,6 +310,17 @@ impl<'a> Extractor<'a> {
|
|||||||
bail!("previously seen url");
|
bail!("previously seen url");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !self.handles.config.url_denylist.is_empty()
|
||||||
|
&& should_deny_url(&new_url, self.handles.clone())?
|
||||||
|
{
|
||||||
|
// can't allow a denied url to be requested
|
||||||
|
bail!(
|
||||||
|
"prevented request to {} due to {:?}",
|
||||||
|
url,
|
||||||
|
self.handles.config.url_denylist
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// make the request and store the response
|
// make the request and store the response
|
||||||
let new_response = logged_request(&new_url, self.handles.clone()).await?;
|
let new_response = logged_request(&new_url, self.handles.clone()).await?;
|
||||||
|
|
||||||
@@ -332,7 +351,7 @@ impl<'a> Extractor<'a> {
|
|||||||
if let Some(new_path) = capture.name("url_path") {
|
if let Some(new_path) = capture.name("url_path") {
|
||||||
let mut new_url = Url::parse(&self.url)?;
|
let mut new_url = Url::parse(&self.url)?;
|
||||||
new_url.set_path(new_path.as_str());
|
new_url.set_path(new_path.as_str());
|
||||||
if self.add_all_sub_paths(&new_url.path(), &mut links).is_err() {
|
if self.add_all_sub_paths(new_url.path(), &mut links).is_err() {
|
||||||
log::warn!("could not add sub-paths from {} to {:?}", new_url, links);
|
log::warn!("could not add sub-paths from {} to {:?}", new_url, links);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -391,7 +410,7 @@ impl<'a> Extractor<'a> {
|
|||||||
FeroxResponse::from(response, true, self.handles.config.output_level).await;
|
FeroxResponse::from(response, true, self.handles.config.output_level).await;
|
||||||
|
|
||||||
log::trace!("exit: get_robots_file -> {}", ferox_response);
|
log::trace!("exit: get_robots_file -> {}", ferox_response);
|
||||||
return Ok(ferox_response);
|
Ok(ferox_response)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// update total number of links extracted and expected responses
|
/// update total number of links extracted and expected responses
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ fn setup_extractor(target: ExtractionTarget, scanned_urls: Arc<FeroxScans>) -> E
|
|||||||
/// in the expected array
|
/// in the expected array
|
||||||
fn extractor_get_sub_paths_from_path_with_multiple_paths() {
|
fn extractor_get_sub_paths_from_path_with_multiple_paths() {
|
||||||
let path = "homepage/assets/img/icons/handshake.svg";
|
let path = "homepage/assets/img/icons/handshake.svg";
|
||||||
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(&path);
|
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(path);
|
||||||
let b_paths = BODY_EXT.get_sub_paths_from_path(&path);
|
let b_paths = BODY_EXT.get_sub_paths_from_path(path);
|
||||||
let expected = vec![
|
let expected = vec![
|
||||||
"homepage/",
|
"homepage/",
|
||||||
"homepage/assets/",
|
"homepage/assets/",
|
||||||
@@ -67,8 +67,8 @@ fn extractor_get_sub_paths_from_path_with_multiple_paths() {
|
|||||||
assert_eq!(r_paths.len(), expected.len());
|
assert_eq!(r_paths.len(), expected.len());
|
||||||
assert_eq!(b_paths.len(), expected.len());
|
assert_eq!(b_paths.len(), expected.len());
|
||||||
for expected_path in expected {
|
for expected_path in expected {
|
||||||
assert_eq!(r_paths.contains(&expected_path.to_string()), true);
|
assert!(r_paths.contains(&expected_path.to_string()));
|
||||||
assert_eq!(b_paths.contains(&expected_path.to_string()), true);
|
assert!(b_paths.contains(&expected_path.to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,15 +78,15 @@ fn extractor_get_sub_paths_from_path_with_multiple_paths() {
|
|||||||
/// returned
|
/// returned
|
||||||
fn extractor_get_sub_paths_from_path_with_enclosing_slashes() {
|
fn extractor_get_sub_paths_from_path_with_enclosing_slashes() {
|
||||||
let path = "/homepage/assets/";
|
let path = "/homepage/assets/";
|
||||||
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(&path);
|
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(path);
|
||||||
let b_paths = BODY_EXT.get_sub_paths_from_path(&path);
|
let b_paths = BODY_EXT.get_sub_paths_from_path(path);
|
||||||
let expected = vec!["homepage/", "homepage/assets"];
|
let expected = vec!["homepage/", "homepage/assets"];
|
||||||
|
|
||||||
assert_eq!(r_paths.len(), expected.len());
|
assert_eq!(r_paths.len(), expected.len());
|
||||||
assert_eq!(b_paths.len(), expected.len());
|
assert_eq!(b_paths.len(), expected.len());
|
||||||
for expected_path in expected {
|
for expected_path in expected {
|
||||||
assert_eq!(r_paths.contains(&expected_path.to_string()), true);
|
assert!(r_paths.contains(&expected_path.to_string()));
|
||||||
assert_eq!(b_paths.contains(&expected_path.to_string()), true);
|
assert!(b_paths.contains(&expected_path.to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,15 +95,15 @@ fn extractor_get_sub_paths_from_path_with_enclosing_slashes() {
|
|||||||
/// included
|
/// included
|
||||||
fn extractor_get_sub_paths_from_path_with_only_a_word() {
|
fn extractor_get_sub_paths_from_path_with_only_a_word() {
|
||||||
let path = "homepage";
|
let path = "homepage";
|
||||||
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(&path);
|
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(path);
|
||||||
let b_paths = BODY_EXT.get_sub_paths_from_path(&path);
|
let b_paths = BODY_EXT.get_sub_paths_from_path(path);
|
||||||
let expected = vec!["homepage"];
|
let expected = vec!["homepage"];
|
||||||
|
|
||||||
assert_eq!(r_paths.len(), expected.len());
|
assert_eq!(r_paths.len(), expected.len());
|
||||||
assert_eq!(b_paths.len(), expected.len());
|
assert_eq!(b_paths.len(), expected.len());
|
||||||
for expected_path in expected {
|
for expected_path in expected {
|
||||||
assert_eq!(r_paths.contains(&expected_path.to_string()), true);
|
assert!(r_paths.contains(&expected_path.to_string()));
|
||||||
assert_eq!(b_paths.contains(&expected_path.to_string()), true);
|
assert!(b_paths.contains(&expected_path.to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,15 +111,15 @@ fn extractor_get_sub_paths_from_path_with_only_a_word() {
|
|||||||
/// extract sub paths from the given url fragment; expect 1 sub path, forward slash removed
|
/// extract sub paths from the given url fragment; expect 1 sub path, forward slash removed
|
||||||
fn extractor_get_sub_paths_from_path_with_an_absolute_word() {
|
fn extractor_get_sub_paths_from_path_with_an_absolute_word() {
|
||||||
let path = "/homepage";
|
let path = "/homepage";
|
||||||
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(&path);
|
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(path);
|
||||||
let b_paths = BODY_EXT.get_sub_paths_from_path(&path);
|
let b_paths = BODY_EXT.get_sub_paths_from_path(path);
|
||||||
let expected = vec!["homepage"];
|
let expected = vec!["homepage"];
|
||||||
|
|
||||||
assert_eq!(r_paths.len(), expected.len());
|
assert_eq!(r_paths.len(), expected.len());
|
||||||
assert_eq!(b_paths.len(), expected.len());
|
assert_eq!(b_paths.len(), expected.len());
|
||||||
for expected_path in expected {
|
for expected_path in expected {
|
||||||
assert_eq!(r_paths.contains(&expected_path.to_string()), true);
|
assert!(r_paths.contains(&expected_path.to_string()));
|
||||||
assert_eq!(b_paths.contains(&expected_path.to_string()), true);
|
assert!(b_paths.contains(&expected_path.to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ impl FeroxFilters {
|
|||||||
if let Ok(filters) = self.filters.lock() {
|
if let Ok(filters) = self.filters.lock() {
|
||||||
for filter in filters.iter() {
|
for filter in filters.iter() {
|
||||||
// wildcard.should_filter goes here
|
// wildcard.should_filter goes here
|
||||||
if filter.should_filter_response(&response) {
|
if filter.should_filter_response(response) {
|
||||||
if filter.as_any().downcast_ref::<WildcardFilter>().is_some() {
|
if filter.as_any().downcast_ref::<WildcardFilter>().is_some() {
|
||||||
tx_stats
|
tx_stats
|
||||||
.send(AddToUsizeField(WildcardsFiltered, 1))
|
.send(AddToUsizeField(WildcardsFiltered, 1))
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ pub async fn initialize(handles: Arc<Handles>) -> Result<()> {
|
|||||||
// add any regex filters to filters handler's FeroxFilters (-X|--filter-regex)
|
// add any regex filters to filters handler's FeroxFilters (-X|--filter-regex)
|
||||||
for regex_filter in &handles.config.filter_regex {
|
for regex_filter in &handles.config.filter_regex {
|
||||||
let raw = regex_filter;
|
let raw = regex_filter;
|
||||||
let compiled = skip_fail!(Regex::new(&raw));
|
let compiled = skip_fail!(Regex::new(raw));
|
||||||
|
|
||||||
let filter = RegexFilter {
|
let filter = RegexFilter {
|
||||||
raw_string: raw.to_owned(),
|
raw_string: raw.to_owned(),
|
||||||
@@ -69,7 +69,7 @@ pub async fn initialize(handles: Arc<Handles>) -> Result<()> {
|
|||||||
// add any similarity filters to filters handler's FeroxFilters (--filter-similar-to)
|
// add any similarity filters to filters handler's FeroxFilters (--filter-similar-to)
|
||||||
for similarity_filter in &handles.config.filter_similar {
|
for similarity_filter in &handles.config.filter_similar {
|
||||||
// url as-is based on input, ignores user-specified url manipulation options (add-slash etc)
|
// url as-is based on input, ignores user-specified url manipulation options (add-slash etc)
|
||||||
let url = skip_fail!(Url::parse(&similarity_filter));
|
let url = skip_fail!(Url::parse(similarity_filter));
|
||||||
|
|
||||||
// attempt to request the given url
|
// attempt to request the given url
|
||||||
let resp = skip_fail!(logged_request(&url, handles.clone()).await);
|
let resp = skip_fail!(logged_request(&url, handles.clone()).await);
|
||||||
|
|||||||
@@ -127,6 +127,19 @@ fn wildcard_should_filter_when_static_wildcard_found() {
|
|||||||
assert!(filter.should_filter_response(&resp));
|
assert!(filter.should_filter_response(&resp));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// test should_filter on WilcardFilter where static logic matches but response length is 0
|
||||||
|
fn wildcard_should_filter_when_static_wildcard_len_is_zero() {
|
||||||
|
let mut resp = FeroxResponse::default();
|
||||||
|
resp.set_wildcard(true);
|
||||||
|
resp.set_url("http://localhost");
|
||||||
|
|
||||||
|
// default WildcardFilter is used in the code that executes when response.content_length() == 0
|
||||||
|
let filter = WildcardFilter::new(false);
|
||||||
|
|
||||||
|
assert!(filter.should_filter_response(&resp));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// test should_filter on WilcardFilter where dynamic logic matches
|
/// test should_filter on WilcardFilter where dynamic logic matches
|
||||||
fn wildcard_should_filter_when_dynamic_wildcard_found() {
|
fn wildcard_should_filter_when_dynamic_wildcard_found() {
|
||||||
|
|||||||
@@ -68,6 +68,14 @@ impl FeroxFilter for WildcardFilter {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.size == u64::MAX && response.content_length() == 0 {
|
||||||
|
// static wildcard size found during testing
|
||||||
|
// but response length was zero; pointed out by @Tib3rius
|
||||||
|
log::debug!("static wildcard: filtered out {}", response.url());
|
||||||
|
log::trace!("exit: should_filter_response -> true");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if self.dynamic != u64::MAX {
|
if self.dynamic != u64::MAX {
|
||||||
// dynamic wildcard offset found during testing
|
// dynamic wildcard offset found during testing
|
||||||
|
|
||||||
@@ -76,7 +84,7 @@ impl FeroxFilter for WildcardFilter {
|
|||||||
// except that I don't want an empty string taking up the last index in the
|
// except that I don't want an empty string taking up the last index in the
|
||||||
// event that the url ends with a forward slash. It's ugly enough to be split
|
// event that the url ends with a forward slash. It's ugly enough to be split
|
||||||
// into its own function for readability.
|
// into its own function for readability.
|
||||||
let url_len = FeroxUrl::path_length_of_url(&response.url());
|
let url_len = FeroxUrl::path_length_of_url(response.url());
|
||||||
|
|
||||||
if url_len + self.dynamic == response.content_length() {
|
if url_len + self.dynamic == response.content_length() {
|
||||||
log::debug!("dynamic wildcard: filtered out {}", response.url());
|
log::debug!("dynamic wildcard: filtered out {}", response.url());
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ impl HeuristicTests {
|
|||||||
let mut good_urls = vec![];
|
let mut good_urls = vec![];
|
||||||
|
|
||||||
for target_url in target_urls {
|
for target_url in target_urls {
|
||||||
let url = FeroxUrl::from_string(&target_url, self.handles.clone());
|
let url = FeroxUrl::from_string(target_url, self.handles.clone());
|
||||||
let request = skip_fail!(url.format("", None));
|
let request = skip_fail!(url.format("", None));
|
||||||
|
|
||||||
let result = logged_request(&request, self.handles.clone()).await;
|
let result = logged_request(&request, self.handles.clone()).await;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ pub(crate) type FeroxChannel<T> = (UnboundedSender<T>, UnboundedReceiver<T>);
|
|||||||
pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION");
|
pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
/// Maximum number of file descriptors that can be opened during a scan
|
/// Maximum number of file descriptors that can be opened during a scan
|
||||||
pub const DEFAULT_OPEN_FILE_LIMIT: usize = 8192;
|
pub const DEFAULT_OPEN_FILE_LIMIT: u64 = 8192;
|
||||||
|
|
||||||
/// Default value used to determine near-duplicate web pages (equivalent to 95%)
|
/// Default value used to determine near-duplicate web pages (equivalent to 95%)
|
||||||
pub const SIMILARITY_THRESHOLD: u32 = 95;
|
pub const SIMILARITY_THRESHOLD: u32 = 95;
|
||||||
@@ -73,7 +73,8 @@ pub const HIGH_ERROR_RATIO: f64 = 0.90;
|
|||||||
/// * 401 Unauthorized
|
/// * 401 Unauthorized
|
||||||
/// * 403 Forbidden
|
/// * 403 Forbidden
|
||||||
/// * 405 Method Not Allowed
|
/// * 405 Method Not Allowed
|
||||||
pub const DEFAULT_STATUS_CODES: [StatusCode; 9] = [
|
/// * 500 Internal Server Error
|
||||||
|
pub const DEFAULT_STATUS_CODES: [StatusCode; 10] = [
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
StatusCode::NO_CONTENT,
|
StatusCode::NO_CONTENT,
|
||||||
StatusCode::MOVED_PERMANENTLY,
|
StatusCode::MOVED_PERMANENTLY,
|
||||||
@@ -83,6 +84,7 @@ pub const DEFAULT_STATUS_CODES: [StatusCode; 9] = [
|
|||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
StatusCode::FORBIDDEN,
|
StatusCode::FORBIDDEN,
|
||||||
StatusCode::METHOD_NOT_ALLOWED,
|
StatusCode::METHOD_NOT_ALLOWED,
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Default filename for config file settings
|
/// Default filename for config file settings
|
||||||
|
|||||||
65
src/main.rs
65
src/main.rs
@@ -1,8 +1,9 @@
|
|||||||
use std::{
|
use std::{
|
||||||
env::args,
|
env::args,
|
||||||
fs::File,
|
fs::{create_dir, remove_file, File},
|
||||||
io::{stderr, BufRead, BufReader},
|
io::{stderr, BufRead, BufReader},
|
||||||
ops::Index,
|
ops::Index,
|
||||||
|
path::Path,
|
||||||
process::Command,
|
process::Command,
|
||||||
sync::{atomic::Ordering, Arc},
|
sync::{atomic::Ordering, Arc},
|
||||||
};
|
};
|
||||||
@@ -27,7 +28,7 @@ use feroxbuster::{
|
|||||||
progress::{PROGRESS_BAR, PROGRESS_PRINTER},
|
progress::{PROGRESS_BAR, PROGRESS_PRINTER},
|
||||||
scan_manager::{self},
|
scan_manager::{self},
|
||||||
scanner,
|
scanner,
|
||||||
utils::fmt_err,
|
utils::{fmt_err, slugify_filename},
|
||||||
};
|
};
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
use feroxbuster::{utils::set_open_file_limit, DEFAULT_OPEN_FILE_LIMIT};
|
use feroxbuster::{utils::set_open_file_limit, DEFAULT_OPEN_FILE_LIMIT};
|
||||||
@@ -267,10 +268,56 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
|
|||||||
// from removing --parallel)
|
// from removing --parallel)
|
||||||
original.remove(parallel_index);
|
original.remove(parallel_index);
|
||||||
|
|
||||||
|
// to log unique files to a shared folder, we need to first check for the presence
|
||||||
|
// of -o|--output.
|
||||||
|
let out_dir = if !config.output.is_empty() {
|
||||||
|
// -o|--output was used, so we'll attempt to create a directory to store the files
|
||||||
|
let output_path = Path::new(&handles.config.output);
|
||||||
|
|
||||||
|
// this only returns None if the path terminates in `..`. Since I don't want to
|
||||||
|
// hand-hold to that degree, we'll unwrap and fail if the output path ends in `..`
|
||||||
|
let base_name = output_path.file_name().unwrap();
|
||||||
|
|
||||||
|
let new_folder = slugify_filename(&base_name.to_string_lossy(), "", "logs");
|
||||||
|
|
||||||
|
let final_path = output_path.with_file_name(&new_folder);
|
||||||
|
|
||||||
|
// create the directory or fail silently, assuming the reason for failure is that
|
||||||
|
// the path exists already
|
||||||
|
create_dir(&final_path).unwrap_or(());
|
||||||
|
|
||||||
|
final_path.to_string_lossy().to_string()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
// unvalidated targets fresh from stdin, just spawn children and let them do all checks
|
// unvalidated targets fresh from stdin, just spawn children and let them do all checks
|
||||||
for target in targets {
|
for target in targets {
|
||||||
// add the current target to the provided command
|
// add the current target to the provided command
|
||||||
let mut cloned = original.clone();
|
let mut cloned = original.clone();
|
||||||
|
|
||||||
|
if !out_dir.is_empty() {
|
||||||
|
// output directory value is not empty, need to join output directory with
|
||||||
|
// unique scan filename
|
||||||
|
|
||||||
|
// unwrap is ok, we already know -o was used
|
||||||
|
let out_idx = original
|
||||||
|
.iter()
|
||||||
|
.position(|s| *s == "--output" || *s == "-o")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let filename = slugify_filename(&target, "ferox", "log");
|
||||||
|
|
||||||
|
let full_path = Path::new(&out_dir)
|
||||||
|
.join(filename)
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// a +1 to the index is fine here, as clap has already validated that
|
||||||
|
// -o|--output has a value associated with it
|
||||||
|
cloned[out_idx + 1] = full_path;
|
||||||
|
}
|
||||||
|
|
||||||
cloned.push("-u".to_string());
|
cloned.push("-u".to_string());
|
||||||
cloned.push(target);
|
cloned.push(target);
|
||||||
|
|
||||||
@@ -294,8 +341,22 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the output handler creates an empty file to which it will try to write, because
|
||||||
|
// this happens before we enter the --parallel branch, we need to remove that file
|
||||||
|
// if it's empty
|
||||||
|
let output = handles.config.output.to_owned();
|
||||||
|
|
||||||
clean_up(handles, tasks).await?;
|
clean_up(handles, tasks).await?;
|
||||||
|
|
||||||
|
let file = Path::new(&output);
|
||||||
|
if file.exists() {
|
||||||
|
// expectation is that this is always true for the first ferox process
|
||||||
|
if file.metadata()?.len() == 0 {
|
||||||
|
// empty file, attempt to remove it
|
||||||
|
remove_file(file)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log::trace!("exit: parallel branch && wrapped main");
|
log::trace!("exit: parallel branch && wrapped main");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use clap::{App, Arg, ArgGroup};
|
use clap::{App, Arg, ArgGroup};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use std::env;
|
||||||
|
use std::process;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
/// Regex used to validate values passed to --time-limit
|
/// Regex used to validate values passed to --time-limit
|
||||||
@@ -16,7 +18,7 @@ lazy_static! {
|
|||||||
|
|
||||||
/// Create and return an instance of [clap::App](https://docs.rs/clap/latest/clap/struct.App.html), i.e. the Command Line Interface's configuration
|
/// Create and return an instance of [clap::App](https://docs.rs/clap/latest/clap/struct.App.html), i.e. the Command Line Interface's configuration
|
||||||
pub fn initialize() -> App<'static, 'static> {
|
pub fn initialize() -> App<'static, 'static> {
|
||||||
App::new("feroxbuster")
|
let mut app = App::new("feroxbuster")
|
||||||
.version(env!("CARGO_PKG_VERSION"))
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
.author("Ben 'epi' Risher (@epi052)")
|
.author("Ben 'epi' Risher (@epi052)")
|
||||||
.about("A fast, simple, recursive content discovery tool written in Rust")
|
.about("A fast, simple, recursive content discovery tool written in Rust")
|
||||||
@@ -216,6 +218,17 @@ pub fn initialize() -> App<'static, 'static> {
|
|||||||
"File extension(s) to search for (ex: -x php -x pdf js)",
|
"File extension(s) to search for (ex: -x php -x pdf js)",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("url_denylist")
|
||||||
|
.long("dont-scan")
|
||||||
|
.value_name("URL")
|
||||||
|
.takes_value(true)
|
||||||
|
.multiple(true)
|
||||||
|
.use_delimiter(true)
|
||||||
|
.help(
|
||||||
|
"URL(s) to exclude from recursion/scans",
|
||||||
|
),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("headers")
|
Arg::with_name("headers")
|
||||||
.short("H")
|
.short("H")
|
||||||
@@ -410,7 +423,20 @@ EXAMPLES:
|
|||||||
|
|
||||||
Ludicrous speed... go!
|
Ludicrous speed... go!
|
||||||
./feroxbuster -u http://127.1 -t 200
|
./feroxbuster -u http://127.1 -t 200
|
||||||
"#)
|
"#);
|
||||||
|
|
||||||
|
for arg in env::args() {
|
||||||
|
// secure-77 noticed that when an incorrect flag/option is used, the short help message is printed
|
||||||
|
// which is fine, but if you add -h|--help, it still errors out on the bad flag/option,
|
||||||
|
// never showing the full help message. This code addresses that behavior
|
||||||
|
if arg == "--help" || arg == "-h" {
|
||||||
|
app.print_long_help().unwrap();
|
||||||
|
println!(); // just a newline to mirror original --help output
|
||||||
|
process::exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate that a string is formatted as a number followed by s, m, h, or d (10d, 30s, etc...)
|
/// Validate that a string is formatted as a number followed by s, m, h, or d (10d, 30s, etc...)
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ pub fn add_bar(prefix: &str, length: u64, bar_type: BarType) -> ProgressBar {
|
|||||||
|
|
||||||
progress_bar.set_style(style);
|
progress_bar.set_style(style);
|
||||||
|
|
||||||
progress_bar.set_prefix(&prefix);
|
progress_bar.set_prefix(prefix);
|
||||||
|
|
||||||
progress_bar
|
progress_bar
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ impl FeroxResponse {
|
|||||||
|
|
||||||
/// Set `FeroxResponse`'s `url` attribute, has no affect if an error occurs
|
/// Set `FeroxResponse`'s `url` attribute, has no affect if an error occurs
|
||||||
pub fn set_url(&mut self, url: &str) {
|
pub fn set_url(&mut self, url: &str) {
|
||||||
match Url::parse(&url) {
|
match Url::parse(url) {
|
||||||
Ok(url) => {
|
Ok(url) => {
|
||||||
self.url = url;
|
self.url = url;
|
||||||
}
|
}
|
||||||
@@ -339,7 +339,7 @@ impl FeroxSerialize for FeroxResponse {
|
|||||||
lines,
|
lines,
|
||||||
words,
|
words,
|
||||||
chars,
|
chars,
|
||||||
status_colorizer(&status),
|
status_colorizer(status),
|
||||||
self.url(),
|
self.url(),
|
||||||
FeroxUrl::path_length_of_url(&self.url)
|
FeroxUrl::path_length_of_url(&self.url)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,9 +31,10 @@ impl Menu {
|
|||||||
let separator = "─".to_string();
|
let separator = "─".to_string();
|
||||||
|
|
||||||
let instructions = format!(
|
let instructions = format!(
|
||||||
"Enter a {} list of indexes to {} (ex: 2,3)",
|
"Enter a {} list of indexes/ranges to {} ({}: 1-4,8,9-13)",
|
||||||
style("comma-separated").yellow(),
|
style("comma-separated").yellow(),
|
||||||
style("cancel").red(),
|
style("cancel").red(),
|
||||||
|
style("ex").cyan(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let name = format!(
|
let name = format!(
|
||||||
@@ -43,14 +44,22 @@ impl Menu {
|
|||||||
"💀"
|
"💀"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let force_msg = format!(
|
||||||
|
"Add {} to {} confirmation ({}: 3-5 -f)",
|
||||||
|
style("-f").yellow(),
|
||||||
|
style("skip").yellow(),
|
||||||
|
style("ex").cyan(),
|
||||||
|
);
|
||||||
|
|
||||||
let longest = measure_text_width(&instructions).max(measure_text_width(&name));
|
let longest = measure_text_width(&instructions).max(measure_text_width(&name));
|
||||||
|
|
||||||
let border = separator.repeat(longest);
|
let border = separator.repeat(longest);
|
||||||
|
|
||||||
let padded_name = pad_str(&name, longest, Alignment::Center, None);
|
let padded_name = pad_str(&name, longest, Alignment::Center, None);
|
||||||
|
let padded_force = pad_str(&force_msg, longest, Alignment::Center, None);
|
||||||
|
|
||||||
let header = format!("{}\n{}\n{}", border, padded_name, border);
|
let header = format!("{}\n{}\n{}", border, padded_name, border);
|
||||||
let footer = format!("{}\n{}\n{}", border, instructions, border);
|
let footer = format!("{}\n{}\n{}\n{}", border, instructions, padded_force, border);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
separator,
|
separator,
|
||||||
@@ -93,23 +102,71 @@ impl Menu {
|
|||||||
self.term.write_line(msg).unwrap_or_default();
|
self.term.write_line(msg).unwrap_or_default();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// split a string into vec of usizes
|
/// Helper for parsing a usize from a str
|
||||||
pub(super) fn split_to_nums(&self, line: &str) -> Vec<usize> {
|
fn str_to_usize(&self, value: &str) -> usize {
|
||||||
line.split(',')
|
if value.is_empty() {
|
||||||
.map(|s| {
|
return 0;
|
||||||
s.trim().to_string().parse::<usize>().unwrap_or_else(|e| {
|
}
|
||||||
self.println(&format!("Found non-numeric input: {}", e));
|
|
||||||
0
|
value
|
||||||
})
|
.trim()
|
||||||
|
.to_string()
|
||||||
|
.parse::<usize>()
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
self.println(&format!("Found non-numeric input: {}: {:?}", e, value));
|
||||||
|
0
|
||||||
})
|
})
|
||||||
.filter(|m| *m != 0)
|
}
|
||||||
.collect()
|
|
||||||
|
/// split a comma delimited string into vec of usizes
|
||||||
|
pub(super) fn split_to_nums(&self, line: &str) -> Vec<usize> {
|
||||||
|
let mut nums = Vec::new();
|
||||||
|
let values = line.split(',');
|
||||||
|
|
||||||
|
for mut value in values {
|
||||||
|
value = value.trim();
|
||||||
|
|
||||||
|
if value.contains('-') {
|
||||||
|
// range of two values, needs further processing
|
||||||
|
|
||||||
|
let range: Vec<usize> = value
|
||||||
|
.split('-')
|
||||||
|
.map(|s| self.str_to_usize(s))
|
||||||
|
.filter(|m| *m != 0)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if range.len() != 2 {
|
||||||
|
// expecting [1, 4] or similar, if a 0 was used, we'd be left with a vec of size 1
|
||||||
|
self.println(&format!("Found invalid range of scans: {}", value));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
(range[0]..=range[1]).for_each(|n| {
|
||||||
|
// iterate from lower to upper bound and add all interim values, skipping
|
||||||
|
// any already known
|
||||||
|
if !nums.contains(&n) {
|
||||||
|
nums.push(n)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let value = self.str_to_usize(value);
|
||||||
|
|
||||||
|
if value != 0 && !nums.contains(&value) {
|
||||||
|
// the zeroth scan is always skipped, skip already known values
|
||||||
|
nums.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nums
|
||||||
}
|
}
|
||||||
|
|
||||||
/// get comma-separated list of scan indexes from the user
|
/// get comma-separated list of scan indexes from the user
|
||||||
pub(super) fn get_scans_from_user(&self) -> Option<Vec<usize>> {
|
pub(super) fn get_scans_from_user(&self) -> Option<(Vec<usize>, bool)> {
|
||||||
if let Ok(line) = self.term.read_line() {
|
if let Ok(line) = self.term.read_line() {
|
||||||
Some(self.split_to_nums(&line))
|
let force = line.contains("-f");
|
||||||
|
let line = line.replace("-f", "");
|
||||||
|
Some((self.split_to_nums(&line), force))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ pub struct FeroxScan {
|
|||||||
pub(super) scan_type: ScanType,
|
pub(super) scan_type: ScanType,
|
||||||
|
|
||||||
/// The order in which the scan was received
|
/// The order in which the scan was received
|
||||||
pub(super) scan_order: ScanOrder,
|
pub(crate) scan_order: ScanOrder,
|
||||||
|
|
||||||
/// Number of requests to populate the progress bar with
|
/// Number of requests to populate the progress bar with
|
||||||
pub(super) num_requests: u64,
|
pub(super) num_requests: u64,
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ impl FeroxScans {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Given a list of indexes, cancel their associated FeroxScans
|
/// Given a list of indexes, cancel their associated FeroxScans
|
||||||
async fn cancel_scans(&self, indexes: Vec<usize>) -> usize {
|
async fn cancel_scans(&self, indexes: Vec<usize>, force: bool) -> usize {
|
||||||
let menu_pause_duration = Duration::from_millis(SLEEP_DURATION);
|
let menu_pause_duration = Duration::from_millis(SLEEP_DURATION);
|
||||||
|
|
||||||
let mut num_cancelled = 0_usize;
|
let mut num_cancelled = 0_usize;
|
||||||
@@ -273,7 +273,11 @@ impl FeroxScans {
|
|||||||
Err(..) => continue,
|
Err(..) => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
let input = self.menu.confirm_cancellation(&selected.url);
|
let input = if force {
|
||||||
|
'y'
|
||||||
|
} else {
|
||||||
|
self.menu.confirm_cancellation(&selected.url)
|
||||||
|
};
|
||||||
|
|
||||||
if input == 'y' || input == '\n' {
|
if input == 'y' || input == '\n' {
|
||||||
self.menu.println(&format!("Stopping {}...", selected.url));
|
self.menu.println(&format!("Stopping {}...", selected.url));
|
||||||
@@ -305,8 +309,8 @@ impl FeroxScans {
|
|||||||
|
|
||||||
let mut num_cancelled = 0_usize;
|
let mut num_cancelled = 0_usize;
|
||||||
|
|
||||||
if let Some(input) = self.menu.get_scans_from_user() {
|
if let Some((input, force)) = self.menu.get_scans_from_user() {
|
||||||
num_cancelled += self.cancel_scans(input).await;
|
num_cancelled += self.cancel_scans(input, force).await;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.menu.clear_screen();
|
self.menu.clear_screen();
|
||||||
@@ -427,7 +431,7 @@ impl FeroxScans {
|
|||||||
OutputLevel::Silent => BarType::Hidden,
|
OutputLevel::Silent => BarType::Hidden,
|
||||||
};
|
};
|
||||||
|
|
||||||
let progress_bar = add_bar(&url, bar_length, bar_type);
|
let progress_bar = add_bar(url, bar_length, bar_type);
|
||||||
|
|
||||||
progress_bar.reset_elapsed();
|
progress_bar.reset_elapsed();
|
||||||
|
|
||||||
@@ -437,7 +441,7 @@ impl FeroxScans {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let ferox_scan = FeroxScan::new(
|
let ferox_scan = FeroxScan::new(
|
||||||
&url,
|
url,
|
||||||
scan_type,
|
scan_type,
|
||||||
scan_order,
|
scan_order,
|
||||||
bar_length,
|
bar_length,
|
||||||
@@ -458,7 +462,7 @@ impl FeroxScans {
|
|||||||
///
|
///
|
||||||
/// Also return a reference to the new `FeroxScan`
|
/// Also return a reference to the new `FeroxScan`
|
||||||
pub fn add_directory_scan(&self, url: &str, scan_order: ScanOrder) -> (bool, Arc<FeroxScan>) {
|
pub fn add_directory_scan(&self, url: &str, scan_order: ScanOrder) -> (bool, Arc<FeroxScan>) {
|
||||||
self.add_scan(&url, ScanType::Directory, scan_order)
|
self.add_scan(url, ScanType::Directory, scan_order)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Given a url, create a new `FeroxScan` and add it to `FeroxScans` as a File Scan
|
/// Given a url, create a new `FeroxScan` and add it to `FeroxScans` as a File Scan
|
||||||
@@ -467,7 +471,7 @@ impl FeroxScans {
|
|||||||
///
|
///
|
||||||
/// Also return a reference to the new `FeroxScan`
|
/// Also return a reference to the new `FeroxScan`
|
||||||
pub fn add_file_scan(&self, url: &str, scan_order: ScanOrder) -> (bool, Arc<FeroxScan>) {
|
pub fn add_file_scan(&self, url: &str, scan_order: ScanOrder) -> (bool, Arc<FeroxScan>) {
|
||||||
self.add_scan(&url, ScanType::File, scan_order)
|
self.add_scan(url, ScanType::File, scan_order)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// small helper to determine whether any scans are active or not
|
/// small helper to determine whether any scans are active or not
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ impl FeroxSerialize for FeroxState {
|
|||||||
|
|
||||||
/// Simple call to produce a JSON string using the given FeroxState
|
/// Simple call to produce a JSON string using the given FeroxState
|
||||||
fn as_json(&self) -> Result<String> {
|
fn as_json(&self) -> Result<String> {
|
||||||
Ok(serde_json::to_string(&self)
|
serde_json::to_string(&self)
|
||||||
.with_context(|| fmt_err("Could not convert scan's running state to JSON"))?)
|
.with_context(|| fmt_err("Could not convert scan's running state to JSON"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ fn add_url_to_list_of_scanned_urls_with_unknown_url() {
|
|||||||
let urls = FeroxScans::default();
|
let urls = FeroxScans::default();
|
||||||
let url = "http://unknown_url";
|
let url = "http://unknown_url";
|
||||||
let (result, _scan) = urls.add_scan(url, ScanType::Directory, ScanOrder::Latest);
|
let (result, _scan) = urls.add_scan(url, ScanType::Directory, ScanOrder::Latest);
|
||||||
assert_eq!(result, true);
|
assert!(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -71,11 +71,11 @@ fn add_url_to_list_of_scanned_urls_with_known_url() {
|
|||||||
Some(pb),
|
Some(pb),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(urls.insert(scan), true);
|
assert!(urls.insert(scan));
|
||||||
|
|
||||||
let (result, _scan) = urls.add_scan(url, ScanType::Directory, ScanOrder::Latest);
|
let (result, _scan) = urls.add_scan(url, ScanType::Directory, ScanOrder::Latest);
|
||||||
|
|
||||||
assert_eq!(result, false);
|
assert!(!result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -93,27 +93,23 @@ fn stop_progress_bar_stops_bar() {
|
|||||||
Some(pb),
|
Some(pb),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert!(!scan
|
||||||
scan.progress_bar
|
.progress_bar
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.is_finished(),
|
.is_finished());
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
scan.stop_progress_bar();
|
scan.stop_progress_bar();
|
||||||
|
|
||||||
assert_eq!(
|
assert!(scan
|
||||||
scan.progress_bar
|
.progress_bar
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.is_finished(),
|
.is_finished());
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -131,11 +127,11 @@ fn add_url_to_list_of_scanned_urls_with_known_url_without_slash() {
|
|||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(urls.insert(scan), true);
|
assert!(urls.insert(scan));
|
||||||
|
|
||||||
let (result, _scan) = urls.add_scan(url, ScanType::File, ScanOrder::Latest);
|
let (result, _scan) = urls.add_scan(url, ScanType::File, ScanOrder::Latest);
|
||||||
|
|
||||||
assert_eq!(result, false);
|
assert!(!result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
@@ -171,8 +167,8 @@ async fn call_display_scans() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(urls.insert(scan), true);
|
assert!(urls.insert(scan));
|
||||||
assert_eq!(urls.insert(scan_two), true);
|
assert!(urls.insert(scan_two));
|
||||||
|
|
||||||
urls.display_scans().await;
|
urls.display_scans().await;
|
||||||
}
|
}
|
||||||
@@ -330,7 +326,7 @@ fn ferox_response_serialize_and_deserialize() {
|
|||||||
|
|
||||||
assert_eq!(response.url().as_str(), "https://nerdcore.com/css");
|
assert_eq!(response.url().as_str(), "https://nerdcore.com/css");
|
||||||
assert_eq!(response.url().path(), "/css");
|
assert_eq!(response.url().path(), "/css");
|
||||||
assert_eq!(response.wildcard(), true);
|
assert!(response.wildcard());
|
||||||
assert_eq!(response.status().as_u16(), 301);
|
assert_eq!(response.status().as_u16(), 301);
|
||||||
assert_eq!(response.content_length(), 173);
|
assert_eq!(response.content_length(), 173);
|
||||||
assert_eq!(response.line_count(), 10);
|
assert_eq!(response.line_count(), 10);
|
||||||
@@ -383,7 +379,7 @@ fn feroxstates_feroxserialize_implementation() {
|
|||||||
|
|
||||||
let json_state = ferox_state.as_json().unwrap();
|
let json_state = ferox_state.as_json().unwrap();
|
||||||
let expected = format!(
|
let expected = format!(
|
||||||
r#"{{"scans":[{{"id":"{}","url":"https://spiritanimal.com","scan_type":"Directory","status":"NotStarted","num_requests":0}}],"config":{{"type":"configuration","wordlist":"/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt","config":"","proxy":"","replay_proxy":"","target_url":"","status_codes":[200,204,301,302,307,308,401,403,405],"replay_codes":[200,204,301,302,307,308,401,403,405],"filter_status":[],"threads":50,"timeout":7,"verbosity":0,"silent":false,"quiet":false,"auto_bail":false,"auto_tune":false,"json":false,"output":"","debug_log":"","user_agent":"feroxbuster/{}","redirects":false,"insecure":false,"extensions":[],"headers":{{}},"queries":[],"no_recursion":false,"extract_links":false,"add_slash":false,"stdin":false,"depth":4,"scan_limit":0,"parallel":0,"rate_limit":0,"filter_size":[],"filter_line_count":[],"filter_word_count":[],"filter_regex":[],"dont_filter":false,"resumed":false,"resume_from":"","save_state":false,"time_limit":"","filter_similar":[]}},"responses":[{{"type":"response","url":"https://nerdcore.com/css","path":"/css","wildcard":true,"status":301,"content_length":173,"line_count":10,"word_count":16,"headers":{{"server":"nginx/1.16.1"}}}}]"#,
|
r#"{{"scans":[{{"id":"{}","url":"https://spiritanimal.com","scan_type":"Directory","status":"NotStarted","num_requests":0}}],"config":{{"type":"configuration","wordlist":"/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt","config":"","proxy":"","replay_proxy":"","target_url":"","status_codes":[200,204,301,302,307,308,401,403,405,500],"replay_codes":[200,204,301,302,307,308,401,403,405,500],"filter_status":[],"threads":50,"timeout":7,"verbosity":0,"silent":false,"quiet":false,"auto_bail":false,"auto_tune":false,"json":false,"output":"","debug_log":"","user_agent":"feroxbuster/{}","redirects":false,"insecure":false,"extensions":[],"headers":{{}},"queries":[],"no_recursion":false,"extract_links":false,"add_slash":false,"stdin":false,"depth":4,"scan_limit":0,"parallel":0,"rate_limit":0,"filter_size":[],"filter_line_count":[],"filter_word_count":[],"filter_regex":[],"dont_filter":false,"resumed":false,"resume_from":"","save_state":false,"time_limit":"","filter_similar":[],"url_denylist":[]}},"responses":[{{"type":"response","url":"https://nerdcore.com/css","path":"/css","wildcard":true,"status":301,"content_length":173,"line_count":10,"word_count":16,"headers":{{"server":"nginx/1.16.1"}}}}]"#,
|
||||||
saved_id, VERSION
|
saved_id, VERSION
|
||||||
);
|
);
|
||||||
println!("{}\n{}", expected, json_state);
|
println!("{}\n{}", expected, json_state);
|
||||||
@@ -521,9 +517,13 @@ fn menu_print_header_and_footer() {
|
|||||||
fn split_to_nums_is_correct() {
|
fn split_to_nums_is_correct() {
|
||||||
let menu = Menu::new();
|
let menu = Menu::new();
|
||||||
|
|
||||||
let nums = menu.split_to_nums("1, 3, 4");
|
let nums = menu.split_to_nums("1, 3, 4, 7 - 12, 10-10, 10-11, 9-12, 12-6, -1, 4-");
|
||||||
|
|
||||||
assert_eq!(nums, vec![1, 3, 4]);
|
assert_eq!(nums, vec![1, 3, 4, 7, 8, 9, 10, 11, 12]);
|
||||||
|
assert_eq!(menu.split_to_nums("9-12"), vec![9, 10, 11, 12]);
|
||||||
|
assert!(menu.split_to_nums("-12").is_empty());
|
||||||
|
assert!(menu.split_to_nums("12-").is_empty());
|
||||||
|
assert!(menu.split_to_nums("\n").is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ pub async fn start_max_time_thread(handles: Arc<Handles>) {
|
|||||||
log::trace!("exit: start_max_time_thread");
|
log::trace!("exit: start_max_time_thread");
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
panic!(handles);
|
panic!("{:?}", handles);
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
let _ = TermInputHandler::sigint_handler(handles.clone());
|
let _ = TermInputHandler::sigint_handler(handles.clone());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,8 @@ impl FeroxScanner {
|
|||||||
.target(RobotsTxt)
|
.target(RobotsTxt)
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
let _ = extractor.extract().await;
|
let links = extractor.extract().await?;
|
||||||
|
extractor.request_links(links).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let scanned_urls = self.handles.ferox_scans()?;
|
let scanned_urls = self.handles.ferox_scans()?;
|
||||||
|
|||||||
@@ -90,11 +90,9 @@ impl PolicyData {
|
|||||||
atomic_store!(self.remove_limit, true);
|
atomic_store!(self.remove_limit, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.set_limit(heap.value() as usize);
|
|
||||||
} else if heap.has_children() {
|
} else if heap.has_children() {
|
||||||
// streak not at 3, just check that we can move down, and do so
|
// streak not at 3, just check that we can move down, and do so
|
||||||
heap.move_left();
|
heap.move_left();
|
||||||
self.set_limit(heap.value() as usize);
|
|
||||||
} else {
|
} else {
|
||||||
// tree bottomed out, need to move back up the tree a bit
|
// tree bottomed out, need to move back up the tree a bit
|
||||||
let current = heap.value();
|
let current = heap.value();
|
||||||
@@ -104,9 +102,8 @@ impl PolicyData {
|
|||||||
if current > heap.value() {
|
if current > heap.value() {
|
||||||
heap.move_up();
|
heap.move_up();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.set_limit(heap.value() as usize);
|
|
||||||
}
|
}
|
||||||
|
self.set_limit(heap.value() as usize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +197,7 @@ mod tests {
|
|||||||
pd.adjust_up(&3);
|
pd.adjust_up(&3);
|
||||||
assert_eq!(pd.heap.read().unwrap().value(), 300);
|
assert_eq!(pd.heap.read().unwrap().value(), 300);
|
||||||
assert_eq!(pd.limit.load(Ordering::Relaxed), 300);
|
assert_eq!(pd.limit.load(Ordering::Relaxed), 300);
|
||||||
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), false);
|
assert!(!pd.remove_limit.load(Ordering::Relaxed));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -217,7 +214,7 @@ mod tests {
|
|||||||
pd.adjust_up(&3);
|
pd.adjust_up(&3);
|
||||||
assert_eq!(pd.heap.read().unwrap().value(), 200);
|
assert_eq!(pd.heap.read().unwrap().value(), 200);
|
||||||
assert_eq!(pd.limit.load(Ordering::Relaxed), 200);
|
assert_eq!(pd.limit.load(Ordering::Relaxed), 200);
|
||||||
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), true);
|
assert!(pd.remove_limit.load(Ordering::Relaxed));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -234,7 +231,7 @@ mod tests {
|
|||||||
pd.adjust_up(&3);
|
pd.adjust_up(&3);
|
||||||
assert_eq!(pd.heap.read().unwrap().value(), 350);
|
assert_eq!(pd.heap.read().unwrap().value(), 350);
|
||||||
assert_eq!(pd.limit.load(Ordering::Relaxed), 350);
|
assert_eq!(pd.limit.load(Ordering::Relaxed), 350);
|
||||||
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), false);
|
assert!(!pd.remove_limit.load(Ordering::Relaxed));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -251,7 +248,7 @@ mod tests {
|
|||||||
pd.adjust_up(&3);
|
pd.adjust_up(&3);
|
||||||
assert_eq!(pd.heap.read().unwrap().value(), 300);
|
assert_eq!(pd.heap.read().unwrap().value(), 300);
|
||||||
assert_eq!(pd.limit.load(Ordering::Relaxed), 300);
|
assert_eq!(pd.limit.load(Ordering::Relaxed), 300);
|
||||||
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), false);
|
assert!(!pd.remove_limit.load(Ordering::Relaxed));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -269,7 +266,7 @@ mod tests {
|
|||||||
pd.adjust_up(&0);
|
pd.adjust_up(&0);
|
||||||
assert_eq!(pd.heap.read().unwrap().value(), 43);
|
assert_eq!(pd.heap.read().unwrap().value(), 43);
|
||||||
assert_eq!(pd.limit.load(Ordering::Relaxed), 43);
|
assert_eq!(pd.limit.load(Ordering::Relaxed), 43);
|
||||||
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), false);
|
assert!(!pd.remove_limit.load(Ordering::Relaxed));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -287,7 +284,7 @@ mod tests {
|
|||||||
pd.adjust_up(&0);
|
pd.adjust_up(&0);
|
||||||
assert_eq!(pd.heap.read().unwrap().value(), 37);
|
assert_eq!(pd.heap.read().unwrap().value(), 37);
|
||||||
assert_eq!(pd.limit.load(Ordering::Relaxed), 37);
|
assert_eq!(pd.limit.load(Ordering::Relaxed), 37);
|
||||||
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), false);
|
assert!(!pd.remove_limit.load(Ordering::Relaxed));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::{policy_data::PolicyData, FeroxScanner, PolicyTrigger};
|
use super::{policy_data::PolicyData, FeroxScanner, PolicyTrigger};
|
||||||
|
use crate::utils::should_deny_url;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
/// Makes multiple requests based on the presence of extensions
|
/// Makes multiple requests based on the presence of extensions
|
||||||
pub(super) struct Requester {
|
pub(super) struct Requester {
|
||||||
@@ -45,11 +47,16 @@ pub(super) struct Requester {
|
|||||||
/// FeroxScan associated with the creation of this Requester
|
/// FeroxScan associated with the creation of this Requester
|
||||||
ferox_scan: Arc<FeroxScan>,
|
ferox_scan: Arc<FeroxScan>,
|
||||||
|
|
||||||
|
/// cache of previously seen links gotten via link extraction. since the requester is passed
|
||||||
|
/// around as an arc, and seen_links needs to be mutable, putting it behind a lock for
|
||||||
|
/// interior mutability, similar to the tuning_lock below
|
||||||
|
seen_links: RwLock<HashSet<String>>,
|
||||||
|
|
||||||
/// simple lock to control access to tuning to a single thread (per-scan)
|
/// simple lock to control access to tuning to a single thread (per-scan)
|
||||||
///
|
///
|
||||||
/// need a usize to determine the number of consecutive non-error calls that a requester has
|
/// need a usize to determine the number of consecutive non-error calls that a requester has
|
||||||
/// seen; this will satisfy the non-mut self constraint (due to us being behind an Arc, and
|
/// seen; this will satisfy the non-mut self constraint (due to us being behind an Arc, and
|
||||||
/// the need for a counter
|
/// the need for a counter)
|
||||||
tuning_lock: Mutex<usize>,
|
tuning_lock: Mutex<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +80,7 @@ impl Requester {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
ferox_scan,
|
ferox_scan,
|
||||||
policy_data,
|
policy_data,
|
||||||
|
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||||
rate_limiter: RwLock::new(rate_limiter),
|
rate_limiter: RwLock::new(rate_limiter),
|
||||||
handles: scanner.handles.clone(),
|
handles: scanner.handles.clone(),
|
||||||
target_url: scanner.target_url.to_owned(),
|
target_url: scanner.target_url.to_owned(),
|
||||||
@@ -298,6 +306,8 @@ impl Requester {
|
|||||||
let urls =
|
let urls =
|
||||||
FeroxUrl::from_string(&self.target_url, self.handles.clone()).formatted_urls(word)?;
|
FeroxUrl::from_string(&self.target_url, self.handles.clone()).formatted_urls(word)?;
|
||||||
|
|
||||||
|
let should_test_deny = !self.handles.config.url_denylist.is_empty();
|
||||||
|
|
||||||
for url in urls {
|
for url in urls {
|
||||||
// auto_tune is true, or rate_limit was set (mutually exclusive to user)
|
// auto_tune is true, or rate_limit was set (mutually exclusive to user)
|
||||||
// and a rate_limiter has been created
|
// and a rate_limiter has been created
|
||||||
@@ -313,6 +323,11 @@ impl Requester {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if should_test_deny && should_deny_url(&url, self.handles.clone())? {
|
||||||
|
// can't allow a denied url to be requested
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let response = logged_request(&url, self.handles.clone()).await?;
|
let response = logged_request(&url, self.handles.clone()).await?;
|
||||||
|
|
||||||
if (should_tune || self.handles.config.auto_bail)
|
if (should_tune || self.handles.config.auto_bail)
|
||||||
@@ -367,7 +382,26 @@ impl Requester {
|
|||||||
.handles(self.handles.clone())
|
.handles(self.handles.clone())
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
extractor.extract().await?;
|
let new_links: HashSet<_>;
|
||||||
|
let extracted = extractor.extract().await?;
|
||||||
|
|
||||||
|
{
|
||||||
|
// gain and quickly drop the read lock on seen_links, using it while unlocked
|
||||||
|
// to determine if there are any new links to process
|
||||||
|
let read_links = self.seen_links.read().await;
|
||||||
|
new_links = extracted.difference(&read_links).cloned().collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !new_links.is_empty() {
|
||||||
|
// using is_empty instead of direct iteration to acquire the write lock behind
|
||||||
|
// some kind of less expensive gate (and not in a loop, obv)
|
||||||
|
let mut write_links = self.seen_links.write().await;
|
||||||
|
for new_link in &new_links {
|
||||||
|
write_links.insert(new_link.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extractor.request_links(new_links).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// everything else should be reported
|
// everything else should be reported
|
||||||
@@ -536,6 +570,7 @@ mod tests {
|
|||||||
|
|
||||||
let requester = Requester {
|
let requester = Requester {
|
||||||
handles,
|
handles,
|
||||||
|
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||||
tuning_lock: Mutex::new(0),
|
tuning_lock: Mutex::new(0),
|
||||||
ferox_scan: Arc::new(FeroxScan::default()),
|
ferox_scan: Arc::new(FeroxScan::default()),
|
||||||
target_url: "http://localhost".to_string(),
|
target_url: "http://localhost".to_string(),
|
||||||
@@ -563,6 +598,7 @@ mod tests {
|
|||||||
|
|
||||||
let requester = Requester {
|
let requester = Requester {
|
||||||
handles,
|
handles,
|
||||||
|
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||||
tuning_lock: Mutex::new(0),
|
tuning_lock: Mutex::new(0),
|
||||||
ferox_scan: ferox_scan.clone(),
|
ferox_scan: ferox_scan.clone(),
|
||||||
target_url: "http://localhost".to_string(),
|
target_url: "http://localhost".to_string(),
|
||||||
@@ -587,6 +623,7 @@ mod tests {
|
|||||||
|
|
||||||
let requester = Requester {
|
let requester = Requester {
|
||||||
handles,
|
handles,
|
||||||
|
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||||
tuning_lock: Mutex::new(0),
|
tuning_lock: Mutex::new(0),
|
||||||
ferox_scan: ferox_scan.clone(),
|
ferox_scan: ferox_scan.clone(),
|
||||||
target_url: "http://localhost".to_string(),
|
target_url: "http://localhost".to_string(),
|
||||||
@@ -626,6 +663,7 @@ mod tests {
|
|||||||
|
|
||||||
let requester = Requester {
|
let requester = Requester {
|
||||||
handles,
|
handles,
|
||||||
|
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||||
tuning_lock: Mutex::new(0),
|
tuning_lock: Mutex::new(0),
|
||||||
ferox_scan: ferox_scan.clone(),
|
ferox_scan: ferox_scan.clone(),
|
||||||
target_url: "http://localhost".to_string(),
|
target_url: "http://localhost".to_string(),
|
||||||
@@ -680,6 +718,7 @@ mod tests {
|
|||||||
let req_clone = scan_two.clone();
|
let req_clone = scan_two.clone();
|
||||||
let requester = Requester {
|
let requester = Requester {
|
||||||
handles,
|
handles,
|
||||||
|
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||||
tuning_lock: Mutex::new(0),
|
tuning_lock: Mutex::new(0),
|
||||||
ferox_scan: req_clone,
|
ferox_scan: req_clone,
|
||||||
target_url: "http://one/one/stuff.php".to_string(),
|
target_url: "http://one/one/stuff.php".to_string(),
|
||||||
@@ -713,6 +752,7 @@ mod tests {
|
|||||||
|
|
||||||
let requester = Requester {
|
let requester = Requester {
|
||||||
handles,
|
handles,
|
||||||
|
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||||
tuning_lock: Mutex::new(0),
|
tuning_lock: Mutex::new(0),
|
||||||
ferox_scan: Arc::new(FeroxScan::default()),
|
ferox_scan: Arc::new(FeroxScan::default()),
|
||||||
target_url: "http://one/one/stuff.php".to_string(),
|
target_url: "http://one/one/stuff.php".to_string(),
|
||||||
@@ -734,6 +774,7 @@ mod tests {
|
|||||||
|
|
||||||
let requester = Requester {
|
let requester = Requester {
|
||||||
handles,
|
handles,
|
||||||
|
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||||
tuning_lock: Mutex::new(0),
|
tuning_lock: Mutex::new(0),
|
||||||
ferox_scan: Arc::new(FeroxScan::default()),
|
ferox_scan: Arc::new(FeroxScan::default()),
|
||||||
target_url: "http://localhost".to_string(),
|
target_url: "http://localhost".to_string(),
|
||||||
@@ -756,6 +797,7 @@ mod tests {
|
|||||||
|
|
||||||
let requester = Arc::new(Requester {
|
let requester = Arc::new(Requester {
|
||||||
handles,
|
handles,
|
||||||
|
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||||
tuning_lock: Mutex::new(0),
|
tuning_lock: Mutex::new(0),
|
||||||
ferox_scan: Arc::new(FeroxScan::default()),
|
ferox_scan: Arc::new(FeroxScan::default()),
|
||||||
target_url: "http://localhost".to_string(),
|
target_url: "http://localhost".to_string(),
|
||||||
@@ -772,7 +814,7 @@ mod tests {
|
|||||||
|
|
||||||
requester.cool_down().await;
|
requester.cool_down().await;
|
||||||
|
|
||||||
assert_eq!(resp.await.unwrap(), true);
|
assert!(resp.await.unwrap());
|
||||||
println!("{}", start.elapsed().as_millis());
|
println!("{}", start.elapsed().as_millis());
|
||||||
assert!(start.elapsed().as_millis() >= 3500);
|
assert!(start.elapsed().as_millis() >= 3500);
|
||||||
}
|
}
|
||||||
@@ -785,6 +827,7 @@ mod tests {
|
|||||||
|
|
||||||
let requester = Requester {
|
let requester = Requester {
|
||||||
handles,
|
handles,
|
||||||
|
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||||
tuning_lock: Mutex::new(0),
|
tuning_lock: Mutex::new(0),
|
||||||
ferox_scan: Arc::new(FeroxScan::default()),
|
ferox_scan: Arc::new(FeroxScan::default()),
|
||||||
target_url: "http://localhost".to_string(),
|
target_url: "http://localhost".to_string(),
|
||||||
@@ -822,6 +865,7 @@ mod tests {
|
|||||||
|
|
||||||
let requester = Requester {
|
let requester = Requester {
|
||||||
handles,
|
handles,
|
||||||
|
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||||
tuning_lock: Mutex::new(0),
|
tuning_lock: Mutex::new(0),
|
||||||
ferox_scan: Arc::new(scan),
|
ferox_scan: Arc::new(scan),
|
||||||
target_url: "http://localhost".to_string(),
|
target_url: "http://localhost".to_string(),
|
||||||
@@ -857,6 +901,7 @@ mod tests {
|
|||||||
|
|
||||||
let requester = Requester {
|
let requester = Requester {
|
||||||
handles,
|
handles,
|
||||||
|
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||||
tuning_lock: Mutex::new(0),
|
tuning_lock: Mutex::new(0),
|
||||||
ferox_scan: Arc::new(scan),
|
ferox_scan: Arc::new(scan),
|
||||||
target_url: "http://localhost".to_string(),
|
target_url: "http://localhost".to_string(),
|
||||||
@@ -884,6 +929,7 @@ mod tests {
|
|||||||
|
|
||||||
let mut requester = Requester {
|
let mut requester = Requester {
|
||||||
handles,
|
handles,
|
||||||
|
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||||
tuning_lock: Mutex::new(0),
|
tuning_lock: Mutex::new(0),
|
||||||
ferox_scan: Arc::new(FeroxScan::default()),
|
ferox_scan: Arc::new(FeroxScan::default()),
|
||||||
target_url: "http://localhost".to_string(),
|
target_url: "http://localhost".to_string(),
|
||||||
@@ -891,28 +937,16 @@ mod tests {
|
|||||||
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
|
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert!(!requester.too_many_status_errors(PolicyTrigger::Errors));
|
||||||
requester.too_many_status_errors(PolicyTrigger::Errors),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
assert!(!requester.too_many_status_errors(PolicyTrigger::Status429));
|
||||||
requester.too_many_status_errors(PolicyTrigger::Status429),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
requester.ferox_scan.progress_bar().set_position(10);
|
requester.ferox_scan.progress_bar().set_position(10);
|
||||||
requester.ferox_scan.add_429();
|
requester.ferox_scan.add_429();
|
||||||
requester.ferox_scan.add_429();
|
requester.ferox_scan.add_429();
|
||||||
requester.ferox_scan.add_429();
|
requester.ferox_scan.add_429();
|
||||||
assert_eq!(
|
assert!(requester.too_many_status_errors(PolicyTrigger::Status429));
|
||||||
requester.too_many_status_errors(PolicyTrigger::Status429),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
assert!(!requester.too_many_status_errors(PolicyTrigger::Status403));
|
||||||
requester.too_many_status_errors(PolicyTrigger::Status403),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
requester.ferox_scan = Arc::new(FeroxScan::default());
|
requester.ferox_scan = Arc::new(FeroxScan::default());
|
||||||
requester.ferox_scan.progress_bar().set_position(10);
|
requester.ferox_scan.progress_bar().set_position(10);
|
||||||
requester.ferox_scan.add_403();
|
requester.ferox_scan.add_403();
|
||||||
@@ -924,10 +958,7 @@ mod tests {
|
|||||||
requester.ferox_scan.add_403();
|
requester.ferox_scan.add_403();
|
||||||
requester.ferox_scan.add_403();
|
requester.ferox_scan.add_403();
|
||||||
requester.ferox_scan.add_403();
|
requester.ferox_scan.add_403();
|
||||||
assert_eq!(
|
assert!(requester.too_many_status_errors(PolicyTrigger::Status403));
|
||||||
requester.too_many_status_errors(PolicyTrigger::Status403),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
@@ -941,6 +972,7 @@ mod tests {
|
|||||||
|
|
||||||
let requester = Requester {
|
let requester = Requester {
|
||||||
handles,
|
handles,
|
||||||
|
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||||
tuning_lock: Mutex::new(0),
|
tuning_lock: Mutex::new(0),
|
||||||
ferox_scan: Arc::new(FeroxScan::default()),
|
ferox_scan: Arc::new(FeroxScan::default()),
|
||||||
target_url: "http://localhost".to_string(),
|
target_url: "http://localhost".to_string(),
|
||||||
@@ -983,6 +1015,7 @@ mod tests {
|
|||||||
|
|
||||||
let requester = Requester {
|
let requester = Requester {
|
||||||
handles,
|
handles,
|
||||||
|
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||||
tuning_lock: Mutex::new(0),
|
tuning_lock: Mutex::new(0),
|
||||||
ferox_scan: scan.clone(),
|
ferox_scan: scan.clone(),
|
||||||
target_url: "http://localhost".to_string(),
|
target_url: "http://localhost".to_string(),
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
convert::TryFrom,
|
||||||
fs::File,
|
fs::File,
|
||||||
io::BufReader,
|
io::BufReader,
|
||||||
sync::{
|
sync::{
|
||||||
@@ -9,7 +11,8 @@ use std::{
|
|||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
traits::FeroxSerialize,
|
traits::FeroxSerialize,
|
||||||
@@ -19,9 +22,8 @@ use crate::{
|
|||||||
use super::{error::StatError, field::StatField};
|
use super::{error::StatError, field::StatField};
|
||||||
|
|
||||||
/// Data collection of statistics related to a scan
|
/// Data collection of statistics related to a scan
|
||||||
#[derive(Default, Deserialize, Debug, Serialize)]
|
#[derive(Default, Debug)]
|
||||||
pub struct Stats {
|
pub struct Stats {
|
||||||
#[serde(rename = "type")]
|
|
||||||
/// Name of this type of struct, used for serialization, i.e. `{"type":"statistics"}`
|
/// Name of this type of struct, used for serialization, i.e. `{"type":"statistics"}`
|
||||||
kind: String,
|
kind: String,
|
||||||
|
|
||||||
@@ -125,11 +127,9 @@ pub struct Stats {
|
|||||||
total_runtime: Mutex<Vec<f64>>,
|
total_runtime: Mutex<Vec<f64>>,
|
||||||
|
|
||||||
/// tracker for the number of extensions the user specified
|
/// tracker for the number of extensions the user specified
|
||||||
#[serde(skip)]
|
|
||||||
num_extensions: usize,
|
num_extensions: usize,
|
||||||
|
|
||||||
/// tracker for whether to use json during serialization or not
|
/// tracker for whether to use json during serialization or not
|
||||||
#[serde(skip)]
|
|
||||||
json: bool,
|
json: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +147,301 @@ impl FeroxSerialize for Stats {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serialize implementation for Stats
|
||||||
|
impl Serialize for Stats {
|
||||||
|
/// Function that handles serialization of Stats
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let mut state = serializer.serialize_struct("Stats", 32)?;
|
||||||
|
|
||||||
|
state.serialize_field("type", &self.kind)?;
|
||||||
|
state.serialize_field("timeouts", &atomic_load!(self.timeouts))?;
|
||||||
|
state.serialize_field("requests", &atomic_load!(self.requests))?;
|
||||||
|
state.serialize_field("expected_per_scan", &atomic_load!(self.expected_per_scan))?;
|
||||||
|
state.serialize_field("total_expected", &atomic_load!(self.total_expected))?;
|
||||||
|
state.serialize_field("errors", &atomic_load!(self.errors))?;
|
||||||
|
state.serialize_field("successes", &atomic_load!(self.successes))?;
|
||||||
|
state.serialize_field("redirects", &atomic_load!(self.redirects))?;
|
||||||
|
state.serialize_field("client_errors", &atomic_load!(self.client_errors))?;
|
||||||
|
state.serialize_field("server_errors", &atomic_load!(self.server_errors))?;
|
||||||
|
state.serialize_field("total_scans", &atomic_load!(self.total_scans))?;
|
||||||
|
state.serialize_field("initial_targets", &atomic_load!(self.initial_targets))?;
|
||||||
|
state.serialize_field("links_extracted", &atomic_load!(self.links_extracted))?;
|
||||||
|
state.serialize_field("status_200s", &atomic_load!(self.status_200s))?;
|
||||||
|
state.serialize_field("status_301s", &atomic_load!(self.status_301s))?;
|
||||||
|
state.serialize_field("status_302s", &atomic_load!(self.status_302s))?;
|
||||||
|
state.serialize_field("status_401s", &atomic_load!(self.status_401s))?;
|
||||||
|
state.serialize_field("status_403s", &atomic_load!(self.status_403s))?;
|
||||||
|
state.serialize_field("status_429s", &atomic_load!(self.status_429s))?;
|
||||||
|
state.serialize_field("status_500s", &atomic_load!(self.status_500s))?;
|
||||||
|
state.serialize_field("status_503s", &atomic_load!(self.status_503s))?;
|
||||||
|
state.serialize_field("status_504s", &atomic_load!(self.status_504s))?;
|
||||||
|
state.serialize_field("status_508s", &atomic_load!(self.status_508s))?;
|
||||||
|
state.serialize_field("wildcards_filtered", &atomic_load!(self.wildcards_filtered))?;
|
||||||
|
state.serialize_field("responses_filtered", &atomic_load!(self.responses_filtered))?;
|
||||||
|
state.serialize_field(
|
||||||
|
"resources_discovered",
|
||||||
|
&atomic_load!(self.resources_discovered),
|
||||||
|
)?;
|
||||||
|
state.serialize_field("url_format_errors", &atomic_load!(self.url_format_errors))?;
|
||||||
|
state.serialize_field("redirection_errors", &atomic_load!(self.redirection_errors))?;
|
||||||
|
state.serialize_field("connection_errors", &atomic_load!(self.connection_errors))?;
|
||||||
|
state.serialize_field("request_errors", &atomic_load!(self.request_errors))?;
|
||||||
|
state.serialize_field("directory_scan_times", &self.directory_scan_times)?;
|
||||||
|
state.serialize_field("total_runtime", &self.total_runtime)?;
|
||||||
|
|
||||||
|
state.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserialize implementation for Stats
|
||||||
|
impl<'a> Deserialize<'a> for Stats {
|
||||||
|
/// Deserialize a Stats object from a serde_json::Value
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'a>,
|
||||||
|
{
|
||||||
|
let stats = Self::new(0, false);
|
||||||
|
|
||||||
|
let map: HashMap<String, Value> = HashMap::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
for (key, value) in &map {
|
||||||
|
match key.as_str() {
|
||||||
|
"timeouts" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.timeouts, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"requests" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.requests, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"expected_per_scan" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.expected_per_scan, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"total_expected" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.total_expected, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"errors" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.errors, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"successes" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.successes, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"redirects" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.redirects, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"client_errors" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.client_errors, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"server_errors" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.server_errors, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"total_scans" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.total_scans, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"initial_targets" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.initial_targets, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"links_extracted" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.links_extracted, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"status_200s" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.status_200s, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"status_301s" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.status_301s, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"status_302s" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.status_302s, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"status_401s" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.status_401s, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"status_403s" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.status_403s, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"status_429s" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.status_429s, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"status_500s" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.status_500s, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"status_503s" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.status_503s, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"status_504s" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.status_504s, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"status_508s" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.status_508s, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"wildcards_filtered" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.wildcards_filtered, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"responses_filtered" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.responses_filtered, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"resources_discovered" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.resources_discovered, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"url_format_errors" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.url_format_errors, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"redirection_errors" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.redirection_errors, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"connection_errors" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.connection_errors, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"request_errors" => {
|
||||||
|
if let Some(num) = value.as_u64() {
|
||||||
|
if let Ok(parsed) = usize::try_from(num) {
|
||||||
|
atomic_increment!(stats.request_errors, parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"directory_scan_times" => {
|
||||||
|
if let Some(arr) = value.as_array() {
|
||||||
|
for val in arr {
|
||||||
|
if let Some(parsed) = val.as_f64() {
|
||||||
|
if let Ok(mut guard) = stats.directory_scan_times.lock() {
|
||||||
|
guard.push(parsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"total_runtime" => {
|
||||||
|
if let Some(arr) = value.as_array() {
|
||||||
|
for val in arr {
|
||||||
|
if let Some(parsed) = val.as_f64() {
|
||||||
|
if let Ok(mut guard) = stats.total_runtime.lock() {
|
||||||
|
guard.push(parsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(stats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// implementation of statistics data collection struct
|
/// implementation of statistics data collection struct
|
||||||
impl Stats {
|
impl Stats {
|
||||||
/// Small wrapper for default to set `kind` to "statistics" and `total_runtime` to have at least
|
/// Small wrapper for default to set `kind` to "statistics" and `total_runtime` to have at least
|
||||||
@@ -325,12 +620,10 @@ impl Stats {
|
|||||||
atomic_increment!(self.expected_per_scan, value);
|
atomic_increment!(self.expected_per_scan, value);
|
||||||
}
|
}
|
||||||
StatField::TotalScans => {
|
StatField::TotalScans => {
|
||||||
let multiplier = self.num_extensions.max(1);
|
|
||||||
|
|
||||||
atomic_increment!(self.total_scans, value);
|
atomic_increment!(self.total_scans, value);
|
||||||
atomic_increment!(
|
atomic_increment!(
|
||||||
self.total_expected,
|
self.total_expected,
|
||||||
value * self.expected_per_scan.load(Ordering::Relaxed) * multiplier
|
value * self.expected_per_scan.load(Ordering::Relaxed)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
StatField::TotalExpected => {
|
StatField::TotalExpected => {
|
||||||
|
|||||||
@@ -55,10 +55,7 @@ fn save_writes_stats_object_to_disk() {
|
|||||||
stats.add_status_code(StatusCode::OK);
|
stats.add_status_code(StatusCode::OK);
|
||||||
stats.add_status_code(StatusCode::OK);
|
stats.add_status_code(StatusCode::OK);
|
||||||
let outfile = NamedTempFile::new().unwrap();
|
let outfile = NamedTempFile::new().unwrap();
|
||||||
if stats
|
if stats.save(174.33, outfile.path().to_str().unwrap()).is_ok() {}
|
||||||
.save(174.33, &outfile.path().to_str().unwrap())
|
|
||||||
.is_ok()
|
|
||||||
{}
|
|
||||||
|
|
||||||
assert!(stats.as_json().unwrap().contains("statistics"));
|
assert!(stats.as_json().unwrap().contains("statistics"));
|
||||||
assert!(stats.as_json().unwrap().contains("11")); // requests made
|
assert!(stats.as_json().unwrap().contains("11")); // requests made
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ impl FeroxUrl {
|
|||||||
pub fn format(&self, word: &str, extension: Option<&str>) -> Result<Url> {
|
pub fn format(&self, word: &str, extension: Option<&str>) -> Result<Url> {
|
||||||
log::trace!("enter: format({}, {:?})", word, extension);
|
log::trace!("enter: format({}, {:?})", word, extension);
|
||||||
|
|
||||||
if Url::parse(&word).is_ok() {
|
if Url::parse(word).is_ok() {
|
||||||
// when a full url is passed in as a word to be joined to a base url using
|
// when a full url is passed in as a word to be joined to a base url using
|
||||||
// reqwest::Url::join, the result is that the word (url) completely overwrites the base
|
// reqwest::Url::join, the result is that the word (url) completely overwrites the base
|
||||||
// url, potentially resulting in requests to places that aren't actually the target
|
// url, potentially resulting in requests to places that aren't actually the target
|
||||||
|
|||||||
321
src/utils.rs
321
src/utils.rs
@@ -3,11 +3,13 @@ use console::{strip_ansi_codes, style, user_attended};
|
|||||||
use indicatif::ProgressBar;
|
use indicatif::ProgressBar;
|
||||||
use reqwest::{Client, Response, StatusCode, Url};
|
use reqwest::{Client, Response, StatusCode, Url};
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
use rlimit::{getrlimit, setrlimit, Resource, Rlim};
|
use rlimit::{getrlimit, setrlimit, Resource};
|
||||||
use std::{
|
use std::{
|
||||||
fs,
|
fs,
|
||||||
io::{self, BufWriter, Write},
|
io::{self, BufWriter, Write},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
time::Duration,
|
||||||
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
@@ -217,16 +219,15 @@ pub fn create_report_string(
|
|||||||
/// as the adjustment made here is only valid for the scan itself (and any child processes, of which
|
/// as the adjustment made here is only valid for the scan itself (and any child processes, of which
|
||||||
/// there are none).
|
/// there are none).
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
pub fn set_open_file_limit(limit: usize) -> bool {
|
pub fn set_open_file_limit(limit: u64) -> bool {
|
||||||
log::trace!("enter: set_open_file_limit");
|
log::trace!("enter: set_open_file_limit");
|
||||||
|
|
||||||
if let Ok((soft, hard)) = getrlimit(Resource::NOFILE) {
|
if let Ok((soft, hard)) = getrlimit(Resource::NOFILE) {
|
||||||
if hard.as_usize() > limit {
|
if hard > limit {
|
||||||
// our default open file limit is less than the current hard limit, this means we can
|
// our default open file limit is less than the current hard limit, this means we can
|
||||||
// set the soft limit to our default
|
// 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() {
|
if setrlimit(Resource::NOFILE, limit, hard).is_ok() {
|
||||||
log::debug!("set open file descriptor limit to {}", limit);
|
log::debug!("set open file descriptor limit to {}", limit);
|
||||||
|
|
||||||
log::trace!("exit: set_open_file_limit -> {}", true);
|
log::trace!("exit: set_open_file_limit -> {}", true);
|
||||||
@@ -287,15 +288,141 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// determines whether or not a given url should be denied based on the user-supplied --dont-scan
|
||||||
|
/// flag
|
||||||
|
pub fn should_deny_url(url: &Url, handles: Arc<Handles>) -> Result<bool> {
|
||||||
|
log::trace!(
|
||||||
|
"enter: should_deny_url({}, {:?}, {:?})",
|
||||||
|
url.as_str(),
|
||||||
|
handles.config.url_denylist,
|
||||||
|
handles.ferox_scans()?
|
||||||
|
);
|
||||||
|
// normalization for comparison is to remove the trailing / if one exists, this is done for
|
||||||
|
// the given url and any url to which it's compared
|
||||||
|
let normed_url = Url::parse(url.to_string().trim_end_matches('/'))?;
|
||||||
|
|
||||||
|
for deny_url in &handles.config.url_denylist {
|
||||||
|
// parse the denying url for easier comparison
|
||||||
|
let denier = Url::parse(deny_url.trim_end_matches('/'))
|
||||||
|
.with_context(|| format!("Could not parse {} as a url", deny_url))?;
|
||||||
|
|
||||||
|
// simplest case is an exact match, check for it first
|
||||||
|
if normed_url == denier {
|
||||||
|
log::trace!("exit: should_deny_url -> true");
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
match (normed_url.host(), denier.host()) {
|
||||||
|
// .host() will return an enum with ipv4|6 or domain and is comparable
|
||||||
|
// whereas .domain() returns None for ip addresses
|
||||||
|
(Some(normed_host), Some(denier_host)) => {
|
||||||
|
if normed_host != denier_host {
|
||||||
|
// domains don't even match, keep on keepin' on...
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// one or the other couldn't determine the host value, which probably means
|
||||||
|
// it's not suitable for further comparison
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let normed_host = normed_url.host().unwrap(); // match above will catch errors
|
||||||
|
|
||||||
|
// at this point, we have a matching set of ips or domain names. now we can process the
|
||||||
|
// url path. The goal is to determine whether the given url's path is a subpath of any
|
||||||
|
// url in the deny list, for example
|
||||||
|
// GIVEN URL URL DENY LIST USER-SPECIFIED URLS TO SCAN
|
||||||
|
// http://some.domain/stuff/things, [http://some.domain/stuff], [http://some.domain] => true
|
||||||
|
// http://some.domain/stuff/things, [http://some.domain/stuff/things], [http://some.domain] => true
|
||||||
|
// http://some.domain/stuff/things, [http://some.domain/api], [http://some.domain] => false
|
||||||
|
// the examples above are all pretty obvious, the kicker comes when the blocking url's
|
||||||
|
// path is a parent to a scanned url
|
||||||
|
// http://some.domain/stuff/things, [http://some.domain/], [http://some.domain/stuff] => false
|
||||||
|
// http://some.domain/api, [http://some.domain/], [http://some.domain/stuff] => true
|
||||||
|
// we want to deny all children of the parent, unless that child is a child of a scan
|
||||||
|
// we specified through -u(s) or --stdin
|
||||||
|
|
||||||
|
let deny_path = denier.path();
|
||||||
|
let norm_path = normed_url.path();
|
||||||
|
|
||||||
|
if norm_path.starts_with(deny_path) {
|
||||||
|
// at this point, we know that the given normalized path is a sub-path of the
|
||||||
|
// current deny-url, now we just need to check to see if this deny-url is a parent
|
||||||
|
// to a scanned url that is also a parent of the given url
|
||||||
|
for ferox_scan in handles.ferox_scans()?.get_active_scans() {
|
||||||
|
let scanner = Url::parse(ferox_scan.url().trim_end_matches('/'))
|
||||||
|
.with_context(|| format!("Could not parse {} as a url", ferox_scan))?;
|
||||||
|
|
||||||
|
if let Some(scan_host) = scanner.host() {
|
||||||
|
// same domain/ip check we perform on the denier above
|
||||||
|
if normed_host != scan_host {
|
||||||
|
// domains don't even match, keep on keepin' on...
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// couldn't process .host from scanner
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let scan_path = scanner.path();
|
||||||
|
|
||||||
|
if scan_path.starts_with(deny_path) && norm_path.starts_with(scan_path) {
|
||||||
|
// user-specified scan url is a sub-path of the deny-urls's path AND the
|
||||||
|
// url to check is a sub-path of the user-specified scan url
|
||||||
|
//
|
||||||
|
// the assumption is the user knew what they wanted and we're going to give
|
||||||
|
// the scanned url precedence, even though it's a sub-path
|
||||||
|
log::trace!("exit: should_deny_url -> false");
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::trace!("exit: should_deny_url -> true");
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::trace!("exit: should_deny_url -> false");
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// given a url and filename-suffix, return a unique filename comprised of the slugified url,
|
||||||
|
/// current unix timestamp and suffix
|
||||||
|
///
|
||||||
|
/// ex: ferox-http_telsa_com-1606947491.state
|
||||||
|
pub fn slugify_filename(url: &str, prefix: &str, suffix: &str) -> String {
|
||||||
|
log::trace!("enter: slugify({:?}, {:?}, {:?})", url, prefix, suffix);
|
||||||
|
|
||||||
|
let ts = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_else(|_| Duration::from_secs(0))
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
let altered_prefix = if !prefix.is_empty() {
|
||||||
|
format!("{}-", prefix)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let slug = url.replace("://", "_").replace("/", "_").replace(".", "_");
|
||||||
|
|
||||||
|
let filename = format!("{}{}-{}.{}", altered_prefix, slug, ts, suffix);
|
||||||
|
|
||||||
|
log::trace!("exit: slugify -> {}", filename);
|
||||||
|
filename
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::config::Configuration;
|
||||||
|
use crate::scan_manager::{FeroxScans, ScanOrder};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// set_open_file_limit with a low requested limit succeeds
|
/// set_open_file_limit with a low requested limit succeeds
|
||||||
fn utils_set_open_file_limit_with_low_requested_limit() {
|
fn utils_set_open_file_limit_with_low_requested_limit() {
|
||||||
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
|
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
|
||||||
let lower_limit = hard.as_usize() - 1;
|
let lower_limit = hard - 1;
|
||||||
assert!(set_open_file_limit(lower_limit));
|
assert!(set_open_file_limit(lower_limit));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,9 +430,9 @@ mod tests {
|
|||||||
/// set_open_file_limit with a high requested limit succeeds
|
/// set_open_file_limit with a high requested limit succeeds
|
||||||
fn utils_set_open_file_limit_with_high_requested_limit() {
|
fn utils_set_open_file_limit_with_high_requested_limit() {
|
||||||
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
|
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
|
||||||
let higher_limit = hard.as_usize() + 1;
|
let higher_limit = hard + 1;
|
||||||
// calculate a new soft to ensure soft != hard and hit that logic branch
|
// calculate a new soft to ensure soft != hard and hit that logic branch
|
||||||
let new_soft = Rlim::from_usize(hard.as_usize() - 1);
|
let new_soft = hard - 1;
|
||||||
setrlimit(Resource::NOFILE, new_soft, hard).unwrap();
|
setrlimit(Resource::NOFILE, new_soft, hard).unwrap();
|
||||||
assert!(set_open_file_limit(higher_limit));
|
assert!(set_open_file_limit(higher_limit));
|
||||||
}
|
}
|
||||||
@@ -316,7 +443,7 @@ mod tests {
|
|||||||
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
|
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
|
||||||
// calculate a new soft to ensure soft == hard and hit the failure logic branch
|
// calculate a new soft to ensure soft == hard and hit the failure logic branch
|
||||||
setrlimit(Resource::NOFILE, hard, hard).unwrap();
|
setrlimit(Resource::NOFILE, hard, hard).unwrap();
|
||||||
assert!(!set_open_file_limit(hard.as_usize())); // returns false
|
assert!(!set_open_file_limit(hard)); // returns false
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -366,4 +493,180 @@ mod tests {
|
|||||||
fn status_colorizer_returns_as_is() {
|
fn status_colorizer_returns_as_is() {
|
||||||
assert_eq!(status_colorizer("farfignewton"), "farfignewton".to_string());
|
assert_eq!(status_colorizer("farfignewton"), "farfignewton".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// provide a url that should be blocked where the denier is an exact match for the tested url
|
||||||
|
/// expect true
|
||||||
|
fn should_deny_url_blocks_when_denier_is_exact_match() {
|
||||||
|
let scan_url = "https://testdomain.com/";
|
||||||
|
let deny_url = "https://testdomain.com/denied";
|
||||||
|
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
|
||||||
|
|
||||||
|
let scans = Arc::new(FeroxScans::default());
|
||||||
|
scans.add_directory_scan(scan_url, ScanOrder::Initial);
|
||||||
|
|
||||||
|
let mut config = Configuration::new().unwrap();
|
||||||
|
config.url_denylist = vec![String::from(deny_url)];
|
||||||
|
let config = Arc::new(config);
|
||||||
|
|
||||||
|
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
|
||||||
|
|
||||||
|
assert!(should_deny_url(&tested_url, handles).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// provide a url that has a different host than the denier but the same path, expect false
|
||||||
|
fn should_deny_url_doesnt_compare_mismatched_domains() {
|
||||||
|
let scan_url = "https://testdomain.com/";
|
||||||
|
let deny_url = "https://dev.testdomain.com/denied";
|
||||||
|
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
|
||||||
|
|
||||||
|
let scans = Arc::new(FeroxScans::default());
|
||||||
|
scans.add_directory_scan(scan_url, ScanOrder::Initial);
|
||||||
|
|
||||||
|
let mut config = Configuration::new().unwrap();
|
||||||
|
config.url_denylist = vec![String::from(deny_url)];
|
||||||
|
let config = Arc::new(config);
|
||||||
|
|
||||||
|
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
|
||||||
|
|
||||||
|
assert!(!should_deny_url(&tested_url, handles).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// provide a denier from which we can't check a host, which results in no comparison, expect false
|
||||||
|
fn should_deny_url_doesnt_compare_non_domains() {
|
||||||
|
let scan_url = "https://testdomain.com/";
|
||||||
|
let deny_url = "unix:/run/foo.socket";
|
||||||
|
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
|
||||||
|
|
||||||
|
let scans = Arc::new(FeroxScans::default());
|
||||||
|
scans.add_directory_scan(scan_url, ScanOrder::Initial);
|
||||||
|
|
||||||
|
let mut config = Configuration::new().unwrap();
|
||||||
|
config.url_denylist = vec![String::from(deny_url)];
|
||||||
|
let config = Arc::new(config);
|
||||||
|
|
||||||
|
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
|
||||||
|
|
||||||
|
assert!(!should_deny_url(&tested_url, handles).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// provide a url that has a different host than the denier but the same path, expect false
|
||||||
|
/// because the denier is a parent to the tested, even tho the scanned doesn't compare, it
|
||||||
|
/// still returns true
|
||||||
|
fn should_deny_url_doesnt_compare_mismatched_domains_in_scanned() {
|
||||||
|
let deny_url = "https://testdomain.com/";
|
||||||
|
let scan_url = "https://dev.testdomain.com/denied";
|
||||||
|
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
|
||||||
|
|
||||||
|
let scans = Arc::new(FeroxScans::default());
|
||||||
|
scans.add_directory_scan(scan_url, ScanOrder::Initial);
|
||||||
|
|
||||||
|
let mut config = Configuration::new().unwrap();
|
||||||
|
config.url_denylist = vec![String::from(deny_url)];
|
||||||
|
let config = Arc::new(config);
|
||||||
|
|
||||||
|
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
|
||||||
|
|
||||||
|
assert!(should_deny_url(&tested_url, handles).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// provide a denier from which we can't check a host, which results in no comparison, expect false
|
||||||
|
/// because the denier is a parent to the tested, even tho the scanned doesn't compare, it
|
||||||
|
/// still returns true
|
||||||
|
fn should_deny_url_doesnt_compare_non_domains_in_scanned() {
|
||||||
|
let deny_url = "https://testdomain.com/";
|
||||||
|
let scan_url = "unix:/run/foo.socket";
|
||||||
|
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
|
||||||
|
|
||||||
|
let scans = Arc::new(FeroxScans::default());
|
||||||
|
scans.add_directory_scan(scan_url, ScanOrder::Initial);
|
||||||
|
|
||||||
|
let mut config = Configuration::new().unwrap();
|
||||||
|
config.url_denylist = vec![String::from(deny_url)];
|
||||||
|
let config = Arc::new(config);
|
||||||
|
|
||||||
|
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
|
||||||
|
|
||||||
|
assert!(should_deny_url(&tested_url, handles).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// provide a denier where the tested url is a sub-path and the scanned url is not, expect true
|
||||||
|
fn should_deny_url_blocks_child() {
|
||||||
|
let scan_url = "https://testdomain.com/";
|
||||||
|
let deny_url = "https://testdomain.com/api";
|
||||||
|
let tested_url = Url::parse("https://testdomain.com/api/denied/").unwrap();
|
||||||
|
|
||||||
|
let scans = Arc::new(FeroxScans::default());
|
||||||
|
scans.add_directory_scan(scan_url, ScanOrder::Initial);
|
||||||
|
|
||||||
|
let mut config = Configuration::new().unwrap();
|
||||||
|
config.url_denylist = vec![String::from(deny_url)];
|
||||||
|
let config = Arc::new(config);
|
||||||
|
|
||||||
|
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
|
||||||
|
|
||||||
|
assert!(should_deny_url(&tested_url, handles).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// provide a denier where the tested url is not a sub-path and the scanned url is not, expect false
|
||||||
|
fn should_deny_url_doesnt_block_non_child() {
|
||||||
|
let scan_url = "https://testdomain.com/";
|
||||||
|
let deny_url = "https://testdomain.com/api";
|
||||||
|
let tested_url = Url::parse("https://testdomain.com/not-denied/").unwrap();
|
||||||
|
|
||||||
|
let scans = Arc::new(FeroxScans::default());
|
||||||
|
scans.add_directory_scan(scan_url, ScanOrder::Initial);
|
||||||
|
|
||||||
|
let mut config = Configuration::new().unwrap();
|
||||||
|
config.url_denylist = vec![String::from(deny_url)];
|
||||||
|
let config = Arc::new(config);
|
||||||
|
|
||||||
|
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
|
||||||
|
|
||||||
|
assert!(!should_deny_url(&tested_url, handles).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// provide a denier where the tested url is a sub-path and the scanned url is not, expect true
|
||||||
|
fn should_deny_url_blocks_child_when_scan_url_isnt_parent() {
|
||||||
|
let scan_url = "https://testdomain.com/api";
|
||||||
|
let deny_url = "https://testdomain.com/";
|
||||||
|
let tested_url = Url::parse("https://testdomain.com/stuff/").unwrap();
|
||||||
|
|
||||||
|
let scans = Arc::new(FeroxScans::default());
|
||||||
|
scans.add_directory_scan(scan_url, ScanOrder::Initial);
|
||||||
|
|
||||||
|
let mut config = Configuration::new().unwrap();
|
||||||
|
config.url_denylist = vec![String::from(deny_url)];
|
||||||
|
let config = Arc::new(config);
|
||||||
|
|
||||||
|
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
|
||||||
|
|
||||||
|
assert!(should_deny_url(&tested_url, handles).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// provide a denier where the tested url is not a sub-path and the scanned url is not, expect false
|
||||||
|
fn should_deny_url_doesnt_block_child_when_scan_url_is_parent() {
|
||||||
|
let scan_url = "https://testdomain.com/api";
|
||||||
|
let deny_url = "https://testdomain.com/";
|
||||||
|
let tested_url = Url::parse("https://testdomain.com/api/not-denied/").unwrap();
|
||||||
|
|
||||||
|
let scans = Arc::new(FeroxScans::default());
|
||||||
|
scans.add_directory_scan(scan_url, ScanOrder::Initial);
|
||||||
|
|
||||||
|
let mut config = Configuration::new().unwrap();
|
||||||
|
config.url_denylist = vec![String::from(deny_url)];
|
||||||
|
let config = Arc::new(config);
|
||||||
|
|
||||||
|
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
|
||||||
|
|
||||||
|
assert!(!should_deny_url(&tested_url, handles).unwrap());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,36 @@ fn banner_prints_headers() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
||||||
|
/// expect to see all mandatory prints + multiple dont scan entries
|
||||||
|
fn banner_prints_denied_urls() {
|
||||||
|
Command::cargo_bin("feroxbuster")
|
||||||
|
.unwrap()
|
||||||
|
.arg("--url")
|
||||||
|
.arg("http://localhost")
|
||||||
|
.arg("--dont-scan")
|
||||||
|
.arg("http://dont-scan.me")
|
||||||
|
.arg("--dont-scan")
|
||||||
|
.arg("https://also-not.me")
|
||||||
|
.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("Don't Scan"))
|
||||||
|
.and(predicate::str::contains("http://dont-scan.me"))
|
||||||
|
.and(predicate::str::contains("https://also-not.me"))
|
||||||
|
.and(predicate::str::contains("─┴─")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[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
|
||||||
|
|||||||
212
tests/test_deny_list.rs
Normal file
212
tests/test_deny_list.rs
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
mod utils;
|
||||||
|
use assert_cmd::prelude::*;
|
||||||
|
use assert_cmd::Command;
|
||||||
|
use httpmock::Method::GET;
|
||||||
|
use httpmock::MockServer;
|
||||||
|
use predicates::prelude::*;
|
||||||
|
use utils::{setup_tmp_directory, teardown_tmp_directory};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// test that the deny list prevents a request if the requested url is a match
|
||||||
|
fn deny_list_works_during_with_a_normal_scan() {
|
||||||
|
let srv = MockServer::start();
|
||||||
|
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
|
||||||
|
|
||||||
|
let mock = srv.mock(|when, then| {
|
||||||
|
when.method(GET).path("/LICENSE");
|
||||||
|
then.status(200).body("this is a test");
|
||||||
|
});
|
||||||
|
|
||||||
|
let cmd = Command::cargo_bin("feroxbuster")
|
||||||
|
.unwrap()
|
||||||
|
.arg("--url")
|
||||||
|
.arg(srv.url("/"))
|
||||||
|
.arg("--wordlist")
|
||||||
|
.arg(file.as_os_str())
|
||||||
|
.arg("--dont-scan")
|
||||||
|
.arg(srv.url("/LICENSE"))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
teardown_tmp_directory(tmp_dir);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains(srv.url("/LICENSE")).not());
|
||||||
|
|
||||||
|
assert_eq!(mock.hits(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// test that the deny list prevents requests of urls found during extraction
|
||||||
|
fn deny_list_works_during_extraction() {
|
||||||
|
let srv = MockServer::start();
|
||||||
|
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
|
||||||
|
|
||||||
|
let mock = srv.mock(|when, then| {
|
||||||
|
when.method(GET).path("/LICENSE");
|
||||||
|
then.status(200)
|
||||||
|
.body(&srv.url("'/homepage/assets/img/icons/handshake.svg'"));
|
||||||
|
});
|
||||||
|
|
||||||
|
let mock_two = srv.mock(|when, then| {
|
||||||
|
when.method(GET)
|
||||||
|
.path("/homepage/assets/img/icons/handshake.svg");
|
||||||
|
then.status(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
let cmd = Command::cargo_bin("feroxbuster")
|
||||||
|
.unwrap()
|
||||||
|
.arg("--url")
|
||||||
|
.arg(srv.url("/"))
|
||||||
|
.arg("--wordlist")
|
||||||
|
.arg(file.as_os_str())
|
||||||
|
.arg("--extract-links")
|
||||||
|
.arg("--dont-scan")
|
||||||
|
.arg(srv.url("/homepage/"))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cmd.assert().success().stdout(
|
||||||
|
predicate::str::contains("/LICENSE")
|
||||||
|
.and(predicate::str::contains("200"))
|
||||||
|
.and(predicate::str::contains("/homepage/assets/img/icons/handshake.svg").not()),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(mock.hits(), 1);
|
||||||
|
assert_eq!(mock_two.hits(), 0);
|
||||||
|
teardown_tmp_directory(tmp_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// test that the deny list prevents requests of urls found during recursion
|
||||||
|
fn deny_list_works_during_recursion() {
|
||||||
|
let srv = MockServer::start();
|
||||||
|
let urls = [
|
||||||
|
"js".to_string(),
|
||||||
|
"prod".to_string(),
|
||||||
|
"dev".to_string(),
|
||||||
|
"file.js".to_string(),
|
||||||
|
];
|
||||||
|
let (tmp_dir, file) = setup_tmp_directory(&urls, "wordlist").unwrap();
|
||||||
|
|
||||||
|
let js_mock = srv.mock(|when, then| {
|
||||||
|
when.method(GET).path("/js");
|
||||||
|
then.status(301).header("Location", &srv.url("/js/"));
|
||||||
|
});
|
||||||
|
|
||||||
|
let js_prod_mock = srv.mock(|when, then| {
|
||||||
|
when.method(GET).path("/js/prod");
|
||||||
|
then.status(301).header("Location", &srv.url("/js/prod/"));
|
||||||
|
});
|
||||||
|
|
||||||
|
let js_dev_mock = srv.mock(|when, then| {
|
||||||
|
when.method(GET).path("/js/dev");
|
||||||
|
then.status(301).header("Location", &srv.url("/js/dev/"));
|
||||||
|
});
|
||||||
|
|
||||||
|
let js_dev_file_mock = srv.mock(|when, then| {
|
||||||
|
when.method(GET).path("/js/dev/file.js");
|
||||||
|
then.status(200)
|
||||||
|
.body("this is a test and is more bytes than other ones");
|
||||||
|
});
|
||||||
|
|
||||||
|
let cmd = Command::cargo_bin("feroxbuster")
|
||||||
|
.unwrap()
|
||||||
|
.arg("--url")
|
||||||
|
.arg(srv.url("/"))
|
||||||
|
.arg("--wordlist")
|
||||||
|
.arg(file.as_os_str())
|
||||||
|
.arg("-t")
|
||||||
|
.arg("1")
|
||||||
|
.arg("--dont-scan")
|
||||||
|
.arg(srv.url("/js/dev"))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cmd.assert().success().stdout(
|
||||||
|
predicate::str::is_match("301.*js")
|
||||||
|
.unwrap()
|
||||||
|
.and(predicate::str::is_match("301.*js/prod").unwrap())
|
||||||
|
.and(predicate::str::is_match("301.*js/dev").unwrap())
|
||||||
|
.not()
|
||||||
|
.and(predicate::str::is_match("200.*js/dev/file.js").unwrap())
|
||||||
|
.not(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(js_mock.hits(), 1);
|
||||||
|
assert_eq!(js_prod_mock.hits(), 1);
|
||||||
|
assert_eq!(js_dev_mock.hits(), 0);
|
||||||
|
assert_eq!(js_dev_file_mock.hits(), 0);
|
||||||
|
|
||||||
|
teardown_tmp_directory(tmp_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// test that the deny list prevents requests of urls found during recursion when the denier is a
|
||||||
|
/// parent of a user-specified scan
|
||||||
|
fn deny_list_works_during_recursion_with_inverted_parents() {
|
||||||
|
let srv = MockServer::start();
|
||||||
|
let urls = [
|
||||||
|
"js".to_string(),
|
||||||
|
"prod".to_string(),
|
||||||
|
"dev".to_string(),
|
||||||
|
"api".to_string(),
|
||||||
|
"file.js".to_string(),
|
||||||
|
];
|
||||||
|
let (tmp_dir, file) = setup_tmp_directory(&urls, "wordlist").unwrap();
|
||||||
|
|
||||||
|
let js_mock = srv.mock(|when, then| {
|
||||||
|
when.method(GET).path("/js");
|
||||||
|
then.status(301).header("Location", &srv.url("/js/"));
|
||||||
|
});
|
||||||
|
|
||||||
|
let api_mock = srv.mock(|when, then| {
|
||||||
|
when.method(GET).path("/api");
|
||||||
|
then.status(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
let js_prod_mock = srv.mock(|when, then| {
|
||||||
|
when.method(GET).path("/js/prod");
|
||||||
|
then.status(301).header("Location", &srv.url("/js/prod/"));
|
||||||
|
});
|
||||||
|
|
||||||
|
let js_dev_mock = srv.mock(|when, then| {
|
||||||
|
when.method(GET).path("/js/dev");
|
||||||
|
then.status(301).header("Location", &srv.url("/js/dev/"));
|
||||||
|
});
|
||||||
|
|
||||||
|
let js_dev_file_mock = srv.mock(|when, then| {
|
||||||
|
when.method(GET).path("/js/dev/file.js");
|
||||||
|
then.status(200)
|
||||||
|
.body("this is a test and is more bytes than other ones");
|
||||||
|
});
|
||||||
|
|
||||||
|
let cmd = Command::cargo_bin("feroxbuster")
|
||||||
|
.unwrap()
|
||||||
|
.arg("--url")
|
||||||
|
.arg(srv.url("/js"))
|
||||||
|
.arg("--wordlist")
|
||||||
|
.arg(file.as_os_str())
|
||||||
|
.arg("-t")
|
||||||
|
.arg("1")
|
||||||
|
.arg("-vvvv")
|
||||||
|
.arg("--dont-scan")
|
||||||
|
.arg(srv.url("/"))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cmd.assert().success().stdout(
|
||||||
|
predicate::str::is_match("301.*js")
|
||||||
|
.unwrap()
|
||||||
|
.and(predicate::str::is_match("301.*js/prod").unwrap())
|
||||||
|
.and(predicate::str::is_match("301.*js/dev").unwrap())
|
||||||
|
.and(predicate::str::is_match("200.*js/dev/file.js").unwrap())
|
||||||
|
.and(predicate::str::is_match("200.*api").unwrap())
|
||||||
|
.not(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(js_mock.hits(), 1);
|
||||||
|
assert_eq!(js_prod_mock.hits(), 1);
|
||||||
|
assert_eq!(js_dev_mock.hits(), 1);
|
||||||
|
assert_eq!(js_dev_file_mock.hits(), 1);
|
||||||
|
assert_eq!(api_mock.hits(), 0);
|
||||||
|
|
||||||
|
teardown_tmp_directory(tmp_dir);
|
||||||
|
}
|
||||||
@@ -224,11 +224,11 @@ fn test_dynamic_wildcard_request_found() {
|
|||||||
|
|
||||||
teardown_tmp_directory(tmp_dir);
|
teardown_tmp_directory(tmp_dir);
|
||||||
|
|
||||||
assert_eq!(contents.contains("WLD"), true);
|
assert!(contents.contains("WLD"));
|
||||||
assert_eq!(contents.contains("Got"), true);
|
assert!(contents.contains("Got"));
|
||||||
assert_eq!(contents.contains("200"), true);
|
assert!(contents.contains("200"));
|
||||||
assert_eq!(contents.contains("(url length: 32)"), true);
|
assert!(contents.contains("(url length: 32)"));
|
||||||
assert_eq!(contents.contains("(url length: 96)"), true);
|
assert!(contents.contains("(url length: 96)"));
|
||||||
|
|
||||||
cmd.assert().success().stdout(
|
cmd.assert().success().stdout(
|
||||||
predicate::str::contains("WLD")
|
predicate::str::contains("WLD")
|
||||||
@@ -391,11 +391,11 @@ fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file() {
|
|||||||
|
|
||||||
teardown_tmp_directory(tmp_dir);
|
teardown_tmp_directory(tmp_dir);
|
||||||
|
|
||||||
assert_eq!(contents.contains("WLD"), true);
|
assert!(contents.contains("WLD"));
|
||||||
assert_eq!(contents.contains("Got"), true);
|
assert!(contents.contains("Got"));
|
||||||
assert_eq!(contents.contains("200"), true);
|
assert!(contents.contains("200"));
|
||||||
assert_eq!(contents.contains("(url length: 32)"), true);
|
assert!(contents.contains("(url length: 32)"));
|
||||||
assert_eq!(contents.contains("(url length: 96)"), true);
|
assert!(contents.contains("(url length: 96)"));
|
||||||
|
|
||||||
cmd.assert().success().stdout(
|
cmd.assert().success().stdout(
|
||||||
predicate::str::contains("WLD")
|
predicate::str::contains("WLD")
|
||||||
@@ -451,12 +451,12 @@ fn heuristics_wildcard_test_with_redirect_as_response_code(
|
|||||||
|
|
||||||
teardown_tmp_directory(tmp_dir);
|
teardown_tmp_directory(tmp_dir);
|
||||||
|
|
||||||
assert_eq!(contents.contains("WLD"), true);
|
assert!(contents.contains("WLD"));
|
||||||
assert_eq!(contents.contains("301"), true);
|
assert!(contents.contains("301"));
|
||||||
assert_eq!(contents.contains("/some-redirect"), true);
|
assert!(contents.contains("/some-redirect"));
|
||||||
assert_eq!(contents.contains("redirects to => "), true);
|
assert!(contents.contains("redirects to => "));
|
||||||
assert_eq!(contents.contains(&srv.url("/")), true);
|
assert!(contents.contains(&srv.url("/")));
|
||||||
assert_eq!(contents.contains("(url length: 32)"), true);
|
assert!(contents.contains("(url length: 32)"));
|
||||||
|
|
||||||
cmd.assert().success().stdout(
|
cmd.assert().success().stdout(
|
||||||
predicate::str::contains("redirects to => ")
|
predicate::str::contains("redirects to => ")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use assert_cmd::Command;
|
|||||||
use httpmock::Method::GET;
|
use httpmock::Method::GET;
|
||||||
use httpmock::{MockServer, Regex};
|
use httpmock::{MockServer, Regex};
|
||||||
use predicates::prelude::*;
|
use predicates::prelude::*;
|
||||||
use std::fs::read_to_string;
|
use std::fs::{read_dir, read_to_string};
|
||||||
use utils::{setup_tmp_directory, teardown_tmp_directory};
|
use utils::{setup_tmp_directory, teardown_tmp_directory};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -153,3 +153,73 @@ fn main_parallel_spawns_children() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// send three targets over stdin with --output enabled, expect parallel to create a new directory
|
||||||
|
/// and the log files therein
|
||||||
|
fn main_parallel_creates_output_directory() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let t1 = MockServer::start();
|
||||||
|
let t2 = MockServer::start();
|
||||||
|
let t3 = MockServer::start();
|
||||||
|
|
||||||
|
let words = [
|
||||||
|
String::from("LICENSE"),
|
||||||
|
String::from("stuff"),
|
||||||
|
String::from("things"),
|
||||||
|
String::from("mostuff"),
|
||||||
|
String::from("mothings"),
|
||||||
|
];
|
||||||
|
let (word_tmp_dir, wordlist) = setup_tmp_directory(&words, "wordlist")?;
|
||||||
|
let (output_dir, outfile) = setup_tmp_directory(&[], "output-file")?;
|
||||||
|
let (tgt_tmp_dir, targets) =
|
||||||
|
setup_tmp_directory(&[t1.url("/"), t2.url("/"), t3.url("/")], "targets")?;
|
||||||
|
|
||||||
|
Command::cargo_bin("feroxbuster")
|
||||||
|
.unwrap()
|
||||||
|
.arg("--stdin")
|
||||||
|
.arg("--parallel")
|
||||||
|
.arg("2")
|
||||||
|
.arg("--output")
|
||||||
|
.arg(outfile.as_os_str())
|
||||||
|
.arg("--wordlist")
|
||||||
|
.arg(wordlist.as_os_str())
|
||||||
|
.pipe_stdin(targets)
|
||||||
|
.unwrap()
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stderr(
|
||||||
|
predicate::str::contains("Could not connect to any target provided")
|
||||||
|
.and(predicate::str::contains("Target Url"))
|
||||||
|
.not(), // no target url found
|
||||||
|
);
|
||||||
|
|
||||||
|
// output_dir should return something similar to output-file-1627845244.logs with the
|
||||||
|
// line below. if it ever fails, can use the regex below to filter out the right directory
|
||||||
|
let sub_dir = read_dir(&output_dir)?.next().unwrap()?.file_name();
|
||||||
|
|
||||||
|
let mut num_logs = 0;
|
||||||
|
let file_regex = Regex::new("ferox-[a-zA-Z_:0-9]+-[0-9]+.log").unwrap();
|
||||||
|
let dir_regex = Regex::new("output-file-[0-9]+.logs").unwrap();
|
||||||
|
|
||||||
|
let sub_dir = output_dir.as_ref().join(&sub_dir);
|
||||||
|
|
||||||
|
// created directory like output-file-1627845741.logs/
|
||||||
|
assert!(dir_regex.is_match(&sub_dir.to_string_lossy().to_string()));
|
||||||
|
|
||||||
|
for entry in sub_dir.read_dir()? {
|
||||||
|
let entry = entry?;
|
||||||
|
// created each file like ferox-https_localhost-1627845741.log
|
||||||
|
println!("name: {:?}", entry.file_name().to_string_lossy());
|
||||||
|
assert!(file_regex.is_match(&entry.file_name().to_string_lossy()));
|
||||||
|
num_logs += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// should be 3 log files total
|
||||||
|
assert_eq!(num_logs, 3);
|
||||||
|
|
||||||
|
teardown_tmp_directory(word_tmp_dir);
|
||||||
|
teardown_tmp_directory(tgt_tmp_dir);
|
||||||
|
teardown_tmp_directory(output_dir);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
46
tests/test_parser.rs
Normal file
46
tests/test_parser.rs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
use assert_cmd::Command;
|
||||||
|
use predicates::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// specify an incorrect param (-fc) with --help after it on the command line
|
||||||
|
/// old behavior printed
|
||||||
|
/// error: Found argument '-c' which wasn't expected, or isn't valid in this context
|
||||||
|
///
|
||||||
|
/// USAGE:
|
||||||
|
/// feroxbuster --add-slash --url <URL>...
|
||||||
|
///
|
||||||
|
/// For more information try --help
|
||||||
|
///
|
||||||
|
/// the new behavior we expect to see is to print the long form help message, of which
|
||||||
|
/// Ludicrous speed... go! is near the bottom of that output, so we can test for that
|
||||||
|
fn parser_incorrect_param_with_tack_tack_help() {
|
||||||
|
Command::cargo_bin("feroxbuster")
|
||||||
|
.unwrap()
|
||||||
|
.arg("-fc")
|
||||||
|
.arg("--help")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("Ludicrous speed... go!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// specify an incorrect param (-fc) with --help after it on the command line
|
||||||
|
/// old behavior printed
|
||||||
|
/// error: Found argument '-c' which wasn't expected, or isn't valid in this context
|
||||||
|
///
|
||||||
|
/// USAGE:
|
||||||
|
/// feroxbuster --add-slash --url <URL>...
|
||||||
|
///
|
||||||
|
/// For more information try --help
|
||||||
|
///
|
||||||
|
/// the new behavior we expect to see is to print the long form help message, of which
|
||||||
|
/// Ludicrous speed... go! is near the bottom of that output, so we can test for that
|
||||||
|
fn parser_incorrect_param_with_tack_h() {
|
||||||
|
Command::cargo_bin("feroxbuster")
|
||||||
|
.unwrap()
|
||||||
|
.arg("-fc")
|
||||||
|
.arg("-h")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("Ludicrous speed... go!"));
|
||||||
|
}
|
||||||
@@ -77,7 +77,7 @@ fn auto_bail_cancels_scan_with_timeouts() {
|
|||||||
|
|
||||||
// read debug log to get the number of errors enforced
|
// read debug log to get the number of errors enforced
|
||||||
for line in debug_log.lines() {
|
for line in debug_log.lines() {
|
||||||
let log: serde_json::Value = serde_json::from_str(&line).unwrap_or_default();
|
let log: serde_json::Value = serde_json::from_str(line).unwrap_or_default();
|
||||||
if let Some(message) = log.get("message") {
|
if let Some(message) = log.get("message") {
|
||||||
let str_msg = message.as_str().unwrap_or_default().to_string();
|
let str_msg = message.as_str().unwrap_or_default().to_string();
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ fn auto_bail_cancels_scan_with_403s() {
|
|||||||
|
|
||||||
// read debug log to get the number of errors enforced
|
// read debug log to get the number of errors enforced
|
||||||
for line in debug_log.lines() {
|
for line in debug_log.lines() {
|
||||||
let log: serde_json::Value = serde_json::from_str(&line).unwrap_or_default();
|
let log: serde_json::Value = serde_json::from_str(line).unwrap_or_default();
|
||||||
if let Some(message) = log.get("message") {
|
if let Some(message) = log.get("message") {
|
||||||
let str_msg = message.as_str().unwrap_or_default().to_string();
|
let str_msg = message.as_str().unwrap_or_default().to_string();
|
||||||
|
|
||||||
@@ -238,7 +238,7 @@ fn auto_bail_cancels_scan_with_429s() {
|
|||||||
|
|
||||||
// read debug log to get the number of errors enforced
|
// read debug log to get the number of errors enforced
|
||||||
for line in debug_log.lines() {
|
for line in debug_log.lines() {
|
||||||
let log: serde_json::Value = serde_json::from_str(&line).unwrap_or_default();
|
let log: serde_json::Value = serde_json::from_str(line).unwrap_or_default();
|
||||||
if let Some(message) = log.get("message") {
|
if let Some(message) = log.get("message") {
|
||||||
let str_msg = message.as_str().unwrap_or_default().to_string();
|
let str_msg = message.as_str().unwrap_or_default().to_string();
|
||||||
|
|
||||||
|
|||||||
@@ -93,9 +93,13 @@ fn resume_scan_works() {
|
|||||||
#[test]
|
#[test]
|
||||||
/// kick off scan with a time limit;
|
/// kick off scan with a time limit;
|
||||||
fn time_limit_enforced_when_specified() {
|
fn time_limit_enforced_when_specified() {
|
||||||
let srv = MockServer::start();
|
let t1 = MockServer::start();
|
||||||
|
let t2 = MockServer::start();
|
||||||
|
|
||||||
let (tmp_dir, file) =
|
let (tmp_dir, file) =
|
||||||
setup_tmp_directory(&["css".to_string(), "stuff".to_string()], "wordlist").unwrap();
|
setup_tmp_directory(&["css".to_string(), "stuff".to_string()], "wordlist").unwrap();
|
||||||
|
let (tgt_tmp_dir, targets) =
|
||||||
|
setup_tmp_directory(&[t1.url("/"), t2.url("/")], "targets").unwrap();
|
||||||
|
|
||||||
// ensure the command will run long enough by adding crap to the wordlist
|
// ensure the command will run long enough by adding crap to the wordlist
|
||||||
let more_words = read_to_string(Path::new("tests/extra-words")).unwrap();
|
let more_words = read_to_string(Path::new("tests/extra-words")).unwrap();
|
||||||
@@ -109,12 +113,13 @@ fn time_limit_enforced_when_specified() {
|
|||||||
|
|
||||||
Command::cargo_bin("feroxbuster")
|
Command::cargo_bin("feroxbuster")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.arg("--url")
|
.arg("--stdin")
|
||||||
.arg(srv.url("/"))
|
|
||||||
.arg("--wordlist")
|
.arg("--wordlist")
|
||||||
.arg(file.as_os_str())
|
.arg(file.as_os_str())
|
||||||
.arg("--time-limit")
|
.arg("--time-limit")
|
||||||
.arg("5s")
|
.arg("5s")
|
||||||
|
.pipe_stdin(targets)
|
||||||
|
.unwrap()
|
||||||
.assert()
|
.assert()
|
||||||
.failure();
|
.failure();
|
||||||
|
|
||||||
@@ -127,4 +132,5 @@ fn time_limit_enforced_when_specified() {
|
|||||||
assert!(now.elapsed() > lower_bound && now.elapsed() < upper_bound);
|
assert!(now.elapsed() > lower_bound && now.elapsed() < upper_bound);
|
||||||
|
|
||||||
teardown_tmp_directory(tmp_dir);
|
teardown_tmp_directory(tmp_dir);
|
||||||
|
teardown_tmp_directory(tgt_tmp_dir);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user