diff --git a/Cargo.lock b/Cargo.lock index 69451f0..3e25afc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anyhow" version = "1.0.53" @@ -26,6 +35,29 @@ dependencies = [ "term", ] +[[package]] +name = "assay" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238d82aacd5cfde8ccae5c981912be68ec3cfa2d92ff4ce34090be40584c96a6" +dependencies = [ + "assay-proc-macro", + "pretty_assertions", + "rusty-fork", + "tempdir", + "tokio", +] + +[[package]] +name = "assay-proc-macro" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5fd637c6a75fe224b372556511913f12d6ad481fbfef2fb7ecea2f7cb4965d" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "assert-json-diff" version = "2.0.1" @@ -674,6 +706,7 @@ name = "feroxbuster" version = "2.6.0" dependencies = [ "anyhow", + "assay", "assert_cmd", "clap", "clap_complete", @@ -752,6 +785,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "futf" version = "0.1.5" @@ -1506,6 +1545,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "output_vt100" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" +dependencies = [ + "winapi", +] + [[package]] name = "parking" version = "2.0.0" @@ -1581,7 +1629,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" dependencies = [ "phf_shared 0.8.0", - "rand", + "rand 0.7.3", ] [[package]] @@ -1715,6 +1763,18 @@ dependencies = [ "termtree", ] +[[package]] +name = "pretty_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d5b548b725018ab5496482b45cb8bef21e9fed1858a6d674e3a8a0f0bb5d50" +dependencies = [ + "ansi_term", + "ctor", + "diff", + "output_vt100", +] + [[package]] name = "proc-macro-hack" version = "0.5.19" @@ -1730,6 +1790,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.15" @@ -1739,6 +1805,19 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + [[package]] name = "rand" version = "0.7.3" @@ -1748,7 +1827,7 @@ dependencies = [ "getrandom 0.1.16", "libc", "rand_chacha", - "rand_core", + "rand_core 0.5.1", "rand_hc", "rand_pcg", ] @@ -1760,9 +1839,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.5.1", ] +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + [[package]] name = "rand_core" version = "0.5.1" @@ -1778,7 +1872,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ - "rand_core", + "rand_core 0.5.1", ] [[package]] @@ -1787,7 +1881,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" dependencies = [ - "rand_core", + "rand_core 0.5.1", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", ] [[package]] @@ -1902,6 +2005,18 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.9" @@ -2176,6 +2291,16 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand 0.4.6", + "remove_dir_all", +] + [[package]] name = "tempfile" version = "3.3.0" diff --git a/Cargo.toml b/Cargo.toml index abf7551..3e44961 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ tempfile = "3.3" httpmock = "0.6" assert_cmd = "2.0" predicates = "2.1" +assay = "0.1.0" [profile.release] lto = true diff --git a/src/heuristics.rs b/src/heuristics.rs index e999133..1a5fb25 100644 --- a/src/heuristics.rs +++ b/src/heuristics.rs @@ -407,6 +407,7 @@ impl HeuristicTests { #[cfg(test)] mod tests { use super::*; + use assay::assay; #[test] /// request a unique string of 32bytes * a value returns correct result @@ -417,4 +418,51 @@ mod tests { assert_eq!(tester.unique_string(i).len(), i * 32); } } + + #[assay] + /// `detect_directory_listing` correctly identifies tomcat/python instances + fn detect_directory_listing_finds_tomcat_python() { + let html = "directory listing for /"; + let parsed = Html::parse_document(html); + let handles = Handles::for_testing(None, None); + let heuristics = HeuristicTests::new(Arc::new(handles.0)); + let dirlist_type = heuristics.detect_directory_listing(&parsed); + assert!(matches!( + dirlist_type.unwrap(), + DirListingType::TomCatOrPython + )); + } + + #[assay] + /// `detect_directory_listing` correctly identifies apache instances + fn detect_directory_listing_finds_apache() { + let html = "index of /"; + let parsed = Html::parse_document(html); + let handles = Handles::for_testing(None, None); + let heuristics = HeuristicTests::new(Arc::new(handles.0)); + let dirlist_type = heuristics.detect_directory_listing(&parsed); + assert!(matches!(dirlist_type.unwrap(), DirListingType::Apache)); + } + + #[assay] + /// `detect_directory_listing` correctly identifies ASP.NET instances + fn detect_directory_listing_finds_asp_dot_net() { + let html = "directory listing -- /"; + let parsed = Html::parse_document(html); + let handles = Handles::for_testing(None, None); + let heuristics = HeuristicTests::new(Arc::new(handles.0)); + let dirlist_type = heuristics.detect_directory_listing(&parsed); + assert!(matches!(dirlist_type.unwrap(), DirListingType::AspDotNet)); + } + + #[assay] + /// `detect_directory_listing` returns None when heuristic doesn't match + fn detect_directory_listing_returns_none_as_default() { + let html = "derp listing -- /"; + let parsed = Html::parse_document(html); + let handles = Handles::for_testing(None, None); + let heuristics = HeuristicTests::new(Arc::new(handles.0)); + let dirlist_type = heuristics.detect_directory_listing(&parsed); + assert!(dirlist_type.is_none()); + } } diff --git a/src/response.rs b/src/response.rs index 27938ac..e4084ef 100644 --- a/src/response.rs +++ b/src/response.rs @@ -294,6 +294,11 @@ impl FeroxResponse { // only add extensions to those responses that pass our checks; filtered out // status codes are handled by should_filter, but we need to still check against // the allow list for what we want to keep + #[cfg(test)] + handles + .send_scan_command(Command::AddDiscoveredExtension(extension.to_owned())) + .unwrap_or_default(); + #[cfg(not(test))] handles.send_scan_command(Command::AddDiscoveredExtension(extension.to_owned()))?; } } @@ -652,6 +657,7 @@ impl<'de> Deserialize<'de> for FeroxResponse { #[cfg(test)] mod tests { use super::*; + use crate::config::Configuration; use std::default::Default; #[test] @@ -723,4 +729,65 @@ mod tests { let result = response.reached_max_depth(0, 2, handles); assert!(result); } + + #[test] + /// simple case of a single extension gets parsed correctly and stored on the `FeroxResponse` + fn parse_extension_finds_simple_extension() { + let config = Configuration { + collect_extensions: true, + ..Default::default() + }; + + let (handles, _) = Handles::for_testing(None, Some(Arc::new(config))); + + let url = Url::parse("http://localhost/derp.js").unwrap(); + + let mut response = FeroxResponse { + url, + ..Default::default() + }; + + response.parse_extension(Arc::new(handles)).unwrap(); + + assert_eq!(response.extension, Some(String::from("js"))); + } + + #[test] + /// hidden files shouldn't be parsed as extensions, i.e. `/.bash_history` + fn parse_extension_ignores_hidden_files() { + let config = Configuration { + collect_extensions: true, + ..Default::default() + }; + + let (handles, _) = Handles::for_testing(None, Some(Arc::new(config))); + + let url = Url::parse("http://localhost/.bash_history").unwrap(); + + let mut response = FeroxResponse { + url, + ..Default::default() + }; + + response.parse_extension(Arc::new(handles)).unwrap(); + + assert_eq!(response.extension, None); + } + + #[test] + /// `parse_extension` should return immediately if `--collect-extensions` isn't used + fn parse_extension_early_returns_based_on_config() { + let (handles, _) = Handles::for_testing(None, None); + + let url = Url::parse("http://localhost/derp.js").unwrap(); + + let mut response = FeroxResponse { + url, + ..Default::default() + }; + + response.parse_extension(Arc::new(handles)).unwrap(); + + assert_eq!(response.extension, None); + } } diff --git a/tests/test_scanner.rs b/tests/test_scanner.rs index 76a1d3b..12a1ce7 100644 --- a/tests/test_scanner.rs +++ b/tests/test_scanner.rs @@ -1,8 +1,12 @@ mod utils; +use assay::assay; use assert_cmd::prelude::*; use httpmock::Method::GET; use httpmock::MockServer; use predicates::prelude::*; +use std::env::temp_dir; +use std::thread::sleep; +use std::time::Duration; use std::{process::Command, time}; use utils::{setup_tmp_directory, teardown_tmp_directory}; @@ -638,3 +642,43 @@ fn rate_limit_enforced_when_specified() { teardown_tmp_directory(tmp_dir); } + +#[assay] +/// ensure that auto-discovered extensions are tracked in statistics and bar lengths are updated +fn add_discovered_extension_updates_bars_and_stats() { + let srv = MockServer::start(); + let (tmp_dir, file) = setup_tmp_directory( + &["LICENSE".to_string(), "stuff.php".to_string()], + "wordlist", + ) + .unwrap(); + + let mock = srv.mock(|when, then| { + when.method(GET).path("/stuff.php"); + then.status(200).body("cool... coolcoolcool"); + }); + + let file_path = tmp_dir.path().join("debug-file.txt"); + + assert!(!file_path.exists()); + + Command::cargo_bin("feroxbuster")? + .arg("--url") + .arg(srv.url("/")) + .arg("--wordlist") + .arg(file.as_os_str()) + .arg("--extract-links") + .arg("--collect-extensions") + .arg("-vvvv") + .arg("--debug-log") + .arg(file_path.as_os_str()) + .unwrap() + .assert() + .success(); + + let contents = std::fs::read_to_string(file_path).unwrap(); + println!("{}", contents); + assert!(contents.contains("discovered new extension: php")); + assert!(contents.contains("extensions_collected: 1")); + assert!(contents.contains("expected_per_scan: 6")); +}