Compare commits

...

54 Commits

Author SHA1 Message Date
epi
b1f5ed507b Merge pull request #750 from epi052/742-748-state-file-bug-fixes
multiple bug fixes / small improvements
2022-12-29 19:57:23 -06:00
epi
79edc42b17 bumped version to 2.7.3 2022-12-29 15:56:56 -06:00
epi
1b223b0867 fixed #716; wordlist entries with leading slash are trimmed 2022-12-29 15:55:50 -06:00
epi
0c6d5193a9 fixed #743; redirects always show full url as Location 2022-12-29 15:43:57 -06:00
epi
c637355796 clippy 2022-12-29 15:32:22 -06:00
epi
a114cc8f85 fixed #748; cancelled scans persist across ctrl+c 2022-12-29 15:28:58 -06:00
epi
c8503faf02 updated dependencies 2022-12-29 07:03:03 -06:00
epi
cbbd642510 Merge pull request #749 from n0kovo/main
Fix incorrect username in Contributors
2022-12-29 06:59:34 -06:00
n0kovo
2c8e9bace9 Fix incorrect username in Contributors 2022-12-29 13:37:41 +01:00
epi
f4fe8c0544 Merge pull request #734 from epi052/all-contributors/add-kmanc
docs: add kmanc as a contributor for bug, and code
2022-12-14 06:09:50 -06:00
allcontributors[bot]
73109483fe docs: update .all-contributorsrc [skip ci] 2022-12-14 12:09:23 +00:00
allcontributors[bot]
aee33012b1 docs: update README.md [skip ci] 2022-12-14 12:09:22 +00:00
epi
eab95e0435 Merge pull request #733 from kmanc/bugfix-no-state-with-time-limit
FIX 732 ensure --no-state is respected even through --time-limit
2022-12-14 06:08:50 -06:00
koins
acb2f42f69 ensure --no-state is respected even through --time-limit 2022-12-13 22:37:27 -08:00
epi
ac20b213ec Merge branch 'main' of github.com:epi052/feroxbuster 2022-11-16 16:53:02 -06:00
epi
201873d7ac bumped version to 2.7.2 2022-11-16 16:52:35 -06:00
epi
9678b8f31c Merge pull request #708 from epi052/all-contributors/add-udoprog
docs: add udoprog as a contributor for code
2022-11-16 16:41:55 -06:00
allcontributors[bot]
20a826fc0f docs: update .all-contributorsrc [skip ci] 2022-11-16 22:41:13 +00:00
allcontributors[bot]
56b78a4e04 docs: update README.md [skip ci] 2022-11-16 22:41:12 +00:00
epi
4b6bf3645d bumped version to 2.7.2 2022-11-16 16:35:34 -06:00
epi
6fd201b717 clippy 2022-11-16 16:21:32 -06:00
epi
5f39d71fe8 updated deps; clippy 2022-11-16 16:20:09 -06:00
epi
c23850208b added link tag to html extraction 2022-11-16 16:01:01 -06:00
epi
d5605efb08 Merge pull request #706 from epi052/689-invalid-uri-during-extraction
fixed invalid uri exception during extraction
2022-11-16 07:44:48 -06:00
epi
5b8d3f5661 removed cruft 2022-11-16 07:42:09 -06:00
epi
ce7f3b79b8 fixed invalid uri exception during extraction 2022-11-16 07:09:02 -06:00
epi
c9c63bebd0 Merge pull request #672 from epi052/661-fix-double-dir-scan
661 fix double dir scan
2022-10-05 05:32:09 -05:00
epi
1f60e06247 turned off builds for all but main 2022-10-05 05:30:00 -05:00
epi
04e3ad69cc allowing a test build to happen 2022-10-04 07:09:40 -05:00
epi
fd5b1f6f25 refined the fix; updated tests and serialization 2022-10-04 07:07:24 -05:00
epi
a9dde3f7e1 normalized directory scan input + search in feroxscans 2022-10-04 05:45:34 -05:00
epi
7a9ee39941 Merge pull request #671 from epi052/update-clap-major
updated clap from 3.x to 4.x
2022-10-03 05:27:10 -05:00
epi
6befae1a93 updated clap from 3.x to 4.x 2022-10-03 05:16:50 -05:00
epi
e6b422b92a Merge pull request #670 from epi052/update-console
updated deps
2022-10-01 13:22:54 -05:00
epi
fb4bfa27fd updated deps 2022-10-01 13:21:41 -05:00
epi
2795ec4e72 fixed typo; closes #660 2022-09-27 06:21:51 -05:00
epi
e9d283bc59 Merge pull request #655 from epi052/update-deps
Update deps
2022-09-20 04:53:36 -05:00
epi
3a128df2fc clippy 2022-09-20 04:52:11 -05:00
epi
38f1b917c4 clippy 2022-09-20 04:49:58 -05:00
epi
afa7d6804c clippy 2022-09-18 05:51:13 -05:00
epi
28c3e25eeb updated deps 2022-09-18 05:50:39 -05:00
epi
55e22467ce clippy issues 2022-09-18 05:50:15 -05:00
epi
bbfaddaedd fixed #644; methods respected from config 2022-09-06 08:02:30 -05:00
epi
53e3420efd updated deps 2022-07-10 16:44:16 -05:00
epi
d390bbc12d Merge branch 'leakybucket-update' 2022-07-10 16:33:22 -05:00
epi
0c3b91855a Merge pull request #604 from udoprog/bump-leaky-bucket
Bump leaky-bucket to 0.12.1
2022-07-10 16:33:07 -05:00
epi
48f5362f5f appeased clippy 2022-07-10 16:30:52 -05:00
John-John Tedro
24514faf9e Bump leaky-bucket to 0.12.1 2022-07-10 18:20:48 +02:00
epi
a424057166 Merge pull request #581 from epi052/all-contributors/add-herrcykel
docs: add herrcykel as a contributor for code
2022-05-17 18:42:47 -05:00
epi
7d483b6edd Merge pull request #580 from herrcykel/patch-1
Remove superfluous if statement
2022-05-17 18:42:19 -05:00
allcontributors[bot]
1a717e878d docs: update .all-contributorsrc [skip ci] 2022-05-17 23:41:59 +00:00
allcontributors[bot]
e35a6dda9f docs: update README.md [skip ci] 2022-05-17 23:41:58 +00:00
O
f3b2193b2f Remove superfluous if statement 2022-05-17 23:31:29 +02:00
epi
07a7ac652e updated deps 2022-05-12 06:15:17 -05:00
42 changed files with 980 additions and 735 deletions

View File

@@ -440,6 +440,34 @@
"contributions": [
"ideas"
]
},
{
"login": "herrcykel",
"name": "O",
"avatar_url": "https://avatars.githubusercontent.com/u/1936757?v=4",
"profile": "https://github.com/herrcykel",
"contributions": [
"code"
]
},
{
"login": "udoprog",
"name": "John-John Tedro",
"avatar_url": "https://avatars.githubusercontent.com/u/111092?v=4",
"profile": "http://udoprog.github.io/",
"contributions": [
"code"
]
},
{
"login": "kmanc",
"name": "kmanc",
"avatar_url": "https://avatars.githubusercontent.com/u/14863147?v=4",
"profile": "https://github.com/kmanc",
"contributions": [
"bug",
"code"
]
}
],
"contributorsPerLine": 7,
@@ -447,5 +475,6 @@
"projectOwner": "epi052",
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": true
"skipCi": true,
"commitConvention": "angular"
}

855
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "feroxbuster"
version = "2.7.1"
version = "2.7.3"
authors = ["Ben 'epi' Risher (@epi052)"]
license = "MIT"
edition = "2021"
@@ -22,40 +22,40 @@ build = "build.rs"
maintenance = { status = "actively-developed" }
[build-dependencies]
clap = { version = "3.1.18", features = ["wrap_help", "cargo"] }
clap_complete = "3.1.4"
clap = { version = "4.0.8", features = ["wrap_help", "cargo"] }
clap_complete = "4.0.2"
regex = "1.5.5"
lazy_static = "1.4.0"
dirs = "4.0.0"
[dependencies]
scraper = "0.13.0"
scraper = "0.14.0"
futures = "0.3.21"
tokio = { version = "1.18.2", features = ["full"] }
tokio-util = { version = "0.7.1", features = ["codec"] }
log = "0.4.17"
env_logger = "0.9.0"
env_logger = "0.10.0"
reqwest = { version = "0.11.10", features = ["socks"] }
# uses feature unification to add 'serde' to reqwest::Url
url = { version = "2.2.2", features = ["serde"] }
serde_regex = "1.1.0"
clap = { version = "3.1.18", features = ["wrap_help", "cargo"] }
clap = { version = "4.0.8", features = ["wrap_help", "cargo"] }
lazy_static = "1.4.0"
toml = "0.5.9"
serde = { version = "1.0.137", features = ["derive", "rc"] }
serde_json = "1.0.81"
uuid = { version = "1.0.0", features = ["v4"] }
indicatif = "0.15"
console = "0.15.0"
console = "0.15.2"
openssl = { version = "0.10.40", features = ["vendored"] }
dirs = "4.0.0"
regex = "1.5.5"
crossterm = "0.23.2"
rlimit = "0.8.3"
crossterm = "0.25.0"
rlimit = "0.9.0"
ctrlc = "3.2.2"
fuzzyhash = "0.2.1"
anyhow = "1.0.57"
leaky-bucket = "0.10.0"
leaky-bucket = "0.12.1"
[dev-dependencies]
tempfile = "3.3.0"

View File

@@ -11,7 +11,7 @@ rm ferox-*.state
# dependency management
[tasks.upgrade-deps]
command = "cargo"
args = ["upgrade", "--exclude", "indicatif", "leaky-bucket"]
args = ["upgrade", "--exclude", "indicatif"]
[tasks.update]
command = "cargo"

129
README.md
View File

@@ -181,67 +181,74 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://io.fi"><img src="https://avatars.githubusercontent.com/u/5235109?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Joona Hoikkala</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=joohoi" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/jsav0"><img src="https://avatars.githubusercontent.com/u/20546041?v=4?s=100" width="100px;" alt=""/><br /><sub><b>J Savage</b></sub></a><br /><a href="#infra-jsav0" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=jsav0" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.tgotwig.dev"><img src="https://avatars.githubusercontent.com/u/30773779?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Thomas Gotwig</b></sub></a><br /><a href="#infra-TGotwig" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=TGotwig" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/spikecodes"><img src="https://avatars.githubusercontent.com/u/19519553?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Spike</b></sub></a><br /><a href="#infra-spikecodes" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=spikecodes" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/evanrichter"><img src="https://avatars.githubusercontent.com/u/330292?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Evan Richter</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=evanrichter" title="Code">💻</a> <a href="https://github.com/epi052/feroxbuster/commits?author=evanrichter" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/mzpqnxow"><img src="https://avatars.githubusercontent.com/u/8016228?v=4?s=100" width="100px;" alt=""/><br /><sub><b>AG</b></sub></a><br /><a href="#ideas-mzpqnxow" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=mzpqnxow" title="Documentation">📖</a></td>
<td align="center"><a href="https://n-thumann.de/"><img src="https://avatars.githubusercontent.com/u/46975855?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nicolas Thumann</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=n-thumann" title="Code">💻</a> <a href="https://github.com/epi052/feroxbuster/commits?author=n-thumann" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/tomtastic"><img src="https://avatars.githubusercontent.com/u/302127?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tom Matthews</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=tomtastic" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/bsysop"><img src="https://avatars.githubusercontent.com/u/9998303?v=4?s=100" width="100px;" alt=""/><br /><sub><b>bsysop</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=bsysop" title="Documentation">📖</a></td>
<td align="center"><a href="http://bpsizemore.me"><img src="https://avatars.githubusercontent.com/u/11645898?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Brian Sizemore</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=bpsizemore" title="Code">💻</a></td>
<td align="center"><a href="https://pwn.by/noraj"><img src="https://avatars.githubusercontent.com/u/16578570?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alexandre ZANNI</b></sub></a><br /><a href="#infra-noraj" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=noraj" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/craig"><img src="https://avatars.githubusercontent.com/u/99729?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Craig</b></sub></a><br /><a href="#infra-craig" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://www.reddit.com/u/EONRaider"><img src="https://avatars.githubusercontent.com/u/15611424?v=4?s=100" width="100px;" alt=""/><br /><sub><b>EONRaider</b></sub></a><br /><a href="#infra-EONRaider" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/wtwver"><img src="https://avatars.githubusercontent.com/u/53866088?v=4?s=100" width="100px;" alt=""/><br /><sub><b>wtwver</b></sub></a><br /><a href="#infra-wtwver" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</tr>
<tr>
<td align="center"><a href="https://tib3rius.com"><img src="https://avatars.githubusercontent.com/u/48113936?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tib3rius</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ATib3rius" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/0xdf"><img src="https://avatars.githubusercontent.com/u/1489045?v=4?s=100" width="100px;" alt=""/><br /><sub><b>0xdf</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3A0xdf" title="Bug reports">🐛</a></td>
<td align="center"><a href="http://secure77.de"><img src="https://avatars.githubusercontent.com/u/31564517?v=4?s=100" width="100px;" alt=""/><br /><sub><b>secure-77</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asecure-77" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/sbrun"><img src="https://avatars.githubusercontent.com/u/7712154?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sophie Brun</b></sub></a><br /><a href="#infra-sbrun" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/black-A"><img src="https://avatars.githubusercontent.com/u/30686803?v=4?s=100" width="100px;" alt=""/><br /><sub><b>black-A</b></sub></a><br /><a href="#ideas-black-A" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/dinosn"><img src="https://avatars.githubusercontent.com/u/3851678?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nicolas Krassas</b></sub></a><br /><a href="#ideas-dinosn" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/N0ur5"><img src="https://avatars.githubusercontent.com/u/24260009?v=4?s=100" width="100px;" alt=""/><br /><sub><b>N0ur5</b></sub></a><br /><a href="#ideas-N0ur5" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/moscowchill"><img src="https://avatars.githubusercontent.com/u/72578879?v=4?s=100" width="100px;" alt=""/><br /><sub><b>mchill</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Amoscowchill" title="Bug reports">🐛</a></td>
<td align="center"><a href="http://BitThr3at.github.io"><img src="https://avatars.githubusercontent.com/u/45028933?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Naman</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ABitThr3at" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/Sicks3c"><img src="https://avatars.githubusercontent.com/u/32225186?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ayoub Elaich</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asicks3c" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/HenryHoggard"><img src="https://avatars.githubusercontent.com/u/1208121?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Henry</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AHenryHoggard" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/SleepiPanda"><img src="https://avatars.githubusercontent.com/u/6428561?v=4?s=100" width="100px;" alt=""/><br /><sub><b>SleepiPanda</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ASleepiPanda" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/uBadRequest"><img src="https://avatars.githubusercontent.com/u/47282747?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Bad Requests</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AuBadRequest" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://home.dnaka91.rocks"><img src="https://avatars.githubusercontent.com/u/36804488?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dominik Nakamura</b></sub></a><br /><a href="#infra-dnaka91" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/hunter0x8"><img src="https://avatars.githubusercontent.com/u/46222314?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Muhammad Ahsan</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Ahunter0x8" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/cortantief"><img src="https://avatars.githubusercontent.com/u/34527333?v=4?s=100" width="100px;" alt=""/><br /><sub><b>cortantief</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Acortantief" title="Bug reports">🐛</a> <a href="https://github.com/epi052/feroxbuster/commits?author=cortantief" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/dsaxton"><img src="https://avatars.githubusercontent.com/u/2658661?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Daniel Saxton</b></sub></a><br /><a href="#ideas-dsaxton" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=dsaxton" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/narkopolo"><img src="https://avatars.githubusercontent.com/u/16690056?v=4?s=100" width="100px;" alt=""/><br /><sub><b>narkopolo</b></sub></a><br /><a href="#ideas-narkopolo" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://ring0.lol"><img src="https://avatars.githubusercontent.com/u/1893909?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Justin Steven</b></sub></a><br /><a href="#ideas-justinsteven" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/7047payloads"><img src="https://avatars.githubusercontent.com/u/95562424?v=4?s=100" width="100px;" alt=""/><br /><sub><b>7047payloads</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=7047payloads" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/unkn0wnsyst3m"><img src="https://avatars.githubusercontent.com/u/21272239?v=4?s=100" width="100px;" alt=""/><br /><sub><b>unkn0wnsyst3m</b></sub></a><br /><a href="#ideas-unkn0wnsyst3m" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center"><a href="https://ironwort.me/"><img src="https://avatars.githubusercontent.com/u/15280042?v=4?s=100" width="100px;" alt=""/><br /><sub><b>0x08</b></sub></a><br /><a href="#ideas-its0x08" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/MD-Levitan"><img src="https://avatars.githubusercontent.com/u/12116508?v=4?s=100" width="100px;" alt=""/><br /><sub><b>kusok</b></sub></a><br /><a href="#ideas-MD-Levitan" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=MD-Levitan" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/godylockz"><img src="https://avatars.githubusercontent.com/u/81207744?v=4?s=100" width="100px;" alt=""/><br /><sub><b>godylockz</b></sub></a><br /><a href="#ideas-godylockz" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=godylockz" title="Code">💻</a></td>
<td align="center"><a href="http://ryanmontgomery.me"><img src="https://avatars.githubusercontent.com/u/44453666?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ryan Montgomery</b></sub></a><br /><a href="#ideas-0dayCTF" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/IppSec"><img src="https://avatars.githubusercontent.com/u/24677271?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ippsec</b></sub></a><br /><a href="#ideas-ippsec" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/gtjamesa"><img src="https://avatars.githubusercontent.com/u/2078364?v=4?s=100" width="100px;" alt=""/><br /><sub><b>James</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Agtjamesa" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://twitter.com/Jhaddix"><img src="https://avatars.githubusercontent.com/u/3488554?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jason Haddix</b></sub></a><br /><a href="#ideas-jhaddix" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/issues?q=author%3Ajhaddix" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/ThisLimn0"><img src="https://avatars.githubusercontent.com/u/67125885?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Limn0</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AThisLimn0" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/0xdf223"><img src="https://avatars.githubusercontent.com/u/76954092?v=4?s=100" width="100px;" alt=""/><br /><sub><b>0xdf</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3A0xdf223" title="Bug reports">🐛</a> <a href="#ideas-0xdf223" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/Flangyver"><img src="https://avatars.githubusercontent.com/u/59575870?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Flangyver</b></sub></a><br /><a href="#ideas-Flangyver" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/DonatoReis"><img src="https://avatars.githubusercontent.com/u/93531354?v=4?s=100" width="100px;" alt=""/><br /><sub><b>PeakyBlinder</b></sub></a><br /><a href="#ideas-DonatoReis" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://postmodern.github.io/"><img src="https://avatars.githubusercontent.com/u/12671?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Postmodern</b></sub></a><br /><a href="#ideas-postmodern" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tbody>
<tr>
<td align="center"><a href="https://io.fi"><img src="https://avatars.githubusercontent.com/u/5235109?v=4?s=100" width="100px;" alt="Joona Hoikkala"/><br /><sub><b>Joona Hoikkala</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=joohoi" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/jsav0"><img src="https://avatars.githubusercontent.com/u/20546041?v=4?s=100" width="100px;" alt="J Savage"/><br /><sub><b>J Savage</b></sub></a><br /><a href="#infra-jsav0" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=jsav0" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.tgotwig.dev"><img src="https://avatars.githubusercontent.com/u/30773779?v=4?s=100" width="100px;" alt="Thomas Gotwig"/><br /><sub><b>Thomas Gotwig</b></sub></a><br /><a href="#infra-TGotwig" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=TGotwig" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/spikecodes"><img src="https://avatars.githubusercontent.com/u/19519553?v=4?s=100" width="100px;" alt="Spike"/><br /><sub><b>Spike</b></sub></a><br /><a href="#infra-spikecodes" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=spikecodes" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/evanrichter"><img src="https://avatars.githubusercontent.com/u/330292?v=4?s=100" width="100px;" alt="Evan Richter"/><br /><sub><b>Evan Richter</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=evanrichter" title="Code">💻</a> <a href="https://github.com/epi052/feroxbuster/commits?author=evanrichter" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/mzpqnxow"><img src="https://avatars.githubusercontent.com/u/8016228?v=4?s=100" width="100px;" alt="AG"/><br /><sub><b>AG</b></sub></a><br /><a href="#ideas-mzpqnxow" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=mzpqnxow" title="Documentation">📖</a></td>
<td align="center"><a href="https://n-thumann.de/"><img src="https://avatars.githubusercontent.com/u/46975855?v=4?s=100" width="100px;" alt="Nicolas Thumann"/><br /><sub><b>Nicolas Thumann</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=n-thumann" title="Code">💻</a> <a href="https://github.com/epi052/feroxbuster/commits?author=n-thumann" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/tomtastic"><img src="https://avatars.githubusercontent.com/u/302127?v=4?s=100" width="100px;" alt="Tom Matthews"/><br /><sub><b>Tom Matthews</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=tomtastic" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/bsysop"><img src="https://avatars.githubusercontent.com/u/9998303?v=4?s=100" width="100px;" alt="bsysop"/><br /><sub><b>bsysop</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=bsysop" title="Documentation">📖</a></td>
<td align="center"><a href="http://bpsizemore.me"><img src="https://avatars.githubusercontent.com/u/11645898?v=4?s=100" width="100px;" alt="Brian Sizemore"/><br /><sub><b>Brian Sizemore</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=bpsizemore" title="Code">💻</a></td>
<td align="center"><a href="https://pwn.by/noraj"><img src="https://avatars.githubusercontent.com/u/16578570?v=4?s=100" width="100px;" alt="Alexandre ZANNI"/><br /><sub><b>Alexandre ZANNI</b></sub></a><br /><a href="#infra-noraj" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=noraj" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/craig"><img src="https://avatars.githubusercontent.com/u/99729?v=4?s=100" width="100px;" alt="Craig"/><br /><sub><b>Craig</b></sub></a><br /><a href="#infra-craig" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://www.reddit.com/u/EONRaider"><img src="https://avatars.githubusercontent.com/u/15611424?v=4?s=100" width="100px;" alt="EONRaider"/><br /><sub><b>EONRaider</b></sub></a><br /><a href="#infra-EONRaider" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/wtwver"><img src="https://avatars.githubusercontent.com/u/53866088?v=4?s=100" width="100px;" alt="wtwver"/><br /><sub><b>wtwver</b></sub></a><br /><a href="#infra-wtwver" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</tr>
<tr>
<td align="center"><a href="https://tib3rius.com"><img src="https://avatars.githubusercontent.com/u/48113936?v=4?s=100" width="100px;" alt="Tib3rius"/><br /><sub><b>Tib3rius</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ATib3rius" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/0xdf"><img src="https://avatars.githubusercontent.com/u/1489045?v=4?s=100" width="100px;" alt="0xdf"/><br /><sub><b>0xdf</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3A0xdf" title="Bug reports">🐛</a></td>
<td align="center"><a href="http://secure77.de"><img src="https://avatars.githubusercontent.com/u/31564517?v=4?s=100" width="100px;" alt="secure-77"/><br /><sub><b>secure-77</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asecure-77" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/sbrun"><img src="https://avatars.githubusercontent.com/u/7712154?v=4?s=100" width="100px;" alt="Sophie Brun"/><br /><sub><b>Sophie Brun</b></sub></a><br /><a href="#infra-sbrun" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/black-A"><img src="https://avatars.githubusercontent.com/u/30686803?v=4?s=100" width="100px;" alt="black-A"/><br /><sub><b>black-A</b></sub></a><br /><a href="#ideas-black-A" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/dinosn"><img src="https://avatars.githubusercontent.com/u/3851678?v=4?s=100" width="100px;" alt="Nicolas Krassas"/><br /><sub><b>Nicolas Krassas</b></sub></a><br /><a href="#ideas-dinosn" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/N0ur5"><img src="https://avatars.githubusercontent.com/u/24260009?v=4?s=100" width="100px;" alt="N0ur5"/><br /><sub><b>N0ur5</b></sub></a><br /><a href="#ideas-N0ur5" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/moscowchill"><img src="https://avatars.githubusercontent.com/u/72578879?v=4?s=100" width="100px;" alt="mchill"/><br /><sub><b>mchill</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Amoscowchill" title="Bug reports">🐛</a></td>
<td align="center"><a href="http://BitThr3at.github.io"><img src="https://avatars.githubusercontent.com/u/45028933?v=4?s=100" width="100px;" alt="Naman"/><br /><sub><b>Naman</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ABitThr3at" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/Sicks3c"><img src="https://avatars.githubusercontent.com/u/32225186?v=4?s=100" width="100px;" alt="Ayoub Elaich"/><br /><sub><b>Ayoub Elaich</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asicks3c" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/HenryHoggard"><img src="https://avatars.githubusercontent.com/u/1208121?v=4?s=100" width="100px;" alt="Henry"/><br /><sub><b>Henry</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AHenryHoggard" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/SleepiPanda"><img src="https://avatars.githubusercontent.com/u/6428561?v=4?s=100" width="100px;" alt="SleepiPanda"/><br /><sub><b>SleepiPanda</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ASleepiPanda" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/uBadRequest"><img src="https://avatars.githubusercontent.com/u/47282747?v=4?s=100" width="100px;" alt="Bad Requests"/><br /><sub><b>Bad Requests</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AuBadRequest" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://home.dnaka91.rocks"><img src="https://avatars.githubusercontent.com/u/36804488?v=4?s=100" width="100px;" alt="Dominik Nakamura"/><br /><sub><b>Dominik Nakamura</b></sub></a><br /><a href="#infra-dnaka91" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/hunter0x8"><img src="https://avatars.githubusercontent.com/u/46222314?v=4?s=100" width="100px;" alt="Muhammad Ahsan"/><br /><sub><b>Muhammad Ahsan</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Ahunter0x8" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/cortantief"><img src="https://avatars.githubusercontent.com/u/34527333?v=4?s=100" width="100px;" alt="cortantief"/><br /><sub><b>cortantief</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Acortantief" title="Bug reports">🐛</a> <a href="https://github.com/epi052/feroxbuster/commits?author=cortantief" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/dsaxton"><img src="https://avatars.githubusercontent.com/u/2658661?v=4?s=100" width="100px;" alt="Daniel Saxton"/><br /><sub><b>Daniel Saxton</b></sub></a><br /><a href="#ideas-dsaxton" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=dsaxton" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/n0kovo"><img src="https://avatars.githubusercontent.com/u/16690056?v=4?s=100" width="100px;" alt="narkopolo"/><br /><sub><b>n0kovo</b></sub></a><br /><a href="#ideas-n0kovo" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://ring0.lol"><img src="https://avatars.githubusercontent.com/u/1893909?v=4?s=100" width="100px;" alt="Justin Steven"/><br /><sub><b>Justin Steven</b></sub></a><br /><a href="#ideas-justinsteven" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/7047payloads"><img src="https://avatars.githubusercontent.com/u/95562424?v=4?s=100" width="100px;" alt="7047payloads"/><br /><sub><b>7047payloads</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=7047payloads" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/unkn0wnsyst3m"><img src="https://avatars.githubusercontent.com/u/21272239?v=4?s=100" width="100px;" alt="unkn0wnsyst3m"/><br /><sub><b>unkn0wnsyst3m</b></sub></a><br /><a href="#ideas-unkn0wnsyst3m" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center"><a href="https://ironwort.me/"><img src="https://avatars.githubusercontent.com/u/15280042?v=4?s=100" width="100px;" alt="0x08"/><br /><sub><b>0x08</b></sub></a><br /><a href="#ideas-its0x08" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/MD-Levitan"><img src="https://avatars.githubusercontent.com/u/12116508?v=4?s=100" width="100px;" alt="kusok"/><br /><sub><b>kusok</b></sub></a><br /><a href="#ideas-MD-Levitan" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=MD-Levitan" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/godylockz"><img src="https://avatars.githubusercontent.com/u/81207744?v=4?s=100" width="100px;" alt="godylockz"/><br /><sub><b>godylockz</b></sub></a><br /><a href="#ideas-godylockz" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=godylockz" title="Code">💻</a></td>
<td align="center"><a href="http://ryanmontgomery.me"><img src="https://avatars.githubusercontent.com/u/44453666?v=4?s=100" width="100px;" alt="Ryan Montgomery"/><br /><sub><b>Ryan Montgomery</b></sub></a><br /><a href="#ideas-0dayCTF" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/IppSec"><img src="https://avatars.githubusercontent.com/u/24677271?v=4?s=100" width="100px;" alt="ippsec"/><br /><sub><b>ippsec</b></sub></a><br /><a href="#ideas-ippsec" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/gtjamesa"><img src="https://avatars.githubusercontent.com/u/2078364?v=4?s=100" width="100px;" alt="James"/><br /><sub><b>James</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Agtjamesa" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://twitter.com/Jhaddix"><img src="https://avatars.githubusercontent.com/u/3488554?v=4?s=100" width="100px;" alt="Jason Haddix"/><br /><sub><b>Jason Haddix</b></sub></a><br /><a href="#ideas-jhaddix" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/issues?q=author%3Ajhaddix" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/ThisLimn0"><img src="https://avatars.githubusercontent.com/u/67125885?v=4?s=100" width="100px;" alt="Limn0"/><br /><sub><b>Limn0</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AThisLimn0" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/0xdf223"><img src="https://avatars.githubusercontent.com/u/76954092?v=4?s=100" width="100px;" alt="0xdf"/><br /><sub><b>0xdf</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3A0xdf223" title="Bug reports">🐛</a> <a href="#ideas-0xdf223" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/Flangyver"><img src="https://avatars.githubusercontent.com/u/59575870?v=4?s=100" width="100px;" alt="Flangyver"/><br /><sub><b>Flangyver</b></sub></a><br /><a href="#ideas-Flangyver" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/DonatoReis"><img src="https://avatars.githubusercontent.com/u/93531354?v=4?s=100" width="100px;" alt="PeakyBlinder"/><br /><sub><b>PeakyBlinder</b></sub></a><br /><a href="#ideas-DonatoReis" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://postmodern.github.io/"><img src="https://avatars.githubusercontent.com/u/12671?v=4?s=100" width="100px;" alt="Postmodern"/><br /><sub><b>Postmodern</b></sub></a><br /><a href="#ideas-postmodern" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/herrcykel"><img src="https://avatars.githubusercontent.com/u/1936757?v=4?s=100" width="100px;" alt="O"/><br /><sub><b>O</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=herrcykel" title="Code">💻</a></td>
<td align="center"><a href="http://udoprog.github.io/"><img src="https://avatars.githubusercontent.com/u/111092?v=4?s=100" width="100px;" alt="John-John Tedro"/><br /><sub><b>John-John Tedro</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=udoprog" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/kmanc"><img src="https://avatars.githubusercontent.com/u/14863147?v=4?s=100" width="100px;" alt="kmanc"/><br /><sub><b>kmanc</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Akmanc" title="Bug reports">🐛</a> <a href="https://github.com/epi052/feroxbuster/commits?author=kmanc" title="Code">💻</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->

View File

@@ -59,15 +59,11 @@ fn main() {
if !config_dir.exists() {
// recursively create the feroxbuster directory and all of its parent components if
// they are missing
if !config_dir.exists() {
// recursively create the feroxbuster directory and all of its parent components if
// they are missing
if create_dir_all(&config_dir).is_err() {
// only copy the config file when we're not running in the CI/CD pipeline
// which fails with permission denied
eprintln!("Couldn't create one or more directories needed to copy the config file");
return;
}
if create_dir_all(&config_dir).is_err() {
// only copy the config file when we're not running in the CI/CD pipeline
// which fails with permission denied
eprintln!("Couldn't create one or more directories needed to copy the config file");
return;
}
}

View File

@@ -24,8 +24,8 @@ _feroxbuster() {
'--replay-proxy=[Send only unfiltered requests through a Replay Proxy, instead of all requests]:REPLAY_PROXY:_urls' \
'*-R+[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \
'*--replay-codes=[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \
'-a+[Sets the User-Agent (default: feroxbuster/2.7.1)]:USER_AGENT: ' \
'--user-agent=[Sets the User-Agent (default: feroxbuster/2.7.1)]:USER_AGENT: ' \
'-a+[Sets the User-Agent (default: feroxbuster/2.7.3)]:USER_AGENT: ' \
'--user-agent=[Sets the User-Agent (default: feroxbuster/2.7.3)]:USER_AGENT: ' \
'*-x+[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \
'*--extensions=[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \
'*-m+[Which HTTP request method(s) should be sent (default: GET)]:HTTP_METHODS: ' \
@@ -69,10 +69,6 @@ _feroxbuster() {
'-o+[Output file to write results to (use w/ --json for JSON entries)]:FILE:_files' \
'--output=[Output file to write results to (use w/ --json for JSON entries)]:FILE:_files' \
'--debug-log=[Output file to write log entries (use w/ --json for JSON entries)]:FILE:_files' \
'-h[Print help information]' \
'--help[Print help information]' \
'-V[Print version information]' \
'--version[Print version information]' \
'(-u --url)--stdin[Read url(s) from STDIN]' \
'(-p --proxy -k --insecure --burp-replay)--burp[Set --proxy to http://127.0.0.1:8080 and set --insecure to true]' \
'(-P --replay-proxy -k --insecure)--burp-replay[Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true]' \
@@ -108,6 +104,10 @@ _feroxbuster() {
'--quiet[Hide progress bars and banner (good for tmux windows w/ notifications)]' \
'--json[Emit JSON logs to --output and --debug-log instead of normal text]' \
'--no-state[Disable state output file (*.state)]' \
'-h[Print help information (use `--help` for more detail)]' \
'--help[Print help information (use `--help` for more detail)]' \
'-V[Print version information]' \
'--version[Print version information]' \
&& ret=0
}

View File

@@ -30,8 +30,8 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('--replay-proxy', 'replay-proxy', [CompletionResultType]::ParameterName, 'Send only unfiltered requests through a Replay Proxy, instead of all requests')
[CompletionResult]::new('-R', 'R', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)')
[CompletionResult]::new('--replay-codes', 'replay-codes', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)')
[CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.7.1)')
[CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.7.1)')
[CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.7.3)')
[CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.7.3)')
[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('-m', 'm', [CompletionResultType]::ParameterName, 'Which HTTP request method(s) should be sent (default: GET)')
@@ -75,10 +75,6 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('-o', 'o', [CompletionResultType]::ParameterName, 'Output file to write results to (use w/ --json for JSON entries)')
[CompletionResult]::new('--output', 'output', [CompletionResultType]::ParameterName, 'Output file to write results to (use w/ --json for JSON entries)')
[CompletionResult]::new('--debug-log', 'debug-log', [CompletionResultType]::ParameterName, 'Output file to write log entries (use w/ --json for JSON entries)')
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information')
[CompletionResult]::new('--stdin', 'stdin', [CompletionResultType]::ParameterName, 'Read url(s) from STDIN')
[CompletionResult]::new('--burp', 'burp', [CompletionResultType]::ParameterName, 'Set --proxy to http://127.0.0.1:8080 and set --insecure to true')
[CompletionResult]::new('--burp-replay', 'burp-replay', [CompletionResultType]::ParameterName, 'Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true')
@@ -114,6 +110,10 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('--quiet', 'quiet', [CompletionResultType]::ParameterName, 'Hide progress bars and banner (good for tmux windows w/ notifications)')
[CompletionResult]::new('--json', 'json', [CompletionResultType]::ParameterName, 'Emit JSON logs to --output and --debug-log instead of normal text')
[CompletionResult]::new('--no-state', 'no-state', [CompletionResultType]::ParameterName, 'Disable state output file (*.state)')
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information (use `--help` for more detail)')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information (use `--help` for more detail)')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information')
break
}
})

View File

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

View File

@@ -27,8 +27,8 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand --replay-proxy 'Send only unfiltered requests through a Replay Proxy, instead of all requests'
cand -R 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)'
cand --replay-codes 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)'
cand -a 'Sets the User-Agent (default: feroxbuster/2.7.1)'
cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.7.1)'
cand -a 'Sets the User-Agent (default: feroxbuster/2.7.3)'
cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.7.3)'
cand -x 'File extension(s) to search for (ex: -x php -x pdf js)'
cand --extensions 'File extension(s) to search for (ex: -x php -x pdf js)'
cand -m 'Which HTTP request method(s) should be sent (default: GET)'
@@ -72,10 +72,6 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand -o 'Output file to write results to (use w/ --json for JSON entries)'
cand --output 'Output file to write results to (use w/ --json for JSON entries)'
cand --debug-log 'Output file to write log entries (use w/ --json for JSON entries)'
cand -h 'Print help information'
cand --help 'Print help information'
cand -V 'Print version information'
cand --version 'Print version information'
cand --stdin 'Read url(s) from STDIN'
cand --burp 'Set --proxy to http://127.0.0.1:8080 and set --insecure to true'
cand --burp-replay 'Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true'
@@ -111,6 +107,10 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand --quiet 'Hide progress bars and banner (good for tmux windows w/ notifications)'
cand --json 'Emit JSON logs to --output and --debug-log instead of normal text'
cand --no-state 'Disable state output file (*.state)'
cand -h 'Print help information (use `--help` for more detail)'
cand --help 'Print help information (use `--help` for more detail)'
cand -V 'Print version information'
cand --version 'Print version information'
}
]
$completions[$command]

View File

@@ -9,7 +9,7 @@ use crate::{
DEFAULT_CONFIG_NAME,
};
use anyhow::{anyhow, Context, Result};
use clap::ArgMatches;
use clap::{parser::ValueSource, ArgMatches};
use regex::Regex;
use reqwest::{Client, Method, StatusCode, Url};
use serde::{Deserialize, Serialize};
@@ -22,15 +22,10 @@ use std::{
/// macro helper to abstract away repetitive configuration updates
macro_rules! update_config_if_present {
($conf_val:expr, $matches:ident, $arg_name:expr) => {
match $matches.value_of_t($arg_name) {
Ok(value) => *$conf_val = value, // Update value
Err(err) => {
if !matches!(err.kind(), clap::ErrorKind::ArgumentNotFound) {
// Do nothing if argument not found
err.exit() // Exit with error on any other parse error
}
}
($conf_val:expr, $matches:ident, $arg_name:expr, $arg_type:ty) => {
match $matches.get_one::<$arg_type>($arg_name) {
Some(value) => *$conf_val = value.to_owned(), // Update value
None => {}
}
};
}
@@ -44,6 +39,35 @@ macro_rules! update_if_not_default {
};
}
/// macro helper to abstract away repetitive checks to see if the user has specified a value
/// for a given argument from the commandline or if we just had a default value in the parser
macro_rules! came_from_cli {
($matches:ident, $arg_name:expr) => {
matches!(
$matches.value_source($arg_name),
Some(ValueSource::CommandLine)
)
};
}
/// macro helper to abstract away repetitive if not default: update checks, specifically for
/// values that are number types, i.e. usize, u64, etc
macro_rules! update_config_with_num_type_if_present {
($conf_val:expr, $matches:ident, $arg_name:expr, $arg_type:ty) => {
if let Some(val) = $matches.get_one::<String>($arg_name) {
match val.parse::<$arg_type>() {
Ok(v) => *$conf_val = v,
Err(_) => {
report_and_exit(&format!(
"Invalid value for --{}, must be a positive integer",
$arg_name
));
}
}
}
};
}
/// Represents the final, global configuration of the program.
///
/// This struct is the combination of the following:
@@ -460,7 +484,7 @@ impl Configuration {
// --resume-from used, need to first read the Configuration from disk, and then
// merge the cli_config into the resumed config
if let Some(filename) = args.value_of("resume_from") {
if let Some(filename) = args.get_one::<String>("resume_from") {
// when resuming a scan, instead of normal configuration loading, we just
// load the config from disk by calling resume_scan
let mut previous_config = resume_scan(filename);
@@ -546,18 +570,21 @@ impl Configuration {
fn parse_cli_args(args: &ArgMatches) -> Self {
let mut config = Configuration::default();
update_config_if_present!(&mut config.threads, args, "threads");
update_config_if_present!(&mut config.depth, args, "depth");
update_config_if_present!(&mut config.scan_limit, args, "scan_limit");
update_config_if_present!(&mut config.parallel, args, "parallel");
update_config_if_present!(&mut config.rate_limit, args, "rate_limit");
update_config_if_present!(&mut config.wordlist, args, "wordlist");
update_config_if_present!(&mut config.output, args, "output");
update_config_if_present!(&mut config.debug_log, args, "debug_log");
update_config_if_present!(&mut config.time_limit, args, "time_limit");
update_config_if_present!(&mut config.resume_from, args, "resume_from");
update_config_with_num_type_if_present!(&mut config.threads, args, "threads", usize);
update_config_with_num_type_if_present!(&mut config.parallel, args, "parallel", usize);
update_config_with_num_type_if_present!(&mut config.depth, args, "depth", usize);
update_config_with_num_type_if_present!(&mut config.scan_limit, args, "scan_limit", usize);
update_config_with_num_type_if_present!(&mut config.rate_limit, args, "rate_limit", usize);
update_config_if_present!(&mut config.wordlist, args, "wordlist", String);
update_config_if_present!(&mut config.output, args, "output", String);
update_config_if_present!(&mut config.debug_log, args, "debug_log", String);
update_config_if_present!(&mut config.resume_from, args, "resume_from", String);
if let Some(arg) = args.values_of("status_codes") {
if let Ok(Some(inner)) = args.try_get_one::<String>("time_limit") {
config.time_limit = inner.to_owned();
}
if let Some(arg) = args.get_many::<String>("status_codes") {
config.status_codes = arg
.map(|code| {
StatusCode::from_bytes(code.as_bytes())
@@ -567,7 +594,7 @@ impl Configuration {
.collect();
}
if let Some(arg) = args.values_of("replay_codes") {
if let Some(arg) = args.get_many::<String>("replay_codes") {
// replay codes passed in by the user
config.replay_codes = arg
.map(|code| {
@@ -581,7 +608,7 @@ impl Configuration {
config.replay_codes = config.status_codes.clone();
}
if let Some(arg) = args.values_of("filter_status") {
if let Some(arg) = args.get_many::<String>("filter_status") {
config.filter_status = arg
.map(|code| {
StatusCode::from_bytes(code.as_bytes())
@@ -591,17 +618,17 @@ impl Configuration {
.collect();
}
if let Some(arg) = args.values_of("extensions") {
if let Some(arg) = args.get_many::<String>("extensions") {
config.extensions = arg
.map(|val| val.trim_start_matches('.').to_string())
.collect();
}
if let Some(arg) = args.values_of("dont_collect") {
if let Some(arg) = args.get_many::<String>("dont_collect") {
config.dont_collect = arg.map(|val| val.to_string()).collect();
}
if let Some(arg) = args.values_of("methods") {
if let Some(arg) = args.get_many::<String>("methods") {
config.methods = arg
.map(|val| {
// Check methods if they are correct
@@ -613,7 +640,7 @@ impl Configuration {
.collect();
}
if let Some(arg) = args.value_of("data") {
if let Some(arg) = args.get_one::<String>("data") {
if let Some(stripped) = arg.strip_prefix('@') {
config.data =
std::fs::read(stripped).unwrap_or_else(|e| report_and_exit(&e.to_string()));
@@ -622,13 +649,13 @@ impl Configuration {
}
}
if args.is_present("stdin") {
if came_from_cli!(args, "stdin") {
config.stdin = true;
} else if let Some(url) = args.value_of("url") {
config.target_url = String::from(url);
} else if let Some(url) = args.get_one::<String>("url") {
config.target_url = url.into();
}
if let Some(arg) = args.values_of("url_denylist") {
if let Some(arg) = args.get_many::<String>("url_denylist") {
// compile all regular expressions and absolute urls used for --dont-scan
//
// when --dont-scan is used, the should_deny_url function is called at least once per
@@ -672,15 +699,15 @@ impl Configuration {
}
}
if let Some(arg) = args.values_of("filter_regex") {
if let Some(arg) = args.get_many::<String>("filter_regex") {
config.filter_regex = arg.map(|val| val.to_string()).collect();
}
if let Some(arg) = args.values_of("filter_similar") {
if let Some(arg) = args.get_many::<String>("filter_similar") {
config.filter_similar = arg.map(|val| val.to_string()).collect();
}
if let Some(arg) = args.values_of("filter_size") {
if let Some(arg) = args.get_many::<String>("filter_size") {
config.filter_size = arg
.map(|size| {
size.parse::<u64>()
@@ -689,7 +716,7 @@ impl Configuration {
.collect();
}
if let Some(arg) = args.values_of("filter_words") {
if let Some(arg) = args.get_many::<String>("filter_words") {
config.filter_word_count = arg
.map(|size| {
size.parse::<usize>()
@@ -698,7 +725,7 @@ impl Configuration {
.collect();
}
if let Some(arg) = args.values_of("filter_lines") {
if let Some(arg) = args.get_many::<String>("filter_lines") {
config.filter_line_count = arg
.map(|size| {
size.parse::<usize>()
@@ -707,7 +734,7 @@ impl Configuration {
.collect();
}
if args.is_present("silent") {
if came_from_cli!(args, "silent") {
// the reason this is protected by an if statement:
// consider a user specifying silent = true in ferox-config.toml
// if the line below is outside of the if, we'd overwrite true with
@@ -716,106 +743,111 @@ impl Configuration {
config.output_level = OutputLevel::Silent;
}
if args.is_present("quiet") {
if came_from_cli!(args, "quiet") {
config.quiet = true;
config.output_level = OutputLevel::Quiet;
}
if args.is_present("auto_tune") || args.is_present("smart") || args.is_present("thorough") {
if came_from_cli!(args, "auto_tune")
|| came_from_cli!(args, "smart")
|| came_from_cli!(args, "thorough")
{
config.auto_tune = true;
config.requester_policy = RequesterPolicy::AutoTune;
}
if args.is_present("auto_bail") {
if came_from_cli!(args, "auto_bail") {
config.auto_bail = true;
config.requester_policy = RequesterPolicy::AutoBail;
}
if args.is_present("no_state") {
if came_from_cli!(args, "no_state") {
config.save_state = false;
}
if args.is_present("dont_filter") {
if came_from_cli!(args, "dont_filter") {
config.dont_filter = true;
}
if args.is_present("collect_extensions") || args.is_present("thorough") {
if came_from_cli!(args, "collect_extensions") || came_from_cli!(args, "thorough") {
config.collect_extensions = true;
}
if args.is_present("collect_backups")
|| args.is_present("smart")
|| args.is_present("thorough")
if came_from_cli!(args, "collect_backups")
|| came_from_cli!(args, "smart")
|| came_from_cli!(args, "thorough")
{
config.collect_backups = true;
}
if args.is_present("collect_words")
|| args.is_present("smart")
|| args.is_present("thorough")
if came_from_cli!(args, "collect_words")
|| came_from_cli!(args, "smart")
|| came_from_cli!(args, "thorough")
{
config.collect_words = true;
}
if args.occurrences_of("verbosity") > 0 {
if args.get_count("verbosity") > 0 {
// occurrences_of returns 0 if none are found; this is protected in
// an if block for the same reason as the quiet option
config.verbosity = args.occurrences_of("verbosity") as u8;
config.verbosity = args.get_count("verbosity");
}
if args.is_present("no_recursion") {
if came_from_cli!(args, "no_recursion") {
config.no_recursion = true;
}
if args.is_present("add_slash") {
if came_from_cli!(args, "add_slash") {
config.add_slash = true;
}
if args.is_present("extract_links")
|| args.is_present("smart")
|| args.is_present("thorough")
if came_from_cli!(args, "extract_links")
|| came_from_cli!(args, "smart")
|| came_from_cli!(args, "thorough")
{
config.extract_links = true;
}
if args.is_present("json") {
if came_from_cli!(args, "json") {
config.json = true;
}
if args.is_present("force_recursion") {
if came_from_cli!(args, "force_recursion") {
config.force_recursion = true;
}
////
// organizational breakpoint; all options below alter the Client configuration
////
update_config_if_present!(&mut config.proxy, args, "proxy");
update_config_if_present!(&mut config.replay_proxy, args, "replay_proxy");
update_config_if_present!(&mut config.user_agent, args, "user_agent");
update_config_if_present!(&mut config.timeout, args, "timeout");
update_config_if_present!(&mut config.proxy, args, "proxy", String);
update_config_if_present!(&mut config.replay_proxy, args, "replay_proxy", String);
update_config_if_present!(&mut config.user_agent, args, "user_agent", String);
update_config_with_num_type_if_present!(&mut config.timeout, args, "timeout", u64);
if args.is_present("burp") {
if came_from_cli!(args, "burp") {
config.proxy = String::from("http://127.0.0.1:8080");
}
if args.is_present("burp_replay") {
if came_from_cli!(args, "burp_replay") {
config.replay_proxy = String::from("http://127.0.0.1:8080");
}
if args.is_present("random_agent") {
if came_from_cli!(args, "random_agent") {
config.random_agent = true;
}
if args.is_present("redirects") {
if came_from_cli!(args, "redirects") {
config.redirects = true;
}
if args.is_present("insecure") || args.is_present("burp") || args.is_present("burp_replay")
if came_from_cli!(args, "insecure")
|| came_from_cli!(args, "burp")
|| came_from_cli!(args, "burp_replay")
{
config.insecure = true;
}
if let Some(headers) = args.values_of("headers") {
if let Some(headers) = args.get_many::<String>("headers") {
for val in headers {
let mut split_val = val.split(':');
@@ -829,7 +861,7 @@ impl Configuration {
}
}
if let Some(cookies) = args.values_of("cookies") {
if let Some(cookies) = args.get_many::<String>("cookies") {
config.headers.insert(
// we know the header name is always "cookie"
"Cookie".to_string(),
@@ -845,7 +877,7 @@ impl Configuration {
);
}
if let Some(queries) = args.values_of("queries") {
if let Some(queries) = args.get_many::<String>("queries") {
for val in queries {
// same basic logic used as reading in the headers HashMap above
let mut split_val = val.split('=');
@@ -957,7 +989,7 @@ impl Configuration {
update_if_not_default!(&mut conf.force_recursion, new.force_recursion, 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.methods, new.methods, Vec::<String>::new());
update_if_not_default!(&mut conf.methods, new.methods, methods());
update_if_not_default!(&mut conf.data, new.data, Vec::<u8>::new());
update_if_not_default!(&mut conf.url_denylist, new.url_denylist, Vec::<Url>::new());
if !new.regex_denylist.is_empty() {

View File

@@ -81,7 +81,7 @@ pub(super) fn depth() -> usize {
}
/// enum representing the three possible states for informational output (not logging verbosity)
#[derive(Debug, Copy, Clone, PartialEq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum OutputLevel {
/// normal scan, no --quiet|--silent
Default,
@@ -116,7 +116,7 @@ pub fn determine_output_level(quiet: bool, silent: bool) -> OutputLevel {
}
/// represents actions the Requester should take in certain situations
#[derive(Debug, PartialEq, Copy, Clone)]
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum RequesterPolicy {
/// automatically try to lower request rate in order to reduce errors
AutoTune,

View File

@@ -101,10 +101,13 @@ impl TermInputHandler {
handles.filters.data.clone(),
);
let state_file = open_file(&filename);
// User didn't set the --no-state flag (so saved_state is still the default true)
if handles.config.save_state {
let state_file = open_file(&filename);
let mut buffered_file = state_file?;
write_to(&state, &mut buffered_file, true)?;
let mut buffered_file = state_file?;
write_to(&state, &mut buffered_file, true)?;
}
log::trace!("exit: sigint_handler (end of program)");
std::process::exit(1);

View File

@@ -13,6 +13,11 @@ pub(super) const LINKFINDER_REGEX: &str = r#"(?:"|')(((?:[a-zA-Z]{1,10}://|//)[^
pub(super) const ROBOTS_TXT_REGEX: &str =
r#"(?m)^ *(Allow|Disallow): *(?P<url_path>[a-zA-Z0-9._/?#@!&'()+,;%=-]+?)$"#; // multi-line (?m)
/// Regular expression to filter bad characters from extracted url paths
///
/// ref: https://www.rfc-editor.org/rfc/rfc3986#section-2
pub(super) const URL_CHARS_REGEX: &str = r#"["<>\\^`{|} ]"#;
/// Which type of extraction should be performed
#[derive(Debug, Copy, Clone)]
pub enum ExtractionTarget {
@@ -90,6 +95,7 @@ impl<'a> ExtractorBuilder<'a> {
Ok(Extractor {
links_regex: Regex::new(LINKFINDER_REGEX).unwrap(),
robots_regex: Regex::new(ROBOTS_TXT_REGEX).unwrap(),
url_regex: Regex::new(URL_CHARS_REGEX).unwrap(),
response: if self.response.is_some() {
Some(self.response.unwrap())
} else {

View File

@@ -17,7 +17,7 @@ use crate::{
use anyhow::{bail, Context, Result};
use reqwest::{Client, StatusCode, Url};
use scraper::{Html, Selector};
use std::collections::HashSet;
use std::{borrow::Cow, collections::HashSet};
/// Whether an active scan is recursive or not
#[derive(Debug)]
@@ -38,6 +38,9 @@ pub struct Extractor<'a> {
/// `ROBOTS_TXT_REGEX` as a regex::Regex type
pub(super) robots_regex: Regex,
/// regex to validate a url
pub(super) url_regex: Regex,
/// Response from which to extract links
pub(super) response: Option<&'a FeroxResponse>,
@@ -220,6 +223,7 @@ impl<'a> Extractor<'a> {
self.extract_links_by_attr(resp_url, links, html, "div", "src");
self.extract_links_by_attr(resp_url, links, html, "frame", "src");
self.extract_links_by_attr(resp_url, links, html, "embed", "src");
self.extract_links_by_attr(resp_url, links, html, "link", "href");
}
/// Given the body of a `reqwest::Response`, perform the following actions
@@ -332,8 +336,9 @@ impl<'a> Extractor<'a> {
let normalized_path = self.normalize_url_path(path);
// filter out any empty strings caused by .split
let mut parts: Vec<&str> = normalized_path
let mut parts: Vec<Cow<_>> = normalized_path
.split('/')
.map(|s| self.url_regex.replace_all(s, ""))
.filter(|s| !s.is_empty())
.collect();
@@ -392,6 +397,17 @@ impl<'a> Extractor<'a> {
.join(link)
.with_context(|| format!("Could not join {} with {}", old_url, link))?;
if old_url.domain() != new_url.domain() || old_url.host() != old_url.host() {
// domains/ips are not the same, don't scan things that aren't part of the original
// target url
log::debug!(
"Skipping {} because it's not part of the original target",
new_url
);
log::trace!("exit: add_link_to_set_of_links");
return Ok(());
}
links.insert(new_url.to_string());
log::trace!("exit: add_link_to_set_of_links");
@@ -413,7 +429,7 @@ impl<'a> Extractor<'a> {
let scanned_urls = self.handles.ferox_scans()?;
if scanned_urls.get_scan_by_url(&new_url.to_string()).is_some() {
if scanned_urls.get_scan_by_url(new_url.as_ref()).is_some() {
//we've seen the url before and don't need to scan again
log::trace!("exit: request_link -> None");
bail!("previously seen url");

View File

@@ -1,4 +1,4 @@
use super::builder::{LINKFINDER_REGEX, ROBOTS_TXT_REGEX};
use super::builder::{LINKFINDER_REGEX, ROBOTS_TXT_REGEX, URL_CHARS_REGEX};
use super::*;
use crate::config::{Configuration, OutputLevel};
use crate::scan_manager::ScanOrder;
@@ -273,6 +273,7 @@ async fn extractor_get_links_with_absolute_url_that_differs_from_target_domain()
let extractor = Extractor {
links_regex: Regex::new(LINKFINDER_REGEX).unwrap(),
robots_regex: Regex::new(ROBOTS_TXT_REGEX).unwrap(),
url_regex: Regex::new(URL_CHARS_REGEX).unwrap(),
response: Some(&ferox_response),
url: String::new(),
target: ExtractionTarget::ResponseBody,
@@ -301,6 +302,7 @@ async fn request_robots_txt_without_proxy() -> Result<()> {
let extractor = Extractor {
links_regex: Regex::new(LINKFINDER_REGEX).unwrap(),
robots_regex: Regex::new(ROBOTS_TXT_REGEX).unwrap(),
url_regex: Regex::new(URL_CHARS_REGEX).unwrap(),
response: None,
url: srv.url("/api/users/stuff/things"),
target: ExtractionTarget::RobotsTxt,

View File

@@ -1,7 +1,7 @@
use super::*;
/// Dummy filter for internal shenanigans
#[derive(Default, Debug, PartialEq)]
#[derive(Default, Debug, PartialEq, Eq)]
pub struct EmptyFilter {}
impl FeroxFilter for EmptyFilter {

View File

@@ -2,7 +2,7 @@ use super::*;
/// Simple implementor of FeroxFilter; used to filter out responses based on the number of lines
/// in a Response body; specified using -N|--filter-lines
#[derive(Default, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct LinesFilter {
/// Number of lines in a Response's body that should be filtered
pub line_count: usize,

View File

@@ -3,7 +3,7 @@ use fuzzyhash::FuzzyHash;
/// Simple implementor of FeroxFilter; used to filter out responses based on the similarity of a
/// Response body with a known response; specified using --filter-similar-to
#[derive(Default, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SimilarityFilter {
/// Hash of Response's body to be used during similarity comparison
pub hash: String,
@@ -20,9 +20,9 @@ impl FeroxFilter for SimilarityFilter {
/// Check `FeroxResponse::text` against what was requested from the site passed in via
/// --filter-similar-to
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
let other = FuzzyHash::new(&response.text());
let other = FuzzyHash::new(response.text());
if let Ok(result) = FuzzyHash::compare(&self.hash, &other.to_string()) {
if let Ok(result) = FuzzyHash::compare(&self.hash, other.to_string()) {
return result >= self.threshold;
}

View File

@@ -2,7 +2,7 @@ use super::*;
/// Simple implementor of FeroxFilter; used to filter out responses based on the length of a
/// Response body; specified using -S|--filter-size
#[derive(Default, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SizeFilter {
/// Overall length of a Response's body that should be filtered
pub content_length: u64,

View File

@@ -2,7 +2,7 @@ use super::*;
/// Simple implementor of FeroxFilter; used to filter out status codes specified using
/// -C|--filter-status
#[derive(Default, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct StatusCodeFilter {
/// Status code that should not be displayed to the user
pub filter_code: u16,

View File

@@ -41,7 +41,7 @@ pub(crate) async fn create_similarity_filter(
}
// hash the response body and store the resulting hash in the filter object
let hash = FuzzyHash::new(&fr.text()).to_string();
let hash = FuzzyHash::new(fr.text()).to_string();
Ok(SimilarityFilter {
hash,

View File

@@ -9,7 +9,7 @@ use crate::{url::FeroxUrl, DEFAULT_METHOD};
///
/// `size` is size of the response that should be included with filters passed via runtime
/// configuration and any static wildcard lengths.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WildcardFilter {
/// size of the response that will later be combined with the length of the path of the url
/// requested

View File

@@ -2,7 +2,7 @@ use super::*;
/// Simple implementor of FeroxFilter; used to filter out responses based on the number of words
/// in a Response body; specified using -W|--filter-words
#[derive(Default, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WordsFilter {
/// Number of words in a Response's body that should be filtered
pub word_count: usize,

View File

@@ -65,7 +65,7 @@ pub fn initialize(config: Arc<Configuration>) -> Result<()> {
kind: "log".to_string(),
};
PROGRESS_PRINTER.println(&log_entry.as_str());
PROGRESS_PRINTER.println(log_entry.as_str());
if let Some(buffered_file) = file.clone() {
if let Ok(mut unlocked) = buffered_file.write() {

View File

@@ -44,11 +44,12 @@ lazy_static! {
static ref PARALLEL_LIMITER: Semaphore = Semaphore::new(0);
}
/// Create a HashSet of Strings from the given wordlist then stores it inside an Arc
/// Create a Vec of Strings from the given wordlist then stores it inside an Arc
fn get_unique_words_from_wordlist(path: &str) -> Result<Arc<Vec<String>>> {
log::trace!("enter: get_unique_words_from_wordlist({})", path);
let mut trimmed_word = false;
let file = File::open(&path).with_context(|| format!("Could not open {}", path))?;
let file = File::open(path).with_context(|| format!("Could not open {}", path))?;
let reader = BufReader::new(file);
@@ -61,12 +62,21 @@ fn get_unique_words_from_wordlist(path: &str) -> Result<Arc<Vec<String>>> {
for line in reader.lines() {
line.map(|result| {
if !result.starts_with('#') && !result.is_empty() {
words.push(result);
if result.starts_with('/') {
words.push(result.trim_start_matches('/').to_string());
trimmed_word = true;
} else {
words.push(result);
}
}
})
.ok();
}
if trimmed_word {
log::warn!("Some words in the wordlist started with a leading forward-slash; those words were trimmed (i.e. /word -> word)");
}
log::trace!(
"exit: get_unique_words_from_wordlist -> Arc<wordlist[{} words...]>",
words.len()
@@ -328,7 +338,7 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
let new_folder = slugify_filename(&base_name.to_string_lossy(), "", "logs");
let final_path = output_path.with_file_name(&new_folder);
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

View File

@@ -1,3 +1,4 @@
use clap::ArgAction;
use clap::{
crate_authors, crate_description, crate_name, crate_version, Arg, ArgGroup, Command, ValueHint,
};
@@ -25,7 +26,7 @@ lazy_static! {
}
/// Create and return an instance of [clap::App](https://docs.rs/clap/latest/clap/struct.App.html), i.e. the Command Line Interface's configuration
pub fn initialize() -> Command<'static> {
pub fn initialize() -> Command {
let app = Command::new(crate_name!())
.version(crate_version!())
.author(crate_authors!())
@@ -39,7 +40,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("url")
.short('u')
.long("url")
.required_unless_present_any(&["stdin", "resume_from"])
.required_unless_present_any(["stdin", "resume_from"])
.help_heading("Target selection")
.value_name("URL")
.use_value_delimiter(true)
@@ -50,7 +51,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("stdin")
.long("stdin")
.help_heading("Target selection")
.takes_value(false)
.num_args(0)
.help("Read url(s) from STDIN")
.conflicts_with("url")
)
@@ -62,7 +63,7 @@ pub fn initialize() -> Command<'static> {
.help_heading("Target selection")
.help("State file from which to resume a partially complete scan (ex. --resume-from ferox-1606586780.state)")
.conflicts_with("url")
.takes_value(true),
.num_args(1),
);
/////////////////////////////////////////////////////////////////////
@@ -72,25 +73,29 @@ pub fn initialize() -> Command<'static> {
.arg(
Arg::new("burp")
.long("burp")
.num_args(0)
.help_heading("Composite settings")
.conflicts_with_all(&["proxy", "insecure", "burp_replay"])
.conflicts_with_all(["proxy", "insecure", "burp_replay"])
.help("Set --proxy to http://127.0.0.1:8080 and set --insecure to true"),
)
.arg(
Arg::new("burp_replay")
.long("burp-replay")
.num_args(0)
.help_heading("Composite settings")
.conflicts_with_all(&["replay_proxy", "insecure"])
.conflicts_with_all(["replay_proxy", "insecure"])
.help("Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true"),
)
.arg(
Arg::new("smart")
.long("smart")
.num_args(0)
.help_heading("Composite settings")
.help("Set --extract-links, --auto-tune, --collect-words, and --collect-backups to true"),
).arg(
Arg::new("thorough")
.long("thorough")
.num_args(0)
.help_heading("Composite settings")
.help("Use the same settings as --smart and set --collect-extensions to true"),
);
@@ -103,7 +108,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("proxy")
.short('p')
.long("proxy")
.takes_value(true)
.num_args(1)
.value_name("PROXY")
.value_hint(ValueHint::Url)
.help_heading("Proxy settings")
@@ -115,7 +120,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("replay_proxy")
.short('P')
.long("replay-proxy")
.takes_value(true)
.num_args(1)
.value_hint(ValueHint::Url)
.value_name("REPLAY_PROXY")
.help_heading("Proxy settings")
@@ -128,9 +133,8 @@ pub fn initialize() -> Command<'static> {
.short('R')
.long("replay-codes")
.value_name("REPLAY_CODE")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.requires("replay_proxy")
.help_heading("Proxy settings")
@@ -148,7 +152,7 @@ pub fn initialize() -> Command<'static> {
.short('a')
.long("user-agent")
.value_name("USER_AGENT")
.takes_value(true)
.num_args(1)
.help_heading("Request settings")
.help(&**DEFAULT_USER_AGENT),
)
@@ -156,7 +160,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("random_agent")
.short('A')
.long("random-agent")
.takes_value(false)
.num_args(0)
.help_heading("Request settings")
.help("Use a random User-Agent"),
)
@@ -165,9 +169,8 @@ pub fn initialize() -> Command<'static> {
.short('x')
.long("extensions")
.value_name("FILE_EXTENSION")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Request settings")
.help(
@@ -179,9 +182,8 @@ pub fn initialize() -> Command<'static> {
.short('m')
.long("methods")
.value_name("HTTP_METHODS")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Request settings")
.help(
@@ -192,7 +194,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("data")
.long("data")
.value_name("DATA")
.takes_value(true)
.num_args(1)
.help_heading("Request settings")
.help(
"Request's Body; can read data from a file if input starts with an @ (ex: @post.bin)",
@@ -203,10 +205,9 @@ pub fn initialize() -> Command<'static> {
.short('H')
.long("headers")
.value_name("HEADER")
.takes_value(true)
.num_args(1..)
.action(ArgAction::Append)
.help_heading("Request settings")
.multiple_values(true)
.multiple_occurrences(true)
.use_value_delimiter(true)
.help(
"Specify HTTP headers to be used in each request (ex: -H Header:val -H 'stuff: things')",
@@ -217,9 +218,8 @@ pub fn initialize() -> Command<'static> {
.short('b')
.long("cookies")
.value_name("COOKIE")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Request settings")
.help(
@@ -231,9 +231,8 @@ pub fn initialize() -> Command<'static> {
.short('Q')
.long("query")
.value_name("QUERY")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Request settings")
.help(
@@ -245,7 +244,7 @@ pub fn initialize() -> Command<'static> {
.short('f')
.long("add-slash")
.help_heading("Request settings")
.takes_value(false)
.num_args(0)
.help("Append / to each request's URL")
);
@@ -256,9 +255,8 @@ pub fn initialize() -> Command<'static> {
Arg::new("url_denylist")
.long("dont-scan")
.value_name("URL")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Request filters")
.help("URL(s) or Regex Pattern(s) to exclude from recursion/scans"),
@@ -273,9 +271,8 @@ pub fn initialize() -> Command<'static> {
.short('S')
.long("filter-size")
.value_name("SIZE")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Response filters")
.help(
@@ -287,9 +284,8 @@ pub fn initialize() -> Command<'static> {
.short('X')
.long("filter-regex")
.value_name("REGEX")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Response filters")
.help(
@@ -301,9 +297,8 @@ pub fn initialize() -> Command<'static> {
.short('W')
.long("filter-words")
.value_name("WORDS")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Response filters")
.help(
@@ -315,9 +310,8 @@ pub fn initialize() -> Command<'static> {
.short('N')
.long("filter-lines")
.value_name("LINES")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Response filters")
.help(
@@ -329,9 +323,8 @@ pub fn initialize() -> Command<'static> {
.short('C')
.long("filter-status")
.value_name("STATUS_CODE")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.conflicts_with("status_codes")
.help_heading("Response filters")
@@ -343,9 +336,8 @@ pub fn initialize() -> Command<'static> {
Arg::new("filter_similar")
.long("filter-similar-to")
.value_name("UNWANTED_PAGE")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.value_hint(ValueHint::Url)
.use_value_delimiter(true)
.help_heading("Response filters")
@@ -358,9 +350,8 @@ pub fn initialize() -> Command<'static> {
.short('s')
.long("status-codes")
.value_name("STATUS_CODE")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Response filters")
.help(
@@ -377,7 +368,7 @@ pub fn initialize() -> Command<'static> {
.short('T')
.long("timeout")
.value_name("SECONDS")
.takes_value(true)
.num_args(1)
.help_heading("Client settings")
.help("Number of seconds before a client's request times out (default: 7)"),
)
@@ -385,7 +376,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("redirects")
.short('r')
.long("redirects")
.takes_value(false)
.num_args(0)
.help_heading("Client settings")
.help("Allow client to follow redirects"),
)
@@ -393,7 +384,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("insecure")
.short('k')
.long("insecure")
.takes_value(false)
.num_args(0)
.help_heading("Client settings")
.help("Disables TLS certificate validation in the client"),
);
@@ -407,7 +398,7 @@ pub fn initialize() -> Command<'static> {
.short('t')
.long("threads")
.value_name("THREADS")
.takes_value(true)
.num_args(1)
.help_heading("Scan settings")
.help("Number of concurrent threads (default: 50)"),
)
@@ -415,7 +406,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("no_recursion")
.short('n')
.long("no-recursion")
.takes_value(false)
.num_args(0)
.help_heading("Scan settings")
.help("Do not scan recursively"),
)
@@ -424,12 +415,13 @@ pub fn initialize() -> Command<'static> {
.short('d')
.long("depth")
.value_name("RECURSION_DEPTH")
.takes_value(true)
.num_args(1)
.help_heading("Scan settings")
.help("Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)"),
).arg(
Arg::new("force_recursion")
.long("force-recursion")
.num_args(0)
.conflicts_with("no_recursion")
.help_heading("Scan settings")
.help("Force recursion attempts on all 'found' endpoints (still respects recursion depth)"),
@@ -437,7 +429,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("extract_links")
.short('e')
.long("extract-links")
.takes_value(false)
.num_args(0)
.help_heading("Scan settings")
.help("Extract links from response body (html, javascript, etc...); make new requests based on findings")
)
@@ -446,7 +438,7 @@ pub fn initialize() -> Command<'static> {
.short('L')
.long("scan-limit")
.value_name("SCAN_LIMIT")
.takes_value(true)
.num_args(1)
.help_heading("Scan settings")
.help("Limit total number of concurrent scans (default: 0, i.e. no limit)")
)
@@ -454,7 +446,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("parallel")
.long("parallel")
.value_name("PARALLEL_SCANS")
.takes_value(true)
.num_args(1)
.requires("stdin")
.help_heading("Scan settings")
.help("Run parallel feroxbuster instances (one child process per url passed via stdin)")
@@ -463,7 +455,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("rate_limit")
.long("rate-limit")
.value_name("RATE_LIMIT")
.takes_value(true)
.num_args(1)
.conflicts_with("auto_tune")
.help_heading("Scan settings")
.help("Limit number of requests per second (per directory) (default: 0, i.e. no limit)")
@@ -472,8 +464,8 @@ pub fn initialize() -> Command<'static> {
Arg::new("time_limit")
.long("time-limit")
.value_name("TIME_SPEC")
.takes_value(true)
.validator(valid_time_spec)
.num_args(1)
.value_parser(valid_time_spec)
.help_heading("Scan settings")
.help("Limit total run time of all scans (ex: --time-limit 10m)")
)
@@ -485,11 +477,11 @@ pub fn initialize() -> Command<'static> {
.value_name("FILE")
.help("Path to the wordlist")
.help_heading("Scan settings")
.takes_value(true),
.num_args(1),
).arg(
Arg::new("auto_tune")
.long("auto-tune")
.takes_value(false)
.num_args(0)
.conflicts_with("auto_bail")
.help_heading("Scan settings")
.help("Automatically lower scan rate when an excessive amount of errors are encountered")
@@ -497,35 +489,35 @@ pub fn initialize() -> Command<'static> {
.arg(
Arg::new("auto_bail")
.long("auto-bail")
.takes_value(false)
.num_args(0)
.help_heading("Scan settings")
.help("Automatically stop scanning when an excessive amount of errors are encountered")
).arg(
Arg::new("dont_filter")
.short('D')
.long("dont-filter")
.takes_value(false)
.num_args(0)
.help_heading("Scan settings")
.help("Don't auto-filter wildcard responses")
).arg(
Arg::new("collect_extensions")
.short('E')
.long("collect-extensions")
.takes_value(false)
.num_args(0)
.help_heading("Dynamic collection settings")
.help("Automatically discover extensions and add them to --extensions (unless they're in --dont-collect)")
).arg(
Arg::new("collect_backups")
.short('B')
.long("collect-backups")
.takes_value(false)
.num_args(0)
.help_heading("Dynamic collection settings")
.help("Automatically request likely backup extensions for \"found\" urls")
).arg(
Arg::new("collect_words")
.short('g')
.long("collect-words")
.takes_value(false)
.num_args(0)
.help_heading("Dynamic collection settings")
.help("Automatically discover important words from within responses and add them to the wordlist")
).arg(
@@ -533,9 +525,8 @@ pub fn initialize() -> Command<'static> {
.short('I')
.long("dont-collect")
.value_name("FILE_EXTENSION")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Dynamic collection settings")
.help(
@@ -551,15 +542,15 @@ pub fn initialize() -> Command<'static> {
Arg::new("verbosity")
.short('v')
.long("verbosity")
.takes_value(false)
.multiple_occurrences(true)
.num_args(0)
.action(ArgAction::Count)
.conflicts_with("silent")
.help_heading("Output settings")
.help("Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v's is probably too much)"),
).arg(
Arg::new("silent")
.long("silent")
.takes_value(false)
.num_args(0)
.conflicts_with("quiet")
.help_heading("Output settings")
.help("Only print URLs + turn off logging (good for piping a list of urls to other commands)")
@@ -568,7 +559,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("quiet")
.short('q')
.long("quiet")
.takes_value(false)
.num_args(0)
.help_heading("Output settings")
.help("Hide progress bars and banner (good for tmux windows w/ notifications)")
)
@@ -576,7 +567,7 @@ pub fn initialize() -> Command<'static> {
.arg(
Arg::new("json")
.long("json")
.takes_value(false)
.num_args(0)
.requires("output_files")
.help_heading("Output settings")
.help("Emit JSON logs to --output and --debug-log instead of normal text")
@@ -588,7 +579,7 @@ pub fn initialize() -> Command<'static> {
.value_name("FILE")
.help_heading("Output settings")
.help("Output file to write results to (use w/ --json for JSON entries)")
.takes_value(true),
.num_args(1),
)
.arg(
Arg::new("debug_log")
@@ -597,12 +588,12 @@ pub fn initialize() -> Command<'static> {
.value_hint(ValueHint::FilePath)
.help_heading("Output settings")
.help("Output file to write log entries (use w/ --json for JSON entries)")
.takes_value(true),
.num_args(1),
)
.arg(
Arg::new("no_state")
.long("no-state")
.takes_value(false)
.num_args(0)
.help_heading("Output settings")
.help("Disable state output file (*.state)")
);
@@ -613,7 +604,7 @@ pub fn initialize() -> Command<'static> {
let mut app = app
.group(
ArgGroup::new("output_files")
.args(&["debug_log", "output"])
.args(["debug_log", "output"])
.multiple(true),
)
.after_long_help(EPILOGUE);
@@ -641,9 +632,9 @@ pub fn initialize() -> Command<'static> {
}
/// Validate that a string is formatted as a number followed by s, m, h, or d (10d, 30s, etc...)
fn valid_time_spec(time_spec: &str) -> Result<(), String> {
fn valid_time_spec(time_spec: &str) -> Result<String, String> {
match TIMESPEC_REGEX.is_match(time_spec) {
true => Ok(()),
true => Ok(time_spec.to_string()),
false => {
let msg = format!(
"Expected a non-negative, whole number followed by s, m, h, or d (case insensitive); received {}",
@@ -687,7 +678,7 @@ EXAMPLES:
./feroxbuster -u http://127.1 --extract-links
Ludicrous speed... go!
./feroxbuster -u http://127.1 -threads 200
./feroxbuster -u http://127.1 --threads 200
Limit to a total of 60 active requests at any given time (threads * scan limit)
./feroxbuster -u http://127.1 --threads 30 --scan-limit 2

View File

@@ -415,7 +415,18 @@ impl FeroxSerialize for FeroxResponse {
.get("Location")
.unwrap() // known Some() already
.to_str()
.unwrap_or("Unknown");
.unwrap_or("Unknown")
.to_string();
let loc = if loc.starts_with('/') {
if let Ok(joined) = self.url().join(&loc) {
joined.to_string()
} else {
loc
}
} else {
loc
};
// prettify the redirect target
let loc = style(loc).yellow();

View File

@@ -32,6 +32,9 @@ pub struct FeroxScan {
/// The URL that to be scanned
pub(super) url: String,
/// A url used solely for comparison to other URLs
pub(super) normalized_url: String,
/// The type of scan
pub scan_type: ScanType,
@@ -79,6 +82,7 @@ impl Default for FeroxScan {
num_requests: 0,
scan_order: ScanOrder::Latest,
url: String::new(),
normalized_url: String::new(),
progress_bar: Mutex::new(None),
scan_type: ScanType::File,
output_level: Default::default(),
@@ -191,6 +195,7 @@ impl FeroxScan {
) -> Arc<Self> {
Arc::new(Self {
url: url.to_string(),
normalized_url: format!("{}/", url.trim_end_matches('/')),
scan_type,
scan_order,
num_requests,
@@ -228,6 +233,14 @@ impl FeroxScan {
false
}
/// small wrapper to inspect ScanStatus and see if it's Cancelled
pub fn is_cancelled(&self) -> bool {
if let Ok(guard) = self.status.lock() {
return matches!(*guard, ScanStatus::Cancelled);
}
false
}
/// await a task's completion, similar to a thread's join; perform necessary bookkeeping
pub async fn join(&self) {
log::trace!("enter join({:?})", self);
@@ -332,10 +345,11 @@ impl Serialize for FeroxScan {
where
S: Serializer,
{
let mut state = serializer.serialize_struct("FeroxScan", 4)?;
let mut state = serializer.serialize_struct("FeroxScan", 6)?;
state.serialize_field("id", &self.id)?;
state.serialize_field("url", &self.url)?;
state.serialize_field("normalized_url", &self.normalized_url)?;
state.serialize_field("scan_type", &self.scan_type)?;
state.serialize_field("status", &self.status)?;
state.serialize_field("num_requests", &self.num_requests)?;
@@ -387,6 +401,11 @@ impl<'de> Deserialize<'de> for FeroxScan {
scan.url = url.to_string();
}
}
"normalized_url" => {
if let Some(normalized_url) = value.as_str() {
scan.normalized_url = normalized_url.to_string();
}
}
"num_requests" => {
if let Some(num_requests) = value.as_u64() {
scan.num_requests = num_requests;
@@ -480,6 +499,7 @@ mod tests {
let scan = FeroxScan {
id: "".to_string(),
url: "".to_string(),
normalized_url: String::from("/"),
scan_type: ScanType::Directory,
scan_order: ScanOrder::Initial,
num_requests: 0,

View File

@@ -75,7 +75,7 @@ impl Serialize for FeroxScans {
let mut seq = serializer.serialize_seq(Some(scans.len() + 1))?;
for scan in scans.iter() {
seq.serialize_element(&*scan).unwrap_or_default();
seq.serialize_element(scan).unwrap_or_default();
}
seq.end()
}
@@ -138,6 +138,15 @@ impl FeroxScans {
let mut deser_scan: FeroxScan =
serde_json::from_value(scan.clone()).unwrap_or_default();
if deser_scan.is_cancelled() {
// if the scan was cancelled by the user, mark it as complete. This will
// prevent the scan from being resumed as well as prevent the wordlist
// from requesting it again
if let Ok(mut guard) = deser_scan.status.lock() {
*guard = ScanStatus::Complete;
}
}
// FeroxScans gets -q value from config as usual; the FeroxScans themselves
// rely on that value being passed in. If the user starts a scan without -q
// and resumes the scan but adds -q, FeroxScan will not have the proper value
@@ -213,8 +222,10 @@ impl FeroxScans {
/// on the given URL
pub fn contains(&self, url: &str) -> bool {
if let Ok(scans) = self.scans.read() {
let normalized = format!("{}/", url.trim_end_matches('/'));
for scan in scans.iter() {
if scan.url == url {
if scan.normalized_url == normalized {
return true;
}
}
@@ -225,8 +236,10 @@ impl FeroxScans {
/// Find and return a `FeroxScan` based on the given URL
pub fn get_scan_by_url(&self, url: &str) -> Option<Arc<FeroxScan>> {
if let Ok(guard) = self.scans.read() {
let normalized = format!("{}/", url.trim_end_matches('/'));
for scan in guard.iter() {
if scan.url == url {
if scan.normalized_url == normalized {
return Some(scan.clone());
}
}
@@ -589,7 +602,8 @@ 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)
let normalized = format!("{}/", url.trim_end_matches('/'));
self.add_scan(&normalized, ScanType::Directory, scan_order)
}
/// Given a url, create a new `FeroxScan` and add it to `FeroxScans` as a File Scan

View File

@@ -277,7 +277,7 @@ fn ferox_scan_serialize() {
None,
);
let fs_json = format!(
r#"{{"id":"{}","url":"https://spiritanimal.com","scan_type":"Directory","status":"NotStarted","num_requests":0}}"#,
r#"{{"id":"{}","url":"https://spiritanimal.com","normalized_url":"https://spiritanimal.com/","scan_type":"Directory","status":"NotStarted","num_requests":0}}"#,
fs.id
);
assert_eq!(fs_json, serde_json::to_string(&*fs).unwrap());
@@ -296,7 +296,7 @@ fn ferox_scans_serialize() {
);
let ferox_scans = FeroxScans::default();
let ferox_scans_json = format!(
r#"[{{"id":"{}","url":"https://spiritanimal.com","scan_type":"Directory","status":"NotStarted","num_requests":0}}]"#,
r#"[{{"id":"{}","url":"https://spiritanimal.com","normalized_url":"https://spiritanimal.com/","scan_type":"Directory","status":"NotStarted","num_requests":0}}]"#,
ferox_scan.id
);
ferox_scans.scans.write().unwrap().push(ferox_scan);
@@ -556,6 +556,7 @@ fn feroxscan_display() {
let scan = FeroxScan {
id: "".to_string(),
url: String::from("http://localhost"),
normalized_url: String::from("http://localhost/"),
scan_order: ScanOrder::Latest,
scan_type: Default::default(),
num_requests: 0,
@@ -600,6 +601,7 @@ async fn ferox_scan_abort() {
let scan = FeroxScan {
id: "".to_string(),
url: String::from("http://localhost"),
normalized_url: String::from("http://localhost/"),
scan_order: ScanOrder::Latest,
scan_type: Default::default(),
num_requests: 0,

View File

@@ -1,3 +1,4 @@
use std::fmt::Write as _;
use std::sync::atomic::AtomicBool;
use std::{ops::Deref, sync::atomic::Ordering, sync::Arc, time::Instant};
@@ -284,8 +285,7 @@ impl FeroxScanner {
let mut message = format!("=> {}", style("Directory listing").blue().bright());
if !self.handles.config.extract_links {
message
.push_str(&format!(" (add {} to scan)", style("-e").bright().yellow()))
write!(message, " (add {} to scan)", style("-e").bright().yellow())?;
}
progress_bar.reset_eta();
@@ -328,7 +328,7 @@ impl FeroxScanner {
log::info!(
"requesting {} collected words: {:?}...",
new_words_len,
&new_words[..new_words_len.min(3) as usize]
&new_words[..new_words_len.min(3)]
);
self.stream_requests(

View File

@@ -6,7 +6,7 @@ use std::{
use anyhow::Result;
use lazy_static::lazy_static;
use leaky_bucket::LeakyBucket;
use leaky_bucket::RateLimiter;
use tokio::{
sync::RwLock,
time::{sleep, Duration},
@@ -45,7 +45,7 @@ pub(super) struct Requester {
target_url: String,
/// limits requests per second if present
rate_limiter: RwLock<Option<LeakyBucket>>,
rate_limiter: RwLock<Option<RateLimiter>>,
/// data regarding policy and metadata about last enforced trigger etc...
policy_data: PolicyData,
@@ -94,18 +94,18 @@ impl Requester {
})
}
/// build a LeakyBucket, given a rate limit (as requests per second)
fn build_a_bucket(limit: usize) -> Result<LeakyBucket> {
/// build a RateLimiter, given a rate limit (as requests per second)
fn build_a_bucket(limit: usize) -> Result<RateLimiter> {
let refill = max((limit as f64 / 10.0).round() as usize, 1); // minimum of 1 per second
let tokens = max((limit as f64 / 2.0).round() as usize, 1);
let interval = if refill == 1 { 1000 } else { 100 }; // 1 second if refill is 1
Ok(LeakyBucket::builder()
.refill_interval(Duration::from_millis(interval)) // add tokens every 0.1s
.refill_amount(refill) // ex: 100 req/s -> 10 tokens per 0.1s
.tokens(tokens) // reduce initial burst, 2 is arbitrary, but felt good
Ok(RateLimiter::builder()
.interval(Duration::from_millis(interval)) // add tokens every 0.1s
.refill(refill) // ex: 100 req/s -> 10 tokens per 0.1s
.initial(tokens) // reduce initial burst, 2 is arbitrary, but felt good
.max(limit)
.build()?)
.build())
}
/// sleep and set a flag that can be checked by other threads
@@ -127,7 +127,7 @@ impl Requester {
let guard = self.rate_limiter.read().await;
if guard.is_some() {
guard.as_ref().unwrap().acquire_one().await?;
guard.as_ref().unwrap().acquire_one().await;
}
Ok(())
@@ -208,7 +208,7 @@ impl Requester {
} else {
// errors can only be incremented, so an else is sufficient
*guard += 1;
self.policy_data.adjust_up(&*guard);
self.policy_data.adjust_up(&guard);
}
}
@@ -925,10 +925,10 @@ mod tests {
/// decrease the scan rate
async fn adjust_limit_resets_streak_counter_on_downward_movement() {
let (handles, _) = setup_requester_test(None).await;
let mut buckets = leaky_bucket::LeakyBuckets::new();
let coordinator = buckets.coordinate().unwrap();
tokio::spawn(async move { coordinator.await.expect("coordinator errored") });
let limiter = buckets.rate_limiter().max(200).build().unwrap();
let limiter = RateLimiter::builder()
.interval(Duration::from_secs(1))
.max(200)
.build();
let scan = FeroxScan::default();
scan.add_error();
@@ -947,9 +947,10 @@ mod tests {
requester.policy_data.set_reqs_sec(400);
requester.policy_data.set_errors(1);
let mut guard = requester.tuning_lock.lock().unwrap();
*guard = 2;
drop(guard);
{
let mut guard = requester.tuning_lock.lock().unwrap();
*guard = 2;
}
requester
.adjust_limit(PolicyTrigger::Errors, false)
@@ -1036,10 +1037,10 @@ mod tests {
/// set_rate_limiter should exit early when new limit equals the current bucket's max
async fn set_rate_limiter_early_exit() {
let (handles, _) = setup_requester_test(None).await;
let mut buckets = leaky_bucket::LeakyBuckets::new();
let coordinator = buckets.coordinate().unwrap();
tokio::spawn(async move { coordinator.await.expect("coordinator errored") });
let limiter = buckets.rate_limiter().max(200).build().unwrap();
let limiter = RateLimiter::builder()
.interval(Duration::from_secs(1))
.max(200)
.build();
let requester = Requester {
handles,
@@ -1068,10 +1069,10 @@ mod tests {
async fn tune_sets_expected_values_and_then_waits() {
let (handles, _) = setup_requester_test(None).await;
let mut buckets = leaky_bucket::LeakyBuckets::new();
let coordinator = buckets.coordinate().unwrap();
tokio::spawn(async move { coordinator.await.expect("coordinator errored") });
let limiter = buckets.rate_limiter().max(200).build().unwrap();
let limiter = RateLimiter::builder()
.interval(Duration::from_secs(1))
.max(200)
.build();
let scan = FeroxScan::new(
"http://localhost",

View File

@@ -1,4 +1,4 @@
#[derive(Copy, Clone, PartialEq, Debug)]
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
/// represents different situations where different criteria can trigger auto-tune/bail behavior
pub enum PolicyTrigger {
/// excessive 403 trigger

View File

@@ -526,7 +526,7 @@ pub fn slugify_filename(url: &str, prefix: &str, suffix: &str) -> String {
String::new()
};
let slug = url.replace("://", "_").replace('/', "_").replace('.', "_");
let slug = url.replace("://", "_").replace(['/', '.'], "_");
let filename = format!("{}{}-{}.{}", altered_prefix, slug, ts, suffix);

View File

@@ -45,7 +45,7 @@ fn deny_list_works_during_extraction() {
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200)
.body(&srv.url("'/homepage/assets/img/icons/handshake.svg'"));
.body(srv.url("'/homepage/assets/img/icons/handshake.svg'"));
});
let mock_two = srv.mock(|when, then| {
@@ -90,17 +90,17 @@ fn deny_list_works_during_recursion() {
let js_mock = srv.mock(|when, then| {
when.method(GET).path("/js");
then.status(301).header("Location", &srv.url("/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/"));
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/"));
then.status(301).header("Location", srv.url("/js/dev/"));
});
let js_dev_file_mock = srv.mock(|when, then| {
@@ -155,7 +155,7 @@ fn deny_list_works_during_recursion_with_inverted_parents() {
let js_mock = srv.mock(|when, then| {
when.method(GET).path("/js");
then.status(301).header("Location", &srv.url("/js/"));
then.status(301).header("Location", srv.url("/js/"));
});
let api_mock = srv.mock(|when, then| {
@@ -165,12 +165,12 @@ fn deny_list_works_during_recursion_with_inverted_parents() {
let js_prod_mock = srv.mock(|when, then| {
when.method(GET).path("/js/prod");
then.status(301).header("Location", &srv.url("/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/"));
then.status(301).header("Location", srv.url("/js/dev/"));
});
let js_dev_file_mock = srv.mock(|when, then| {

View File

@@ -16,7 +16,7 @@ fn extractor_finds_absolute_url() -> Result<(), Box<dyn std::error::Error>> {
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200)
.body(&srv.url("'/homepage/assets/img/icons/handshake.svg'"));
.body(srv.url("'/homepage/assets/img/icons/handshake.svg'"));
});
let mock_two = srv.mock(|when, then| {
@@ -136,13 +136,13 @@ fn extractor_finds_same_relative_url_twice() {
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200)
.body(&srv.url("\"/homepage/assets/img/icons/handshake.svg\""));
.body(srv.url("\"/homepage/assets/img/icons/handshake.svg\""));
});
let mock_two = srv.mock(|when, then| {
when.method(GET).path("/README");
then.status(200)
.body(&srv.url("\"/homepage/assets/img/icons/handshake.svg\""));
.body(srv.url("\"/homepage/assets/img/icons/handshake.svg\""));
});
let mock_three = srv.mock(|when, then| {
@@ -185,7 +185,7 @@ fn extractor_finds_filtered_content() -> Result<(), Box<dyn std::error::Error>>
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200)
.body(&srv.url("\"/homepage/assets/img/icons/handshake.svg\""));
.body(srv.url("\"/homepage/assets/img/icons/handshake.svg\""));
});
let mock_two = srv.mock(|when, then| {
@@ -413,7 +413,7 @@ fn extractor_finds_directory_listing_links_and_displays_files() {
let mock_dir_redir = srv.mock(|when, then| {
when.method(GET).path("/misc");
then.status(301).header("Location", &srv.url("/misc/"));
then.status(301).header("Location", srv.url("/misc/"));
});
let mock_dir = srv.mock(|when, then| {
when.method(GET).path("/misc/");
@@ -522,7 +522,7 @@ fn extractor_finds_directory_listing_links_and_displays_files_non_recursive() {
let mock_dir_redir = srv.mock(|when, then| {
when.method(GET).path("/misc");
then.status(301).header("Location", &srv.url("/misc/"));
then.status(301).header("Location", srv.url("/misc/"));
});
let mock_dir = srv.mock(|when, then| {
when.method(GET).path("/misc/");
@@ -600,7 +600,7 @@ fn extractor_recurses_into_403_directories() -> Result<(), Box<dyn std::error::E
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200)
.body(&srv.url("'/homepage/assets/img/icons/handshake.svg'"));
.body(srv.url("'/homepage/assets/img/icons/handshake.svg'"));
});
let mock_two = srv.mock(|when, then| {

View File

@@ -196,10 +196,10 @@ fn main_parallel_creates_output_directory() -> Result<(), Box<dyn std::error::Er
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);
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()));
assert!(dir_regex.is_match(&sub_dir.to_string_lossy()));
for entry in sub_dir.read_dir()? {
let entry = entry?;

View File

@@ -42,7 +42,13 @@ fn parser_incorrect_param_with_tack_h() {
.arg("-h")
.assert()
.success()
.stdout(predicate::str::contains(
"[CAUTION] 4 -v's is probably too much",
));
.stdout(
predicate::str::contains("[CAUTION]")
.and(predicate::str::contains("4"))
.and(predicate::str::contains("-v's"))
.and(predicate::str::contains("is"))
.and(predicate::str::contains("probably"))
.and(predicate::str::contains("too"))
.and(predicate::str::contains("much")),
);
}

View File

@@ -20,11 +20,13 @@ fn resume_scan_works() {
// localhost:PORT/ <- complete
// localhost:PORT/js <- will get scanned with /css and /stuff
let complete_scan = format!(
r#"{{"id":"057016a14769414aac9a7a62707598cb","url":"{}","scan_type":"Directory","status":"Complete"}}"#,
srv.url("/")
r#"{{"id":"057016a14769414aac9a7a62707598cb","url":"{}","normalized_url":"{}","scan_type":"Directory","status":"Complete"}}"#,
srv.url("/"),
srv.url("/"),
);
let incomplete_scan = format!(
r#"{{"id":"400b2323a16f43468a04ffcbbeba34c6","url":"{}","scan_type":"Directory","status":"NotStarted"}}"#,
r#"{{"id":"400b2323a16f43468a04ffcbbeba34c6","url":"{}","normalized_url":"{}/","scan_type":"Directory","status":"NotStarted"}}"#,
srv.url("/js"),
srv.url("/js")
);
let scans = format!(r#""scans":[{},{}]"#, complete_scan, incomplete_scan);

View File

@@ -53,17 +53,17 @@ fn scanner_recursive_request_scan() -> Result<(), Box<dyn std::error::Error>> {
let js_mock = srv.mock(|when, then| {
when.method(GET).path("/js");
then.status(301).header("Location", &srv.url("/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/"));
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/"));
then.status(301).header("Location", srv.url("/js/dev/"));
});
let js_dev_file_mock = srv.mock(|when, then| {
@@ -116,17 +116,17 @@ fn scanner_recursive_request_scan_using_only_success_responses(
let js_mock = srv.mock(|when, then| {
when.method(GET).path("/js/");
then.status(200).header("Location", &srv.url("/js/"));
then.status(200).header("Location", srv.url("/js/"));
});
let js_prod_mock = srv.mock(|when, then| {
when.method(GET).path("/js/prod/");
then.status(200).header("Location", &srv.url("/js/prod/"));
then.status(200).header("Location", srv.url("/js/prod/"));
});
let js_dev_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev/");
then.status(200).header("Location", &srv.url("/js/dev/"));
then.status(200).header("Location", srv.url("/js/dev/"));
});
let js_dev_file_mock = srv.mock(|when, then| {
@@ -864,7 +864,7 @@ fn scanner_forced_recursion_ignores_normal_redirect_logic() -> Result<(), Box<dy
when.method(GET).path("/LICENSE");
then.status(301)
.body("this is a test")
.header("Location", &srv.url("/LICENSE"));
.header("Location", srv.url("/LICENSE"));
});
let mock2 = srv.mock(|when, then| {

View File

@@ -9,7 +9,7 @@ pub fn setup_tmp_directory(
filename: &str,
) -> Result<(TempDir, PathBuf), Box<dyn std::error::Error>> {
let tmp_dir = TempDir::new()?;
let file = tmp_dir.path().join(&filename);
let file = tmp_dir.path().join(filename);
write(&file, words.join("\n"))?;
Ok((tmp_dir, file))
}