Compare commits

...

71 Commits

Author SHA1 Message Date
epi
7c036e587e fixed possible thread panic due to multiple calls to join 2020-10-05 18:52:22 -05:00
epi
b733477a61 Update README.md 2020-10-05 16:18:40 -05:00
epi
58e367b5c3 Update README.md 2020-10-05 14:47:56 -05:00
epi
99021db091 Update README.md 2020-10-05 14:47:18 -05:00
epi
7f145f11df Update README.md 2020-10-05 14:46:13 -05:00
epi
68ee5883b8 Update README.md 2020-10-05 14:45:11 -05:00
epi
9b929fdb15 Merge pull request #53 from joohoi/ffuf_corrections
[Documentation] README.md matrix fixes for ffuf
2020-10-05 06:23:48 -05:00
Joona Hoikkala
a87dc64e8e ffuf corrections for the README.md matrix 2020-10-05 10:51:08 +03:00
epi
70918582e5 version bump to 1.0.0 🥳 2020-10-04 10:10:00 -05:00
epi
b445198b67 added missing docstrings to a few tests 2020-10-04 09:57:35 -05:00
epi
97b5bcdde6 removed logger init from scanner tests 2020-10-04 09:53:12 -05:00
epi
e15f6e9bd2 added recursive scan test 2020-10-04 09:01:01 -05:00
epi
e74678edc3 increased test coverage for main 2020-10-04 07:04:46 -05:00
epi
40cce2ee37 added codecov to CI pipeline 2020-10-04 06:22:59 -05:00
epi
e980cee570 added codecov to CI pipeline 2020-10-04 06:18:51 -05:00
epi
73bd7c1514 added codecov to CI pipeline 2020-10-04 06:14:25 -05:00
epi
a2728e1df0 began integration tests on main 2020-10-03 21:09:37 -05:00
epi
95dec44766 added tests in lib.rs 2020-10-03 20:44:44 -05:00
epi
c31cfe8673 more banner tests for coverage 2020-10-03 20:36:25 -05:00
epi
aaa7412bb1 added multiple status codes/targets test for banner 2020-10-03 19:35:18 -05:00
epi
cdbd0030dd updated reame 2020-10-03 19:21:42 -05:00
epi
e144caddc0 updated reame 2020-10-03 19:19:44 -05:00
epi
61c4b6d523 updated reame 2020-10-03 19:18:49 -05:00
epi
a70c9d9413 updated reame 2020-10-03 19:16:45 -05:00
epi
098584c945 updated reame 2020-10-03 19:14:47 -05:00
epi
11f32ea8c6 integration tests for banner complete 2020-10-03 19:08:18 -05:00
epi
afcfa4849c fixed 2 heuristics tests 2020-10-03 16:25:49 -05:00
epi
28d6c7dd97 increased code coverage for utils 2020-10-03 16:03:23 -05:00
epi
b538aad7d5 clippy/lint/format 2020-10-03 13:12:18 -05:00
epi
d3561a5823 added unit tests for create_urls; closes #35 2020-10-03 12:04:31 -05:00
epi
f23e4a5ed1 removed dependency on ansi_term; closes #52 2020-10-03 11:34:56 -05:00
epi
dd305bfa65 readme update 2020-10-03 10:21:24 -05:00
epi
6b0c847b52 formatting on config.rs 2020-10-03 09:00:40 -05:00
epi
9edd414442 added /etc/feroxbuster as a valid config location
updated .deb to install the example config at /etc/feroxbuster
version bumped to 0.2.1
2020-10-03 08:59:28 -05:00
epi
d8afb58ddd updated gitignore for easier crates.io publishing 2020-10-03 08:50:53 -05:00
epi
51799e101c version bump to 0.2.0 2020-10-03 08:00:45 -05:00
epi
e005537064 config file searched for in multiple locations
added some better error messaging
updated docs/readme
closes #51
2020-10-03 08:00:01 -05:00
epi
6d6069a5b8 updated demo 2020-10-03 05:08:54 -05:00
epi
d2dc2b1a9c updated README 2020-10-03 04:56:28 -05:00
epi
68975dc4df nitpickery 2020-10-02 21:04:45 -05:00
epi
c9c9f51110 lint 2020-10-02 21:03:14 -05:00
epi
641117537a added logo 2020-10-02 21:01:49 -05:00
epi
84f67628d1 added logo 2020-10-02 21:00:56 -05:00
epi
de6444a0ed cargo fmt/lint 2020-10-02 20:23:24 -05:00
epi
9ad59333f3 version bump to test docs on docs.rs 2020-10-02 19:58:48 -05:00
epi
585ae8fd14 added some missing documentation 2020-10-02 19:58:06 -05:00
epi
41df8807fd updated links in toc 2020-10-02 19:35:47 -05:00
epi
ac31ac6ad2 removed TODO items; updated README 2020-10-02 18:27:29 -05:00
epi
93c8ab2bcf fixed version badge on readme 2020-10-02 14:56:19 -05:00
epi
0f39c99f62 readme update w/ new install methods 2020-10-02 09:15:12 -05:00
epi
3bfabe255e removed rpm build for now 2020-10-02 08:03:02 -05:00
epi
7e0b6999ee added extra exclude targets 2020-10-02 07:14:30 -05:00
epi
4b2764c6c4 added demo gif; updated rpm info for build 2020-10-02 07:07:42 -05:00
epi
daf9a1f707 added RPM build 2020-10-02 06:48:33 -05:00
epi
0c09a573eb testing deb package build 2020-10-02 06:09:42 -05:00
epi
15bd899908 added badges 2020-10-02 05:54:37 -05:00
epi
2684ced562 updated README 2020-10-01 20:57:58 -05:00
epi
0b72272431 updated README 2020-10-01 20:49:59 -05:00
epi
f5177bd5a6 moved make_request to utils 2020-10-01 20:20:23 -05:00
epi
836b60d2c0 clippy passes; ran fmt; fixed tests 2020-10-01 20:14:44 -05:00
epi
55bd24d38d more readme updates 2020-10-01 17:34:23 -05:00
epi
1ce0ef1c13 added some example usage to readme 2020-10-01 17:28:44 -05:00
epi
35c69822ce moved format_url to utils; validated docs 2020-10-01 17:09:58 -05:00
epi
0b03b7345a Merge pull request #47 from epi052/trigger-ci-builds-on-master
fixed CI so that it triggers on master properly
2020-10-01 16:48:35 -05:00
epi
f00fcecb84 fixed CI so that it triggers on master properly 2020-10-01 16:47:51 -05:00
epi
c0d17c59a7 Merge pull request #46 from epi052/39-fix-ci-cd-pipeline
39 fix ci cd pipeline
2020-10-01 07:50:01 -05:00
epi
d907b25e7c musl build fixed for 32 and 64bit; closes #39 2020-10-01 07:49:31 -05:00
epi
ea97de7dbb testing 32bit build 2020-10-01 07:35:22 -05:00
epi
bf59f22c02 ci musl build test 2020-10-01 07:28:55 -05:00
epi
2b3369980c ci musl build test 2020-10-01 07:15:15 -05:00
epi
d716c100dd ci musl build test 2020-10-01 07:09:09 -05:00
24 changed files with 1762 additions and 409 deletions

7
.github/actions-rs/grcov.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
branch: true
ignore-not-existing: true
llvm: true
output-type: lcov
output-path: ./lcov.info
ignore:
- "../*"

View File

@@ -1,72 +1,11 @@
name: CI Pipeline
name: CD Pipeline
on: [push]
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
with:
command: check
test:
name: Test Suite
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
with:
command: test
fmt:
name: Rust fmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup component add clippy
- uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-targets --all-features -- -D warnings -A clippy::unnecessary_unwrap
build-nix:
runs-on: ${{ matrix.os }}
if: github.ref == 'refs/heads/master-not-a-thing-revert-this-later-before-release'
if: github.ref == 'refs/heads/master'
strategy:
matrix:
type: [ubuntu-x64, ubuntu-x86]
@@ -89,7 +28,6 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends libssl-dev pkg-config
sudo apt-get clean -y
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
@@ -108,9 +46,22 @@ jobs:
name: ${{ matrix.name }}
path: ${{ matrix.path }}
build-deb:
needs: [build-nix]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Deb Build
uses: ebbflow-io/cargo-deb-amd64-ubuntu@1.0
- name: Upload Deb Artifact
uses: actions/upload-artifact@v2
with:
name: feroxbuster_amd64.deb
path: ./target/x86_64-unknown-linux-musl/debian/*
build-rest:
runs-on: ${{ matrix.os }}
if: github.ref == 'refs/heads/master-not-a-thing-revert-this-later-before-release'
if: github.ref == 'refs/heads/master'
strategy:
matrix:
type: [windows-x64, windows-x86, macos]

64
.github/workflows/check.yml vendored Normal file
View File

@@ -0,0 +1,64 @@
name: CI Pipeline
on: [push]
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
with:
command: check
test:
name: Test Suite
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
with:
command: test
fmt:
name: Rust fmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup component add clippy
- uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-targets --all-features -- -D warnings -A clippy::unnecessary_unwrap

36
.github/workflows/coverage.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
on: [push]
name: Code Coverage Pipeline
jobs:
upload-coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly
override: true
- uses: actions-rs/cargo@v1
with:
command: clean
- uses: actions-rs/cargo@v1
with:
command: test
args: --all-features --no-fail-fast
env:
CARGO_INCREMENTAL: '0'
RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests'
RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests'
- uses: actions-rs/grcov@v0.1
- name: Convert lcov to xml
run: |
curl -O https://raw.githubusercontent.com/eriwen/lcov-to-cobertura-xml/master/lcov_cobertura/lcov_cobertura.py
chmod +x lcov_cobertura.py
./lcov_cobertura.py ./lcov.info
- uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
name: codecov-umbrella
fail_ci_if_error: true

7
.gitignore vendored
View File

@@ -15,3 +15,10 @@ Cargo.lock
# personal feroxbuster config for testing
ferox-config.toml
# images for the README on github
img/**
# scripts to check code coverage using nightly compiler
check-coverage.sh
lcov_cobertura.py

View File

@@ -1,8 +1,18 @@
[package]
name = "feroxbuster"
version = "0.1.0"
authors = ["epi <epibar052@gmail.com>"]
version = "1.0.1"
authors = ["Ben 'epi' Risher <epibar052@gmail.com>"]
license = "MIT"
edition = "2018"
homepage = "https://github.com/epi052/feroxbuster"
repository = "https://github.com/epi052/feroxbuster"
description = "A fast, simple, recursive content discovery tool."
categories = ["command-line-utilities"]
keywords = ["pentest", "enumeration", "url-bruteforce", "content-discovery", "web"]
exclude = [".github/*", "img/*", "check-coverage.sh"]
[badges]
maintenance = { status = "actively-developed" }
[dependencies]
futures = { version = "0.3"}
@@ -16,9 +26,10 @@ lazy_static = "1.4"
toml = "0.5"
serde = { version = "1.0", features = ["derive"] }
uuid = { version = "0.8", features = ["v4"] }
ansi_term = "0.12"
indicatif = "0.15"
console = "0.12"
openssl = { version = "0.10", features = ["vendored"] }
dirs = "3.0"
[dev-dependencies]
tempfile = "3.1"
@@ -27,7 +38,15 @@ assert_cmd = "1.0.1"
predicates = "1.0.5"
[profile.release]
opt-level = 'z' # optimize for size
lto = true
codegen-units = 1
panic = 'abort'
[package.metadata.deb]
section = "utility"
license-file = ["LICENSE", "4"]
conf-files = ["/etc/feroxbuster/ferox-config.toml"]
assets = [
["target/release/feroxbuster", "/usr/bin/", "755"],
["ferox-config.toml.example", "/etc/feroxbuster/ferox-config.toml", "644"],
]

224
README.md
View File

@@ -1,41 +1,112 @@
# HOLUP / Hacktoberfest / Pre-release Version
<h1 align="center">
<br>
<a href="https://github.com/epi052/feroxbuster"><img src="img/logo/default-cropped.png" alt="feroxbuster"></a>
<br>
</h1>
I'm making this project public earlier than I normally would for Hacktoberfest. It is not done. I make no guarantees
about master even being in a state where the tool works. I'll remove this message once things stabilize, which should
be relatively soon.
<h4 align="center">A simple, fast, recursive content discovery tool written in Rust</h4>
If you want to submit a PR as part of hacktoberfest, I'm mostly working off of the items in the
[Pre-release project](https://github.com/epi052/feroxbuster/projects/1). It's very fluid as I've been working on it
myself up to this point. I'll look at formalizing what's there into issues soon.
<p align="center">
<a href="https://github.com/epi052/feroxbuster/actions?query=workflow%3A%22CI+Pipeline%22">
<img src="https://img.shields.io/github/workflow/status/epi052/feroxbuster/CI%20Pipeline/master?logo=github">
</a>
Happy Hacktoberfest!
<a href="https://github.com/epi052/feroxbuster/releases">
<img src="https://img.shields.io/github/downloads/epi052/feroxbuster/total?label=downloads&logo=github&color=inactive" alt="github downloads">
</a>
# feroxbuster
<a href="https://github.com/epi052/feroxbuster/commits/master">
<img src="https://img.shields.io/github/last-commit/epi052/feroxbuster?logo=github">
</a>
`feroxbuster` is a fast, simple, recursive content discovery tool written in Rust.
<a href="https://crates.io/crates/feroxbuster">
<img src="https://img.shields.io/crates/v/feroxbuster?color=blue&label=version&logo=rust">
</a>
<a href="https://crates.io/crates/feroxbuster">
<img src="https://img.shields.io/crates/d/feroxbuster?label=downloads&logo=rust&color=inactive">
</a>
Table of Contents
<a href="https://codecov.io/gh/epi052/feroxbuster">
<img src="https://codecov.io/gh/epi052/feroxbuster/branch/master/graph/badge.svg" />
</a>
</p>
![demo](img/demo.gif)
<p align="center">
🦀
<a href="https://github.com/epi052/feroxbuster/releases">Releases</a> ✨
<a href="#-example-usage">Example Usage</a> ✨
<a href="https://github.com/epi052/feroxbuster/blob/master/CONTRIBUTING.md">Contributing</a> ✨
<a href="https://docs.rs/feroxbuster/latest/feroxbuster/">Documentation</a>
🦀
</p>
## 😕 What the heck is a ferox anyway?
Ferox is short for Ferric Oxide. Ferric Oxide, simply put, is rust. The name rustbuster was taken, so I decided on a variation. 🤷
## 🤔 What's it do tho?
`feroxbuster` is a tool designed to perform [Forced Browsing](https://owasp.org/www-community/attacks/Forced_browsing).
Forced browsing is an attack where the aim is to enumerate and access resources that are not referenced by the web application, but are still accessible by an attacker.
`feroxbuster` uses brute force combined with a wordlist to search for unlinked content in target directories. These resources may store sensitive information about web applications and operational systems, such as source code, credentials, internal network addressing, etc...
This attack is also known as Predictable Resource Location, File Enumeration, Directory Enumeration, and Resource Enumeration.
📖 Table of Contents
-----------------
- [Downloads](#downloads)
- [Installation](#installation)
- [Configuration](#configuration)
- [Downloads](#-downloads)
- [Installation](#-installation)
- [Download a Release](#download-a-release)
- [Cargo Install](#cargo-install)
- [apt Install](#apt-install)
- [Configuration](#-configuration)
- [Default Values](#default-values)
- [ferox-config.toml](#ferox-configtoml)
- [Command Line Parsing](#command-line-parsing)
- [Example Usage](#example-usage)
- [Comparison w/ Similar Tools](#comparison-w-similar-tools)
- [Example Usage](#-example-usage)
- [Multiple Values](#multiple-values)
- [Include Headers](#include-headers)
- [IPv6, Non-recursive scan with INFO logging enabled](#ipv6-non-recursive-scan-with-info-level-logging-enabled)
- [Read urls from STDIN; pipe only resulting urls out to another tool](#read-urls-from-stdin-pipe-only-resulting-urls-out-to-another-tool)
- [Proxy traffic through Burp](#proxy-traffic-through-burp)
- [Proxy traffic through a SOCKS proxy](#proxy-traffic-through-a-socks-proxy)
- [Pass auth token via query parameter](#pass-auth-token-via-query-parameter)
- [Comparison w/ Similar Tools](#-comparison-w-similar-tools)
## Downloads
There are pre-built binaries for the following systems:
## 💿 Installation
- [Linux x86](https://github.com/epi052/feroxbuster/releases/latest/download/x86-linux-feroxbuster.zip)
- [Linux x86_64](https://github.com/epi052/feroxbuster/releases/latest/download/x86_64-linux-feroxbuster.zip)
- [MacOS x86_64](https://github.com/epi052/feroxbuster/releases/latest/download/x86_64-macos-feroxbuster.zip)
- [Windows x86](https://github.com/epi052/feroxbuster/releases/latest/download/x86-windows-feroxbuster.exe.zip)
- [Windows x86_64](https://github.com/epi052/feroxbuster/releases/latest/download/x86_64-windows-feroxbuster.exe.zip)
### Download a Release
## Installation
## Configuration
Releases for multiple architectures can be found in the [Releases](https://github.com/epi052/feroxbuster/releases) section. Builds for the following systems are currently supported:
- Linux x86
- Linux x86_64
- MacOS x86_64
- Windows x86
- Windows x86_64
### Cargo Install
`feroxbuster` is published on crates.io, making it easy to install if you already have rust installed on your system.
```
cargo install feroxbuster
```
### apt Install
Head to the [Releases](https://github.com/epi052/feroxbuster/releases) section and download `feroxbuster_amd64.deb`. After that, use your favorite package manager to install the .deb.
```
sudo apt install ./feroxbuster_amd64.deb
```
## ⚙️ Configuration
### Default Values
Configuration begins with with the following built-in default values baked into the binary:
@@ -52,9 +123,19 @@ Configuration begins with with the following built-in default values baked into
### ferox-config.toml
After setting built-in default values, any values defined in a `ferox-config.toml` config file will override the
built-in defaults. If `ferox-config.toml` is not found in the **same directory** as `feroxbuster`, nothing happens at this stage.
built-in defaults.
For example, say that we prefer to use a different wordlist as our default when scanning; we can
`feroxbuster` searches for `ferox-config.toml` in the following locations (in the order shown):
- `/etc/feroxbuster/` (global)
- `CONFIG_DIR/ferxobuster/` (per-user)
- The same directory as the `feroxbuster` executable (per-user)
- The user's current working directory (per-target)
If more than one valid configuration file is found, each one overwrites the values found previously.
If no configuration file is found, nothing happens at this stage.
As an example, let's say that we prefer to use a different wordlist as our default when scanning; we can
set the `wordlist` value in the config file to override the baked-in default.
Notes of interest:
@@ -148,9 +229,58 @@ OPTIONS:
-w, --wordlist <FILE> Path to the wordlist
```
## Example Usage
## 🧰 Example Usage
## Comparison w/ Similar Tools
### Multiple Values
Options that take multiple values are very flexible. Consider the following ways of specifying extensions:
```
./feroxbuster -u http://127.1 -x pdf -x js,html -x php txt json,docx
```
The command above adds .pdf, .js, .html, .php, .txt, .json, and .docx to each url
All of the methods above (multiple flags, space separated, comma separated, etc...) are valid and interchangeable. The same goes for urls, headers, status codes, queries, and size filters.
### Include Headers
```
./feroxbuster -u http://127.1 -H Accept:application/json "Authorization: Bearer {token}"
```
### IPv6, non-recursive scan with INFO-level logging enabled
```
./feroxbuster -u http://[::1] --norecursion -vv
```
### Read urls from STDIN; pipe only resulting urls out to another tool
```
cat targets | ./feroxbuster --stdin --quiet -s 200 301 302 --redirects -x js | fff -s 200 -o js-files
```
### Proxy traffic through Burp
```
./feroxbuster -u http://127.1 --insecure --proxy http://127.0.0.1:8080
```
### Proxy traffic through a SOCKS proxy
```
./feroxbuster -u http://127.1 --proxy socks5://127.0.0.1:9050
```
### Pass auth token via query parameter
```
./feroxbuster -u http://127.1 --query token=0123456789ABCDEF
```
## 🧐 Comparison w/ Similar Tools
There are quite a few similar tools for forced browsing/content discovery. Burp Suite Pro, Dirb, Dirbuster, etc...
However, in my opinion, there are two that set the standard: [gobuster](https://github.com/OJ/gobuster) and
@@ -167,26 +297,26 @@ a few of the use-cases in which feroxbuster may be a better fit:
- You want **recursion** along with some other thing mentioned above (ffuf also does recursion)
- You want a **configuration file** option for overriding built-in default values for your scans
| | feroxbuster | gobuster | ffuf |
|-----------------------------------------------------|--------------------|--------------------|--------------------|
| fast | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| easy to use | :heavy_check_mark: | :heavy_check_mark: | |
| blacklist status codes (in addition to whitelist) | | :heavy_check_mark: | :heavy_check_mark: |
| allows recursion | :heavy_check_mark: | | :heavy_check_mark: |
| can specify query parameters | :heavy_check_mark: | | :heavy_check_mark: |
| SOCKS proxy support | :heavy_check_mark: | | |
| multiple target scan (via stdin or multiple -u) | :heavy_check_mark: | | |
| configuration file for default value override | :heavy_check_mark: | | :heavy_check_mark: |
| can accept urls via STDIN as part of a pipeline | :heavy_check_mark: | | |
| can accept wordlists via STDIN | | :heavy_check_mark: | |
| filter by response size | :heavy_check_mark: | | :heavy_check_mark: |
| auto-filter wildcard responses | :heavy_check_mark: | | :heavy_check_mark: |
| performs other scans (vhost, dns, etc) | | :heavy_check_mark: | :heavy_check_mark: |
| time delay / rate limiting | | :heavy_check_mark: | :heavy_check_mark: |
| **huge** number of other options | | | :heavy_check_mark: |
| | feroxbuster | gobuster | ffuf |
|-----------------------------------------------------|---|---|---|
| fast | ✔ | ✔ | ✔ |
| easy to use | ✔ | ✔ | |
| blacklist status codes (in addition to whitelist) | | ✔ | ✔ |
| allows recursion | ✔ | | ✔ |
| can specify query parameters | ✔ | | ✔ |
| SOCKS proxy support | ✔ | | |
| multiple target scan (via stdin or multiple -u) | ✔ | | ✔ |
| configuration file for default value override | ✔ | | ✔ |
| can accept urls via STDIN as part of a pipeline | ✔ | | ✔ |
| can accept wordlists via STDIN | | ✔ | ✔ |
| filter by response size | ✔ | | ✔ |
| auto-filter wildcard responses | ✔ | | ✔ |
| performs other scans (vhost, dns, etc) | | ✔ | ✔ |
| time delay / rate limiting | | ✔ | ✔ |
| **huge** number of other options | | | ✔ |
Of note, there's another written-in-rust content discovery tool, [rustbuster](https://github.com/phra/rustbuster). I
came across rustbuster when I was naming my tool (:cry:). I don't have any experience using it, but it appears to
came across rustbuster when I was naming my tool (😢). I don't have any experience using it, but it appears to
be able to do POST requests with an HTTP body, has SOCKS support, and has an 8.3 shortname scanner (in addition to vhost
dns, directory, etc...). In short, it definitely looks interesting and may be what you're looking for as it has some
capability I haven't seen in similar tools.

BIN
img/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,5 +1,6 @@
use crate::{config::CONFIGURATION, utils::status_colorizer, VERSION};
use crate::{config::Configuration, utils::status_colorizer, VERSION};
/// macro helper to abstract away repetitive string formatting
macro_rules! format_banner_entry_helper {
// \u{0020} -> unicode space
// \u{2502} -> vertical box drawing character, i.e. │
@@ -26,6 +27,7 @@ macro_rules! format_banner_entry_helper {
};
}
/// macro that wraps another macro helper to abstract away repetitive string formatting
macro_rules! format_banner_entry {
// 4 -> unicode emoji padding width
// 22 -> column width (when unicode rune is 4 bytes wide, 23 when it's 3)
@@ -41,7 +43,7 @@ macro_rules! format_banner_entry {
/// Prints the banner to stdout.
///
/// Only prints those settings which are either always present, or passed in by the user.
pub fn initialize(targets: &[String]) {
pub fn initialize(targets: &[String], config: &Configuration) {
let artwork = format!(
r#"
___ ___ __ __ __ __ __ ___
@@ -67,17 +69,17 @@ by Ben "epi" Risher {} ver: {}"#,
let mut codes = vec![];
for code in &CONFIGURATION.statuscodes {
for code in &config.statuscodes {
codes.push(status_colorizer(&code.to_string()))
}
eprintln!(
"{}",
format_banner_entry!("\u{1F680}", "Threads", CONFIGURATION.threads)
format_banner_entry!("\u{1F680}", "Threads", config.threads)
); // 🚀
eprintln!(
"{}",
format_banner_entry!("\u{1f4d6}", "Wordlist", CONFIGURATION.wordlist)
format_banner_entry!("\u{1f4d6}", "Wordlist", config.wordlist)
); // 📖
eprintln!(
"{}",
@@ -89,23 +91,30 @@ by Ben "epi" Risher {} ver: {}"#,
); // 🆗
eprintln!(
"{}",
format_banner_entry!("\u{1f4a5}", "Timeout (secs)", CONFIGURATION.timeout)
format_banner_entry!("\u{1f4a5}", "Timeout (secs)", config.timeout)
); // 💥
eprintln!(
"{}",
format_banner_entry!("\u{1F9a1}", "User-Agent", CONFIGURATION.useragent)
format_banner_entry!("\u{1F9a1}", "User-Agent", config.useragent)
); // 🦡
// followed by the maybe printed or variably displayed values
if !CONFIGURATION.proxy.is_empty() {
if !config.config.is_empty() {
eprintln!(
"{}",
format_banner_entry!("\u{1f48e}", "Proxy", CONFIGURATION.proxy)
format_banner_entry!("\u{1f489}", "Config File", config.config)
); // 💉
}
if !config.proxy.is_empty() {
eprintln!(
"{}",
format_banner_entry!("\u{1f48e}", "Proxy", config.proxy)
); // 💎
}
if !CONFIGURATION.headers.is_empty() {
for (name, value) in &CONFIGURATION.headers {
if !config.headers.is_empty() {
for (name, value) in &config.headers {
eprintln!(
"{}",
format_banner_entry!("\u{1f92f}", "Header", name, value)
@@ -113,8 +122,8 @@ by Ben "epi" Risher {} ver: {}"#,
}
}
if !CONFIGURATION.sizefilters.is_empty() {
for filter in &CONFIGURATION.sizefilters {
if !config.sizefilters.is_empty() {
for filter in &config.sizefilters {
eprintln!(
"{}",
format_banner_entry!("\u{1f4a2}", "Size Filter", filter)
@@ -122,8 +131,8 @@ by Ben "epi" Risher {} ver: {}"#,
}
}
if !CONFIGURATION.queries.is_empty() {
for query in &CONFIGURATION.queries {
if !config.queries.is_empty() {
for query in &config.queries {
eprintln!(
"{}",
format_banner_entry!(
@@ -135,83 +144,83 @@ by Ben "epi" Risher {} ver: {}"#,
}
}
if !CONFIGURATION.output.is_empty() {
if !config.output.is_empty() {
eprintln!(
"{}",
format_banner_entry!("\u{1f4be}", "Output File", CONFIGURATION.output)
format_banner_entry!("\u{1f4be}", "Output File", config.output)
); // 💾
}
if !CONFIGURATION.extensions.is_empty() {
if !config.extensions.is_empty() {
eprintln!(
"{}",
format_banner_entry!(
"\u{1f4b2}",
"Extensions",
format!("[{}]", CONFIGURATION.extensions.join(", "))
format!("[{}]", config.extensions.join(", "))
)
); // 💲
}
if CONFIGURATION.insecure {
if config.insecure {
eprintln!(
"{}",
format_banner_entry!("\u{1f513}", "Insecure", CONFIGURATION.insecure)
format_banner_entry!("\u{1f513}", "Insecure", config.insecure)
); // 🔓
}
if CONFIGURATION.redirects {
if config.redirects {
eprintln!(
"{}",
format_banner_entry!("\u{1f4cd}", "Follow Redirects", CONFIGURATION.redirects)
format_banner_entry!("\u{1f4cd}", "Follow Redirects", config.redirects)
); // 📍
}
if CONFIGURATION.dontfilter {
if config.dontfilter {
eprintln!(
"{}",
format_banner_entry!("\u{1f92a}", "Filter Wildcards", !CONFIGURATION.dontfilter)
format_banner_entry!("\u{1f92a}", "Filter Wildcards", !config.dontfilter)
); // 🤪
}
match CONFIGURATION.verbosity {
match config.verbosity {
//speaker medium volume (increasing with verbosity to loudspeaker)
1 => {
eprintln!(
"{}",
format_banner_entry!("\u{1f508}", "Verbosity", CONFIGURATION.verbosity)
format_banner_entry!("\u{1f508}", "Verbosity", config.verbosity)
); // 🔈
}
2 => {
eprintln!(
"{}",
format_banner_entry!("\u{1f509}", "Verbosity", CONFIGURATION.verbosity)
format_banner_entry!("\u{1f509}", "Verbosity", config.verbosity)
); // 🔉
}
3 => {
eprintln!(
"{}",
format_banner_entry!("\u{1f50a}", "Verbosity", CONFIGURATION.verbosity)
format_banner_entry!("\u{1f50a}", "Verbosity", config.verbosity)
); // 🔊
}
4 => {
eprintln!(
"{}",
format_banner_entry!("\u{1f4e2}", "Verbosity", CONFIGURATION.verbosity)
format_banner_entry!("\u{1f4e2}", "Verbosity", config.verbosity)
); // 📢
}
_ => {}
}
if CONFIGURATION.addslash {
if config.addslash {
eprintln!(
"{}",
format_banner_entry!("\u{1fa93}", "Add Slash", CONFIGURATION.addslash)
format_banner_entry!("\u{1fa93}", "Add Slash", config.addslash)
); // 🪓
}
if !CONFIGURATION.norecursion {
if CONFIGURATION.depth == 0 {
if !config.norecursion {
if config.depth == 0 {
eprintln!(
"{}",
format_banner_entry!("\u{1f503}", "Recursion Depth", "INFINITE")
@@ -219,15 +228,51 @@ by Ben "epi" Risher {} ver: {}"#,
} else {
eprintln!(
"{}",
format_banner_entry!("\u{1f503}", "Recursion Depth", CONFIGURATION.depth)
format_banner_entry!("\u{1f503}", "Recursion Depth", config.depth)
); // 🔃
}
} else {
eprintln!(
"{}",
format_banner_entry!("\u{1f6ab}", "Do Not Recurse", CONFIGURATION.norecursion)
format_banner_entry!("\u{1f6ab}", "Do Not Recurse", config.norecursion)
); // 🚫
}
eprintln!("{}", bottom);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
/// test to hit no execution of targets for loop in banner
fn banner_without_targets() {
let config = Configuration::default();
initialize(&[], &config);
}
#[test]
/// test to hit no execution of statuscode for loop in banner
fn banner_without_status_codes() {
let mut config = Configuration::default();
config.statuscodes = vec![];
initialize(&[String::from("http://localhost")], &config);
}
#[test]
/// test to hit an empty config file
fn banner_without_config_file() {
let mut config = Configuration::default();
config.config = String::new();
initialize(&[String::from("http://localhost")], &config);
}
#[test]
/// test to hit an empty config file
fn banner_without_queries() {
let mut config = Configuration::default();
config.queries = vec![(String::new(), String::new())];
initialize(&[String::from("http://localhost")], &config);
}
}

View File

@@ -1,4 +1,5 @@
use crate::utils::status_colorizer;
use crate::utils::{module_colorizer, status_colorizer};
use console::style;
use reqwest::header::HeaderMap;
use reqwest::{redirect::Policy, Client, Proxy};
use std::collections::HashMap;
@@ -25,8 +26,9 @@ pub fn initialize(
Ok(map) => map,
Err(e) => {
eprintln!(
"[{}] - Client::initialize: {}",
"{} {} {}",
status_colorizer("ERROR"),
module_colorizer("Client::initialize"),
e
);
exit(1);
@@ -45,13 +47,15 @@ pub fn initialize(
Ok(proxy_obj) => client.proxy(proxy_obj),
Err(e) => {
eprintln!(
"[{}] - Could not add proxy ({:?}) to Client configuration",
"{} {} Could not add proxy ({:?}) to Client configuration",
status_colorizer("ERROR"),
module_colorizer("Client::initialize"),
proxy
);
eprintln!(
"[{}] - Client::initialize: {}",
"{} {} {}",
status_colorizer("ERROR"),
style("Client::initialize").cyan(),
e
);
exit(1);
@@ -65,10 +69,16 @@ pub fn initialize(
Ok(client) => client,
Err(e) => {
eprintln!(
"[{}] - Could not create a Client with the given configuration, exiting.",
status_colorizer("ERROR")
"{} {} Could not create a Client with the given configuration, exiting.",
status_colorizer("ERROR"),
module_colorizer("Client::build")
);
eprintln!(
"{} {} {}",
status_colorizer("ERROR"),
module_colorizer("Client::build"),
e
);
eprintln!("[{}] - Client::build: {}", status_colorizer("ERROR"), e);
exit(1);
}
}

View File

@@ -1,4 +1,4 @@
use crate::utils::status_colorizer;
use crate::utils::{module_colorizer, status_colorizer};
use crate::{client, parser, progress};
use crate::{DEFAULT_CONFIG_NAME, DEFAULT_STATUS_CODES, DEFAULT_WORDLIST, VERSION};
use clap::value_t;
@@ -7,9 +7,9 @@ use lazy_static::lazy_static;
use reqwest::{Client, StatusCode};
use serde::Deserialize;
use std::collections::HashMap;
use std::env::current_exe;
use std::env::{current_dir, current_exe};
use std::fs::read_to_string;
use std::path::Path;
use std::path::PathBuf;
use std::process::exit;
lazy_static! {
@@ -35,48 +35,95 @@ lazy_static! {
/// Inspired by and derived from https://github.com/PhilipDaniels/rust-config-example
#[derive(Debug, Clone, Deserialize)]
pub struct Configuration {
/// Path to the wordlist
#[serde(default = "wordlist")]
pub wordlist: String,
/// Path to the config file used
#[serde(default)]
pub config: String,
/// Proxy to use for requests (ex: http(s)://host:port, socks5://host:port)
#[serde(default)]
pub proxy: String,
/// The target URL
#[serde(default)]
pub target_url: String,
/// Status Codes of interest (default: 200 204 301 302 307 308 401 403 405)
#[serde(default = "statuscodes")]
pub statuscodes: Vec<u16>,
/// Instance of [reqwest::Client](https://docs.rs/reqwest/latest/reqwest/struct.Client.html)
#[serde(skip)]
pub client: Client,
/// Number of concurrent threads (default: 50)
#[serde(default = "threads")]
pub threads: usize,
/// Number of seconds before a request times out (default: 7)
#[serde(default = "timeout")]
pub timeout: u64,
/// Level of verbosity, equates to log level
#[serde(default)]
pub verbosity: u8,
/// Only print URLs
#[serde(default)]
pub quiet: bool,
/// Output file to write results to (default: stdout)
#[serde(default)]
pub output: String,
/// Sets the User-Agent (default: feroxbuster/VERSION)
#[serde(default = "useragent")]
pub useragent: String,
/// Follow redirects
#[serde(default)]
pub redirects: bool,
/// Disables TLS certificate validation
#[serde(default)]
pub insecure: bool,
/// File extension(s) to search for
#[serde(default)]
pub extensions: Vec<String>,
/// HTTP headers to be used in each request
#[serde(default)]
pub headers: HashMap<String, String>,
/// URL query parameters
#[serde(default)]
pub queries: Vec<(String, String)>,
/// Do not scan recursively
#[serde(default)]
pub norecursion: bool,
/// Append / to each request
#[serde(default)]
pub addslash: bool,
/// Read url(s) from STDIN
#[serde(default)]
pub stdin: bool,
/// Maximum recursion depth, a depth of 0 is infinite recursion
#[serde(default = "depth")]
pub depth: usize,
/// Filter out messages of a particular size
#[serde(default)]
pub sizefilters: Vec<u64>,
/// Don't auto-filter wildcard responses
#[serde(default)]
pub dontfilter: bool,
}
@@ -84,29 +131,42 @@ pub struct Configuration {
// functions timeout, threads, statuscodes, useragent, wordlist, and depth are used to provide
// defaults in the event that a ferox-config.toml is found but one or more of the values below
// aren't listed in the config. This way, we get the correct defaults upon Deserialization
/// default timeout value
fn timeout() -> u64 {
7
}
/// default threads value
fn threads() -> usize {
50
}
/// default status codes
fn statuscodes() -> Vec<u16> {
DEFAULT_STATUS_CODES
.iter()
.map(|code| code.as_u16())
.collect()
}
/// default wordlist
fn wordlist() -> String {
String::from(DEFAULT_WORDLIST)
}
/// default useragent
fn useragent() -> String {
format!("feroxbuster/{}", VERSION)
}
/// default recursion depth
fn depth() -> usize {
4
}
impl Default for Configuration {
/// Builds the default Configuration for feroxbuster
fn default() -> Self {
let timeout = timeout();
let useragent = useragent();
@@ -125,6 +185,7 @@ impl Default for Configuration {
norecursion: false,
redirects: false,
proxy: String::new(),
config: String::new(),
output: String::new(),
target_url: String::new(),
queries: Vec::new(),
@@ -146,6 +207,7 @@ impl Configuration {
/// - **timeout**: `5` seconds
/// - **redirects**: `false`
/// - **wordlist**: [`DEFAULT_WORDLIST`](constant.DEFAULT_WORDLIST.html)
/// - **config**: `None`
/// - **threads**: `50`
/// - **timeout**: `7` seconds
/// - **verbosity**: `0` (no logging enabled)
@@ -169,12 +231,25 @@ impl Configuration {
/// [ferox-config.toml](constant.DEFAULT_CONFIG_NAME.html) config file will override the
/// built-in defaults.
///
/// `ferox-config.toml` can be placed in any of the following locations (in the order shown):
/// - `/etc/feroxbuster/`
/// - `CONFIG_DIR/ferxobuster/`
/// - The same directory as the `feroxbuster` executable
/// - The user's current working directory
///
/// If more than one valid configuration file is found, each one overwrites the values found previously.
///
/// Finally, any options/arguments given on the commandline will override both built-in and
/// config-file specified values.
///
/// The resulting [Configuration](struct.Configuration.html) is a singleton with a `static`
/// lifetime.
pub fn new() -> Self {
// when compiling for test, we want to eliminate the runtime dependency of the parser
if cfg!(test) {
return Configuration::default();
}
// Get the default configuration, this is what will apply if nothing
// else is specified.
let mut config = Configuration::default();
@@ -183,33 +258,44 @@ impl Configuration {
// therein to overwrite our default values. Deserialized defaults are specified
// in the Configuration struct so that we don't change anything that isn't
// actually specified in the config file
//
// search for a config using the following order of precedence
// - /etc/feroxbuster/
// - CONFIG_DIR/ferxobuster/
// - same directory as feroxbuster executable
// - current directory
// merge a config found at /etc/feroxbuster/ferox-config.toml
let config_file = PathBuf::new()
.join("/etc/feroxbuster")
.join(DEFAULT_CONFIG_NAME);
Self::parse_and_merge_config(config_file, &mut config);
// merge a config found at ~/.config/feroxbuster/ferox-config.toml
if let Some(config_dir) = dirs::config_dir() {
// config_dir() resolves to one of the following
// - linux: $XDG_CONFIG_HOME or $HOME/.config
// - macOS: $HOME/Library/Application Support
// - windows: {FOLDERID_RoamingAppData}
let config_file = config_dir.join("feroxbuster").join(DEFAULT_CONFIG_NAME);
Self::parse_and_merge_config(config_file, &mut config);
};
// merge a config found in same the directory as feroxbuster executable
if let Ok(exe_path) = current_exe() {
if let Some(bin_dir) = exe_path.parent() {
if let Some(settings) = Self::parse_config(bin_dir) {
config.threads = settings.threads;
config.wordlist = settings.wordlist;
config.statuscodes = settings.statuscodes;
config.proxy = settings.proxy;
config.timeout = settings.timeout;
config.verbosity = settings.verbosity;
config.quiet = settings.quiet;
config.output = settings.output;
config.useragent = settings.useragent;
config.redirects = settings.redirects;
config.insecure = settings.insecure;
config.extensions = settings.extensions;
config.headers = settings.headers;
config.queries = settings.queries;
config.norecursion = settings.norecursion;
config.addslash = settings.addslash;
config.stdin = settings.stdin;
config.depth = settings.depth;
config.sizefilters = settings.sizefilters;
config.dontfilter = settings.dontfilter;
}
let config_file = bin_dir.join(DEFAULT_CONFIG_NAME);
Self::parse_and_merge_config(config_file, &mut config);
};
};
// merge a config found in the user's current working directory
if let Ok(cwd) = current_dir() {
let config_file = cwd.join(DEFAULT_CONFIG_NAME);
Self::parse_and_merge_config(config_file, &mut config);
}
let args = parser::initialize().get_matches();
// the .is_some appears clunky, but it allows default values to be incrementally
@@ -385,23 +471,64 @@ impl Configuration {
config
}
/// If present, read in `/path/to/binary's/parent/DEFAULT_CONFIG_NAME` and deserialize the specified values
/// Given a configuration file's location and an instance of `Configuration`, read in
/// the config file if found and update the current settings with the settings found therein
fn parse_and_merge_config(config_file: PathBuf, mut config: &mut Self) {
if config_file.exists() {
// save off a string version of the path before it goes out of scope
let conf_str = match config_file.to_str() {
Some(cs) => String::from(cs),
None => String::new(),
};
if let Some(settings) = Self::parse_config(config_file) {
// set the config used for viewing in the banner
config.config = conf_str;
// update the settings
Self::merge_config(&mut config, settings);
}
}
}
/// Given two Configurations, overwrite `settings` with the fields found in `settings_to_merge`
fn merge_config(settings: &mut Self, settings_to_merge: Self) {
settings.threads = settings_to_merge.threads;
settings.wordlist = settings_to_merge.wordlist;
settings.statuscodes = settings_to_merge.statuscodes;
settings.proxy = settings_to_merge.proxy;
settings.timeout = settings_to_merge.timeout;
settings.verbosity = settings_to_merge.verbosity;
settings.quiet = settings_to_merge.quiet;
settings.output = settings_to_merge.output;
settings.useragent = settings_to_merge.useragent;
settings.redirects = settings_to_merge.redirects;
settings.insecure = settings_to_merge.insecure;
settings.extensions = settings_to_merge.extensions;
settings.headers = settings_to_merge.headers;
settings.queries = settings_to_merge.queries;
settings.norecursion = settings_to_merge.norecursion;
settings.addslash = settings_to_merge.addslash;
settings.stdin = settings_to_merge.stdin;
settings.depth = settings_to_merge.depth;
settings.sizefilters = settings_to_merge.sizefilters;
settings.dontfilter = settings_to_merge.dontfilter;
}
/// If present, read in `DEFAULT_CONFIG_NAME` and deserialize the specified values
///
/// uses serde to deserialize the toml into a `Configuration` struct
///
/// If toml cannot be parsed a `Configuration::default` instance is returned
fn parse_config(directory: &Path) -> Option<Self> {
let directory = directory.join(DEFAULT_CONFIG_NAME);
if let Ok(content) = read_to_string(directory) {
fn parse_config(config_file: PathBuf) -> Option<Self> {
if let Ok(content) = read_to_string(config_file) {
match toml::from_str(content.as_str()) {
Ok(config) => {
return Some(config);
}
Err(e) => {
println!(
"[{}] - config::parse_config {}",
"{} {} {}",
status_colorizer("ERROR"),
module_colorizer("config::parse_config"),
e
);
}
@@ -417,6 +544,7 @@ mod tests {
use std::fs::write;
use tempfile::TempDir;
/// creates a dummy configuration file for testing
fn setup_config_test() -> Configuration {
let data = r#"
wordlist = "/some/path"
@@ -441,16 +569,18 @@ mod tests {
"#;
let tmp_dir = TempDir::new().unwrap();
let file = tmp_dir.path().join(DEFAULT_CONFIG_NAME);
write(file, data).unwrap();
Configuration::parse_config(tmp_dir.path()).unwrap()
write(&file, data).unwrap();
Configuration::parse_config(file).unwrap()
}
#[test]
/// test that all default config values meet expectations
fn default_configuration() {
let config = Configuration::default();
assert_eq!(config.wordlist, wordlist());
assert_eq!(config.proxy, String::new());
assert_eq!(config.target_url, String::new());
assert_eq!(config.config, String::new());
assert_eq!(config.statuscodes, statuscodes());
assert_eq!(config.threads, threads());
assert_eq!(config.depth, depth());
@@ -470,108 +600,126 @@ mod tests {
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_wordlist() {
let config = setup_config_test();
assert_eq!(config.wordlist, "/some/path");
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_statuscodes() {
let config = setup_config_test();
assert_eq!(config.statuscodes, vec![201, 301, 401]);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_threads() {
let config = setup_config_test();
assert_eq!(config.threads, 40);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_depth() {
let config = setup_config_test();
assert_eq!(config.depth, 1);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_timeout() {
let config = setup_config_test();
assert_eq!(config.timeout, 5);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_proxy() {
let config = setup_config_test();
assert_eq!(config.proxy, "http://127.0.0.1:8080");
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_quiet() {
let config = setup_config_test();
assert_eq!(config.quiet, true);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_verbosity() {
let config = setup_config_test();
assert_eq!(config.verbosity, 1);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_output() {
let config = setup_config_test();
assert_eq!(config.output, "/some/otherpath");
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_redirects() {
let config = setup_config_test();
assert_eq!(config.redirects, true);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_insecure() {
let config = setup_config_test();
assert_eq!(config.insecure, true);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_norecursion() {
let config = setup_config_test();
assert_eq!(config.norecursion, true);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_stdin() {
let config = setup_config_test();
assert_eq!(config.stdin, true);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_dontfilter() {
let config = setup_config_test();
assert_eq!(config.dontfilter, true);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_addslash() {
let config = setup_config_test();
assert_eq!(config.addslash, true);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_extensions() {
let config = setup_config_test();
assert_eq!(config.extensions, vec!["html", "php", "js"]);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_sizefilters() {
let config = setup_config_test();
assert_eq!(config.sizefilters, vec![4120]);
}
#[test]
/// parse the test config and see that the values parsed are correct
fn config_reads_headers() {
let config = setup_config_test();
let mut headers = HashMap::new();
@@ -581,6 +729,7 @@ mod tests {
}
#[test]
/// parse the test config and see that the values parsed are correct
fn config_reads_queries() {
let config = setup_config_test();
let mut queries = vec![];

View File

@@ -1,12 +1,14 @@
use crate::config::{CONFIGURATION, PROGRESS_PRINTER};
use crate::scanner::{format_url, make_request};
use crate::utils::{ferox_print, get_url_path_length, status_colorizer};
use ansi_term::Color::{Cyan, Yellow};
use crate::utils::{
ferox_print, format_url, get_url_path_length, make_request, module_colorizer, status_colorizer,
};
use console::style;
use indicatif::ProgressBar;
use reqwest::Response;
use std::process;
use uuid::Uuid;
/// length of a standard UUID, used when determining wildcard responses
const UUID_LENGTH: u64 = 32;
/// Data holder for two pieces of data needed when auto-filtering out wildcard responses
@@ -86,9 +88,9 @@ pub async fn wildcard_test(target_url: &str, bar: ProgressBar) -> Option<Wildcar
"{} {:>10} Wildcard response is dynamic; {} ({} + url length) responses; toggle this behavior by using {}",
status_colorizer("WLD"),
wc_length - url_len,
Yellow.paint("auto-filtering"),
Cyan.paint(format!("{}", wc_length - url_len)),
Yellow.paint("--dontfilter")
style("auto-filtering").yellow(),
style(wc_length - url_len).cyan(),
style("--dontfilter").yellow()
), &PROGRESS_PRINTER
);
}
@@ -100,9 +102,9 @@ pub async fn wildcard_test(target_url: &str, bar: ProgressBar) -> Option<Wildcar
"{} {:>10} Wildcard response is static; {} {} responses; toggle this behavior by using {}",
status_colorizer("WLD"),
wc_length,
Yellow.paint("auto-filtering"),
Cyan.paint(format!("{}", wc_length)),
Yellow.paint("--dontfilter")
style("auto-filtering").yellow(),
style(wc_length).cyan(),
style("--dontfilter").yellow()
), &PROGRESS_PRINTER);
}
wildcard.size = wc_length;
@@ -186,19 +188,17 @@ async fn make_wildcard_request(target_url: &str, length: usize) -> Option<Respon
&PROGRESS_PRINTER,
);
}
} else {
if !CONFIGURATION.quiet {
ferox_print(
&format!(
"{} {:>10} {} redirects to => {:?}",
wildcard,
content_len,
response.url(),
next_loc
),
&PROGRESS_PRINTER,
);
}
} else if !CONFIGURATION.quiet {
ferox_print(
&format!(
"{} {:>10} {} redirects to => {:?}",
wildcard,
content_len,
response.url(),
next_loc
),
&PROGRESS_PRINTER,
);
}
}
}
@@ -247,7 +247,6 @@ pub async fn connectivity_test(target_urls: &[String]) -> Vec<String> {
}
Err(e) => {
if !CONFIGURATION.quiet {
// todo unwrap
ferox_print(
&format!("Could not connect to {}, skipping...", target_url),
&PROGRESS_PRINTER,
@@ -261,6 +260,12 @@ pub async fn connectivity_test(target_urls: &[String]) -> Vec<String> {
if good_urls.is_empty() {
log::error!("Could not connect to any target provided, exiting.");
log::trace!("exit: connectivity_test");
eprintln!(
"{} {} Could not connect to any target provided",
status_colorizer("ERROR"),
module_colorizer("heuristics::connectivity_test"),
);
process::exit(1);
}
@@ -274,6 +279,7 @@ mod tests {
use super::*;
#[test]
/// request a unique string of 32bytes * a value returns correct result
fn unique_string_returns_correct_length() {
for i in 0..10 {
assert_eq!(unique_string(i).len(), i * 32);

View File

@@ -52,3 +52,29 @@ pub const DEFAULT_STATUS_CODES: [StatusCode; 9] = [
///
/// Expected location is in the same directory as the feroxbuster binary.
pub const DEFAULT_CONFIG_NAME: &str = "ferox-config.toml";
#[cfg(test)]
mod tests {
use super::*;
#[test]
/// asserts default config name is correct
fn default_config_name() {
assert_eq!(DEFAULT_CONFIG_NAME, "ferox-config.toml");
}
#[test]
/// asserts default wordlist is correct
fn default_wordlist() {
assert_eq!(
DEFAULT_WORDLIST,
"/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt"
);
}
#[test]
/// asserts default version is correct
fn default_version() {
assert_eq!(VERSION, env!("CARGO_PKG_VERSION"));
}
}

View File

@@ -4,7 +4,9 @@ use env_logger::Builder;
use std::env;
use std::time::Instant;
/// Create an instance of an [Logger](struct.Logger.html) and set the log level based on `verbosity`
/// Create a customized instance of
/// [env_logger::Logger](https://docs.rs/env_logger/latest/env_logger/struct.Logger.html)
/// with timer offset/color and set the log level based on `verbosity`
pub fn initialize(verbosity: u8) {
// use occurrences of -v on commandline to or verbosity = N in feroxconfig.toml to set
// log level for the application; respects already specified RUST_LOG environment variable
@@ -40,7 +42,7 @@ pub fn initialize(verbosity: u8) {
let msg = format!(
"{} {:10.03} {}",
style(format!("{}", level_name)).bg(level_color).black(),
style(level_name).bg(level_color).black(),
style(t).dim(),
style(record.args()).dim(),
);

View File

@@ -1,11 +1,12 @@
use feroxbuster::config::{CONFIGURATION, PROGRESS_PRINTER};
use feroxbuster::scanner::scan_url;
use feroxbuster::utils::get_current_depth;
use feroxbuster::utils::{ferox_print, get_current_depth, module_colorizer, status_colorizer};
use feroxbuster::{banner, heuristics, logger, FeroxResult};
use futures::StreamExt;
use std::collections::HashSet;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::process;
use std::sync::Arc;
use tokio::io;
use tokio_util::codec::{FramedRead, LinesCodec};
@@ -17,6 +18,12 @@ fn get_unique_words_from_wordlist(path: &str) -> FeroxResult<Arc<HashSet<String>
let file = match File::open(&path) {
Ok(f) => f,
Err(e) => {
eprintln!(
"{} {} {}",
status_colorizer("ERROR"),
module_colorizer("main::get_unique_words_from_wordlist"),
e
);
log::error!("Could not open wordlist: {}", e);
log::trace!("exit: get_unique_words_from_wordlist -> {}", e);
@@ -29,20 +36,14 @@ fn get_unique_words_from_wordlist(path: &str) -> FeroxResult<Arc<HashSet<String>
let mut words = HashSet::new();
for line in reader.lines() {
match line {
Ok(word) => {
words.insert(word);
}
Err(e) => {
log::warn!("Could not parse current line from wordlist : {}", e);
}
}
words.insert(line?);
}
log::trace!(
"exit: get_unique_words_from_wordlist -> Arc<wordlist[{} words...]>",
words.len()
);
Ok(Arc::new(words))
}
@@ -56,6 +57,16 @@ async fn scan(targets: Vec<String>) -> FeroxResult<()> {
tokio::spawn(async move { get_unique_words_from_wordlist(&CONFIGURATION.wordlist) })
.await??;
if words.len() == 0 {
eprintln!(
"{} {} Did not find any words in {}",
status_colorizer("ERROR"),
module_colorizer("main::scan"),
CONFIGURATION.wordlist
);
process::exit(1);
}
let mut tasks = vec![];
for target in targets {
@@ -76,7 +87,7 @@ async fn scan(targets: Vec<String>) -> FeroxResult<()> {
Ok(())
}
async fn get_targets() -> Vec<String> {
async fn get_targets() -> FeroxResult<Vec<String>> {
log::trace!("enter: get_targets");
let mut targets = vec![];
@@ -88,14 +99,7 @@ async fn get_targets() -> Vec<String> {
let mut reader = FramedRead::new(stdin, LinesCodec::new());
while let Some(line) = reader.next().await {
match line {
Ok(target) => {
targets.push(target);
}
Err(e) => {
log::error!("{}", e);
}
}
targets.push(line?);
}
} else {
targets.push(CONFIGURATION.target_url.clone());
@@ -103,7 +107,7 @@ async fn get_targets() -> Vec<String> {
log::trace!("exit: get_targets -> {:?}", targets);
targets
Ok(targets)
}
#[tokio::main]
@@ -114,11 +118,27 @@ async fn main() {
log::debug!("{:#?}", *CONFIGURATION);
// get targets from command line or stdin
let targets = get_targets().await;
let targets = match get_targets().await {
Ok(t) => t,
Err(e) => {
// should only happen in the event that there was an error reading from stdin
log::error!("{}", e);
ferox_print(
&format!(
"{} {} {}",
status_colorizer("ERROR"),
module_colorizer("main::get_targets"),
e
),
&PROGRESS_PRINTER,
);
process::exit(1);
}
};
if !CONFIGURATION.quiet {
// only print banner if -q isn't used
banner::initialize(&targets);
banner::initialize(&targets, &CONFIGURATION);
}
// discard non-responsive targets

View File

@@ -1,6 +1,8 @@
use crate::config::{CONFIGURATION, PROGRESS_BAR};
use indicatif::{ProgressBar, ProgressStyle};
/// Add an [indicatif::ProgressBar](https://docs.rs/indicatif/latest/indicatif/struct.ProgressBar.html)
/// to the global [PROGRESS_BAR](../config/struct.PROGRESS_BAR.html)
pub fn add_bar(prefix: &str, length: u64, hidden: bool) -> ProgressBar {
let style = if hidden || CONFIGURATION.quiet {
ProgressStyle::default_bar().template("")

View File

@@ -1,113 +1,23 @@
use crate::config::{CONFIGURATION, PROGRESS_BAR, PROGRESS_PRINTER};
use crate::heuristics::WildcardFilter;
use crate::utils::{ferox_print, get_current_depth, get_url_path_length, status_colorizer};
use crate::{heuristics, progress, FeroxResult};
use crate::utils::{
ferox_print, format_url, get_current_depth, get_url_path_length, make_request, status_colorizer,
};
use crate::{heuristics, progress};
use futures::future::{BoxFuture, FutureExt};
use futures::{stream, StreamExt};
use reqwest::{Client, Response, Url};
use reqwest::{Response, Url};
use std::collections::HashSet;
use std::convert::TryInto;
use std::ops::Deref;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use tokio::fs;
use tokio::io::{self, AsyncWriteExt};
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
use tokio::task::JoinHandle;
/// Simple helper to generate a `Url`
///
/// Errors during parsing `url` or joining `word` are propagated up the call stack
pub fn format_url(
url: &str,
word: &str,
addslash: bool,
queries: &[(String, String)],
extension: Option<&str>,
) -> FeroxResult<Url> {
log::trace!(
"enter: format_url({}, {}, {}, {:?} {:?})",
url,
word,
addslash,
queries,
extension
);
// from reqwest::Url::join
// Note: a trailing slash is significant. Without it, the last path component
// is considered to be a “file” name to be removed to get at the “directory”
// that is used as the base
//
// the transforms that occur here will need to keep this in mind, i.e. add a slash to preserve
// the current directory sent as part of the url
let url = if !url.ends_with('/') {
format!("{}/", url)
} else {
url.to_string()
};
let base_url = reqwest::Url::parse(&url)?;
// extensions and slashes are mutually exclusive cases
let word = if extension.is_some() {
format!("{}.{}", word, extension.unwrap())
} else if addslash && !word.ends_with('/') {
// -f used, and word doesn't already end with a /
format!("{}/", word)
} else {
String::from(word)
};
match base_url.join(&word) {
Ok(request) => {
if queries.is_empty() {
// no query params to process
log::trace!("exit: format_url -> {}", request);
Ok(request)
} else {
match reqwest::Url::parse_with_params(request.as_str(), queries) {
Ok(req_w_params) => {
log::trace!("exit: format_url -> {}", req_w_params);
Ok(req_w_params) // request with params attached
}
Err(e) => {
log::error!(
"Could not add query params {:?} to {}: {}",
queries,
request,
e
);
log::trace!("exit: format_url -> {}", request);
Ok(request) // couldn't process params, return initially ok url
}
}
}
}
Err(e) => {
log::trace!("exit: format_url -> {}", e);
log::error!("Could not join {} with {}", word, base_url);
Err(Box::new(e))
}
}
}
/// Initiate request to the given `Url` using the pre-configured `Client`
pub async fn make_request(client: &Client, url: &Url) -> FeroxResult<Response> {
log::trace!("enter: make_request(CONFIGURATION.Client, {})", url);
match client.get(url.to_owned()).send().await {
Ok(resp) => {
log::debug!("requested Url: {}", resp.url());
log::trace!("exit: make_request -> {:?}", resp);
Ok(resp)
}
Err(e) => {
log::trace!("exit: make_request -> {}", e);
log::error!("Error while making request: {}", e);
Err(Box::new(e))
}
}
}
static CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
/// Spawn a single consumer task (sc side of mpsc)
///
@@ -182,7 +92,6 @@ async fn spawn_file_reporter(mut report_channel: UnboundedReceiver<Response>) {
/// reporting criteria
async fn spawn_terminal_reporter(mut report_channel: UnboundedReceiver<Response>) {
log::trace!("enter: spawn_terminal_reporter({:?})", report_channel);
//todo trace
while let Some(resp) = report_channel.recv().await {
log::debug!("received {} on reporting channel", resp.url());
@@ -524,10 +433,10 @@ pub async fn scan_url(target_url: &str, wordlist: Arc<HashSet<String>>, base_dep
let progress_bar = progress::add_bar(&target_url, num_reqs_expected, false);
progress_bar.reset_elapsed();
if get_current_depth(&target_url) - base_depth == 0 {
if CALL_COUNT.load(Ordering::Relaxed) == 0 {
// join can only be called once, otherwise it causes the thread to panic
// when current depth - base depth equals zero, we're in the first call to scan_url
tokio::task::spawn_blocking(move || PROGRESS_BAR.join().unwrap());
CALL_COUNT.fetch_add(1, Ordering::Relaxed);
}
let wildcard_bar = progress_bar.clone();
@@ -620,42 +529,58 @@ pub async fn scan_url(target_url: &str, wordlist: Arc<HashSet<String>>, base_dep
#[cfg(test)]
mod tests {
use super::*;
//
#[test]
fn test_format_url_normal() {
assert_eq!(
format_url("http://localhost", "stuff", false, &Vec::new(), None).unwrap(),
reqwest::Url::parse("http://localhost/stuff").unwrap()
);
/// sending url + word without any extensions should get back one url with the joined word
fn create_urls_no_extension_returns_base_url_with_word() {
let urls = create_urls("http://localhost", "turbo", &[]);
assert_eq!(urls, [Url::parse("http://localhost/turbo").unwrap()])
}
#[test]
fn test_format_url_no_word() {
/// sending url + word + 1 extension should get back two urls, one base and one with extension
fn create_urls_one_extension_returns_two_urls() {
let urls = create_urls("http://localhost", "turbo", &[String::from("js")]);
assert_eq!(
format_url("http://localhost", "", false, &Vec::new(), None).unwrap(),
reqwest::Url::parse("http://localhost").unwrap()
);
urls,
[
Url::parse("http://localhost/turbo").unwrap(),
Url::parse("http://localhost/turbo.js").unwrap()
]
)
}
#[test]
#[should_panic]
fn test_format_url_no_url() {
format_url("", "stuff", false, &Vec::new(), None).unwrap();
}
/// sending url + word + multiple extensions should get back n+1 urls
fn create_urls_multiple_extensions_returns_n_plus_one_urls() {
let ext_vec = vec![
vec![String::from("js")],
vec![String::from("js"), String::from("php")],
vec![String::from("js"), String::from("php"), String::from("pdf")],
vec![
String::from("js"),
String::from("php"),
String::from("pdf"),
String::from("tar.gz"),
],
];
#[test]
fn test_format_url_word_with_preslash() {
assert_eq!(
format_url("http://localhost", "/stuff", false, &Vec::new(), None).unwrap(),
reqwest::Url::parse("http://localhost/stuff").unwrap()
);
}
let base = Url::parse("http://localhost/turbo").unwrap();
let js = Url::parse("http://localhost/turbo.js").unwrap();
let php = Url::parse("http://localhost/turbo.php").unwrap();
let pdf = Url::parse("http://localhost/turbo.pdf").unwrap();
let tar = Url::parse("http://localhost/turbo.tar.gz").unwrap();
#[test]
fn test_format_url_word_with_postslash() {
assert_eq!(
format_url("http://localhost", "stuff/", false, &Vec::new(), None).unwrap(),
reqwest::Url::parse("http://localhost/stuff/").unwrap()
);
let expected = vec![
vec![base.clone(), js.clone()],
vec![base.clone(), js.clone(), php.clone()],
vec![base.clone(), js.clone(), php.clone(), pdf.clone()],
vec![base, js, php, pdf, tar],
];
for (i, ext_set) in ext_vec.into_iter().enumerate() {
let urls = create_urls("http://localhost", "turbo", &ext_set);
assert_eq!(urls, expected[i]);
}
}
}

View File

@@ -1,7 +1,8 @@
use ansi_term::Color::{Blue, Cyan, Green, Red, Yellow};
use console::{strip_ansi_codes, user_attended};
use crate::FeroxResult;
use console::{strip_ansi_codes, style, user_attended};
use indicatif::ProgressBar;
use reqwest::Url;
use reqwest::{Client, Response};
use std::convert::TryInto;
/// Helper function that determines the current depth of a given url
@@ -61,17 +62,24 @@ pub fn get_current_depth(target: &str) -> usize {
/// Takes in a string and examines the first character to return a color version of the same string
pub fn status_colorizer(status: &str) -> String {
match status.chars().next() {
Some('1') => Blue.paint(status).to_string(), // informational
Some('2') => Green.bold().paint(status).to_string(), // success
Some('3') => Yellow.paint(status).to_string(), // redirects
Some('4') => Red.paint(status).to_string(), // client error
Some('5') => Red.paint(status).to_string(), // server error
Some('W') => Cyan.paint(status).to_string(), // wildcard
Some('E') => Red.paint(status).to_string(), // wildcard
_ => status.to_string(), // ¯\_(ツ)_/¯
Some('1') => style(status).blue().to_string(), // informational
Some('2') => style(status).green().to_string(), // success
Some('3') => style(status).yellow().to_string(), // redirects
Some('4') => style(status).red().to_string(), // client error
Some('5') => style(status).red().to_string(), // server error
Some('W') => style(status).cyan().to_string(), // wildcard
Some('E') => style(status).red().to_string(), // error
_ => status.to_string(), // ¯\_(ツ)_/¯
}
}
/// Takes in a string and colors it using console::style
///
/// mainly putting this here in case i want to change the color later, making any changes easy
pub fn module_colorizer(modname: &str) -> String {
style(modname).cyan().to_string()
}
/// Gets the length of a url's path
///
/// example: http://localhost/stuff -> 5
@@ -126,31 +134,260 @@ pub fn ferox_print(msg: &str, bar: &ProgressBar) {
}
}
/// Simple helper to generate a `Url`
///
/// Errors during parsing `url` or joining `word` are propagated up the call stack
pub fn format_url(
url: &str,
word: &str,
addslash: bool,
queries: &[(String, String)],
extension: Option<&str>,
) -> FeroxResult<Url> {
log::trace!(
"enter: format_url({}, {}, {}, {:?} {:?})",
url,
word,
addslash,
queries,
extension
);
// from reqwest::Url::join
// Note: a trailing slash is significant. Without it, the last path component
// is considered to be a “file” name to be removed to get at the “directory”
// that is used as the base
//
// the transforms that occur here will need to keep this in mind, i.e. add a slash to preserve
// the current directory sent as part of the url
let url = if !url.ends_with('/') {
format!("{}/", url)
} else {
url.to_string()
};
let base_url = reqwest::Url::parse(&url)?;
// extensions and slashes are mutually exclusive cases
let word = if extension.is_some() {
format!("{}.{}", word, extension.unwrap())
} else if addslash && !word.ends_with('/') {
// -f used, and word doesn't already end with a /
format!("{}/", word)
} else {
String::from(word)
};
match base_url.join(&word) {
Ok(request) => {
if queries.is_empty() {
// no query params to process
log::trace!("exit: format_url -> {}", request);
Ok(request)
} else {
match reqwest::Url::parse_with_params(request.as_str(), queries) {
Ok(req_w_params) => {
log::trace!("exit: format_url -> {}", req_w_params);
Ok(req_w_params) // request with params attached
}
Err(e) => {
log::error!(
"Could not add query params {:?} to {}: {}",
queries,
request,
e
);
log::trace!("exit: format_url -> {}", request);
Ok(request) // couldn't process params, return initially ok url
}
}
}
}
Err(e) => {
log::trace!("exit: format_url -> {}", e);
log::error!("Could not join {} with {}", word, base_url);
Err(Box::new(e))
}
}
}
/// Initiate request to the given `Url` using `Client`
pub async fn make_request(client: &Client, url: &Url) -> FeroxResult<Response> {
log::trace!("enter: make_request(CONFIGURATION.Client, {})", url);
match client.get(url.to_owned()).send().await {
Ok(resp) => {
log::debug!("requested Url: {}", resp.url());
log::trace!("exit: make_request -> {:?}", resp);
Ok(resp)
}
Err(e) => {
log::trace!("exit: make_request -> {}", e);
log::error!("Error while making request: {}", e);
Err(Box::new(e))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base_url_returns_1() {
/// base url returns 1
fn get_current_depth_base_url_returns_1() {
let depth = get_current_depth("http://localhost");
assert_eq!(depth, 1);
}
#[test]
fn base_url_with_slash_returns_1() {
/// base url with slash returns 1
fn get_current_depth_base_url_with_slash_returns_1() {
let depth = get_current_depth("http://localhost/");
assert_eq!(depth, 1);
}
#[test]
fn one_dir_returns_2() {
/// base url + 1 dir returns 2
fn get_current_depth_one_dir_returns_2() {
let depth = get_current_depth("http://localhost/src");
assert_eq!(depth, 2);
}
#[test]
fn one_dir_with_slash_returns_2() {
/// base url + 1 dir and slash returns 2
fn get_current_depth_one_dir_with_slash_returns_2() {
let depth = get_current_depth("http://localhost/src/");
assert_eq!(depth, 2);
}
#[test]
/// base url + 1 dir and slash returns 2
fn get_current_depth_single_forward_slash_is_zero() {
let depth = get_current_depth("");
assert_eq!(depth, 0);
}
#[test]
/// base url + 1 word + no slash + no extension
fn format_url_normal() {
assert_eq!(
format_url("http://localhost", "stuff", false, &Vec::new(), None).unwrap(),
reqwest::Url::parse("http://localhost/stuff").unwrap()
);
}
#[test]
/// base url + no word + no slash + no extension
fn format_url_no_word() {
assert_eq!(
format_url("http://localhost", "", false, &Vec::new(), None).unwrap(),
reqwest::Url::parse("http://localhost").unwrap()
);
}
#[test]
/// base url + word + no slash + no extension + queries
fn format_url_joins_queries() {
assert_eq!(
format_url(
"http://localhost",
"lazer",
false,
&[(String::from("stuff"), String::from("things"))],
None
)
.unwrap(),
reqwest::Url::parse("http://localhost/lazer?stuff=things").unwrap()
);
}
#[test]
/// base url + no word + no slash + no extension + queries
fn format_url_without_word_joins_queries() {
assert_eq!(
format_url(
"http://localhost",
"",
false,
&[(String::from("stuff"), String::from("things"))],
None
)
.unwrap(),
reqwest::Url::parse("http://localhost/?stuff=things").unwrap()
);
}
#[test]
#[should_panic]
/// no base url is an error
fn format_url_no_url() {
format_url("", "stuff", false, &Vec::new(), None).unwrap();
}
#[test]
/// word prepended with slash is adjusted for correctness
fn format_url_word_with_preslash() {
assert_eq!(
format_url("http://localhost", "/stuff", false, &Vec::new(), None).unwrap(),
reqwest::Url::parse("http://localhost/stuff").unwrap()
);
}
#[test]
/// word with appended slash allows the slash to persist
fn format_url_word_with_postslash() {
assert_eq!(
format_url("http://localhost", "stuff/", false, &Vec::new(), None).unwrap(),
reqwest::Url::parse("http://localhost/stuff/").unwrap()
);
}
#[test]
/// status colorizer uses red for 500s
fn status_colorizer_uses_red_for_500s() {
assert_eq!(status_colorizer("500"), style("500").red().to_string());
}
#[test]
/// status colorizer uses red for 400s
fn status_colorizer_uses_red_for_400s() {
assert_eq!(status_colorizer("400"), style("400").red().to_string());
}
#[test]
/// status colorizer uses red for errors
fn status_colorizer_uses_red_for_errors() {
assert_eq!(status_colorizer("ERROR"), style("ERROR").red().to_string());
}
#[test]
/// status colorizer uses cyan for wildcards
fn status_colorizer_uses_cyan_for_wildcards() {
assert_eq!(status_colorizer("WLD"), style("WLD").cyan().to_string());
}
#[test]
/// status colorizer uses blue for 100s
fn status_colorizer_uses_blue_for_100s() {
assert_eq!(status_colorizer("100"), style("100").blue().to_string());
}
#[test]
/// status colorizer uses green for 200s
fn status_colorizer_uses_green_for_200s() {
assert_eq!(status_colorizer("200"), style("200").green().to_string());
}
#[test]
/// status colorizer uses yellow for 300s
fn status_colorizer_uses_yellow_for_300s() {
assert_eq!(status_colorizer("300"), style("300").yellow().to_string());
}
#[test]
/// status colorizer doesnt color anything else
fn status_colorizer_returns_as_is() {
assert_eq!(status_colorizer("farfignewton"), "farfignewton".to_string());
}
}

538
tests/test_banner.rs Normal file
View File

@@ -0,0 +1,538 @@
mod utils;
use assert_cmd::Command;
use predicates::prelude::*;
use utils::{setup_tmp_directory, teardown_tmp_directory};
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + proxy
fn banner_prints_proxy() -> Result<(), Box<dyn std::error::Error>> {
let urls = vec![
String::from("http://localhost"),
String::from("http://schmocalhost"),
];
let (tmp_dir, file) = setup_tmp_directory(&urls)?;
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--stdin")
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--proxy")
.arg("http://127.0.0.1:8080")
.pipe_stdin(file)
.unwrap()
.assert()
.failure()
.stderr(
predicate::str::contains("─┬─")
.and(predicate::str::contains("Target Url"))
.and(predicate::str::contains("http://localhost"))
.and(predicate::str::contains("http://schmocalhost"))
.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("Proxy"))
.and(predicate::str::contains("http://127.0.0.1:8080"))
.and(predicate::str::contains("─┴─")),
);
teardown_tmp_directory(tmp_dir);
Ok(())
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + multiple headers
fn banner_prints_headers() -> Result<(), Box<dyn std::error::Error>> {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("--headers")
.arg("stuff:things")
.arg("-H")
.arg("mostuff:mothings")
.assert()
.failure()
.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("Header"))
.and(predicate::str::contains("stuff: things"))
.and(predicate::str::contains("mostuff: mothings"))
.and(predicate::str::contains("─┴─")),
);
Ok(())
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + multiple size filters
fn banner_prints_size_filters() -> Result<(), Box<dyn std::error::Error>> {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("-S")
.arg("789456123")
.arg("--sizefilter")
.arg("44444444")
.assert()
.failure()
.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("Size Filter"))
.and(predicate::str::contains("789456123"))
.and(predicate::str::contains("44444444"))
.and(predicate::str::contains("─┴─")),
);
Ok(())
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + queries
fn banner_prints_queries() -> Result<(), Box<dyn std::error::Error>> {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("-Q")
.arg("token=supersecret")
.arg("--query")
.arg("stuff=things")
.assert()
.failure()
.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("Query Parameter"))
.and(predicate::str::contains("token=supersecret"))
.and(predicate::str::contains("stuff=things"))
.and(predicate::str::contains("─┴─")),
);
Ok(())
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + status codes
fn banner_prints_status_codes() -> Result<(), Box<dyn std::error::Error>> {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("-s")
.arg("201,301,401")
.assert()
.failure()
.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("Timeout (secs)"))
.and(predicate::str::contains("User-Agent"))
.and(predicate::str::contains("Status Codes"))
.and(predicate::str::contains("[201, 301, 401]"))
.and(predicate::str::contains("─┴─")),
);
Ok(())
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + output file
fn banner_prints_output_file() -> Result<(), Box<dyn std::error::Error>> {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("--output")
.arg("/super/cool/path")
.assert()
.failure()
.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("Output File"))
.and(predicate::str::contains("/super/cool/path"))
.and(predicate::str::contains("─┴─")),
);
Ok(())
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + insecure
fn banner_prints_insecure() -> Result<(), Box<dyn std::error::Error>> {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("-k")
.assert()
.failure()
.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("Insecure"))
.and(predicate::str::contains("true"))
.and(predicate::str::contains("─┴─")),
);
Ok(())
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + follow redirects
fn banner_prints_redirects() -> Result<(), Box<dyn std::error::Error>> {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("-r")
.assert()
.failure()
.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("Follow Redirects"))
.and(predicate::str::contains("true"))
.and(predicate::str::contains("─┴─")),
);
Ok(())
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + extensions
fn banner_prints_extensions() -> Result<(), Box<dyn std::error::Error>> {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("-x")
.arg("js")
.arg("--extensions")
.arg("pdf")
.assert()
.failure()
.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("Extensions"))
.and(predicate::str::contains("[js, pdf]"))
.and(predicate::str::contains("─┴─")),
);
Ok(())
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + dontfilter
fn banner_prints_dontfilter() -> Result<(), Box<dyn std::error::Error>> {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("--dontfilter")
.assert()
.failure()
.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("Filter Wildcards"))
.and(predicate::str::contains("false"))
.and(predicate::str::contains("─┴─")),
);
Ok(())
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + verbosity=1
fn banner_prints_verbosity_one() -> Result<(), Box<dyn std::error::Error>> {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("-v")
.assert()
.failure()
.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("Verbosity"))
.and(predicate::str::contains("│ 1"))
.and(predicate::str::contains("─┴─")),
);
Ok(())
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + verbosity=2
fn banner_prints_verbosity_two() -> Result<(), Box<dyn std::error::Error>> {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("-vv")
.assert()
.failure()
.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("Verbosity"))
.and(predicate::str::contains("│ 2"))
.and(predicate::str::contains("─┴─")),
);
Ok(())
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + verbosity=3
fn banner_prints_verbosity_three() -> Result<(), Box<dyn std::error::Error>> {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("-vvv")
.assert()
.failure()
.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("Verbosity"))
.and(predicate::str::contains("│ 3"))
.and(predicate::str::contains("─┴─")),
);
Ok(())
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + verbosity=4
fn banner_prints_verbosity_four() -> Result<(), Box<dyn std::error::Error>> {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("-vvvv")
.assert()
.failure()
.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("Verbosity"))
.and(predicate::str::contains("│ 4"))
.and(predicate::str::contains("─┴─")),
);
Ok(())
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + add slash
fn banner_prints_add_slash() -> Result<(), Box<dyn std::error::Error>> {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("-f")
.assert()
.failure()
.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("Add Slash"))
.and(predicate::str::contains("true"))
.and(predicate::str::contains("─┴─")),
);
Ok(())
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + INFINITE recursion
fn banner_prints_infinite_depth() -> Result<(), Box<dyn std::error::Error>> {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("--depth")
.arg("0")
.assert()
.failure()
.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("Recursion Depth"))
.and(predicate::str::contains("INFINITE"))
.and(predicate::str::contains("─┴─")),
);
Ok(())
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + recursion depth
fn banner_prints_recursion_depth() -> Result<(), Box<dyn std::error::Error>> {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("--depth")
.arg("343214")
.assert()
.failure()
.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("Recursion Depth"))
.and(predicate::str::contains("343214"))
.and(predicate::str::contains("─┴─")),
);
Ok(())
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + no recursion
fn banner_prints_no_recursion() -> Result<(), Box<dyn std::error::Error>> {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("-n")
.assert()
.failure()
.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("Do Not Recurse"))
.and(predicate::str::contains("true"))
.and(predicate::str::contains("─┴─")),
);
Ok(())
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see only the error of could not connect
fn banner_doesnt_print() -> Result<(), Box<dyn std::error::Error>> {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("-q")
.assert()
.failure()
.stderr(predicate::str::contains(
"ERROR heuristics::connectivity_test Could not connect to any target provided",
));
Ok(())
}

View File

@@ -12,17 +12,19 @@ use utils::{setup_tmp_directory, teardown_tmp_directory};
fn test_single_target_cannot_connect() -> Result<(), Box<dyn std::error::Error>> {
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()])?;
let cmd = std::panic::catch_unwind(|| {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://fjdksafjkdsajfkdsajkfdsajkfsdjkdsfdsafdsafdsajkr3l2ajfdskafdsjk")
.arg("--wordlist")
.arg(file.as_os_str())
.unwrap()
});
assert!(cmd.is_err());
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://fjdksafjkdsajfkdsajkfdsajkfsdjkdsfdsafdsafdsajkr3l2ajfdskafdsjk")
.arg("--wordlist")
.arg(file.as_os_str())
.assert()
.failure()
.stderr(
predicate::str::contains("Could not connect to any target provided")
.and(predicate::str::contains("ERROR"))
.and(predicate::str::contains("heuristics::connectivity_test")),
);
teardown_tmp_directory(tmp_dir);
Ok(())
@@ -37,18 +39,20 @@ fn test_two_targets_cannot_connect() -> Result<(), Box<dyn std::error::Error>> {
let urls = vec![not_real.clone(), not_real];
let (tmp_dir, file) = setup_tmp_directory(&urls)?;
let cmd = std::panic::catch_unwind(|| {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--stdin")
.arg("--wordlist")
.arg(file.as_os_str())
.pipe_stdin(file)
.unwrap()
.unwrap()
});
assert!(cmd.is_err());
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--stdin")
.arg("--wordlist")
.arg(file.as_os_str())
.pipe_stdin(file)
.unwrap()
.assert()
.failure()
.stderr(
predicate::str::contains("Could not connect to any target provided")
.and(predicate::str::contains("ERROR"))
.and(predicate::str::contains("heuristics::connectivity_test")),
);
teardown_tmp_directory(tmp_dir);
Ok(())
@@ -83,8 +87,8 @@ fn test_one_good_and_one_bad_target_scan_succeeds() -> Result<(), Box<dyn std::e
.success()
.stdout(
predicate::str::contains("/LICENSE")
.and(predicate::str::contains("200 OK"))
.and(predicate::str::contains("[14 bytes]")),
.and(predicate::str::contains("200"))
.and(predicate::str::contains("14")),
);
assert_eq!(mock.times_called(), 1);
@@ -93,6 +97,7 @@ fn test_one_good_and_one_bad_target_scan_succeeds() -> Result<(), Box<dyn std::e
}
#[test]
/// test finds a static wildcard and reports as much to stdout
fn test_static_wildcard_request_found() -> Result<(), Box<dyn std::error::Error>> {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()])?;
@@ -115,7 +120,7 @@ fn test_static_wildcard_request_found() -> Result<(), Box<dyn std::error::Error>
teardown_tmp_directory(tmp_dir);
cmd.assert().success().stderr(
cmd.assert().success().stdout(
predicate::str::contains("WLD")
.and(predicate::str::contains("Got"))
.and(predicate::str::contains("200"))
@@ -127,6 +132,7 @@ fn test_static_wildcard_request_found() -> Result<(), Box<dyn std::error::Error>
}
#[test]
/// test finds a dynamic wildcard and reports as much to stdout
fn test_dynamic_wildcard_request_found() -> Result<(), Box<dyn std::error::Error>> {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()])?;

98
tests/test_main.rs Normal file
View File

@@ -0,0 +1,98 @@
mod utils;
use assert_cmd::Command;
use httpmock::Method::GET;
use httpmock::{Mock, MockServer};
use predicates::prelude::*;
use utils::{setup_tmp_directory, teardown_tmp_directory};
#[test]
/// send the function a file to which we dont have permission in order to execute error branch
fn main_use_root_owned_file_as_wordlist() -> Result<(), Box<dyn std::error::Error>> {
let srv = MockServer::start();
let mock = Mock::new()
.expect_method(GET)
.expect_path("/")
.return_status(200)
.return_body("this is a test")
.create_on(&srv);
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg("/etc/shadow")
.arg("-vvvv")
.assert()
.success()
.stderr(predicate::str::contains(
"ERROR main::get_unique_words_from_wordlist Permission denied (os error 13)",
));
// connectivity test hits it once
assert_eq!(mock.times_called(), 1);
Ok(())
}
#[test]
/// send the function an empty file
fn main_use_empty_wordlist() -> Result<(), Box<dyn std::error::Error>> {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&[])?;
let mock = Mock::new()
.expect_method(GET)
.expect_path("/")
.return_status(200)
.return_body("this is a test")
.create_on(&srv);
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("-vvvv")
.assert()
.failure()
.stderr(predicate::str::contains(
"ERROR main::scan Did not find any words in",
));
assert_eq!(mock.times_called(), 1);
teardown_tmp_directory(tmp_dir);
Ok(())
}
#[test]
/// send nothing over stdin, expect heuristics to be upset during connectivity test
fn main_use_empty_stdin_targets() -> Result<(), Box<dyn std::error::Error>> {
let (tmp_dir, file) = setup_tmp_directory(&[])?;
// get_targets is called before scan, so the empty wordlist shouldn't trigger
// the 'Did not find any words' error
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--stdin")
.arg("--wordlist")
.arg(file.as_os_str())
.arg("-vvv")
.pipe_stdin(file)
.unwrap()
.assert()
.failure()
.stderr(
predicate::str::contains("Could not connect to any target provided")
.and(predicate::str::contains("ERROR"))
.and(predicate::str::contains("heuristics::connectivity_test"))
.and(predicate::str::contains("Target Url"))
.not(), // no target url found
);
teardown_tmp_directory(tmp_dir);
Ok(())
}

View File

@@ -7,6 +7,7 @@ use std::process::Command;
use utils::{setup_tmp_directory, teardown_tmp_directory};
#[test]
/// send a single valid request, expect a 200 response
fn test_single_request_scan() -> Result<(), Box<dyn std::error::Error>> {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()])?;
@@ -24,15 +25,85 @@ fn test_single_request_scan() -> Result<(), Box<dyn std::error::Error>> {
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("-vvvv")
.unwrap();
cmd.assert().success().stdout(
predicate::str::contains("/LICENSE")
.and(predicate::str::contains("200 OK"))
.and(predicate::str::contains("[14 bytes]")),
.and(predicate::str::contains("200"))
.and(predicate::str::contains("14")),
);
assert_eq!(mock.times_called(), 1);
teardown_tmp_directory(tmp_dir);
Ok(())
}
#[test]
/// send a valid request, follow redirects into new directories, expect 301/200 responses
fn scanner_recursive_request_scan() -> Result<(), Box<dyn std::error::Error>> {
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)?;
let js_mock = Mock::new()
.expect_method(GET)
.expect_path("/js")
.return_status(301)
.return_header("Location", &srv.url("/js/"))
.create_on(&srv);
let js_prod_mock = Mock::new()
.expect_method(GET)
.expect_path("/js/prod")
.return_status(301)
.return_header("Location", &srv.url("/js/prod/"))
.create_on(&srv);
let js_dev_mock = Mock::new()
.expect_method(GET)
.expect_path("/js/dev")
.return_status(301)
.return_header("Location", &srv.url("/js/dev/"))
.create_on(&srv);
let js_dev_file_mock = Mock::new()
.expect_method(GET)
.expect_path("/js/dev/file.js")
.return_status(200)
.return_body("this is a test and is more bytes than other ones")
.create_on(&srv);
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("-vvvv")
.arg("-t")
.arg("1")
.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()),
);
assert_eq!(js_mock.times_called(), 1);
assert_eq!(js_prod_mock.times_called(), 1);
assert_eq!(js_dev_mock.times_called(), 1);
assert_eq!(js_dev_file_mock.times_called(), 1);
teardown_tmp_directory(tmp_dir);
Ok(())
}

View File

@@ -2,6 +2,8 @@ use std::fs::{remove_dir_all, write};
use std::path::PathBuf;
use tempfile::TempDir;
/// integration test helper: creates a temp directory, and writes `words` to
/// a file named `wordlist` in the temp directory
pub fn setup_tmp_directory(
words: &[String],
) -> Result<(TempDir, PathBuf), Box<dyn std::error::Error>> {
@@ -11,6 +13,8 @@ pub fn setup_tmp_directory(
Ok((tmp_dir, file))
}
/// integration test helper: removes a temporary directory, presumably created with
/// [setup_tmp_directory](fn.setup_tmp_directory.html)
pub fn teardown_tmp_directory(directory: TempDir) {
remove_dir_all(directory).unwrap();
}