Skip to content

Commit

Permalink
Merge pull request #133 from epi052/124-structured-log-output
Browse files Browse the repository at this point in the history
add structured log output and split user output from logging output
  • Loading branch information
epi052 authored Nov 25, 2020
2 parents 3881789 + 2be2da4 commit 4c39944
Show file tree
Hide file tree
Showing 19 changed files with 678 additions and 279 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "feroxbuster"
version = "1.6.3"
version = "1.7.0"
authors = ["Ben 'epi' Risher <[email protected]>"]
license = "MIT"
edition = "2018"
Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,15 +321,17 @@ A pre-made configuration file with examples of all available settings can be fou
# wordlist = "/wordlists/jhaddix/all.txt"
# status_codes = [200, 500]
# filter_status = [301]
# replay_codes = [301]
# threads = 1
# timeout = 5
# proxy = "http://127.0.0.1:8080"
# replay_proxy = "http://127.0.0.1:8081"
# replay_codes = [200, 302]
# verbosity = 1
# scan_limit = 6
# quiet = true
# json = true
# output = "/targets/ellingson_mineral_company/gibson.txt"
# debug_log = "/var/log/find-the-derp.log"
# user_agent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0"
# redirects = true
# insecure = true
Expand Down Expand Up @@ -373,22 +375,25 @@ FLAGS:
findings (default: false)
-h, --help Prints help information
-k, --insecure Disables TLS certificate validation
--json Emit JSON logs to --output and --debug-log instead of normal text
-n, --no-recursion Do not scan recursively
-q, --quiet Only print URLs; Don't print status codes, response size, running config, etc...
-r, --redirects Follow redirects
--stdin Read url(s) from STDIN
-V, --version Prints version information
-v, --verbosity Increase verbosity level (use -vv or more for greater effect)
-v, --verbosity Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v's is probably
too much)
OPTIONS:
--debug-log <FILE> Output file to write log entries (use w/ --json for JSON entries)
-d, --depth <RECURSION_DEPTH> Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)
-x, --extensions <FILE_EXTENSION>... File extension(s) to search for (ex: -x php -x pdf js)
-N, --filter-lines <LINES>... Filter out messages of a particular line count (ex: -N 20 -N 31,30)
-S, --filter-size <SIZE>... Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)
-C, --filter-status <STATUS_CODE>... Filter out status codes (deny list) (ex: -C 200 -C 401)
-W, --filter-words <WORDS>... Filter out messages of a particular word count (ex: -W 312 -W 91,82)
-H, --headers <HEADER>... Specify HTTP headers (ex: -H Header:val 'stuff: things')
-o, --output <FILE> Output file to write results to (default: stdout)
-o, --output <FILE> Output file to write results to (use w/ --json for JSON entries)
-p, --proxy <PROXY> Proxy to use for requests (ex: http(s)://host:port, socks5://host:port)
-Q, --query <QUERY>... Specify URL query parameters (ex: -Q token=stuff -Q secret=key)
-R, --replay-codes <REPLAY_CODE>... Status Codes to send through a Replay Proxy when found (default: --status
Expand Down
2 changes: 2 additions & 0 deletions ferox-config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
# verbosity = 1
# scan_limit = 6
# quiet = true
# json = true
# output = "/targets/ellingson_mineral_company/gibson.txt"
# debug_log = "/var/log/find-the-derp.log"
# user_agent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0"
# redirects = true
# insecure = true
Expand Down
18 changes: 18 additions & 0 deletions src/banner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,15 @@ by Ben "epi" Risher {} ver: {}"#,
.unwrap_or_default(); // 🔎
}

if config.json {
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1F9d4}", "JSON Output", config.json)
)
.unwrap_or_default(); // 🧔
}

if !config.queries.is_empty() {
for query in &config.queries {
writeln!(
Expand All @@ -348,6 +357,15 @@ by Ben "epi" Risher {} ver: {}"#,
.unwrap_or_default(); // 💾
}

if !config.debug_log.is_empty() {
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1fab2}", "Debugging Log", config.debug_log)
)
.unwrap_or_default(); // 🪲
}

if !config.extensions.is_empty() {
writeln!(
&mut writer,
Expand Down
126 changes: 122 additions & 4 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use crate::utils::{module_colorizer, status_colorizer};
use crate::{client, parser, progress};
use crate::{DEFAULT_CONFIG_NAME, DEFAULT_STATUS_CODES, DEFAULT_WORDLIST, VERSION};
use crate::{FeroxSerialize, DEFAULT_CONFIG_NAME, DEFAULT_STATUS_CODES, DEFAULT_WORDLIST, VERSION};
use clap::value_t;
use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget};
use lazy_static::lazy_static;
use reqwest::{Client, StatusCode};
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env::{current_dir, current_exe};
use std::fs::read_to_string;
Expand Down Expand Up @@ -49,8 +49,12 @@ fn report_and_exit(err: &str) -> ! {
/// In that order.
///
/// Inspired by and derived from https://github.com/PhilipDaniels/rust-config-example
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Configuration {
#[serde(rename = "type", default = "serialized_type")]
/// Name of this type of struct, used for serialization, i.e. `{"type":"configuration"}`
kind: String,

/// Path to the wordlist
#[serde(default = "wordlist")]
pub wordlist: String,
Expand Down Expand Up @@ -107,10 +111,19 @@ pub struct Configuration {
#[serde(default)]
pub quiet: bool,

/// Store log output as NDJSON
#[serde(default)]
pub json: bool,

/// Output file to write results to (default: stdout)
#[serde(default)]
pub output: String,

/// File in which to store debug output, used in conjunction with verbosity to dictate which
/// logs are written
#[serde(default)]
pub debug_log: String,

/// Sets the User-Agent (default: feroxbuster/VERSION)
#[serde(default = "user_agent")]
pub user_agent: String,
Expand Down Expand Up @@ -180,6 +193,11 @@ pub struct Configuration {
// defaults in the event that a ferox-config.toml is found but one or more of the values below
// aren't listed in the config. This way, we get the correct defaults upon Deserialization

/// default Configuration type for use in json output
fn serialized_type() -> String {
String::from("configuration")
}

/// default timeout value
fn timeout() -> u64 {
7
Expand Down Expand Up @@ -222,8 +240,10 @@ impl Default for Configuration {
let replay_client = None;
let status_codes = status_codes();
let replay_codes = status_codes.clone();
let kind = serialized_type();

Configuration {
kind,
client,
timeout,
user_agent,
Expand All @@ -233,6 +253,7 @@ impl Default for Configuration {
dont_filter: false,
quiet: false,
stdin: false,
json: false,
verbosity: 0,
scan_limit: 0,
add_slash: false,
Expand All @@ -243,6 +264,7 @@ impl Default for Configuration {
proxy: String::new(),
config: String::new(),
output: String::new(),
debug_log: String::new(),
target_url: String::new(),
replay_proxy: String::new(),
queries: Vec::new(),
Expand Down Expand Up @@ -275,8 +297,9 @@ impl Configuration {
/// - **status_codes**: [`DEFAULT_RESPONSE_CODES`](constant.DEFAULT_RESPONSE_CODES.html)
/// - **filter_status**: `None`
/// - **output**: `None` (print to stdout)
/// - **debug_log**: `None`
/// - **quiet**: `false`
/// - **user_agent**: `feroxer/VERSION`
/// - **user_agent**: `feroxbuster/VERSION`
/// - **insecure**: `false` (don't be insecure, i.e. don't allow invalid certs)
/// - **extensions**: `None`
/// - **filter_size**: `None`
Expand All @@ -287,6 +310,7 @@ impl Configuration {
/// - **no_recursion**: `false` (recursively scan enumerated sub-directories)
/// - **add_slash**: `false`
/// - **stdin**: `false`
/// - **json**: `false`
/// - **dont_filter**: `false` (auto filter wildcard responses)
/// - **depth**: `4` (maximum recursion depth)
/// - **scan_limit**: `0` (no limit on concurrent scans imposed)
Expand Down Expand Up @@ -385,6 +409,7 @@ impl Configuration {
update_config_if_present!(&mut config.scan_limit, args, "scan_limit", usize);
update_config_if_present!(&mut config.wordlist, args, "wordlist", String);
update_config_if_present!(&mut config.output, args, "output", String);
update_config_if_present!(&mut config.debug_log, args, "debug_log", String);

if let Some(arg) = args.values_of("status_codes") {
config.status_codes = arg
Expand Down Expand Up @@ -481,6 +506,10 @@ impl Configuration {
config.extract_links = true;
}

if args.is_present("json") {
config.json = true;
}

if args.is_present("stdin") {
config.stdin = true;
} else {
Expand Down Expand Up @@ -625,6 +654,8 @@ impl Configuration {
settings.scan_limit = settings_to_merge.scan_limit;
settings.replay_proxy = settings_to_merge.replay_proxy;
settings.replay_codes = settings_to_merge.replay_codes;
settings.debug_log = settings_to_merge.debug_log;
settings.json = settings_to_merge.json;
}

/// If present, read in `DEFAULT_CONFIG_NAME` and deserialize the specified values
Expand All @@ -650,6 +681,47 @@ impl Configuration {
}
}

/// Implementation of FeroxMessage
impl FeroxSerialize for Configuration {
/// Simple wrapper around create_report_string
fn as_str(&self) -> String {
format!("{:#?}\n", *self)
}

/// Create an NDJSON representation of the current scan's Configuration
///
/// (expanded for clarity)
/// ex:
/// {
/// "type":"configuration",
/// "wordlist":"test",
/// "config":"/home/epi/.config/feroxbuster/ferox-config.toml",
/// "proxy":"",
/// "replay_proxy":"",
/// "target_url":"https://localhost.com",
/// "status_codes":[
/// 200,
/// 204,
/// 301,
/// 302,
/// 307,
/// 308,
/// 401,
/// 403,
/// 405
/// ],
/// ...
/// }\n
fn as_json(&self) -> String {
if let Ok(mut json) = serde_json::to_string(&self) {
json.push('\n');
json
} else {
String::from("{\"error\":\"could not Configuration convert to json\"}")
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -670,6 +742,7 @@ mod tests {
verbosity = 1
scan_limit = 6
output = "/some/otherpath"
debug_log = "/yet/anotherpath"
redirects = true
insecure = true
extensions = ["html", "php", "js"]
Expand All @@ -680,6 +753,7 @@ mod tests {
stdin = true
dont_filter = true
extract_links = true
json = true
depth = 1
filter_size = [4120]
filter_word_count = [994, 992]
Expand All @@ -699,6 +773,7 @@ mod tests {
assert_eq!(config.wordlist, wordlist());
assert_eq!(config.proxy, String::new());
assert_eq!(config.target_url, String::new());
assert_eq!(config.debug_log, String::new());
assert_eq!(config.config, String::new());
assert_eq!(config.replay_proxy, String::new());
assert_eq!(config.status_codes, status_codes());
Expand All @@ -712,6 +787,7 @@ mod tests {
assert_eq!(config.quiet, false);
assert_eq!(config.dont_filter, false);
assert_eq!(config.no_recursion, false);
assert_eq!(config.json, false);
assert_eq!(config.stdin, false);
assert_eq!(config.add_slash, false);
assert_eq!(config.redirects, false);
Expand All @@ -733,6 +809,13 @@ mod tests {
assert_eq!(config.wordlist, "/some/path");
}

#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_debug_log() {
let config = setup_config_test();
assert_eq!(config.debug_log, "/yet/anotherpath");
}

#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_status_codes() {
Expand Down Expand Up @@ -796,6 +879,13 @@ mod tests {
assert_eq!(config.quiet, true);
}

#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_json() {
let config = setup_config_test();
assert_eq!(config.json, true);
}

#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_verbosity() {
Expand Down Expand Up @@ -920,4 +1010,32 @@ mod tests {
fn config_report_and_exit_works() {
report_and_exit("some message");
}

#[test]
/// test as_str method of Configuration
fn as_str_returns_string_with_newline() {
let config = Configuration::new();
let config_str = config.as_str();
println!("{}", config_str);
assert!(config_str.starts_with("Configuration {"));
assert!(config_str.ends_with("}\n"));
assert!(config_str.contains("replay_codes:"));
assert!(config_str.contains("client: Client {"));
assert!(config_str.contains("user_agent: \"feroxbuster"));
}

#[test]
/// test as_json method of Configuration
fn as_json_returns_json_representation_of_configuration_with_newline() {
let mut config = Configuration::new();
config.timeout = 12;
config.depth = 2;
let config_str = config.as_json();
let json: Configuration = serde_json::from_str(&config_str).unwrap();
assert_eq!(json.config, config.config);
assert_eq!(json.wordlist, config.wordlist);
assert_eq!(json.replay_codes, config.replay_codes);
assert_eq!(json.timeout, config.timeout);
assert_eq!(json.depth, config.depth);
}
}
Loading

0 comments on commit 4c39944

Please sign in to comment.