Compare commits

..

35 Commits

Author SHA1 Message Date
epi
15b4fd04e5 updated status code defaults to include 500 2021-08-02 19:28:09 -05:00
epi
fceba0b68b updated deps 2021-08-02 19:27:55 -05:00
epi
eef4c9b5ed added 500 to status code defaults 2021-08-02 19:23:20 -05:00
epi
24da4e017c adjusted rlimit imports to ignore windows targets 2021-08-02 05:41:50 -05:00
epi
f3cedf01a5 Merge pull request #321 from epi052/319-separate-log-files-for-parallel
Log to separate files when using --parallel
2021-08-02 05:21:16 -05:00
epi
08ee32595f updated documentation for parallel logging change 2021-08-01 21:03:17 -05:00
epi
4c4d1a2a61 updated documentation for parallel logging change 2021-08-01 19:59:28 -05:00
epi
64b54a6308 fixed up a few todo items 2021-08-01 19:57:28 -05:00
epi
e27b3ee8da added coverage for stdin slugifying 2021-08-01 15:17:59 -05:00
epi
129725cedd added test for --parallel with -o 2021-08-01 14:32:14 -05:00
epi
17886da3df handle dir/outfile case to -o 2021-08-01 14:31:48 -05:00
epi
c8a46b7e5a added prefix param to slugify_filename 2021-08-01 09:23:42 -05:00
epi
f97d103fc6 unique file logging with parallel works 2021-08-01 08:07:12 -05:00
epi
aa2fecc5c1 bumped version to 2.3.2 2021-07-31 14:49:15 -05:00
epi
6f2244e1ff put url slug logic into its own function 2021-07-31 14:48:35 -05:00
epi
a1dc90ba06 updated rlimit lib 2021-07-30 20:01:16 -05:00
epi
32f55ddfb7 Merge branch 'main' of github.com:epi052/feroxbuster 2021-07-30 16:14:32 -05:00
epi
9a65c7f1f5 fixed up code for new clippy checks 2021-07-30 16:14:25 -05:00
epi
0f6bc1c160 updated deps 2021-07-30 07:42:25 -05:00
epi
abef7a236b Update README.md 2021-07-12 16:17:08 -05:00
epi
0cff62dbe2 return 0 when -h/--help is used 2021-07-05 06:33:40 -05:00
epi
a590188e44 Merge pull request #293 from epi052/286-url-blacklist
add --dont-scan option for denying urls
2021-06-18 14:40:16 -07:00
epi
dc3aa11966 added tracing to new extractor fns 2021-06-18 16:32:04 -05:00
epi
57714d243a fixed caching for extraction; much better performance now 2021-06-18 16:18:42 -05:00
epi
1d34a5e99f updated readme 2021-06-18 11:05:57 -05:00
epi
9ab3e5515e added short-circuit to deny check 2021-06-18 06:48:41 -05:00
epi
3abef25c8f added integration tests 2021-06-17 20:34:21 -05:00
epi
454f3a4302 satisfied newest version of clippy 2021-06-17 16:13:18 -05:00
epi
acb9c19f4d added should_deny_url function and unit tests 2021-06-17 14:44:33 -05:00
epi
98f06951bd added banner test 2021-06-15 17:31:24 -05:00
epi
c9e1a7adbe added deny list to banner 2021-06-15 16:59:36 -05:00
epi
c57cf82fce added --dont-scan to options parser 2021-06-15 16:45:23 -05:00
epi
a3bcfaf95c added url_denylist to config 2021-06-15 14:31:01 -05:00
epi
c99afec740 bumped version to 2.3.0 2021-06-15 13:52:45 -05:00
epi
fa9fd65c2f bumped version to 2.3.0 2021-06-15 11:47:57 -05:00
41 changed files with 1263 additions and 359 deletions

250
Cargo.lock generated
View File

@@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "0.7.18"
@@ -20,9 +22,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.41"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61"
checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486"
[[package]]
name = "ascii-canvas"
@@ -45,9 +47,9 @@ dependencies = [
[[package]]
name = "assert_cmd"
version = "1.0.5"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a88b6bd5df287567ffdf4ddf4d33060048e1068308e5f62d81c6f9824a045a48"
checksum = "c98233c6673d8601ab23e77eb38f999c51100d46c5703b17288c57fddf3a1ffe"
dependencies = [
"bstr",
"doc-comment",
@@ -100,12 +102,11 @@ dependencies = [
[[package]]
name = "async-io"
version = "1.4.1"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bbfd5cf2794b1e908ea8457e6c45f8f8f1f6ec5f74617bf4662623f47503c3b"
checksum = "a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b"
dependencies = [
"concurrent-queue",
"fastrand",
"futures-lite",
"libc",
"log",
@@ -198,9 +199,9 @@ checksum = "e91831deabf0d6d7ec49552e489aed63b7456a7a3c46cff62adad428110b0af0"
[[package]]
name = "async-trait"
version = "0.1.50"
version = "0.1.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b98e84bbb4cbcdd97da190ba0c58a1bb0de2c1fdf67d159e192ed766aeca722"
checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e"
dependencies = [
"proc-macro2",
"quote",
@@ -313,9 +314,9 @@ checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba"
[[package]]
name = "cc"
version = "1.0.68"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787"
checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2"
[[package]]
name = "cfg-if"
@@ -456,9 +457,9 @@ dependencies = [
[[package]]
name = "curl-sys"
version = "0.4.44+curl-7.77.0"
version = "0.4.45+curl-7.78.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b6d85e9322b193f117c966e79c2d6929ec08c02f339f950044aba12e20bbaf1"
checksum = "de9e5a72b1c744eb5dd20b2be4d7eb84625070bb5c4ab9b347b70464ab1e62eb"
dependencies = [
"cc",
"libc",
@@ -482,6 +483,12 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "dirs"
version = "3.0.2"
@@ -561,9 +568,9 @@ dependencies = [
[[package]]
name = "env_logger"
version = "0.8.4"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3"
checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
dependencies = [
"atty",
"humantime",
@@ -580,16 +587,16 @@ checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59"
[[package]]
name = "fastrand"
version = "1.4.1"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77b705829d1e87f762c2df6da140b26af5839e1033aa84aa5f56bb688e4e1bdb"
checksum = "b394ed3d285a429378d3b384b9eb1285267e7df4b166df24b7a6939a04dc392e"
dependencies = [
"instant",
]
[[package]]
name = "feroxbuster"
version = "2.2.5"
version = "2.3.2"
dependencies = [
"anyhow",
"assert_cmd",
@@ -628,9 +635,9 @@ checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d"
[[package]]
name = "float-cmp"
version = "0.8.0"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1267f4ac4f343772758f7b1bdcbe767c218bbab93bb432acbf5162bbf85a6c4"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
dependencies = [
"num-traits",
]
@@ -668,9 +675,9 @@ dependencies = [
[[package]]
name = "futures"
version = "0.3.15"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7e43a803dae2fa37c1f6a8fe121e1f7bf9548b4dfc0522a42f34145dadfc27"
checksum = "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b"
dependencies = [
"futures-channel",
"futures-core",
@@ -683,9 +690,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.15"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e682a68b29a882df0545c143dc3646daefe80ba479bcdede94d5a703de2871e2"
checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9"
dependencies = [
"futures-core",
"futures-sink",
@@ -693,15 +700,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.15"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1"
checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99"
[[package]]
name = "futures-executor"
version = "0.3.15"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "badaa6a909fac9e7236d0620a2f57f7664640c56575b71a7552fbd68deafab79"
checksum = "4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c"
dependencies = [
"futures-core",
"futures-task",
@@ -710,9 +717,9 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.15"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acc499defb3b348f8d8f3f66415835a9131856ff7714bf10dadfc4ec4bdb29a1"
checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582"
[[package]]
name = "futures-lite"
@@ -731,9 +738,9 @@ dependencies = [
[[package]]
name = "futures-macro"
version = "0.3.15"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4c40298486cdf52cc00cd6d6987892ba502c7656a16a4192a9992b1ccedd121"
checksum = "c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57"
dependencies = [
"autocfg",
"proc-macro-hack",
@@ -744,21 +751,21 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.15"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a57bead0ceff0d6dde8f465ecd96c9338121bb7717d3e7b108059531870c4282"
checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53"
[[package]]
name = "futures-task"
version = "0.3.15"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae"
checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2"
[[package]]
name = "futures-util"
version = "0.3.15"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967"
checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78"
dependencies = [
"autocfg",
"futures-channel",
@@ -826,15 +833,15 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.9.1"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]]
name = "hermit-abi"
version = "0.1.18"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
@@ -908,9 +915,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "0.14.9"
version = "0.14.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07d6baa1b441335f3ce5098ac421fb6547c46dda735ca1bc6d0153c838f9dd83"
checksum = "0b61cf2d1aebcf6e6352c97b81dc2244ca29194be1b276f5d8ad5c6330fffb11"
dependencies = [
"bytes",
"futures-channel",
@@ -956,9 +963,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "1.6.2"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3"
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
dependencies = [
"autocfg",
"hashbrown",
@@ -978,18 +985,18 @@ dependencies = [
[[package]]
name = "instant"
version = "0.1.9"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec"
checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d"
dependencies = [
"cfg-if",
]
[[package]]
name = "ipnet"
version = "2.3.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135"
checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
[[package]]
name = "isahc"
@@ -1034,9 +1041,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
[[package]]
name = "js-sys"
version = "0.3.51"
version = "0.3.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062"
checksum = "ce791b7ca6638aae45be056e068fc756d871eb3b3b10b8efa62d1c9cec616752"
dependencies = [
"wasm-bindgen",
]
@@ -1110,9 +1117,9 @@ checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760"
[[package]]
name = "libc"
version = "0.2.97"
version = "0.2.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6"
checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790"
[[package]]
name = "libnghttp2-sys"
@@ -1279,9 +1286,9 @@ checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]]
name = "openssl"
version = "0.10.34"
version = "0.10.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d7830286ad6a3973c0f1d9b73738f69c76b739301d0229c4b96501695cbe4c8"
checksum = "549430950c79ae24e6d02e0b7404534ecf311d94cc9f861e9e4020187d13d885"
dependencies = [
"bitflags",
"cfg-if",
@@ -1308,9 +1315,9 @@ dependencies = [
[[package]]
name = "openssl-sys"
version = "0.9.63"
version = "0.9.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6b0d6fb7d80f877617dfcb014e605e2b5ab2fb0afdf27935219bb6bd984cb98"
checksum = "7a7907e3bfa08bb85105209cdfcb6c63d109f8f6c1ed6ca318fff5c1853fbc1d"
dependencies = [
"autocfg",
"cc",
@@ -1384,18 +1391,18 @@ checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468"
[[package]]
name = "pin-project"
version = "1.0.7"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7509cc106041c40a4518d2af7a61530e1eed0e6285296a3d8c5472806ccc4a4"
checksum = "576bc800220cc65dac09e99e97b08b358cfab6e17078de8dc5fee223bd2d0c08"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.0.7"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c950132583b500556b1efd71d45b319029f2b71518d979fcc208e16b42426f"
checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389"
dependencies = [
"proc-macro2",
"quote",
@@ -1404,9 +1411,9 @@ dependencies = [
[[package]]
name = "pin-project-lite"
version = "0.2.6"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905"
checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
[[package]]
name = "pin-utils"
@@ -1447,12 +1454,13 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "predicates"
version = "1.0.8"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f49cfaf7fdaa3bfacc6fa3e7054e65148878354a5cfddcf661df4c851f8021df"
checksum = "bc3d91237f5de3bcd9d927e24d03b495adb6135097b001cea7403e2d573d00a9"
dependencies = [
"difference",
"difflib",
"float-cmp",
"itertools",
"normalize-line-endings",
"predicates-core",
"regex",
@@ -1488,9 +1496,9 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
[[package]]
name = "proc-macro2"
version = "1.0.27"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038"
checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612"
dependencies = [
"unicode-xid",
]
@@ -1515,9 +1523,9 @@ dependencies = [
[[package]]
name = "rand"
version = "0.8.3"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e"
checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
dependencies = [
"libc",
"rand_chacha",
@@ -1537,27 +1545,27 @@ dependencies = [
[[package]]
name = "rand_core"
version = "0.6.2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
dependencies = [
"getrandom",
]
[[package]]
name = "rand_hc"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73"
checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
dependencies = [
"rand_core",
]
[[package]]
name = "redox_syscall"
version = "0.2.8"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc"
checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee"
dependencies = [
"bitflags",
]
@@ -1606,9 +1614,9 @@ dependencies = [
[[package]]
name = "reqwest"
version = "0.11.3"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2296f2fac53979e8ccbc4a1136b25dcefd37be9ed7e4a1f6b05a6029c84ff124"
checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22"
dependencies = [
"base64",
"bytes",
@@ -1641,9 +1649,9 @@ dependencies = [
[[package]]
name = "rlimit"
version = "0.5.4"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81a9ed03edbed449d6897c2092c71ab5f7b5fb80f6f0b1a3ed6d40a6f9fc0720"
checksum = "cc0bf25554376fd362f54332b8410a625c71f15445bca32ffdfdf4ec9ac91726"
dependencies = [
"libc",
]
@@ -1701,18 +1709,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.126"
version = "1.0.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.126"
version = "1.0.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43"
checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc"
dependencies = [
"proc-macro2",
"quote",
@@ -1721,9 +1729,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.64"
version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127"
dependencies = [
"itoa",
"ryu",
@@ -1784,9 +1792,9 @@ dependencies = [
[[package]]
name = "siphasher"
version = "0.3.5"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbce6d4507c7e4a3962091436e56e95290cb71fa302d0d270e32130b75fbff27"
checksum = "729a25c17d72b06c68cb47955d44fda88ad2d3e7d77e025663fdd69b93dd71a1"
[[package]]
name = "slab"
@@ -1813,9 +1821,9 @@ checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
[[package]]
name = "socket2"
version = "0.4.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2"
checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad"
dependencies = [
"libc",
"winapi",
@@ -1841,9 +1849,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "syn"
version = "1.0.73"
version = "1.0.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7"
checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c"
dependencies = [
"proc-macro2",
"quote",
@@ -1905,18 +1913,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.25"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6"
checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.25"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d"
checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745"
dependencies = [
"proc-macro2",
"quote",
@@ -1934,9 +1942,9 @@ dependencies = [
[[package]]
name = "tinyvec"
version = "1.2.0"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342"
checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338"
dependencies = [
"tinyvec_macros",
]
@@ -1949,9 +1957,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.6.2"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aea337f72e96efe29acc234d803a5981cd9a2b6ed21655cd7fc21cfe021e8ec7"
checksum = "4b7b349f11a7047e6d1276853e612d152f5e8a352c61917887cc2169e2366b4c"
dependencies = [
"autocfg",
"bytes",
@@ -1969,9 +1977,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "1.2.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37"
checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110"
dependencies = [
"proc-macro2",
"quote",
@@ -2002,9 +2010,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
version = "0.1.6"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8864d706fdb3cc0843a49647ac892720dac98a6eeb818b77190592cf4994066"
checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f"
dependencies = [
"futures-core",
"pin-project-lite",
@@ -2158,9 +2166,9 @@ dependencies = [
[[package]]
name = "vcpkg"
version = "0.2.13"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "025ce40a007e1907e58d5bc1a594def78e5573bb0b1160bc389634e8f12e4faa"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vec_map"
@@ -2207,9 +2215,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "wasm-bindgen"
version = "0.2.74"
version = "0.2.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd"
checksum = "b608ecc8f4198fe8680e2ed18eccab5f0cd4caaf3d83516fa5fb2e927fda2586"
dependencies = [
"cfg-if",
"serde",
@@ -2219,9 +2227,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.74"
version = "0.2.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900"
checksum = "580aa3a91a63d23aac5b6b267e2d13cb4f363e31dce6c352fca4752ae12e479f"
dependencies = [
"bumpalo",
"lazy_static",
@@ -2234,9 +2242,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.24"
version = "0.4.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fba7978c679d53ce2d0ac80c8c175840feb849a161664365d1287b41f2e67f1"
checksum = "16646b21c3add8e13fdb8f20172f8a28c3dbf62f45406bcff0233188226cfe0c"
dependencies = [
"cfg-if",
"js-sys",
@@ -2246,9 +2254,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.74"
version = "0.2.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4"
checksum = "171ebf0ed9e1458810dfcb31f2e766ad6b3a89dbda42d8901f2b268277e5f09c"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -2256,9 +2264,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.74"
version = "0.2.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97"
checksum = "6c2657dd393f03aa2a659c25c6ae18a13a4048cebd220e147933ea837efc589f"
dependencies = [
"proc-macro2",
"quote",
@@ -2269,15 +2277,15 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.74"
version = "0.2.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f"
checksum = "2e0c4a743a309662d45f4ede961d7afa4ba4131a59a639f29b0069c3798bbcc2"
[[package]]
name = "web-sys"
version = "0.3.51"
version = "0.3.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582"
checksum = "01c70a82d842c9979078c772d4a1344685045f1a5628f677c2b2eab4dd7d2696"
dependencies = [
"js-sys",
"wasm-bindgen",

View File

@@ -1,6 +1,6 @@
[package]
name = "feroxbuster"
version = "2.2.5"
version = "2.3.2"
authors = ["Ben 'epi' Risher <epibar052@gmail.com>"]
license = "MIT"
edition = "2018"
@@ -22,17 +22,17 @@ lazy_static = "1.4"
dirs = "3.0"
[dependencies]
futures = { version = "0.3.14"}
tokio = { version = "1.6", features = ["full"] }
futures = { version = "0.3"}
tokio = { version = "1.9", features = ["full"] }
tokio-util = {version = "0.6.6", features = ["codec"]}
log = "0.4"
env_logger = "0.8"
env_logger = "0.9"
reqwest = { version = "0.11", features = ["socks"] }
clap = "2.33"
lazy_static = "1.4"
toml = "0.5"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0.64"
serde_json = "1.0"
uuid = { version = "0.8", features = ["v4"] }
indicatif = "0.15"
console = "0.14"
@@ -40,7 +40,7 @@ openssl = { version = "0.10", features = ["vendored"] }
dirs = "3.0"
regex = "1"
crossterm = "0.20"
rlimit = "0.5.4"
rlimit = "0.6"
ctrlc = "3.1.9"
fuzzyhash = "0.2.1"
anyhow = "1.0"
@@ -50,7 +50,7 @@ leaky-bucket = "0.10.0"
tempfile = "3.1"
httpmock = "0.5.8"
assert_cmd = "1.0"
predicates = "1.0.8"
predicates = "2.0"
[profile.release]
lto = true

182
README.md
View File

@@ -107,6 +107,7 @@ Enumeration.
- [Silence all Output or Be Kinda Quiet (new in `v2.0.0`)](#silence-all-output-or-be-kinda-quiet-new-in-v200)
- [Auto-tune or Auto-bail from Scans (new in `v2.1.0`)](#auto-tune-or-auto-bail-from-scans-new-in-v210)
- [Run Scans in Parallel (new in `v2.2.0`)](#run-scans-in-parallel-new-in-v220)
- [Prevent Specific Domain/Directory Scans aka a Deny List (new in `v2.3.0`)](#prevent-specific-domaindirectory-scans-aka-a-deny-list-new-in-v230)
- [Comparison w/ Similar Tools](#-comparison-w-similar-tools)
- [Common Problems/Issues (FAQ)](#-common-problemsissues-faq)
- [No file descriptors available](#no-file-descriptors-available)
@@ -303,7 +304,7 @@ Configuration begins with with the following built-in default values baked into
- verbosity: `0` (no logging enabled)
- scan_limit: `0` (no limit imposed on concurrent scans)
- rate_limit: `0` (no limit imposed on requests per second)
- status_codes: `200 204 301 302 307 308 401 403 405`
- status_codes: `200 204 301 302 307 308 401 403 405 500`
- user_agent: `feroxbuster/VERSION`
- recursion depth: `4`
- auto-filter wildcards - `true`
@@ -416,6 +417,7 @@ A pre-made configuration file with examples of all available settings can be fou
# dont_filter = true
# extract_links = true
# depth = 1
# url_denylist = ["https://dont-scan-me.com/"]
# filter_size = [5174]
# filter_regex = ["^ignore me$"]
# filter_similar = ["https://somesite.com/soft404"]
@@ -449,49 +451,95 @@ USAGE:
feroxbuster [FLAGS] [OPTIONS] --url <URL>...
FLAGS:
-f, --add-slash Append / to each request
--auto-bail Automatically stop scanning when an excessive amount of errors are encountered
--auto-tune Automatically lower scan rate when an excessive amount of errors are encountered
-D, --dont-filter Don't auto-filter wildcard responses
-e, --extract-links Extract links from response body (html, javascript, etc...); make new requests based on
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 Hide progress bars and banner (good for tmux windows w/ notifications)
-r, --redirects Follow redirects
--silent Only print URLs + turn off logging (good for piping a list of urls to other commands)
--stdin Read url(s) from STDIN
-V, --version Prints version information
-v, --verbosity Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v's is probably
too much)
-f, --add-slash
Append / to each request
--auto-bail
Automatically stop scanning when an excessive amount of errors are encountered
--auto-tune
Automatically lower scan rate when an excessive amount of errors are encountered
-D, --dont-filter
Don't auto-filter wildcard responses
-e, --extract-links
Extract links from response body (html, javascript, etc...); make new requests based on 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
Hide progress bars and banner (good for tmux windows w/ notifications)
-r, --redirects
Follow redirects
--silent
Only print URLs + turn off logging (good for piping a list of urls to other commands)
--stdin
Read url(s) from STDIN
-V, --version
Prints version information
-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)
--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)
-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)
-X, --filter-regex <REGEX>...
Filter out messages via regular expression matching on the response's body (ex: -X '^ignore me$')
--filter-similar-to <UNWANTED_PAGE>...
Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)
-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 (use w/ --json for JSON entries)
-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 (use w/ --json for JSON entries)
--parallel <PARALLEL_SCANS>
Run parallel feroxbuster instances (one child process per url passed via stdin)
-p, --proxy <PROXY>
Proxy to use for requests (ex: http(s)://host:port, socks5(h)://host:port)
-Q, --query <QUERY>... Specify URL query parameters (ex: -Q token=stuff -Q secret=key)
-Q, --query <QUERY>...
Specify URL query parameters (ex: -Q token=stuff -Q secret=key)
--rate-limit <RATE_LIMIT>
Limit number of requests per second (per directory) (default: 0, i.e. no limit)
@@ -504,16 +552,32 @@ OPTIONS:
--resume-from <STATE_FILE>
State file from which to resume a partially complete scan (ex. --resume-from ferox-1606586780.state)
-L, --scan-limit <SCAN_LIMIT> Limit total number of concurrent scans (default: 0, i.e. no limit)
-s, --status-codes <STATUS_CODE>...
Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)
-L, --scan-limit <SCAN_LIMIT>
Limit total number of concurrent scans (default: 0, i.e. no limit)
-t, --threads <THREADS> Number of concurrent threads (default: 50)
--time-limit <TIME_SPEC> Limit total run time of all scans (ex: --time-limit 10m)
-T, --timeout <SECONDS> Number of seconds before a request times out (default: 7)
-u, --url <URL>... The target URL(s) (required, unless --stdin used)
-a, --user-agent <USER_AGENT> Sets the User-Agent (default: feroxbuster/VERSION)
-w, --wordlist <FILE> Path to the wordlist
-s, --status-codes <STATUS_CODE>...
Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405 500)
-t, --threads <THREADS>
Number of concurrent threads (default: 50)
--time-limit <TIME_SPEC>
Limit total run time of all scans (ex: --time-limit 10m)
-T, --timeout <SECONDS>
Number of seconds before a request times out (default: 7)
-u, --url <URL>...
The target URL(s) (required, unless --stdin used)
--dont-scan <URL>...
URL(s) to exclude from recursion/scans
-a, --user-agent <USER_AGENT>
Sets the User-Agent (default: feroxbuster/VERSION)
-w, --wordlist <FILE>
Path to the wordlist
```
## 📊 Scan's Display Explained
@@ -937,7 +1001,7 @@ Example Command:
cat large-target-list | ./feroxbuster --stdin --parallel 10 --extract-links --auto-bail
```
Resuling Process List (illustrative):
Resulting Process List (illustrative):
```
feroxbuster --stdin --parallel 10
\_ feroxbuster --silent --extract-links --auto-bail -u https://target-one
@@ -947,6 +1011,49 @@ feroxbuster --stdin --parallel 10
\_ feroxbuster --silent --extract-links --auto-bail -u https://target-ten
```
As of `v2.3.2`, logging while using `--parallel` uses the value of `-o`|`--output` as a seed to create a directory named `OUTPUT_VALUE-TIMESTAMP.logs/`. Within the directory, an individual log file is created for each target passed over stdin.
Example Command:
```
cat large-target-list | ./feroxbuster --stdin --parallel 10 --output super-cool-mega-scan
```
Resulting directory structure (illustrative):
```
super-cool-mega-scan-1627865696.logs/
├── ferox-https_target_one_com-1627865696.log
├── ...
└── ferox-https_target_two_net-1627865696.log
```
### Prevent Specific Domain/Directory Scans aka a Deny List (new in `v2.3.0`)
> This action is taken BEFORE a request is sent to the target, which differs from the filter-* options that are applied to responses
Version 2.3.0 introduces the `--dont-scan` option. The values passed to `--dont-scan` act as a deny-list. The values
can be an entire domain (`http://some.domain`), a specific folder (`http://some.domain/js`), or a specific file
(`http://some.domain/some-application/stupid-page.php`) If a folder/domain is used any sub-folder/sub-file of the
url passed to `--dont-scan` will be blocked before it can be requested.
For example, given the command
```
./feroxbuster -u http://some.domain --dont-scan http://some.domain/js
```
`http://some.domain` will be scanned recursively, but any url path that begins with `/js/` will not be requested at all.
A caveat to the sub-folder/sub-file rule is when the value passed to `--dont-scan` is a parent of the scan you want to
perform. When denying at a hierarchical level higher than your scan, only sub-files/sub-folders of your `-u|--stdin`
value(s) will be processed.
```
./feroxbuster -u http://some.domain/some-application --dont-scan http://some.domain/
```
In the command above, only `http://some.domain/some-application` and children of that directory found via recursion will
be scanned. Anything 'outside' of `/some-application` will not be scanned.
## 🧐 Comparison w/ Similar Tools
There are quite a few similar tools for forced browsing/content discovery. Burp Suite Pro, Dirb, Dirbuster, etc...
@@ -997,6 +1104,7 @@ few of the use-cases in which feroxbuster may be a better fit:
| automatically tune scans based on errors/403s/429s (`v2.1.0`) | ✔ | | |
| automatically stop scans based on errors/403s/429s (`v2.1.0`) | ✔ | | ✔ |
| run scans in parallel (1 process per target) (`v2.2.0`) | ✔ | | |
| prevent requests to given domain/folder/file (`v2.3.0`) | ✔ | | |
| **huge** number of other options | | | ✔ |
Of note, there's another written-in-rust content discovery tool, [rustbuster](https://github.com/phra/rustbuster). I

View File

@@ -30,6 +30,7 @@
# redirects = true
# insecure = true
# extensions = ["php", "html"]
# url_denylist = ["http://dont-scan.me", "https://also-not.me"]
# no_recursion = true
# add_slash = true
# stdin = true

View File

@@ -41,6 +41,7 @@ _feroxbuster() {
'--user-agent=[Sets the User-Agent (default: feroxbuster/VERSION)]' \
'*-x+[File extension(s) to search for (ex: -x php -x pdf js)]' \
'*--extensions=[File extension(s) to search for (ex: -x php -x pdf js)]' \
'*--dont-scan=[URL(s) to exclude from recursion/scans]' \
'*-H+[Specify HTTP headers (ex: -H Header:val '\''stuff: things'\'')]' \
'*--headers=[Specify HTTP headers (ex: -H Header:val '\''stuff: things'\'')]' \
'*-Q+[Specify URL query parameters (ex: -Q token=stuff -Q secret=key)]' \

View File

@@ -46,6 +46,7 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/VERSION)')
[CompletionResult]::new('-x', 'x', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)')
[CompletionResult]::new('--extensions', 'extensions', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)')
[CompletionResult]::new('--dont-scan', 'dont-scan', [CompletionResultType]::ParameterName, 'URL(s) to exclude from recursion/scans')
[CompletionResult]::new('-H', 'H', [CompletionResultType]::ParameterName, 'Specify HTTP headers (ex: -H Header:val ''stuff: things'')')
[CompletionResult]::new('--headers', 'headers', [CompletionResultType]::ParameterName, 'Specify HTTP headers (ex: -H Header:val ''stuff: things'')')
[CompletionResult]::new('-Q', 'Q', [CompletionResultType]::ParameterName, 'Specify URL query parameters (ex: -Q token=stuff -Q secret=key)')

View File

@@ -20,7 +20,7 @@ _feroxbuster() {
case "${cmd}" in
feroxbuster)
opts=" -v -q -D -r -k -n -f -e -h -V -w -u -t -d -T -p -P -R -s -o -a -x -H -Q -S -X -W -N -C -L --verbosity --silent --quiet --auto-tune --auto-bail --json --dont-filter --redirects --insecure --no-recursion --add-slash --stdin --extract-links --help --version --wordlist --url --threads --depth --timeout --proxy --replay-proxy --replay-codes --status-codes --output --resume-from --debug-log --user-agent --extensions --headers --query --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --scan-limit --parallel --rate-limit --time-limit "
opts=" -v -q -D -r -k -n -f -e -h -V -w -u -t -d -T -p -P -R -s -o -a -x -H -Q -S -X -W -N -C -L --verbosity --silent --quiet --auto-tune --auto-bail --json --dont-filter --redirects --insecure --no-recursion --add-slash --stdin --extract-links --help --version --wordlist --url --threads --depth --timeout --proxy --replay-proxy --replay-codes --status-codes --output --resume-from --debug-log --user-agent --extensions --dont-scan --headers --query --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --scan-limit --parallel --rate-limit --time-limit "
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
@@ -131,6 +131,10 @@ _feroxbuster() {
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--dont-scan)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--headers)
COMPREPLY=($(compgen -f "${cur}"))
return 0

View File

@@ -12,6 +12,7 @@ complete -c feroxbuster -n "__fish_use_subcommand" -l resume-from -d 'State file
complete -c feroxbuster -n "__fish_use_subcommand" -l debug-log -d 'Output file to write log entries (use w/ --json for JSON entries)'
complete -c feroxbuster -n "__fish_use_subcommand" -s a -l user-agent -d 'Sets the User-Agent (default: feroxbuster/VERSION)'
complete -c feroxbuster -n "__fish_use_subcommand" -s x -l extensions -d 'File extension(s) to search for (ex: -x php -x pdf js)'
complete -c feroxbuster -n "__fish_use_subcommand" -l dont-scan -d 'URL(s) to exclude from recursion/scans'
complete -c feroxbuster -n "__fish_use_subcommand" -s H -l headers -d 'Specify HTTP headers (ex: -H Header:val \'stuff: things\')'
complete -c feroxbuster -n "__fish_use_subcommand" -s Q -l query -d 'Specify URL query parameters (ex: -Q token=stuff -Q secret=key)'
complete -c feroxbuster -n "__fish_use_subcommand" -s S -l filter-size -d 'Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)'

View File

@@ -134,6 +134,9 @@ pub struct Banner {
/// represents Configuration.auto_bail
auto_bail: BannerEntry,
/// represents Configuration.url_denylist
url_denylist: Vec<BannerEntry>,
/// current version of feroxbuster
pub(super) version: String,
@@ -146,6 +149,7 @@ impl Banner {
/// Create a new Banner from a Configuration and live targets
pub fn new(tgts: &[String], config: &Configuration) -> Self {
let mut targets = Vec::new();
let mut url_denylist = Vec::new();
let mut code_filters = Vec::new();
let mut replay_codes = Vec::new();
let mut headers = Vec::new();
@@ -160,6 +164,10 @@ impl Banner {
targets.push(BannerEntry::new("🎯", "Target Url", target));
}
for denied_url in &config.url_denylist {
url_denylist.push(BannerEntry::new("🚫", "Don't Scan", denied_url));
}
let mut codes = vec![];
for code in &config.status_codes {
codes.push(status_colorizer(&code.to_string()))
@@ -323,6 +331,7 @@ impl Banner {
rate_limit,
scan_limit,
time_limit,
url_denylist,
config: cfg,
version: VERSION.to_string(),
update_status: UpdateStatus::Unknown,
@@ -413,6 +422,10 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(&mut writer, "{}", target)?;
}
for denied_url in &self.url_denylist {
writeln!(&mut writer, "{}", denied_url)?;
}
writeln!(&mut writer, "{}", self.threads)?;
writeln!(&mut writer, "{}", self.wordlist)?;
writeln!(&mut writer, "{}", self.status_codes)?;

View File

@@ -248,6 +248,10 @@ pub struct Configuration {
/// Filter out response bodies that meet a certain threshold of similarity
#[serde(default)]
pub filter_similar: Vec<String>,
/// URLs that should never be scanned/recursed into
#[serde(default)]
pub url_denylist: Vec<String>,
}
impl Default for Configuration {
@@ -304,6 +308,7 @@ impl Default for Configuration {
extensions: Vec::new(),
filter_size: Vec::new(),
filter_regex: Vec::new(),
url_denylist: Vec::new(),
filter_line_count: Vec::new(),
filter_word_count: Vec::new(),
filter_status: Vec::new(),
@@ -341,6 +346,7 @@ impl Configuration {
/// - **user_agent**: `feroxbuster/VERSION`
/// - **insecure**: `false` (don't be insecure, i.e. don't allow invalid certs)
/// - **extensions**: `None`
/// - **url_denylist**: `None`
/// - **filter_size**: `None`
/// - **filter_similar**: `None`
/// - **filter_regex**: `None`
@@ -538,6 +544,10 @@ impl Configuration {
config.extensions = arg.map(|val| val.to_string()).collect();
}
if let Some(arg) = args.values_of("url_denylist") {
config.url_denylist = arg.map(|val| val.to_string()).collect();
}
if let Some(arg) = args.values_of("filter_regex") {
config.filter_regex = arg.map(|val| val.to_string()).collect();
}
@@ -767,6 +777,11 @@ impl Configuration {
update_if_not_default!(&mut conf.insecure, new.insecure, false);
update_if_not_default!(&mut conf.extract_links, new.extract_links, false);
update_if_not_default!(&mut conf.extensions, new.extensions, Vec::<String>::new());
update_if_not_default!(
&mut conf.url_denylist,
new.url_denylist,
Vec::<String>::new()
);
update_if_not_default!(&mut conf.headers, new.headers, HashMap::new());
update_if_not_default!(&mut conf.queries, new.queries, Vec::new());
update_if_not_default!(&mut conf.no_recursion, new.no_recursion, false);

View File

@@ -29,6 +29,7 @@ fn setup_config_test() -> Configuration {
redirects = true
insecure = true
extensions = ["html", "php", "js"]
url_denylist = ["http://dont-scan.me", "https://also-not.me"]
headers = {stuff = "things", mostuff = "mothings"}
queries = [["name","value"], ["rick", "astley"]]
no_recursion = true
@@ -72,24 +73,25 @@ fn default_configuration() {
assert_eq!(config.timeout, timeout());
assert_eq!(config.verbosity, 0);
assert_eq!(config.scan_limit, 0);
assert_eq!(config.silent, false);
assert_eq!(config.quiet, false);
assert!(!config.silent);
assert!(!config.quiet);
assert_eq!(config.output_level, OutputLevel::Default);
assert_eq!(config.dont_filter, false);
assert_eq!(config.auto_tune, false);
assert_eq!(config.auto_bail, false);
assert!(!config.dont_filter);
assert!(!config.auto_tune);
assert!(!config.auto_bail);
assert_eq!(config.requester_policy, RequesterPolicy::Default);
assert_eq!(config.no_recursion, false);
assert_eq!(config.json, false);
assert_eq!(config.save_state, true);
assert_eq!(config.stdin, false);
assert_eq!(config.add_slash, false);
assert_eq!(config.redirects, false);
assert_eq!(config.extract_links, false);
assert_eq!(config.insecure, false);
assert!(!config.no_recursion);
assert!(!config.json);
assert!(config.save_state);
assert!(!config.stdin);
assert!(!config.add_slash);
assert!(!config.redirects);
assert!(!config.extract_links);
assert!(!config.insecure);
assert_eq!(config.queries, Vec::new());
assert_eq!(config.extensions, Vec::<String>::new());
assert_eq!(config.filter_size, Vec::<u64>::new());
assert_eq!(config.extensions, Vec::<String>::new());
assert_eq!(config.url_denylist, Vec::<String>::new());
assert_eq!(config.filter_regex, Vec::<String>::new());
assert_eq!(config.filter_similar, Vec::<String>::new());
assert_eq!(config.filter_word_count, Vec::<usize>::new());
@@ -186,35 +188,35 @@ fn config_reads_replay_proxy() {
/// parse the test config and see that the value parsed is correct
fn config_reads_silent() {
let config = setup_config_test();
assert_eq!(config.silent, true);
assert!(config.silent);
}
#[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);
assert!(config.quiet);
}
#[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);
assert!(config.json);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_auto_bail() {
let config = setup_config_test();
assert_eq!(config.auto_bail, true);
assert!(config.auto_bail);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_auto_tune() {
let config = setup_config_test();
assert_eq!(config.auto_tune, true);
assert!(config.auto_tune);
}
#[test]
@@ -235,49 +237,49 @@ fn config_reads_output() {
/// 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);
assert!(config.redirects);
}
#[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);
assert!(config.insecure);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_no_recursion() {
let config = setup_config_test();
assert_eq!(config.no_recursion, true);
assert!(config.no_recursion);
}
#[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);
assert!(config.stdin);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_dont_filter() {
let config = setup_config_test();
assert_eq!(config.dont_filter, true);
assert!(config.dont_filter);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_add_slash() {
let config = setup_config_test();
assert_eq!(config.add_slash, true);
assert!(config.add_slash);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_extract_links() {
let config = setup_config_test();
assert_eq!(config.extract_links, true);
assert!(config.extract_links);
}
#[test]
@@ -287,6 +289,16 @@ fn config_reads_extensions() {
assert_eq!(config.extensions, vec!["html", "php", "js"]);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_url_denylist() {
let config = setup_config_test();
assert_eq!(
config.url_denylist,
vec!["http://dont-scan.me", "https://also-not.me"]
);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_filter_regex() {
@@ -333,7 +345,7 @@ fn config_reads_filter_status() {
/// parse the test config and see that the value parsed is correct
fn config_reads_save_state() {
let config = setup_config_test();
assert_eq!(config.save_state, false);
assert!(!config.save_state);
}
#[test]

View File

@@ -4,6 +4,7 @@ use crate::{
scan_manager::{FeroxState, PAUSE_SCAN},
scanner::RESPONSES,
statistics::StatError,
utils::slugify_filename,
utils::{open_file, write_to},
SLEEP_DURATION,
};
@@ -17,7 +18,6 @@ use std::{
},
thread::sleep,
time::Duration,
time::{SystemTime, UNIX_EPOCH},
};
/// Atomic boolean flag, used to determine whether or not the terminal input handler should exit
@@ -77,22 +77,14 @@ impl TermInputHandler {
pub fn sigint_handler(handles: Arc<Handles>) -> Result<()> {
log::trace!("enter: sigint_handler({:?})", handles);
let ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
let slug = if !handles.config.target_url.is_empty() {
let filename = if !handles.config.target_url.is_empty() {
// target url populated
handles
.config
.target_url
.replace("://", "_")
.replace("/", "_")
.replace(".", "_")
slugify_filename(&handles.config.target_url, "ferox", "state")
} else {
// stdin used
"stdin".to_string()
slugify_filename("stdin", "ferox", "state")
};
let filename = format!("ferox-{}-{}.state", slug, ts);
let warning = format!(
"🚨 Caught {} 🚨 saving scan state to {} ...",
style("ctrl+c").yellow(),

View File

@@ -213,7 +213,7 @@ impl TermOutHandler {
// should be replayed; not using logged_request due to replay proxy client
make_request(
self.config.replay_client.as_ref().unwrap(),
&resp.url(),
resp.url(),
self.config.output_level,
tx_stats.clone(),
)

View File

@@ -3,17 +3,19 @@ use std::sync::Arc;
use anyhow::{bail, Result};
use tokio::sync::{mpsc, Semaphore};
use crate::response::FeroxResponse;
use crate::url::FeroxUrl;
use crate::{
response::FeroxResponse,
scan_manager::{FeroxScan, FeroxScans, ScanOrder},
scanner::FeroxScanner,
statistics::StatField::TotalScans,
url::FeroxUrl,
utils::should_deny_url,
CommandReceiver, CommandSender, FeroxChannel, Joiner, SLEEP_DURATION,
};
use super::command::Command::AddToUsizeField;
use super::*;
use reqwest::Url;
use tokio::time::Duration;
#[derive(Debug)]
@@ -187,6 +189,7 @@ impl ScanHandler {
/// wrapper around scanning a url to stay DRY
async fn ordered_scan_url(&mut self, targets: Vec<String>, order: ScanOrder) -> Result<()> {
log::trace!("enter: ordered_scan_url({:?}, {:?})", targets, order);
let should_test_deny = !self.handles.config.url_denylist.is_empty();
for target in targets {
if self.data.contains(&target) && matches!(order, ScanOrder::Latest) {
@@ -203,6 +206,13 @@ impl ScanHandler {
self.data.add_directory_scan(&target, order).1 // add the new target; return FeroxScan
};
if should_test_deny && should_deny_url(&Url::parse(&target)?, self.handles.clone())? {
// response was caught by a user-provided deny list
// checking this last, since it's most susceptible to longer runtimes due to what
// input is received
continue;
}
let list = self.get_wordlist()?;
log::info!("scan handler received {} - beginning scan", target);
@@ -243,6 +253,11 @@ impl ScanHandler {
async fn try_recursion(&mut self, response: Box<FeroxResponse>) -> Result<()> {
log::trace!("enter: try_recursion({:?})", response,);
if !response.is_directory() {
// not a directory, quick exit
return Ok(());
}
let mut base_depth = 1_usize;
for (base_url, base_url_depth) in &self.depths {
@@ -256,11 +271,6 @@ impl ScanHandler {
return Ok(());
}
if !response.is_directory() {
// not a directory
return Ok(());
}
let targets = vec![response.url().to_string()];
self.ordered_scan_url(targets, ScanOrder::Latest).await?;

View File

@@ -1,4 +1,5 @@
use super::*;
use crate::utils::should_deny_url;
use crate::{
client,
event_handlers::{
@@ -53,13 +54,19 @@ pub struct Extractor<'a> {
/// Extractor implementation
impl<'a> Extractor<'a> {
/// business logic that handles getting links from a normal http body response
pub async fn extract(&self) -> Result<()> {
let links = match self.target {
ExtractionTarget::ResponseBody => self.extract_from_body().await?,
ExtractionTarget::RobotsTxt => self.extract_from_robots().await?,
};
/// perform extraction from the given target and return any links found
pub async fn extract(&self) -> Result<HashSet<String>> {
log::trace!("enter: extract (this fn has associated trace exit msg)");
match self.target {
ExtractionTarget::ResponseBody => Ok(self.extract_from_body().await?),
ExtractionTarget::RobotsTxt => Ok(self.extract_from_robots().await?),
}
}
/// given a set of links from a normal http body response, task the request handler to make
/// the requests
pub async fn request_links(&self, links: HashSet<String>) -> Result<()> {
log::trace!("enter: request_links({:?})", links);
let recursive = if self.handles.config.no_recursion {
RecursionStatus::NotRecursive
} else {
@@ -121,6 +128,7 @@ impl<'a> Extractor<'a> {
rx.await?;
}
}
log::trace!("exit: request_links");
Ok(())
}
@@ -141,7 +149,7 @@ impl<'a> Extractor<'a> {
let body = self.response.unwrap().text();
for capture in self.links_regex.captures_iter(&body) {
for capture in self.links_regex.captures_iter(body) {
// remove single & double quotes from both ends of the capture
// capture[0] is the entire match, additional capture groups start at [1]
let link = capture[0].trim_matches(|c| c == '\'' || c == '"');
@@ -267,7 +275,7 @@ impl<'a> Extractor<'a> {
};
let new_url = old_url
.join(&link)
.join(link)
.with_context(|| format!("Could not join {} with {}", old_url, link))?;
links.insert(new_url.to_string());
@@ -289,10 +297,10 @@ impl<'a> Extractor<'a> {
pub(super) async fn request_link(&self, url: &str) -> Result<FeroxResponse> {
log::trace!("enter: request_link({})", url);
let ferox_url = FeroxUrl::from_string(&url, self.handles.clone());
let ferox_url = FeroxUrl::from_string(url, self.handles.clone());
// create a url based on the given command line options
let new_url = ferox_url.format(&"", None)?;
let new_url = ferox_url.format("", None)?;
let scanned_urls = self.handles.ferox_scans()?;
@@ -302,6 +310,17 @@ impl<'a> Extractor<'a> {
bail!("previously seen url");
}
if !self.handles.config.url_denylist.is_empty()
&& should_deny_url(&new_url, self.handles.clone())?
{
// can't allow a denied url to be requested
bail!(
"prevented request to {} due to {:?}",
url,
self.handles.config.url_denylist
);
}
// make the request and store the response
let new_response = logged_request(&new_url, self.handles.clone()).await?;
@@ -332,7 +351,7 @@ impl<'a> Extractor<'a> {
if let Some(new_path) = capture.name("url_path") {
let mut new_url = Url::parse(&self.url)?;
new_url.set_path(new_path.as_str());
if self.add_all_sub_paths(&new_url.path(), &mut links).is_err() {
if self.add_all_sub_paths(new_url.path(), &mut links).is_err() {
log::warn!("could not add sub-paths from {} to {:?}", new_url, links);
}
}
@@ -391,7 +410,7 @@ impl<'a> Extractor<'a> {
FeroxResponse::from(response, true, self.handles.config.output_level).await;
log::trace!("exit: get_robots_file -> {}", ferox_response);
return Ok(ferox_response);
Ok(ferox_response)
}
/// update total number of links extracted and expected responses

View File

@@ -54,8 +54,8 @@ fn setup_extractor(target: ExtractionTarget, scanned_urls: Arc<FeroxScans>) -> E
/// in the expected array
fn extractor_get_sub_paths_from_path_with_multiple_paths() {
let path = "homepage/assets/img/icons/handshake.svg";
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(&path);
let b_paths = BODY_EXT.get_sub_paths_from_path(&path);
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(path);
let b_paths = BODY_EXT.get_sub_paths_from_path(path);
let expected = vec![
"homepage/",
"homepage/assets/",
@@ -67,8 +67,8 @@ fn extractor_get_sub_paths_from_path_with_multiple_paths() {
assert_eq!(r_paths.len(), expected.len());
assert_eq!(b_paths.len(), expected.len());
for expected_path in expected {
assert_eq!(r_paths.contains(&expected_path.to_string()), true);
assert_eq!(b_paths.contains(&expected_path.to_string()), true);
assert!(r_paths.contains(&expected_path.to_string()));
assert!(b_paths.contains(&expected_path.to_string()));
}
}
@@ -78,15 +78,15 @@ fn extractor_get_sub_paths_from_path_with_multiple_paths() {
/// returned
fn extractor_get_sub_paths_from_path_with_enclosing_slashes() {
let path = "/homepage/assets/";
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(&path);
let b_paths = BODY_EXT.get_sub_paths_from_path(&path);
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(path);
let b_paths = BODY_EXT.get_sub_paths_from_path(path);
let expected = vec!["homepage/", "homepage/assets"];
assert_eq!(r_paths.len(), expected.len());
assert_eq!(b_paths.len(), expected.len());
for expected_path in expected {
assert_eq!(r_paths.contains(&expected_path.to_string()), true);
assert_eq!(b_paths.contains(&expected_path.to_string()), true);
assert!(r_paths.contains(&expected_path.to_string()));
assert!(b_paths.contains(&expected_path.to_string()));
}
}
@@ -95,15 +95,15 @@ fn extractor_get_sub_paths_from_path_with_enclosing_slashes() {
/// included
fn extractor_get_sub_paths_from_path_with_only_a_word() {
let path = "homepage";
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(&path);
let b_paths = BODY_EXT.get_sub_paths_from_path(&path);
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(path);
let b_paths = BODY_EXT.get_sub_paths_from_path(path);
let expected = vec!["homepage"];
assert_eq!(r_paths.len(), expected.len());
assert_eq!(b_paths.len(), expected.len());
for expected_path in expected {
assert_eq!(r_paths.contains(&expected_path.to_string()), true);
assert_eq!(b_paths.contains(&expected_path.to_string()), true);
assert!(r_paths.contains(&expected_path.to_string()));
assert!(b_paths.contains(&expected_path.to_string()));
}
}
@@ -111,15 +111,15 @@ fn extractor_get_sub_paths_from_path_with_only_a_word() {
/// extract sub paths from the given url fragment; expect 1 sub path, forward slash removed
fn extractor_get_sub_paths_from_path_with_an_absolute_word() {
let path = "/homepage";
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(&path);
let b_paths = BODY_EXT.get_sub_paths_from_path(&path);
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(path);
let b_paths = BODY_EXT.get_sub_paths_from_path(path);
let expected = vec!["homepage"];
assert_eq!(r_paths.len(), expected.len());
assert_eq!(b_paths.len(), expected.len());
for expected_path in expected {
assert_eq!(r_paths.contains(&expected_path.to_string()), true);
assert_eq!(b_paths.contains(&expected_path.to_string()), true);
assert!(r_paths.contains(&expected_path.to_string()));
assert!(b_paths.contains(&expected_path.to_string()));
}
}

View File

@@ -41,7 +41,7 @@ impl FeroxFilters {
if let Ok(filters) = self.filters.lock() {
for filter in filters.iter() {
// wildcard.should_filter goes here
if filter.should_filter_response(&response) {
if filter.should_filter_response(response) {
if filter.as_any().downcast_ref::<WildcardFilter>().is_some() {
tx_stats
.send(AddToUsizeField(WildcardsFiltered, 1))

View File

@@ -56,7 +56,7 @@ pub async fn initialize(handles: Arc<Handles>) -> Result<()> {
// add any regex filters to filters handler's FeroxFilters (-X|--filter-regex)
for regex_filter in &handles.config.filter_regex {
let raw = regex_filter;
let compiled = skip_fail!(Regex::new(&raw));
let compiled = skip_fail!(Regex::new(raw));
let filter = RegexFilter {
raw_string: raw.to_owned(),
@@ -69,7 +69,7 @@ pub async fn initialize(handles: Arc<Handles>) -> Result<()> {
// add any similarity filters to filters handler's FeroxFilters (--filter-similar-to)
for similarity_filter in &handles.config.filter_similar {
// url as-is based on input, ignores user-specified url manipulation options (add-slash etc)
let url = skip_fail!(Url::parse(&similarity_filter));
let url = skip_fail!(Url::parse(similarity_filter));
// attempt to request the given url
let resp = skip_fail!(logged_request(&url, handles.clone()).await);

View File

@@ -76,7 +76,7 @@ impl FeroxFilter for WildcardFilter {
// except that I don't want an empty string taking up the last index in the
// event that the url ends with a forward slash. It's ugly enough to be split
// into its own function for readability.
let url_len = FeroxUrl::path_length_of_url(&response.url());
let url_len = FeroxUrl::path_length_of_url(response.url());
if url_len + self.dynamic == response.content_length() {
log::debug!("dynamic wildcard: filtered out {}", response.url());

View File

@@ -207,7 +207,7 @@ impl HeuristicTests {
let mut good_urls = vec![];
for target_url in target_urls {
let url = FeroxUrl::from_string(&target_url, self.handles.clone());
let url = FeroxUrl::from_string(target_url, self.handles.clone());
let request = skip_fail!(url.format("", None));
let result = logged_request(&request, self.handles.clone()).await;

View File

@@ -43,7 +43,7 @@ pub(crate) type FeroxChannel<T> = (UnboundedSender<T>, UnboundedReceiver<T>);
pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION");
/// Maximum number of file descriptors that can be opened during a scan
pub const DEFAULT_OPEN_FILE_LIMIT: usize = 8192;
pub const DEFAULT_OPEN_FILE_LIMIT: u64 = 8192;
/// Default value used to determine near-duplicate web pages (equivalent to 95%)
pub const SIMILARITY_THRESHOLD: u32 = 95;
@@ -73,7 +73,8 @@ pub const HIGH_ERROR_RATIO: f64 = 0.90;
/// * 401 Unauthorized
/// * 403 Forbidden
/// * 405 Method Not Allowed
pub const DEFAULT_STATUS_CODES: [StatusCode; 9] = [
/// * 500 Internal Server Error
pub const DEFAULT_STATUS_CODES: [StatusCode; 10] = [
StatusCode::OK,
StatusCode::NO_CONTENT,
StatusCode::MOVED_PERMANENTLY,
@@ -83,6 +84,7 @@ pub const DEFAULT_STATUS_CODES: [StatusCode; 9] = [
StatusCode::UNAUTHORIZED,
StatusCode::FORBIDDEN,
StatusCode::METHOD_NOT_ALLOWED,
StatusCode::INTERNAL_SERVER_ERROR,
];
/// Default filename for config file settings

View File

@@ -1,8 +1,9 @@
use std::{
env::args,
fs::File,
fs::{create_dir, remove_file, File},
io::{stderr, BufRead, BufReader},
ops::Index,
path::Path,
process::Command,
sync::{atomic::Ordering, Arc},
};
@@ -27,7 +28,7 @@ use feroxbuster::{
progress::{PROGRESS_BAR, PROGRESS_PRINTER},
scan_manager::{self},
scanner,
utils::fmt_err,
utils::{fmt_err, slugify_filename},
};
#[cfg(not(target_os = "windows"))]
use feroxbuster::{utils::set_open_file_limit, DEFAULT_OPEN_FILE_LIMIT};
@@ -267,10 +268,56 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
// from removing --parallel)
original.remove(parallel_index);
// to log unique files to a shared folder, we need to first check for the presence
// of -o|--output.
let out_dir = if !config.output.is_empty() {
// -o|--output was used, so we'll attempt to create a directory to store the files
let output_path = Path::new(&handles.config.output);
// this only returns None if the path terminates in `..`. Since I don't want to
// hand-hold to that degree, we'll unwrap and fail if the output path ends in `..`
let base_name = output_path.file_name().unwrap();
let new_folder = slugify_filename(&base_name.to_string_lossy(), "", "logs");
let final_path = output_path.with_file_name(&new_folder);
// create the directory or fail silently, assuming the reason for failure is that
// the path exists already
create_dir(&final_path).unwrap_or(());
final_path.to_string_lossy().to_string()
} else {
String::new()
};
// unvalidated targets fresh from stdin, just spawn children and let them do all checks
for target in targets {
// add the current target to the provided command
let mut cloned = original.clone();
if !out_dir.is_empty() {
// output directory value is not empty, need to join output directory with
// unique scan filename
// unwrap is ok, we already know -o was used
let out_idx = original
.iter()
.position(|s| *s == "--output" || *s == "-o")
.unwrap();
let filename = slugify_filename(&target, "ferox", "log");
let full_path = Path::new(&out_dir)
.join(filename)
.to_string_lossy()
.to_string();
// a +1 to the index is fine here, as clap has already validated that
// -o|--output has a value associated with it
cloned[out_idx + 1] = full_path;
}
cloned.push("-u".to_string());
cloned.push(target);
@@ -294,8 +341,22 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
});
}
// the output handler creates an empty file to which it will try to write, because
// this happens before we enter the --parallel branch, we need to remove that file
// if it's empty
let output = handles.config.output.to_owned();
clean_up(handles, tasks).await?;
let file = Path::new(&output);
if file.exists() {
// expectation is that this is always true for the first ferox process
if file.metadata()?.len() == 0 {
// empty file, attempt to remove it
remove_file(file)?;
}
}
log::trace!("exit: parallel branch && wrapped main");
return Ok(());
}

View File

@@ -218,6 +218,17 @@ pub fn initialize() -> App<'static, 'static> {
"File extension(s) to search for (ex: -x php -x pdf js)",
),
)
.arg(
Arg::with_name("url_denylist")
.long("dont-scan")
.value_name("URL")
.takes_value(true)
.multiple(true)
.use_delimiter(true)
.help(
"URL(s) to exclude from recursion/scans",
),
)
.arg(
Arg::with_name("headers")
.short("H")
@@ -421,7 +432,7 @@ EXAMPLES:
if arg == "--help" || arg == "-h" {
app.print_long_help().unwrap();
println!(); // just a newline to mirror original --help output
process::exit(1);
process::exit(0);
}
}

View File

@@ -51,7 +51,7 @@ pub fn add_bar(prefix: &str, length: u64, bar_type: BarType) -> ProgressBar {
progress_bar.set_style(style);
progress_bar.set_prefix(&prefix);
progress_bar.set_prefix(prefix);
progress_bar
}

View File

@@ -121,7 +121,7 @@ impl FeroxResponse {
/// Set `FeroxResponse`'s `url` attribute, has no affect if an error occurs
pub fn set_url(&mut self, url: &str) {
match Url::parse(&url) {
match Url::parse(url) {
Ok(url) => {
self.url = url;
}
@@ -339,7 +339,7 @@ impl FeroxSerialize for FeroxResponse {
lines,
words,
chars,
status_colorizer(&status),
status_colorizer(status),
self.url(),
FeroxUrl::path_length_of_url(&self.url)
);

View File

@@ -36,7 +36,7 @@ pub struct FeroxScan {
pub(super) scan_type: ScanType,
/// The order in which the scan was received
pub(super) scan_order: ScanOrder,
pub(crate) scan_order: ScanOrder,
/// Number of requests to populate the progress bar with
pub(super) num_requests: u64,

View File

@@ -431,7 +431,7 @@ impl FeroxScans {
OutputLevel::Silent => BarType::Hidden,
};
let progress_bar = add_bar(&url, bar_length, bar_type);
let progress_bar = add_bar(url, bar_length, bar_type);
progress_bar.reset_elapsed();
@@ -441,7 +441,7 @@ impl FeroxScans {
};
let ferox_scan = FeroxScan::new(
&url,
url,
scan_type,
scan_order,
bar_length,
@@ -462,7 +462,7 @@ impl FeroxScans {
///
/// Also return a reference to the new `FeroxScan`
pub fn add_directory_scan(&self, url: &str, scan_order: ScanOrder) -> (bool, Arc<FeroxScan>) {
self.add_scan(&url, ScanType::Directory, scan_order)
self.add_scan(url, ScanType::Directory, scan_order)
}
/// Given a url, create a new `FeroxScan` and add it to `FeroxScans` as a File Scan
@@ -471,7 +471,7 @@ impl FeroxScans {
///
/// Also return a reference to the new `FeroxScan`
pub fn add_file_scan(&self, url: &str, scan_order: ScanOrder) -> (bool, Arc<FeroxScan>) {
self.add_scan(&url, ScanType::File, scan_order)
self.add_scan(url, ScanType::File, scan_order)
}
/// small helper to determine whether any scans are active or not

View File

@@ -52,7 +52,7 @@ fn add_url_to_list_of_scanned_urls_with_unknown_url() {
let urls = FeroxScans::default();
let url = "http://unknown_url";
let (result, _scan) = urls.add_scan(url, ScanType::Directory, ScanOrder::Latest);
assert_eq!(result, true);
assert!(result);
}
#[test]
@@ -71,11 +71,11 @@ fn add_url_to_list_of_scanned_urls_with_known_url() {
Some(pb),
);
assert_eq!(urls.insert(scan), true);
assert!(urls.insert(scan));
let (result, _scan) = urls.add_scan(url, ScanType::Directory, ScanOrder::Latest);
assert_eq!(result, false);
assert!(!result);
}
#[test]
@@ -93,27 +93,23 @@ fn stop_progress_bar_stops_bar() {
Some(pb),
);
assert_eq!(
scan.progress_bar
.lock()
.unwrap()
.as_ref()
.unwrap()
.is_finished(),
false
);
assert!(!scan
.progress_bar
.lock()
.unwrap()
.as_ref()
.unwrap()
.is_finished());
scan.stop_progress_bar();
assert_eq!(
scan.progress_bar
.lock()
.unwrap()
.as_ref()
.unwrap()
.is_finished(),
true
);
assert!(scan
.progress_bar
.lock()
.unwrap()
.as_ref()
.unwrap()
.is_finished());
}
#[test]
@@ -131,11 +127,11 @@ fn add_url_to_list_of_scanned_urls_with_known_url_without_slash() {
None,
);
assert_eq!(urls.insert(scan), true);
assert!(urls.insert(scan));
let (result, _scan) = urls.add_scan(url, ScanType::File, ScanOrder::Latest);
assert_eq!(result, false);
assert!(!result);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
@@ -171,8 +167,8 @@ async fn call_display_scans() {
.await
.unwrap();
assert_eq!(urls.insert(scan), true);
assert_eq!(urls.insert(scan_two), true);
assert!(urls.insert(scan));
assert!(urls.insert(scan_two));
urls.display_scans().await;
}
@@ -330,7 +326,7 @@ fn ferox_response_serialize_and_deserialize() {
assert_eq!(response.url().as_str(), "https://nerdcore.com/css");
assert_eq!(response.url().path(), "/css");
assert_eq!(response.wildcard(), true);
assert!(response.wildcard());
assert_eq!(response.status().as_u16(), 301);
assert_eq!(response.content_length(), 173);
assert_eq!(response.line_count(), 10);
@@ -383,7 +379,7 @@ fn feroxstates_feroxserialize_implementation() {
let json_state = ferox_state.as_json().unwrap();
let expected = format!(
r#"{{"scans":[{{"id":"{}","url":"https://spiritanimal.com","scan_type":"Directory","status":"NotStarted","num_requests":0}}],"config":{{"type":"configuration","wordlist":"/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt","config":"","proxy":"","replay_proxy":"","target_url":"","status_codes":[200,204,301,302,307,308,401,403,405],"replay_codes":[200,204,301,302,307,308,401,403,405],"filter_status":[],"threads":50,"timeout":7,"verbosity":0,"silent":false,"quiet":false,"auto_bail":false,"auto_tune":false,"json":false,"output":"","debug_log":"","user_agent":"feroxbuster/{}","redirects":false,"insecure":false,"extensions":[],"headers":{{}},"queries":[],"no_recursion":false,"extract_links":false,"add_slash":false,"stdin":false,"depth":4,"scan_limit":0,"parallel":0,"rate_limit":0,"filter_size":[],"filter_line_count":[],"filter_word_count":[],"filter_regex":[],"dont_filter":false,"resumed":false,"resume_from":"","save_state":false,"time_limit":"","filter_similar":[]}},"responses":[{{"type":"response","url":"https://nerdcore.com/css","path":"/css","wildcard":true,"status":301,"content_length":173,"line_count":10,"word_count":16,"headers":{{"server":"nginx/1.16.1"}}}}]"#,
r#"{{"scans":[{{"id":"{}","url":"https://spiritanimal.com","scan_type":"Directory","status":"NotStarted","num_requests":0}}],"config":{{"type":"configuration","wordlist":"/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt","config":"","proxy":"","replay_proxy":"","target_url":"","status_codes":[200,204,301,302,307,308,401,403,405,500],"replay_codes":[200,204,301,302,307,308,401,403,405,500],"filter_status":[],"threads":50,"timeout":7,"verbosity":0,"silent":false,"quiet":false,"auto_bail":false,"auto_tune":false,"json":false,"output":"","debug_log":"","user_agent":"feroxbuster/{}","redirects":false,"insecure":false,"extensions":[],"headers":{{}},"queries":[],"no_recursion":false,"extract_links":false,"add_slash":false,"stdin":false,"depth":4,"scan_limit":0,"parallel":0,"rate_limit":0,"filter_size":[],"filter_line_count":[],"filter_word_count":[],"filter_regex":[],"dont_filter":false,"resumed":false,"resume_from":"","save_state":false,"time_limit":"","filter_similar":[],"url_denylist":[]}},"responses":[{{"type":"response","url":"https://nerdcore.com/css","path":"/css","wildcard":true,"status":301,"content_length":173,"line_count":10,"word_count":16,"headers":{{"server":"nginx/1.16.1"}}}}]"#,
saved_id, VERSION
);
println!("{}\n{}", expected, json_state);

View File

@@ -83,7 +83,8 @@ impl FeroxScanner {
.target(RobotsTxt)
.build()?;
let _ = extractor.extract().await;
let links = extractor.extract().await?;
extractor.request_links(links).await?;
}
let scanned_urls = self.handles.ferox_scans()?;

View File

@@ -90,11 +90,9 @@ impl PolicyData {
atomic_store!(self.remove_limit, true);
}
}
self.set_limit(heap.value() as usize);
} else if heap.has_children() {
// streak not at 3, just check that we can move down, and do so
heap.move_left();
self.set_limit(heap.value() as usize);
} else {
// tree bottomed out, need to move back up the tree a bit
let current = heap.value();
@@ -104,9 +102,8 @@ impl PolicyData {
if current > heap.value() {
heap.move_up();
}
self.set_limit(heap.value() as usize);
}
self.set_limit(heap.value() as usize);
}
}
@@ -200,7 +197,7 @@ mod tests {
pd.adjust_up(&3);
assert_eq!(pd.heap.read().unwrap().value(), 300);
assert_eq!(pd.limit.load(Ordering::Relaxed), 300);
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), false);
assert!(!pd.remove_limit.load(Ordering::Relaxed));
}
#[test]
@@ -217,7 +214,7 @@ mod tests {
pd.adjust_up(&3);
assert_eq!(pd.heap.read().unwrap().value(), 200);
assert_eq!(pd.limit.load(Ordering::Relaxed), 200);
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), true);
assert!(pd.remove_limit.load(Ordering::Relaxed));
}
#[test]
@@ -234,7 +231,7 @@ mod tests {
pd.adjust_up(&3);
assert_eq!(pd.heap.read().unwrap().value(), 350);
assert_eq!(pd.limit.load(Ordering::Relaxed), 350);
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), false);
assert!(!pd.remove_limit.load(Ordering::Relaxed));
}
#[test]
@@ -251,7 +248,7 @@ mod tests {
pd.adjust_up(&3);
assert_eq!(pd.heap.read().unwrap().value(), 300);
assert_eq!(pd.limit.load(Ordering::Relaxed), 300);
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), false);
assert!(!pd.remove_limit.load(Ordering::Relaxed));
}
#[test]
@@ -269,7 +266,7 @@ mod tests {
pd.adjust_up(&0);
assert_eq!(pd.heap.read().unwrap().value(), 43);
assert_eq!(pd.limit.load(Ordering::Relaxed), 43);
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), false);
assert!(!pd.remove_limit.load(Ordering::Relaxed));
}
#[test]
@@ -287,7 +284,7 @@ mod tests {
pd.adjust_up(&0);
assert_eq!(pd.heap.read().unwrap().value(), 37);
assert_eq!(pd.limit.load(Ordering::Relaxed), 37);
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), false);
assert!(!pd.remove_limit.load(Ordering::Relaxed));
}
#[test]

View File

@@ -27,6 +27,8 @@ use crate::{
};
use super::{policy_data::PolicyData, FeroxScanner, PolicyTrigger};
use crate::utils::should_deny_url;
use std::collections::HashSet;
/// Makes multiple requests based on the presence of extensions
pub(super) struct Requester {
@@ -45,11 +47,16 @@ pub(super) struct Requester {
/// FeroxScan associated with the creation of this Requester
ferox_scan: Arc<FeroxScan>,
/// cache of previously seen links gotten via link extraction. since the requester is passed
/// around as an arc, and seen_links needs to be mutable, putting it behind a lock for
/// interior mutability, similar to the tuning_lock below
seen_links: RwLock<HashSet<String>>,
/// simple lock to control access to tuning to a single thread (per-scan)
///
/// need a usize to determine the number of consecutive non-error calls that a requester has
/// seen; this will satisfy the non-mut self constraint (due to us being behind an Arc, and
/// the need for a counter
/// the need for a counter)
tuning_lock: Mutex<usize>,
}
@@ -73,6 +80,7 @@ impl Requester {
Ok(Self {
ferox_scan,
policy_data,
seen_links: RwLock::new(HashSet::<String>::new()),
rate_limiter: RwLock::new(rate_limiter),
handles: scanner.handles.clone(),
target_url: scanner.target_url.to_owned(),
@@ -298,6 +306,8 @@ impl Requester {
let urls =
FeroxUrl::from_string(&self.target_url, self.handles.clone()).formatted_urls(word)?;
let should_test_deny = !self.handles.config.url_denylist.is_empty();
for url in urls {
// auto_tune is true, or rate_limit was set (mutually exclusive to user)
// and a rate_limiter has been created
@@ -313,6 +323,11 @@ impl Requester {
}
}
if should_test_deny && should_deny_url(&url, self.handles.clone())? {
// can't allow a denied url to be requested
continue;
}
let response = logged_request(&url, self.handles.clone()).await?;
if (should_tune || self.handles.config.auto_bail)
@@ -367,7 +382,26 @@ impl Requester {
.handles(self.handles.clone())
.build()?;
extractor.extract().await?;
let new_links: HashSet<_>;
let extracted = extractor.extract().await?;
{
// gain and quickly drop the read lock on seen_links, using it while unlocked
// to determine if there are any new links to process
let read_links = self.seen_links.read().await;
new_links = extracted.difference(&read_links).cloned().collect();
}
if !new_links.is_empty() {
// using is_empty instead of direct iteration to acquire the write lock behind
// some kind of less expensive gate (and not in a loop, obv)
let mut write_links = self.seen_links.write().await;
for new_link in &new_links {
write_links.insert(new_link.to_owned());
}
}
extractor.request_links(new_links).await?;
}
// everything else should be reported
@@ -536,6 +570,7 @@ mod tests {
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(FeroxScan::default()),
target_url: "http://localhost".to_string(),
@@ -563,6 +598,7 @@ mod tests {
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: ferox_scan.clone(),
target_url: "http://localhost".to_string(),
@@ -587,6 +623,7 @@ mod tests {
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: ferox_scan.clone(),
target_url: "http://localhost".to_string(),
@@ -626,6 +663,7 @@ mod tests {
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: ferox_scan.clone(),
target_url: "http://localhost".to_string(),
@@ -680,6 +718,7 @@ mod tests {
let req_clone = scan_two.clone();
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: req_clone,
target_url: "http://one/one/stuff.php".to_string(),
@@ -713,6 +752,7 @@ mod tests {
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(FeroxScan::default()),
target_url: "http://one/one/stuff.php".to_string(),
@@ -734,6 +774,7 @@ mod tests {
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(FeroxScan::default()),
target_url: "http://localhost".to_string(),
@@ -756,6 +797,7 @@ mod tests {
let requester = Arc::new(Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(FeroxScan::default()),
target_url: "http://localhost".to_string(),
@@ -772,7 +814,7 @@ mod tests {
requester.cool_down().await;
assert_eq!(resp.await.unwrap(), true);
assert!(resp.await.unwrap());
println!("{}", start.elapsed().as_millis());
assert!(start.elapsed().as_millis() >= 3500);
}
@@ -785,6 +827,7 @@ mod tests {
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(FeroxScan::default()),
target_url: "http://localhost".to_string(),
@@ -822,6 +865,7 @@ mod tests {
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(scan),
target_url: "http://localhost".to_string(),
@@ -857,6 +901,7 @@ mod tests {
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(scan),
target_url: "http://localhost".to_string(),
@@ -884,6 +929,7 @@ mod tests {
let mut requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(FeroxScan::default()),
target_url: "http://localhost".to_string(),
@@ -891,28 +937,16 @@ mod tests {
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
};
assert_eq!(
requester.too_many_status_errors(PolicyTrigger::Errors),
false
);
assert!(!requester.too_many_status_errors(PolicyTrigger::Errors));
assert_eq!(
requester.too_many_status_errors(PolicyTrigger::Status429),
false
);
assert!(!requester.too_many_status_errors(PolicyTrigger::Status429));
requester.ferox_scan.progress_bar().set_position(10);
requester.ferox_scan.add_429();
requester.ferox_scan.add_429();
requester.ferox_scan.add_429();
assert_eq!(
requester.too_many_status_errors(PolicyTrigger::Status429),
true
);
assert!(requester.too_many_status_errors(PolicyTrigger::Status429));
assert_eq!(
requester.too_many_status_errors(PolicyTrigger::Status403),
false
);
assert!(!requester.too_many_status_errors(PolicyTrigger::Status403));
requester.ferox_scan = Arc::new(FeroxScan::default());
requester.ferox_scan.progress_bar().set_position(10);
requester.ferox_scan.add_403();
@@ -924,10 +958,7 @@ mod tests {
requester.ferox_scan.add_403();
requester.ferox_scan.add_403();
requester.ferox_scan.add_403();
assert_eq!(
requester.too_many_status_errors(PolicyTrigger::Status403),
true
);
assert!(requester.too_many_status_errors(PolicyTrigger::Status403));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
@@ -941,6 +972,7 @@ mod tests {
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(FeroxScan::default()),
target_url: "http://localhost".to_string(),
@@ -983,6 +1015,7 @@ mod tests {
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: scan.clone(),
target_url: "http://localhost".to_string(),

View File

@@ -55,10 +55,7 @@ fn save_writes_stats_object_to_disk() {
stats.add_status_code(StatusCode::OK);
stats.add_status_code(StatusCode::OK);
let outfile = NamedTempFile::new().unwrap();
if stats
.save(174.33, &outfile.path().to_str().unwrap())
.is_ok()
{}
if stats.save(174.33, outfile.path().to_str().unwrap()).is_ok() {}
assert!(stats.as_json().unwrap().contains("statistics"));
assert!(stats.as_json().unwrap().contains("11")); // requests made

View File

@@ -66,7 +66,7 @@ impl FeroxUrl {
pub fn format(&self, word: &str, extension: Option<&str>) -> Result<Url> {
log::trace!("enter: format({}, {:?})", word, extension);
if Url::parse(&word).is_ok() {
if Url::parse(word).is_ok() {
// when a full url is passed in as a word to be joined to a base url using
// reqwest::Url::join, the result is that the word (url) completely overwrites the base
// url, potentially resulting in requests to places that aren't actually the target

View File

@@ -3,11 +3,13 @@ use console::{strip_ansi_codes, style, user_attended};
use indicatif::ProgressBar;
use reqwest::{Client, Response, StatusCode, Url};
#[cfg(not(target_os = "windows"))]
use rlimit::{getrlimit, setrlimit, Resource, Rlim};
use rlimit::{getrlimit, setrlimit, Resource};
use std::{
fs,
io::{self, BufWriter, Write},
sync::Arc,
time::Duration,
time::{SystemTime, UNIX_EPOCH},
};
use tokio::sync::mpsc::UnboundedSender;
@@ -217,16 +219,15 @@ pub fn create_report_string(
/// as the adjustment made here is only valid for the scan itself (and any child processes, of which
/// there are none).
#[cfg(not(target_os = "windows"))]
pub fn set_open_file_limit(limit: usize) -> bool {
pub fn set_open_file_limit(limit: u64) -> bool {
log::trace!("enter: set_open_file_limit");
if let Ok((soft, hard)) = getrlimit(Resource::NOFILE) {
if hard.as_usize() > limit {
if hard > limit {
// our default open file limit is less than the current hard limit, this means we can
// set the soft limit to our default
let new_soft_limit = Rlim::from_usize(limit);
if setrlimit(Resource::NOFILE, new_soft_limit, hard).is_ok() {
if setrlimit(Resource::NOFILE, limit, hard).is_ok() {
log::debug!("set open file descriptor limit to {}", limit);
log::trace!("exit: set_open_file_limit -> {}", true);
@@ -287,15 +288,141 @@ where
Ok(())
}
/// determines whether or not a given url should be denied based on the user-supplied --dont-scan
/// flag
pub fn should_deny_url(url: &Url, handles: Arc<Handles>) -> Result<bool> {
log::trace!(
"enter: should_deny_url({}, {:?}, {:?})",
url.as_str(),
handles.config.url_denylist,
handles.ferox_scans()?
);
// normalization for comparison is to remove the trailing / if one exists, this is done for
// the given url and any url to which it's compared
let normed_url = Url::parse(url.to_string().trim_end_matches('/'))?;
for deny_url in &handles.config.url_denylist {
// parse the denying url for easier comparison
let denier = Url::parse(deny_url.trim_end_matches('/'))
.with_context(|| format!("Could not parse {} as a url", deny_url))?;
// simplest case is an exact match, check for it first
if normed_url == denier {
log::trace!("exit: should_deny_url -> true");
return Ok(true);
}
match (normed_url.host(), denier.host()) {
// .host() will return an enum with ipv4|6 or domain and is comparable
// whereas .domain() returns None for ip addresses
(Some(normed_host), Some(denier_host)) => {
if normed_host != denier_host {
// domains don't even match, keep on keepin' on...
continue;
}
}
_ => {
// one or the other couldn't determine the host value, which probably means
// it's not suitable for further comparison
continue;
}
}
let normed_host = normed_url.host().unwrap(); // match above will catch errors
// at this point, we have a matching set of ips or domain names. now we can process the
// url path. The goal is to determine whether the given url's path is a subpath of any
// url in the deny list, for example
// GIVEN URL URL DENY LIST USER-SPECIFIED URLS TO SCAN
// http://some.domain/stuff/things, [http://some.domain/stuff], [http://some.domain] => true
// http://some.domain/stuff/things, [http://some.domain/stuff/things], [http://some.domain] => true
// http://some.domain/stuff/things, [http://some.domain/api], [http://some.domain] => false
// the examples above are all pretty obvious, the kicker comes when the blocking url's
// path is a parent to a scanned url
// http://some.domain/stuff/things, [http://some.domain/], [http://some.domain/stuff] => false
// http://some.domain/api, [http://some.domain/], [http://some.domain/stuff] => true
// we want to deny all children of the parent, unless that child is a child of a scan
// we specified through -u(s) or --stdin
let deny_path = denier.path();
let norm_path = normed_url.path();
if norm_path.starts_with(deny_path) {
// at this point, we know that the given normalized path is a sub-path of the
// current deny-url, now we just need to check to see if this deny-url is a parent
// to a scanned url that is also a parent of the given url
for ferox_scan in handles.ferox_scans()?.get_active_scans() {
let scanner = Url::parse(ferox_scan.url().trim_end_matches('/'))
.with_context(|| format!("Could not parse {} as a url", ferox_scan))?;
if let Some(scan_host) = scanner.host() {
// same domain/ip check we perform on the denier above
if normed_host != scan_host {
// domains don't even match, keep on keepin' on...
continue;
}
} else {
// couldn't process .host from scanner
continue;
};
let scan_path = scanner.path();
if scan_path.starts_with(deny_path) && norm_path.starts_with(scan_path) {
// user-specified scan url is a sub-path of the deny-urls's path AND the
// url to check is a sub-path of the user-specified scan url
//
// the assumption is the user knew what they wanted and we're going to give
// the scanned url precedence, even though it's a sub-path
log::trace!("exit: should_deny_url -> false");
return Ok(false);
}
}
log::trace!("exit: should_deny_url -> true");
return Ok(true);
}
}
log::trace!("exit: should_deny_url -> false");
Ok(false)
}
/// given a url and filename-suffix, return a unique filename comprised of the slugified url,
/// current unix timestamp and suffix
///
/// ex: ferox-http_telsa_com-1606947491.state
pub fn slugify_filename(url: &str, prefix: &str, suffix: &str) -> String {
log::trace!("enter: slugify({:?}, {:?}, {:?})", url, prefix, suffix);
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| Duration::from_secs(0))
.as_secs();
let altered_prefix = if !prefix.is_empty() {
format!("{}-", prefix)
} else {
String::new()
};
let slug = url.replace("://", "_").replace("/", "_").replace(".", "_");
let filename = format!("{}{}-{}.{}", altered_prefix, slug, ts, suffix);
log::trace!("exit: slugify -> {}", filename);
filename
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Configuration;
use crate::scan_manager::{FeroxScans, ScanOrder};
#[test]
/// set_open_file_limit with a low requested limit succeeds
fn utils_set_open_file_limit_with_low_requested_limit() {
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
let lower_limit = hard.as_usize() - 1;
let lower_limit = hard - 1;
assert!(set_open_file_limit(lower_limit));
}
@@ -303,9 +430,9 @@ mod tests {
/// set_open_file_limit with a high requested limit succeeds
fn utils_set_open_file_limit_with_high_requested_limit() {
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
let higher_limit = hard.as_usize() + 1;
let higher_limit = hard + 1;
// calculate a new soft to ensure soft != hard and hit that logic branch
let new_soft = Rlim::from_usize(hard.as_usize() - 1);
let new_soft = hard - 1;
setrlimit(Resource::NOFILE, new_soft, hard).unwrap();
assert!(set_open_file_limit(higher_limit));
}
@@ -316,7 +443,7 @@ mod tests {
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
// calculate a new soft to ensure soft == hard and hit the failure logic branch
setrlimit(Resource::NOFILE, hard, hard).unwrap();
assert!(!set_open_file_limit(hard.as_usize())); // returns false
assert!(!set_open_file_limit(hard)); // returns false
}
#[test]
@@ -366,4 +493,180 @@ mod tests {
fn status_colorizer_returns_as_is() {
assert_eq!(status_colorizer("farfignewton"), "farfignewton".to_string());
}
#[test]
/// provide a url that should be blocked where the denier is an exact match for the tested url
/// expect true
fn should_deny_url_blocks_when_denier_is_exact_match() {
let scan_url = "https://testdomain.com/";
let deny_url = "https://testdomain.com/denied";
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![String::from(deny_url)];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(should_deny_url(&tested_url, handles).unwrap());
}
#[test]
/// provide a url that has a different host than the denier but the same path, expect false
fn should_deny_url_doesnt_compare_mismatched_domains() {
let scan_url = "https://testdomain.com/";
let deny_url = "https://dev.testdomain.com/denied";
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![String::from(deny_url)];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(!should_deny_url(&tested_url, handles).unwrap());
}
#[test]
/// provide a denier from which we can't check a host, which results in no comparison, expect false
fn should_deny_url_doesnt_compare_non_domains() {
let scan_url = "https://testdomain.com/";
let deny_url = "unix:/run/foo.socket";
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![String::from(deny_url)];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(!should_deny_url(&tested_url, handles).unwrap());
}
#[test]
/// provide a url that has a different host than the denier but the same path, expect false
/// because the denier is a parent to the tested, even tho the scanned doesn't compare, it
/// still returns true
fn should_deny_url_doesnt_compare_mismatched_domains_in_scanned() {
let deny_url = "https://testdomain.com/";
let scan_url = "https://dev.testdomain.com/denied";
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![String::from(deny_url)];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(should_deny_url(&tested_url, handles).unwrap());
}
#[test]
/// provide a denier from which we can't check a host, which results in no comparison, expect false
/// because the denier is a parent to the tested, even tho the scanned doesn't compare, it
/// still returns true
fn should_deny_url_doesnt_compare_non_domains_in_scanned() {
let deny_url = "https://testdomain.com/";
let scan_url = "unix:/run/foo.socket";
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![String::from(deny_url)];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(should_deny_url(&tested_url, handles).unwrap());
}
#[test]
/// provide a denier where the tested url is a sub-path and the scanned url is not, expect true
fn should_deny_url_blocks_child() {
let scan_url = "https://testdomain.com/";
let deny_url = "https://testdomain.com/api";
let tested_url = Url::parse("https://testdomain.com/api/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![String::from(deny_url)];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(should_deny_url(&tested_url, handles).unwrap());
}
#[test]
/// provide a denier where the tested url is not a sub-path and the scanned url is not, expect false
fn should_deny_url_doesnt_block_non_child() {
let scan_url = "https://testdomain.com/";
let deny_url = "https://testdomain.com/api";
let tested_url = Url::parse("https://testdomain.com/not-denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![String::from(deny_url)];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(!should_deny_url(&tested_url, handles).unwrap());
}
#[test]
/// provide a denier where the tested url is a sub-path and the scanned url is not, expect true
fn should_deny_url_blocks_child_when_scan_url_isnt_parent() {
let scan_url = "https://testdomain.com/api";
let deny_url = "https://testdomain.com/";
let tested_url = Url::parse("https://testdomain.com/stuff/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![String::from(deny_url)];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(should_deny_url(&tested_url, handles).unwrap());
}
#[test]
/// provide a denier where the tested url is not a sub-path and the scanned url is not, expect false
fn should_deny_url_doesnt_block_child_when_scan_url_is_parent() {
let scan_url = "https://testdomain.com/api";
let deny_url = "https://testdomain.com/";
let tested_url = Url::parse("https://testdomain.com/api/not-denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![String::from(deny_url)];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(!should_deny_url(&tested_url, handles).unwrap());
}
}

View File

@@ -113,6 +113,36 @@ fn banner_prints_headers() {
);
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + multiple dont scan entries
fn banner_prints_denied_urls() {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("--dont-scan")
.arg("http://dont-scan.me")
.arg("--dont-scan")
.arg("https://also-not.me")
.assert()
.success()
.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("Don't Scan"))
.and(predicate::str::contains("http://dont-scan.me"))
.and(predicate::str::contains("https://also-not.me"))
.and(predicate::str::contains("─┴─")),
);
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + multiple size filters

212
tests/test_deny_list.rs Normal file
View File

@@ -0,0 +1,212 @@
mod utils;
use assert_cmd::prelude::*;
use assert_cmd::Command;
use httpmock::Method::GET;
use httpmock::MockServer;
use predicates::prelude::*;
use utils::{setup_tmp_directory, teardown_tmp_directory};
#[test]
/// test that the deny list prevents a request if the requested url is a match
fn deny_list_works_during_with_a_normal_scan() {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200).body("this is a test");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--dont-scan")
.arg(srv.url("/LICENSE"))
.unwrap();
teardown_tmp_directory(tmp_dir);
cmd.assert()
.success()
.stdout(predicate::str::contains(srv.url("/LICENSE")).not());
assert_eq!(mock.hits(), 0);
}
#[test]
/// test that the deny list prevents requests of urls found during extraction
fn deny_list_works_during_extraction() {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200)
.body(&srv.url("'/homepage/assets/img/icons/handshake.svg'"));
});
let mock_two = srv.mock(|when, then| {
when.method(GET)
.path("/homepage/assets/img/icons/handshake.svg");
then.status(200);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--extract-links")
.arg("--dont-scan")
.arg(srv.url("/homepage/"))
.unwrap();
cmd.assert().success().stdout(
predicate::str::contains("/LICENSE")
.and(predicate::str::contains("200"))
.and(predicate::str::contains("/homepage/assets/img/icons/handshake.svg").not()),
);
assert_eq!(mock.hits(), 1);
assert_eq!(mock_two.hits(), 0);
teardown_tmp_directory(tmp_dir);
}
#[test]
/// test that the deny list prevents requests of urls found during recursion
fn deny_list_works_during_recursion() {
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, "wordlist").unwrap();
let js_mock = srv.mock(|when, then| {
when.method(GET).path("/js");
then.status(301).header("Location", &srv.url("/js/"));
});
let js_prod_mock = srv.mock(|when, then| {
when.method(GET).path("/js/prod");
then.status(301).header("Location", &srv.url("/js/prod/"));
});
let js_dev_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev");
then.status(301).header("Location", &srv.url("/js/dev/"));
});
let js_dev_file_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev/file.js");
then.status(200)
.body("this is a test and is more bytes than other ones");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("-t")
.arg("1")
.arg("--dont-scan")
.arg(srv.url("/js/dev"))
.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())
.not()
.and(predicate::str::is_match("200.*js/dev/file.js").unwrap())
.not(),
);
assert_eq!(js_mock.hits(), 1);
assert_eq!(js_prod_mock.hits(), 1);
assert_eq!(js_dev_mock.hits(), 0);
assert_eq!(js_dev_file_mock.hits(), 0);
teardown_tmp_directory(tmp_dir);
}
#[test]
/// test that the deny list prevents requests of urls found during recursion when the denier is a
/// parent of a user-specified scan
fn deny_list_works_during_recursion_with_inverted_parents() {
let srv = MockServer::start();
let urls = [
"js".to_string(),
"prod".to_string(),
"dev".to_string(),
"api".to_string(),
"file.js".to_string(),
];
let (tmp_dir, file) = setup_tmp_directory(&urls, "wordlist").unwrap();
let js_mock = srv.mock(|when, then| {
when.method(GET).path("/js");
then.status(301).header("Location", &srv.url("/js/"));
});
let api_mock = srv.mock(|when, then| {
when.method(GET).path("/api");
then.status(200);
});
let js_prod_mock = srv.mock(|when, then| {
when.method(GET).path("/js/prod");
then.status(301).header("Location", &srv.url("/js/prod/"));
});
let js_dev_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev");
then.status(301).header("Location", &srv.url("/js/dev/"));
});
let js_dev_file_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev/file.js");
then.status(200)
.body("this is a test and is more bytes than other ones");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/js"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("-t")
.arg("1")
.arg("-vvvv")
.arg("--dont-scan")
.arg(srv.url("/"))
.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())
.and(predicate::str::is_match("200.*api").unwrap())
.not(),
);
assert_eq!(js_mock.hits(), 1);
assert_eq!(js_prod_mock.hits(), 1);
assert_eq!(js_dev_mock.hits(), 1);
assert_eq!(js_dev_file_mock.hits(), 1);
assert_eq!(api_mock.hits(), 0);
teardown_tmp_directory(tmp_dir);
}

View File

@@ -224,11 +224,11 @@ fn test_dynamic_wildcard_request_found() {
teardown_tmp_directory(tmp_dir);
assert_eq!(contents.contains("WLD"), true);
assert_eq!(contents.contains("Got"), true);
assert_eq!(contents.contains("200"), true);
assert_eq!(contents.contains("(url length: 32)"), true);
assert_eq!(contents.contains("(url length: 96)"), true);
assert!(contents.contains("WLD"));
assert!(contents.contains("Got"));
assert!(contents.contains("200"));
assert!(contents.contains("(url length: 32)"));
assert!(contents.contains("(url length: 96)"));
cmd.assert().success().stdout(
predicate::str::contains("WLD")
@@ -391,11 +391,11 @@ fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file() {
teardown_tmp_directory(tmp_dir);
assert_eq!(contents.contains("WLD"), true);
assert_eq!(contents.contains("Got"), true);
assert_eq!(contents.contains("200"), true);
assert_eq!(contents.contains("(url length: 32)"), true);
assert_eq!(contents.contains("(url length: 96)"), true);
assert!(contents.contains("WLD"));
assert!(contents.contains("Got"));
assert!(contents.contains("200"));
assert!(contents.contains("(url length: 32)"));
assert!(contents.contains("(url length: 96)"));
cmd.assert().success().stdout(
predicate::str::contains("WLD")
@@ -451,12 +451,12 @@ fn heuristics_wildcard_test_with_redirect_as_response_code(
teardown_tmp_directory(tmp_dir);
assert_eq!(contents.contains("WLD"), true);
assert_eq!(contents.contains("301"), true);
assert_eq!(contents.contains("/some-redirect"), true);
assert_eq!(contents.contains("redirects to => "), true);
assert_eq!(contents.contains(&srv.url("/")), true);
assert_eq!(contents.contains("(url length: 32)"), true);
assert!(contents.contains("WLD"));
assert!(contents.contains("301"));
assert!(contents.contains("/some-redirect"));
assert!(contents.contains("redirects to => "));
assert!(contents.contains(&srv.url("/")));
assert!(contents.contains("(url length: 32)"));
cmd.assert().success().stdout(
predicate::str::contains("redirects to => ")

View File

@@ -3,7 +3,7 @@ use assert_cmd::Command;
use httpmock::Method::GET;
use httpmock::{MockServer, Regex};
use predicates::prelude::*;
use std::fs::read_to_string;
use std::fs::{read_dir, read_to_string};
use utils::{setup_tmp_directory, teardown_tmp_directory};
#[test]
@@ -153,3 +153,73 @@ fn main_parallel_spawns_children() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
#[test]
/// send three targets over stdin with --output enabled, expect parallel to create a new directory
/// and the log files therein
fn main_parallel_creates_output_directory() -> Result<(), Box<dyn std::error::Error>> {
let t1 = MockServer::start();
let t2 = MockServer::start();
let t3 = MockServer::start();
let words = [
String::from("LICENSE"),
String::from("stuff"),
String::from("things"),
String::from("mostuff"),
String::from("mothings"),
];
let (word_tmp_dir, wordlist) = setup_tmp_directory(&words, "wordlist")?;
let (output_dir, outfile) = setup_tmp_directory(&[], "output-file")?;
let (tgt_tmp_dir, targets) =
setup_tmp_directory(&[t1.url("/"), t2.url("/"), t3.url("/")], "targets")?;
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--stdin")
.arg("--parallel")
.arg("2")
.arg("--output")
.arg(outfile.as_os_str())
.arg("--wordlist")
.arg(wordlist.as_os_str())
.pipe_stdin(targets)
.unwrap()
.assert()
.success()
.stderr(
predicate::str::contains("Could not connect to any target provided")
.and(predicate::str::contains("Target Url"))
.not(), // no target url found
);
// output_dir should return something similar to output-file-1627845244.logs with the
// line below. if it ever fails, can use the regex below to filter out the right directory
let sub_dir = read_dir(&output_dir)?.next().unwrap()?.file_name();
let mut num_logs = 0;
let file_regex = Regex::new("ferox-[a-zA-Z_:0-9]+-[0-9]+.log").unwrap();
let dir_regex = Regex::new("output-file-[0-9]+.logs").unwrap();
let sub_dir = output_dir.as_ref().join(&sub_dir);
// created directory like output-file-1627845741.logs/
assert!(dir_regex.is_match(&sub_dir.to_string_lossy().to_string()));
for entry in sub_dir.read_dir()? {
let entry = entry?;
// created each file like ferox-https_localhost-1627845741.log
println!("name: {:?}", entry.file_name().to_string_lossy());
assert!(file_regex.is_match(&entry.file_name().to_string_lossy()));
num_logs += 1;
}
// should be 3 log files total
assert_eq!(num_logs, 3);
teardown_tmp_directory(word_tmp_dir);
teardown_tmp_directory(tgt_tmp_dir);
teardown_tmp_directory(output_dir);
Ok(())
}

View File

@@ -19,7 +19,7 @@ fn parser_incorrect_param_with_tack_tack_help() {
.arg("-fc")
.arg("--help")
.assert()
.failure()
.success()
.stdout(predicate::str::contains("Ludicrous speed... go!"));
}
@@ -41,6 +41,6 @@ fn parser_incorrect_param_with_tack_h() {
.arg("-fc")
.arg("-h")
.assert()
.failure()
.success()
.stdout(predicate::str::contains("Ludicrous speed... go!"));
}

View File

@@ -77,7 +77,7 @@ fn auto_bail_cancels_scan_with_timeouts() {
// read debug log to get the number of errors enforced
for line in debug_log.lines() {
let log: serde_json::Value = serde_json::from_str(&line).unwrap_or_default();
let log: serde_json::Value = serde_json::from_str(line).unwrap_or_default();
if let Some(message) = log.get("message") {
let str_msg = message.as_str().unwrap_or_default().to_string();
@@ -156,7 +156,7 @@ fn auto_bail_cancels_scan_with_403s() {
// read debug log to get the number of errors enforced
for line in debug_log.lines() {
let log: serde_json::Value = serde_json::from_str(&line).unwrap_or_default();
let log: serde_json::Value = serde_json::from_str(line).unwrap_or_default();
if let Some(message) = log.get("message") {
let str_msg = message.as_str().unwrap_or_default().to_string();
@@ -238,7 +238,7 @@ fn auto_bail_cancels_scan_with_429s() {
// read debug log to get the number of errors enforced
for line in debug_log.lines() {
let log: serde_json::Value = serde_json::from_str(&line).unwrap_or_default();
let log: serde_json::Value = serde_json::from_str(line).unwrap_or_default();
if let Some(message) = log.get("message") {
let str_msg = message.as_str().unwrap_or_default().to_string();

View File

@@ -93,9 +93,13 @@ fn resume_scan_works() {
#[test]
/// kick off scan with a time limit;
fn time_limit_enforced_when_specified() {
let srv = MockServer::start();
let t1 = MockServer::start();
let t2 = MockServer::start();
let (tmp_dir, file) =
setup_tmp_directory(&["css".to_string(), "stuff".to_string()], "wordlist").unwrap();
let (tgt_tmp_dir, targets) =
setup_tmp_directory(&[t1.url("/"), t2.url("/")], "targets").unwrap();
// ensure the command will run long enough by adding crap to the wordlist
let more_words = read_to_string(Path::new("tests/extra-words")).unwrap();
@@ -109,12 +113,13 @@ fn time_limit_enforced_when_specified() {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--stdin")
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--time-limit")
.arg("5s")
.pipe_stdin(targets)
.unwrap()
.assert()
.failure();
@@ -127,4 +132,5 @@ fn time_limit_enforced_when_specified() {
assert!(now.elapsed() > lower_bound && now.elapsed() < upper_bound);
teardown_tmp_directory(tmp_dir);
teardown_tmp_directory(tgt_tmp_dir);
}