mirror of
https://github.com/epi052/feroxbuster.git
synced 2026-05-27 08:42:04 -03:00
Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c036e587e | ||
|
|
b733477a61 | ||
|
|
58e367b5c3 | ||
|
|
99021db091 | ||
|
|
7f145f11df | ||
|
|
68ee5883b8 | ||
|
|
9b929fdb15 | ||
|
|
a87dc64e8e | ||
|
|
70918582e5 | ||
|
|
b445198b67 | ||
|
|
97b5bcdde6 | ||
|
|
e15f6e9bd2 | ||
|
|
e74678edc3 | ||
|
|
40cce2ee37 | ||
|
|
e980cee570 | ||
|
|
73bd7c1514 | ||
|
|
a2728e1df0 | ||
|
|
95dec44766 | ||
|
|
c31cfe8673 | ||
|
|
aaa7412bb1 | ||
|
|
cdbd0030dd | ||
|
|
e144caddc0 | ||
|
|
61c4b6d523 | ||
|
|
a70c9d9413 | ||
|
|
098584c945 | ||
|
|
11f32ea8c6 | ||
|
|
afcfa4849c | ||
|
|
28d6c7dd97 | ||
|
|
b538aad7d5 | ||
|
|
d3561a5823 | ||
|
|
f23e4a5ed1 | ||
|
|
dd305bfa65 | ||
|
|
6b0c847b52 | ||
|
|
9edd414442 | ||
|
|
d8afb58ddd | ||
|
|
51799e101c | ||
|
|
e005537064 | ||
|
|
6d6069a5b8 | ||
|
|
d2dc2b1a9c | ||
|
|
68975dc4df | ||
|
|
c9c9f51110 | ||
|
|
641117537a | ||
|
|
84f67628d1 | ||
|
|
de6444a0ed | ||
|
|
9ad59333f3 | ||
|
|
585ae8fd14 | ||
|
|
41df8807fd | ||
|
|
ac31ac6ad2 | ||
|
|
93c8ab2bcf | ||
|
|
0f39c99f62 | ||
|
|
3bfabe255e | ||
|
|
7e0b6999ee | ||
|
|
4b2764c6c4 | ||
|
|
daf9a1f707 | ||
|
|
0c09a573eb | ||
|
|
15bd899908 | ||
|
|
2684ced562 | ||
|
|
0b72272431 | ||
|
|
f5177bd5a6 | ||
|
|
836b60d2c0 | ||
|
|
55bd24d38d | ||
|
|
1ce0ef1c13 | ||
|
|
35c69822ce | ||
|
|
0b03b7345a | ||
|
|
f00fcecb84 | ||
|
|
c0d17c59a7 | ||
|
|
d907b25e7c | ||
|
|
ea97de7dbb | ||
|
|
bf59f22c02 | ||
|
|
2b3369980c | ||
|
|
d716c100dd |
7
.github/actions-rs/grcov.yml
vendored
Normal file
7
.github/actions-rs/grcov.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
branch: true
|
||||
ignore-not-existing: true
|
||||
llvm: true
|
||||
output-type: lcov
|
||||
output-path: ./lcov.info
|
||||
ignore:
|
||||
- "../*"
|
||||
@@ -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
64
.github/workflows/check.yml
vendored
Normal 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
36
.github/workflows/coverage.yml
vendored
Normal 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
7
.gitignore
vendored
@@ -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
|
||||
|
||||
27
Cargo.toml
27
Cargo.toml
@@ -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
224
README.md
@@ -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>
|
||||
|
||||

|
||||
|
||||
<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
BIN
img/demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 MiB |
BIN
img/logo/default-cropped.png
Normal file
BIN
img/logo/default-cropped.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
117
src/banner.rs
117
src/banner.rs
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
219
src/config.rs
219
src/config.rs
@@ -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![];
|
||||
|
||||
@@ -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);
|
||||
|
||||
26
src/lib.rs
26
src/lib.rs
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
62
src/main.rs
62
src/main.rs
@@ -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
|
||||
|
||||
@@ -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("")
|
||||
|
||||
179
src/scanner.rs
179
src/scanner.rs
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
265
src/utils.rs
265
src/utils.rs
@@ -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
538
tests/test_banner.rs
Normal 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(())
|
||||
}
|
||||
@@ -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
98
tests/test_main.rs
Normal 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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user