Compare commits

..

39 Commits

Author SHA1 Message Date
epi
6b0c847b52 formatting on config.rs 2020-10-03 09:00:40 -05:00
epi
9edd414442 added /etc/feroxbuster as a valid config location
updated .deb to install the example config at /etc/feroxbuster
version bumped to 0.2.1
2020-10-03 08:59:28 -05:00
epi
d8afb58ddd updated gitignore for easier crates.io publishing 2020-10-03 08:50:53 -05:00
epi
51799e101c version bump to 0.2.0 2020-10-03 08:00:45 -05:00
epi
e005537064 config file searched for in multiple locations
added some better error messaging
updated docs/readme
closes #51
2020-10-03 08:00:01 -05:00
epi
6d6069a5b8 updated demo 2020-10-03 05:08:54 -05:00
epi
d2dc2b1a9c updated README 2020-10-03 04:56:28 -05:00
epi
68975dc4df nitpickery 2020-10-02 21:04:45 -05:00
epi
c9c9f51110 lint 2020-10-02 21:03:14 -05:00
epi
641117537a added logo 2020-10-02 21:01:49 -05:00
epi
84f67628d1 added logo 2020-10-02 21:00:56 -05:00
epi
de6444a0ed cargo fmt/lint 2020-10-02 20:23:24 -05:00
epi
9ad59333f3 version bump to test docs on docs.rs 2020-10-02 19:58:48 -05:00
epi
585ae8fd14 added some missing documentation 2020-10-02 19:58:06 -05:00
epi
41df8807fd updated links in toc 2020-10-02 19:35:47 -05:00
epi
ac31ac6ad2 removed TODO items; updated README 2020-10-02 18:27:29 -05:00
epi
93c8ab2bcf fixed version badge on readme 2020-10-02 14:56:19 -05:00
epi
0f39c99f62 readme update w/ new install methods 2020-10-02 09:15:12 -05:00
epi
3bfabe255e removed rpm build for now 2020-10-02 08:03:02 -05:00
epi
7e0b6999ee added extra exclude targets 2020-10-02 07:14:30 -05:00
epi
4b2764c6c4 added demo gif; updated rpm info for build 2020-10-02 07:07:42 -05:00
epi
daf9a1f707 added RPM build 2020-10-02 06:48:33 -05:00
epi
0c09a573eb testing deb package build 2020-10-02 06:09:42 -05:00
epi
15bd899908 added badges 2020-10-02 05:54:37 -05:00
epi
2684ced562 updated README 2020-10-01 20:57:58 -05:00
epi
0b72272431 updated README 2020-10-01 20:49:59 -05:00
epi
f5177bd5a6 moved make_request to utils 2020-10-01 20:20:23 -05:00
epi
836b60d2c0 clippy passes; ran fmt; fixed tests 2020-10-01 20:14:44 -05:00
epi
55bd24d38d more readme updates 2020-10-01 17:34:23 -05:00
epi
1ce0ef1c13 added some example usage to readme 2020-10-01 17:28:44 -05:00
epi
35c69822ce moved format_url to utils; validated docs 2020-10-01 17:09:58 -05:00
epi
0b03b7345a Merge pull request #47 from epi052/trigger-ci-builds-on-master
fixed CI so that it triggers on master properly
2020-10-01 16:48:35 -05:00
epi
f00fcecb84 fixed CI so that it triggers on master properly 2020-10-01 16:47:51 -05:00
epi
c0d17c59a7 Merge pull request #46 from epi052/39-fix-ci-cd-pipeline
39 fix ci cd pipeline
2020-10-01 07:50:01 -05:00
epi
d907b25e7c musl build fixed for 32 and 64bit; closes #39 2020-10-01 07:49:31 -05:00
epi
ea97de7dbb testing 32bit build 2020-10-01 07:35:22 -05:00
epi
bf59f22c02 ci musl build test 2020-10-01 07:28:55 -05:00
epi
2b3369980c ci musl build test 2020-10-01 07:15:15 -05:00
epi
d716c100dd ci musl build test 2020-10-01 07:09:09 -05:00
18 changed files with 596 additions and 264 deletions

View File

@@ -66,7 +66,7 @@ jobs:
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 +89,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 +107,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]

6
.gitignore vendored
View File

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

View File

@@ -1,8 +1,18 @@
[package]
name = "feroxbuster"
version = "0.1.0"
authors = ["epi <epibar052@gmail.com>"]
version = "0.2.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"}
@@ -19,6 +29,8 @@ 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 +39,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"],
]

195
README.md
View File

@@ -1,41 +1,83 @@
# 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">
<img src="https://img.shields.io/github/workflow/status/epi052/feroxbuster/CI%20Pipeline/master?logo=github">
<img src="https://img.shields.io/github/downloads/epi052/feroxbuster/total?label=downloads&logo=github&color=inactive" alt="github downloads">
<img src="https://img.shields.io/github/issues-closed-raw/s0md3v/Photon.svg">
<img src="https://img.shields.io/github/last-commit/epi052/feroxbuster?logo=github">
<img src="https://img.shields.io/crates/v/feroxbuster?color=blue&label=version&logo=rust">
<img src="https://img.shields.io/crates/d/feroxbuster?label=downloads&logo=rust&color=inactive">
</p>
Happy Hacktoberfest!
![demo](img/demo.gif)
# feroxbuster
<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>
`feroxbuster` is a fast, simple, recursive content discovery tool written in Rust.
## 😕 What the heck is a ferox anyway?
Table of Contents
Ferox is short for Ferric Oxide. Ferric Oxide, simply put, is rust. The name rustbuster was taken, so I decided on a variation. 🤷
📖 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 +94,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/`
- `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.
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 +200,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 +268,26 @@ a few of the use-cases in which feroxbuster may be a better fit:
- You want **recursion** along with some other thing mentioned above (ffuf also does recursion)
- You want a **configuration file** option for overriding built-in default values for your scans
| | feroxbuster | gobuster | ffuf |
|-----------------------------------------------------|--------------------|--------------------|--------------------|
| fast | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| easy to use | :heavy_check_mark: | :heavy_check_mark: | |
| blacklist status codes (in addition to whitelist) | | :heavy_check_mark: | :heavy_check_mark: |
| allows recursion | :heavy_check_mark: | | :heavy_check_mark: |
| can specify query parameters | :heavy_check_mark: | | :heavy_check_mark: |
| SOCKS proxy support | :heavy_check_mark: | | |
| multiple target scan (via stdin or multiple -u) | :heavy_check_mark: | | |
| configuration file for default value override | :heavy_check_mark: | | :heavy_check_mark: |
| can accept urls via STDIN as part of a pipeline | :heavy_check_mark: | | |
| can accept wordlists via STDIN | | :heavy_check_mark: | |
| filter by response size | :heavy_check_mark: | | :heavy_check_mark: |
| auto-filter wildcard responses | :heavy_check_mark: | | :heavy_check_mark: |
| performs other scans (vhost, dns, etc) | | :heavy_check_mark: | :heavy_check_mark: |
| time delay / rate limiting | | :heavy_check_mark: | :heavy_check_mark: |
| **huge** number of other options | | | :heavy_check_mark: |
| | feroxbuster | gobuster | ffuf |
|-----------------------------------------------------|---|---|---|
| fast | ✔ | ✔ | ✔ |
| easy to use | ✔ | ✔ | |
| blacklist status codes (in addition to whitelist) | | ✔ | ✔ |
| allows recursion | ✔ | | ✔ |
| can specify query parameters | ✔ | | ✔ |
| SOCKS proxy support | ✔ | | |
| multiple target scan (via stdin or multiple -u) | ✔ | | |
| configuration file for default value override | ✔ | | ✔ |
| can accept urls via STDIN as part of a pipeline | ✔ | | |
| can accept wordlists via STDIN | | ✔ | |
| filter by response size | ✔ | | ✔ |
| auto-filter wildcard responses | ✔ | | ✔ |
| performs other scans (vhost, dns, etc) | | ✔ | ✔ |
| time delay / rate limiting | | ✔ | ✔ |
| **huge** number of other options | | | ✔ |
Of note, there's another written-in-rust content discovery tool, [rustbuster](https://github.com/phra/rustbuster). I
came across rustbuster when I was naming my tool (:cry:). I don't have any experience using it, but it appears to
came across rustbuster when I was naming my tool (😢). I don't have any experience using it, but it appears to
be able to do POST requests with an HTTP body, has SOCKS support, and has an 8.3 shortname scanner (in addition to vhost
dns, directory, etc...). In short, it definitely looks interesting and may be what you're looking for as it has some
capability I haven't seen in similar tools.

BIN
img/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,5 +1,6 @@
use crate::{config::CONFIGURATION, utils::status_colorizer, VERSION};
/// 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)
@@ -97,6 +99,13 @@ by Ben "epi" Risher {} ver: {}"#,
); // 🦡
// followed by the maybe printed or variably displayed values
if !CONFIGURATION.config.is_empty() {
eprintln!(
"{}",
format_banner_entry!("\u{1f489}", "Config File", CONFIGURATION.config)
); // 💉
}
if !CONFIGURATION.proxy.is_empty() {
eprintln!(
"{}",

View File

@@ -1,4 +1,5 @@
use crate::utils::status_colorizer;
use ansi_term::Color::Cyan;
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"),
Cyan.paint("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"),
Cyan.paint("Client::initialize"),
proxy
);
eprintln!(
"[{}] - Client::initialize: {}",
"{} {} {}",
status_colorizer("ERROR"),
Cyan.paint("Client::initialize"),
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"),
Cyan.paint("Client::build")
);
eprintln!(
"{} {} {}",
status_colorizer("ERROR"),
Cyan.paint("Client::build"),
e
);
eprintln!("[{}] - Client::build: {}", status_colorizer("ERROR"), e);
exit(1);
}
}

View File

@@ -1,15 +1,16 @@
use crate::utils::status_colorizer;
use crate::{client, parser, progress};
use crate::{DEFAULT_CONFIG_NAME, DEFAULT_STATUS_CODES, DEFAULT_WORDLIST, VERSION};
use ansi_term::Color::Cyan;
use clap::value_t;
use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget};
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 +36,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 +132,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 +186,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 +208,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,6 +232,14 @@ 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.
///
@@ -183,33 +254,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 +467,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"),
Cyan.paint("config::parse_config"),
e
);
}
@@ -417,6 +540,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 +565,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 +596,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 +725,7 @@ mod tests {
}
#[test]
/// parse the test config and see that the values parsed are correct
fn config_reads_queries() {
let config = setup_config_test();
let mut queries = vec![];

View File

@@ -1,12 +1,12 @@
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 crate::utils::{ferox_print, format_url, get_url_path_length, make_request, status_colorizer};
use ansi_term::Color::{Cyan, Yellow};
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
@@ -186,19 +186,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 +245,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 +258,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"),
Cyan.paint("heuristics::connectivity_test"),
);
process::exit(1);
}

View File

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

View File

@@ -1,11 +1,13 @@
use ansi_term::Color::Cyan;
use feroxbuster::config::{CONFIGURATION, PROGRESS_PRINTER};
use feroxbuster::scanner::scan_url;
use feroxbuster::utils::get_current_depth;
use feroxbuster::utils::{get_current_depth, 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 +19,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"),
Cyan.paint("main::get_unique_words_from_wordlist"),
e
);
log::error!("Could not open wordlist: {}", e);
log::trace!("exit: get_unique_words_from_wordlist -> {}", e);
@@ -56,6 +64,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"),
Cyan.paint("main::scan"),
CONFIGURATION.wordlist
);
process::exit(1);
}
let mut tasks = vec![];
for target in targets {

View File

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

View File

@@ -1,10 +1,12 @@
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;
@@ -14,101 +16,6 @@ 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))
}
}
}
/// Spawn a single consumer task (sc side of mpsc)
///
/// The consumer simply receives responses and writes them to the given output file if they meet
@@ -182,7 +89,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());
@@ -616,46 +522,3 @@ pub async fn scan_url(target_url: &str, wordlist: Arc<HashSet<String>>, base_dep
log::trace!("done awaiting report receiver");
log::trace!("exit: scan_url");
}
#[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()
);
}
#[test]
fn test_format_url_no_word() {
assert_eq!(
format_url("http://localhost", "", false, &Vec::new(), None).unwrap(),
reqwest::Url::parse("http://localhost").unwrap()
);
}
#[test]
#[should_panic]
fn test_format_url_no_url() {
format_url("", "stuff", false, &Vec::new(), None).unwrap();
}
#[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()
);
}
#[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()
);
}
}

View File

@@ -1,7 +1,9 @@
use crate::FeroxResult;
use ansi_term::Color::{Blue, Cyan, Green, Red, Yellow};
use console::{strip_ansi_codes, 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
@@ -67,7 +69,7 @@ pub fn status_colorizer(status: &str) -> String {
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
Some('E') => Red.paint(status).to_string(), // error
_ => status.to_string(), // ¯\_(ツ)_/¯
}
}
@@ -126,31 +128,164 @@ 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() {
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() {
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() {
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() {
fn get_current_depth_one_dir_with_slash_returns_2() {
let depth = get_current_depth("http://localhost/src/");
assert_eq!(depth, 2);
}
#[test]
fn format_url_normal() {
assert_eq!(
format_url("http://localhost", "stuff", false, &Vec::new(), None).unwrap(),
reqwest::Url::parse("http://localhost/stuff").unwrap()
);
}
#[test]
fn format_url_no_word() {
assert_eq!(
format_url("http://localhost", "", false, &Vec::new(), None).unwrap(),
reqwest::Url::parse("http://localhost").unwrap()
);
}
#[test]
#[should_panic]
fn format_url_no_url() {
format_url("", "stuff", false, &Vec::new(), None).unwrap();
}
#[test]
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]
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()
);
}
}

View File

@@ -83,8 +83,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 +93,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 +116,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 +128,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()])?;

View File

@@ -28,8 +28,8 @@ fn test_single_request_scan() -> Result<(), Box<dyn std::error::Error>> {
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);

View File

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