Compare commits

...

72 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
epi
f51993cde0 Merge pull request #575 from epi052/all-contributors/add-postmodern
docs: add postmodern as a contributor for ideas
2022-05-12 06:03:16 -05:00
allcontributors[bot]
9093ffb92a docs: update .all-contributorsrc [skip ci] 2022-05-12 11:03:09 +00:00
allcontributors[bot]
d550448229 docs: update README.md [skip ci] 2022-05-12 11:03:08 +00:00
epi
492665154e Merge pull request #574 from epi052/all-contributors/add-DonatoReis
docs: add DonatoReis as a contributor for ideas
2022-05-12 06:02:21 -05:00
allcontributors[bot]
c14e617076 docs: update .all-contributorsrc [skip ci] 2022-05-12 11:02:03 +00:00
allcontributors[bot]
6bb748af17 docs: update README.md [skip ci] 2022-05-12 11:02:03 +00:00
epi
863ea089cc Merge pull request #573 from epi052/all-contributors/add-jhaddix
docs: add jhaddix as a contributor for bug
2022-05-12 06:01:24 -05:00
allcontributors[bot]
ad3091a7db docs: update .all-contributorsrc [skip ci] 2022-05-12 11:00:30 +00:00
allcontributors[bot]
b2bdea71dd docs: update README.md [skip ci] 2022-05-12 11:00:30 +00:00
epi
f478700b86 Merge pull request #564 from epi052/563-fix-leaky-bucket-unwrap-to-none
563 fix leaky bucket unwrap to none
2022-05-11 20:09:46 -05:00
epi
1f2aad5e52 updated deps 2022-05-11 17:29:59 -05:00
epi
3a6a61cc24 updated dependencies 2022-05-11 17:24:09 -05:00
epi
0311a846b3 added secondary wordlist check to main 2022-05-11 17:14:13 -05:00
epi
3066efa848 add https if missing url scheme; check /usr/local/share for wordlist 2022-05-10 06:45:10 -05:00
epi
a8fae65d63 allow extensions with prepended . 2022-05-10 06:44:21 -05:00
epi
970886a68b reverted actions change 2022-05-10 05:51:03 -05:00
epi
494eed81e8 fmt 2022-05-05 19:19:01 -05:00
epi
c8a577b1e7 removed unwrap from limit function 2022-05-05 19:18:29 -05:00
44 changed files with 1133 additions and 831 deletions

View File

@@ -391,7 +391,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/3488554?v=4", "avatar_url": "https://avatars.githubusercontent.com/u/3488554?v=4",
"profile": "https://twitter.com/Jhaddix", "profile": "https://twitter.com/Jhaddix",
"contributions": [ "contributions": [
"ideas" "ideas",
"bug"
] ]
}, },
{ {
@@ -421,6 +422,52 @@
"contributions": [ "contributions": [
"ideas" "ideas"
] ]
},
{
"login": "DonatoReis",
"name": "PeakyBlinder",
"avatar_url": "https://avatars.githubusercontent.com/u/93531354?v=4",
"profile": "https://github.com/DonatoReis",
"contributions": [
"ideas"
]
},
{
"login": "postmodern",
"name": "Postmodern",
"avatar_url": "https://avatars.githubusercontent.com/u/12671?v=4",
"profile": "https://postmodern.github.io/",
"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, "contributorsPerLine": 7,
@@ -428,5 +475,6 @@
"projectOwner": "epi052", "projectOwner": "epi052",
"repoType": "github", "repoType": "github",
"repoHost": "https://github.com", "repoHost": "https://github.com",
"skipCi": true "skipCi": true,
"commitConvention": "angular"
} }

1009
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

127
README.md
View File

@@ -181,65 +181,74 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
<!-- markdownlint-disable --> <!-- markdownlint-disable -->
<table> <table>
<tr> <tbody>
<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> <tr>
<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="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="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/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="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="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/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/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/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://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://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> <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>
</tr> <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=""/><br /><sub><b>Tom Matthews</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=tomtastic" title="Documentation">📖</a></td> <tr>
<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="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="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://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="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="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://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://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://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/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://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> <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>
</tr> <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=""/><br /><sub><b>Tib3rius</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ATib3rius" title="Bug reports">🐛</a></td> <tr>
<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://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="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/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="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="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/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/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/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/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/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> <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>
</tr> <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=""/><br /><sub><b>mchill</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Amoscowchill" title="Bug reports">🐛</a></td> <tr>
<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/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="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="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/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/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/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/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/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://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://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> <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>
</tr> <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=""/><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> <tr>
<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/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/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/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/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://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://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/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://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://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/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> <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>
</tr> <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=""/><br /><sub><b>0x08</b></sub></a><br /><a href="#ideas-its0x08" title="Ideas, Planning, & Feedback">🤔</a></td> <tr>
<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://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/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="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="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/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="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="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/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://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://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></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>
</tr> <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=""/><br /><sub><b>Limn0</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AThisLimn0" title="Bug reports">🐛</a></td> <tr>
<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/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>
</tr> <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> </table>
<!-- markdownlint-restore --> <!-- markdownlint-restore -->

View File

@@ -59,15 +59,11 @@ fn main() {
if !config_dir.exists() { if !config_dir.exists() {
// recursively create the feroxbuster directory and all of its parent components if // recursively create the feroxbuster directory and all of its parent components if
// they are missing // they are missing
if !config_dir.exists() { if create_dir_all(&config_dir).is_err() {
// recursively create the feroxbuster directory and all of its parent components if // only copy the config file when we're not running in the CI/CD pipeline
// they are missing // which fails with permission denied
if create_dir_all(&config_dir).is_err() { eprintln!("Couldn't create one or more directories needed to copy the config file");
// only copy the config file when we're not running in the CI/CD pipeline return;
// 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' \ '--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: ' \ '*-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: ' \ '*--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.0)]:USER_AGENT: ' \ '-a+[Sets the User-Agent (default: feroxbuster/2.7.3)]:USER_AGENT: ' \
'--user-agent=[Sets the User-Agent (default: feroxbuster/2.7.0)]: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: ' \ '*-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: ' \ '*--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: ' \ '*-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' \ '-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' \ '--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' \ '--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]' \ '(-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 --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]' \ '(-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)]' \ '--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]' \ '--json[Emit JSON logs to --output and --debug-log instead of normal text]' \
'--no-state[Disable state output file (*.state)]' \ '--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 && 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('--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('-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('--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.0)') [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.0)') [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('-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('--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)') [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('-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('--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('--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('--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', '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') [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('--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('--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('--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 break
} }
}) })

View File

@@ -8,8 +8,8 @@ _feroxbuster() {
for i in ${COMP_WORDS[@]} for i in ${COMP_WORDS[@]}
do do
case "${i}" in case "${cmd},${i}" in
"$1") ",$1")
cmd="feroxbuster" cmd="feroxbuster"
;; ;;
*) *)
@@ -19,7 +19,7 @@ _feroxbuster() {
case "${cmd}" in case "${cmd}" in
feroxbuster) 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 if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 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 --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 -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 --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.0)' cand -a 'Sets the User-Agent (default: feroxbuster/2.7.3)'
cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.7.0)' 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 -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 --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)' 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 -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 --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 --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 --stdin 'Read url(s) from STDIN'
cand --burp 'Set --proxy to http://127.0.0.1:8080 and set --insecure to true' 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' 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 --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 --json 'Emit JSON logs to --output and --debug-log instead of normal text'
cand --no-state 'Disable state output file (*.state)' 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] $completions[$command]

View File

@@ -9,7 +9,7 @@ use crate::{
DEFAULT_CONFIG_NAME, DEFAULT_CONFIG_NAME,
}; };
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use clap::ArgMatches; use clap::{parser::ValueSource, ArgMatches};
use regex::Regex; use regex::Regex;
use reqwest::{Client, Method, StatusCode, Url}; use reqwest::{Client, Method, StatusCode, Url};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -22,15 +22,10 @@ use std::{
/// macro helper to abstract away repetitive configuration updates /// macro helper to abstract away repetitive configuration updates
macro_rules! update_config_if_present { macro_rules! update_config_if_present {
($conf_val:expr, $matches:ident, $arg_name:expr) => { ($conf_val:expr, $matches:ident, $arg_name:expr, $arg_type:ty) => {
match $matches.value_of_t($arg_name) { match $matches.get_one::<$arg_type>($arg_name) {
Ok(value) => *$conf_val = value, // Update value Some(value) => *$conf_val = value.to_owned(), // Update value
Err(err) => { None => {}
if !matches!(err.kind(), clap::ErrorKind::ArgumentNotFound) {
// Do nothing if argument not found
err.exit() // Exit with error on any other parse error
}
}
} }
}; };
} }
@@ -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. /// Represents the final, global configuration of the program.
/// ///
/// This struct is the combination of the following: /// 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 // --resume-from used, need to first read the Configuration from disk, and then
// merge the cli_config into the resumed config // 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 // when resuming a scan, instead of normal configuration loading, we just
// load the config from disk by calling resume_scan // load the config from disk by calling resume_scan
let mut previous_config = resume_scan(filename); let mut previous_config = resume_scan(filename);
@@ -546,18 +570,21 @@ impl Configuration {
fn parse_cli_args(args: &ArgMatches) -> Self { fn parse_cli_args(args: &ArgMatches) -> Self {
let mut config = Configuration::default(); let mut config = Configuration::default();
update_config_if_present!(&mut config.threads, args, "threads"); update_config_with_num_type_if_present!(&mut config.threads, args, "threads", usize);
update_config_if_present!(&mut config.depth, args, "depth"); update_config_with_num_type_if_present!(&mut config.parallel, args, "parallel", usize);
update_config_if_present!(&mut config.scan_limit, args, "scan_limit"); update_config_with_num_type_if_present!(&mut config.depth, args, "depth", usize);
update_config_if_present!(&mut config.parallel, args, "parallel"); update_config_with_num_type_if_present!(&mut config.scan_limit, args, "scan_limit", usize);
update_config_if_present!(&mut config.rate_limit, args, "rate_limit"); update_config_with_num_type_if_present!(&mut config.rate_limit, args, "rate_limit", usize);
update_config_if_present!(&mut config.wordlist, args, "wordlist"); update_config_if_present!(&mut config.wordlist, args, "wordlist", String);
update_config_if_present!(&mut config.output, args, "output"); update_config_if_present!(&mut config.output, args, "output", String);
update_config_if_present!(&mut config.debug_log, args, "debug_log"); update_config_if_present!(&mut config.debug_log, args, "debug_log", String);
update_config_if_present!(&mut config.time_limit, args, "time_limit"); update_config_if_present!(&mut config.resume_from, args, "resume_from", String);
update_config_if_present!(&mut config.resume_from, args, "resume_from");
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 config.status_codes = arg
.map(|code| { .map(|code| {
StatusCode::from_bytes(code.as_bytes()) StatusCode::from_bytes(code.as_bytes())
@@ -567,7 +594,7 @@ impl Configuration {
.collect(); .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 // replay codes passed in by the user
config.replay_codes = arg config.replay_codes = arg
.map(|code| { .map(|code| {
@@ -581,7 +608,7 @@ impl Configuration {
config.replay_codes = config.status_codes.clone(); 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 config.filter_status = arg
.map(|code| { .map(|code| {
StatusCode::from_bytes(code.as_bytes()) StatusCode::from_bytes(code.as_bytes())
@@ -591,15 +618,17 @@ impl Configuration {
.collect(); .collect();
} }
if let Some(arg) = args.values_of("extensions") { if let Some(arg) = args.get_many::<String>("extensions") {
config.extensions = arg.map(|val| val.to_string()).collect(); 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(); 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 config.methods = arg
.map(|val| { .map(|val| {
// Check methods if they are correct // Check methods if they are correct
@@ -611,7 +640,7 @@ impl Configuration {
.collect(); .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('@') { if let Some(stripped) = arg.strip_prefix('@') {
config.data = config.data =
std::fs::read(stripped).unwrap_or_else(|e| report_and_exit(&e.to_string())); std::fs::read(stripped).unwrap_or_else(|e| report_and_exit(&e.to_string()));
@@ -620,13 +649,13 @@ impl Configuration {
} }
} }
if args.is_present("stdin") { if came_from_cli!(args, "stdin") {
config.stdin = true; config.stdin = true;
} else if let Some(url) = args.value_of("url") { } else if let Some(url) = args.get_one::<String>("url") {
config.target_url = String::from(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 // 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 // when --dont-scan is used, the should_deny_url function is called at least once per
@@ -670,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(); 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(); 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 config.filter_size = arg
.map(|size| { .map(|size| {
size.parse::<u64>() size.parse::<u64>()
@@ -687,7 +716,7 @@ impl Configuration {
.collect(); .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 config.filter_word_count = arg
.map(|size| { .map(|size| {
size.parse::<usize>() size.parse::<usize>()
@@ -696,7 +725,7 @@ impl Configuration {
.collect(); .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 config.filter_line_count = arg
.map(|size| { .map(|size| {
size.parse::<usize>() size.parse::<usize>()
@@ -705,7 +734,7 @@ impl Configuration {
.collect(); .collect();
} }
if args.is_present("silent") { if came_from_cli!(args, "silent") {
// the reason this is protected by an if statement: // the reason this is protected by an if statement:
// consider a user specifying silent = true in ferox-config.toml // consider a user specifying silent = true in ferox-config.toml
// if the line below is outside of the if, we'd overwrite true with // if the line below is outside of the if, we'd overwrite true with
@@ -714,106 +743,111 @@ impl Configuration {
config.output_level = OutputLevel::Silent; config.output_level = OutputLevel::Silent;
} }
if args.is_present("quiet") { if came_from_cli!(args, "quiet") {
config.quiet = true; config.quiet = true;
config.output_level = OutputLevel::Quiet; 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.auto_tune = true;
config.requester_policy = RequesterPolicy::AutoTune; config.requester_policy = RequesterPolicy::AutoTune;
} }
if args.is_present("auto_bail") { if came_from_cli!(args, "auto_bail") {
config.auto_bail = true; config.auto_bail = true;
config.requester_policy = RequesterPolicy::AutoBail; config.requester_policy = RequesterPolicy::AutoBail;
} }
if args.is_present("no_state") { if came_from_cli!(args, "no_state") {
config.save_state = false; config.save_state = false;
} }
if args.is_present("dont_filter") { if came_from_cli!(args, "dont_filter") {
config.dont_filter = true; 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; config.collect_extensions = true;
} }
if args.is_present("collect_backups") if came_from_cli!(args, "collect_backups")
|| args.is_present("smart") || came_from_cli!(args, "smart")
|| args.is_present("thorough") || came_from_cli!(args, "thorough")
{ {
config.collect_backups = true; config.collect_backups = true;
} }
if args.is_present("collect_words") if came_from_cli!(args, "collect_words")
|| args.is_present("smart") || came_from_cli!(args, "smart")
|| args.is_present("thorough") || came_from_cli!(args, "thorough")
{ {
config.collect_words = true; 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 // occurrences_of returns 0 if none are found; this is protected in
// an if block for the same reason as the quiet option // 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; config.no_recursion = true;
} }
if args.is_present("add_slash") { if came_from_cli!(args, "add_slash") {
config.add_slash = true; config.add_slash = true;
} }
if args.is_present("extract_links") if came_from_cli!(args, "extract_links")
|| args.is_present("smart") || came_from_cli!(args, "smart")
|| args.is_present("thorough") || came_from_cli!(args, "thorough")
{ {
config.extract_links = true; config.extract_links = true;
} }
if args.is_present("json") { if came_from_cli!(args, "json") {
config.json = true; config.json = true;
} }
if args.is_present("force_recursion") { if came_from_cli!(args, "force_recursion") {
config.force_recursion = true; config.force_recursion = true;
} }
//// ////
// organizational breakpoint; all options below alter the Client configuration // organizational breakpoint; all options below alter the Client configuration
//// ////
update_config_if_present!(&mut config.proxy, args, "proxy"); update_config_if_present!(&mut config.proxy, args, "proxy", String);
update_config_if_present!(&mut config.replay_proxy, args, "replay_proxy"); update_config_if_present!(&mut config.replay_proxy, args, "replay_proxy", String);
update_config_if_present!(&mut config.user_agent, args, "user_agent"); update_config_if_present!(&mut config.user_agent, args, "user_agent", String);
update_config_if_present!(&mut config.timeout, args, "timeout"); 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"); 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"); 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; config.random_agent = true;
} }
if args.is_present("redirects") { if came_from_cli!(args, "redirects") {
config.redirects = true; 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; config.insecure = true;
} }
if let Some(headers) = args.values_of("headers") { if let Some(headers) = args.get_many::<String>("headers") {
for val in headers { for val in headers {
let mut split_val = val.split(':'); let mut split_val = val.split(':');
@@ -827,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( config.headers.insert(
// we know the header name is always "cookie" // we know the header name is always "cookie"
"Cookie".to_string(), "Cookie".to_string(),
@@ -843,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 { for val in queries {
// same basic logic used as reading in the headers HashMap above // same basic logic used as reading in the headers HashMap above
let mut split_val = val.split('='); let mut split_val = val.split('=');
@@ -955,7 +989,7 @@ impl Configuration {
update_if_not_default!(&mut conf.force_recursion, new.force_recursion, false); 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.extract_links, new.extract_links, false);
update_if_not_default!(&mut conf.extensions, new.extensions, Vec::<String>::new()); 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.data, new.data, Vec::<u8>::new());
update_if_not_default!(&mut conf.url_denylist, new.url_denylist, Vec::<Url>::new()); update_if_not_default!(&mut conf.url_denylist, new.url_denylist, Vec::<Url>::new());
if !new.regex_denylist.is_empty() { if !new.regex_denylist.is_empty() {
@@ -1028,7 +1062,17 @@ impl Configuration {
/// uses serde to deserialize the toml into a `Configuration` struct /// uses serde to deserialize the toml into a `Configuration` struct
pub(super) fn parse_config(config_file: PathBuf) -> Result<Self> { pub(super) fn parse_config(config_file: PathBuf) -> Result<Self> {
let content = read_to_string(config_file)?; let content = read_to_string(config_file)?;
let config: Self = toml::from_str(content.as_str())?; let mut config: Self = toml::from_str(content.as_str())?;
if !config.extensions.is_empty() {
// remove leading periods, if any are found
config.extensions = config
.extensions
.iter()
.map(|ext| ext.trim_start_matches('.').to_string())
.collect();
}
Ok(config) Ok(config)
} }
} }

View File

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

View File

@@ -101,10 +101,13 @@ impl TermInputHandler {
handles.filters.data.clone(), 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?; let mut buffered_file = state_file?;
write_to(&state, &mut buffered_file, true)?; write_to(&state, &mut buffered_file, true)?;
}
log::trace!("exit: sigint_handler (end of program)"); log::trace!("exit: sigint_handler (end of program)");
std::process::exit(1); 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 = pub(super) const ROBOTS_TXT_REGEX: &str =
r#"(?m)^ *(Allow|Disallow): *(?P<url_path>[a-zA-Z0-9._/?#@!&'()+,;%=-]+?)$"#; // multi-line (?m) 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 /// Which type of extraction should be performed
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub enum ExtractionTarget { pub enum ExtractionTarget {
@@ -90,6 +95,7 @@ impl<'a> ExtractorBuilder<'a> {
Ok(Extractor { Ok(Extractor {
links_regex: Regex::new(LINKFINDER_REGEX).unwrap(), links_regex: Regex::new(LINKFINDER_REGEX).unwrap(),
robots_regex: Regex::new(ROBOTS_TXT_REGEX).unwrap(), robots_regex: Regex::new(ROBOTS_TXT_REGEX).unwrap(),
url_regex: Regex::new(URL_CHARS_REGEX).unwrap(),
response: if self.response.is_some() { response: if self.response.is_some() {
Some(self.response.unwrap()) Some(self.response.unwrap())
} else { } else {

View File

@@ -17,7 +17,7 @@ use crate::{
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use reqwest::{Client, StatusCode, Url}; use reqwest::{Client, StatusCode, Url};
use scraper::{Html, Selector}; use scraper::{Html, Selector};
use std::collections::HashSet; use std::{borrow::Cow, collections::HashSet};
/// Whether an active scan is recursive or not /// Whether an active scan is recursive or not
#[derive(Debug)] #[derive(Debug)]
@@ -38,6 +38,9 @@ pub struct Extractor<'a> {
/// `ROBOTS_TXT_REGEX` as a regex::Regex type /// `ROBOTS_TXT_REGEX` as a regex::Regex type
pub(super) robots_regex: Regex, pub(super) robots_regex: Regex,
/// regex to validate a url
pub(super) url_regex: Regex,
/// Response from which to extract links /// Response from which to extract links
pub(super) response: Option<&'a FeroxResponse>, 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, "div", "src");
self.extract_links_by_attr(resp_url, links, html, "frame", "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, "embed", "src");
self.extract_links_by_attr(resp_url, links, html, "link", "href");
} }
/// Given the body of a `reqwest::Response`, perform the following actions /// 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); let normalized_path = self.normalize_url_path(path);
// filter out any empty strings caused by .split // filter out any empty strings caused by .split
let mut parts: Vec<&str> = normalized_path let mut parts: Vec<Cow<_>> = normalized_path
.split('/') .split('/')
.map(|s| self.url_regex.replace_all(s, ""))
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.collect(); .collect();
@@ -392,6 +397,17 @@ impl<'a> Extractor<'a> {
.join(link) .join(link)
.with_context(|| format!("Could not join {} with {}", old_url, 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()); links.insert(new_url.to_string());
log::trace!("exit: add_link_to_set_of_links"); log::trace!("exit: add_link_to_set_of_links");
@@ -413,7 +429,7 @@ impl<'a> Extractor<'a> {
let scanned_urls = self.handles.ferox_scans()?; 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 //we've seen the url before and don't need to scan again
log::trace!("exit: request_link -> None"); log::trace!("exit: request_link -> None");
bail!("previously seen url"); 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 super::*;
use crate::config::{Configuration, OutputLevel}; use crate::config::{Configuration, OutputLevel};
use crate::scan_manager::ScanOrder; 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 { let extractor = Extractor {
links_regex: Regex::new(LINKFINDER_REGEX).unwrap(), links_regex: Regex::new(LINKFINDER_REGEX).unwrap(),
robots_regex: Regex::new(ROBOTS_TXT_REGEX).unwrap(), robots_regex: Regex::new(ROBOTS_TXT_REGEX).unwrap(),
url_regex: Regex::new(URL_CHARS_REGEX).unwrap(),
response: Some(&ferox_response), response: Some(&ferox_response),
url: String::new(), url: String::new(),
target: ExtractionTarget::ResponseBody, target: ExtractionTarget::ResponseBody,
@@ -301,6 +302,7 @@ async fn request_robots_txt_without_proxy() -> Result<()> {
let extractor = Extractor { let extractor = Extractor {
links_regex: Regex::new(LINKFINDER_REGEX).unwrap(), links_regex: Regex::new(LINKFINDER_REGEX).unwrap(),
robots_regex: Regex::new(ROBOTS_TXT_REGEX).unwrap(), robots_regex: Regex::new(ROBOTS_TXT_REGEX).unwrap(),
url_regex: Regex::new(URL_CHARS_REGEX).unwrap(),
response: None, response: None,
url: srv.url("/api/users/stuff/things"), url: srv.url("/api/users/stuff/things"),
target: ExtractionTarget::RobotsTxt, target: ExtractionTarget::RobotsTxt,

View File

@@ -1,7 +1,7 @@
use super::*; use super::*;
/// Dummy filter for internal shenanigans /// Dummy filter for internal shenanigans
#[derive(Default, Debug, PartialEq)] #[derive(Default, Debug, PartialEq, Eq)]
pub struct EmptyFilter {} pub struct EmptyFilter {}
impl FeroxFilter for 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 /// Simple implementor of FeroxFilter; used to filter out responses based on the number of lines
/// in a Response body; specified using -N|--filter-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 { pub struct LinesFilter {
/// Number of lines in a Response's body that should be filtered /// Number of lines in a Response's body that should be filtered
pub line_count: usize, 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 /// 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 /// 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 { pub struct SimilarityFilter {
/// Hash of Response's body to be used during similarity comparison /// Hash of Response's body to be used during similarity comparison
pub hash: String, pub hash: String,
@@ -20,9 +20,9 @@ impl FeroxFilter for SimilarityFilter {
/// Check `FeroxResponse::text` against what was requested from the site passed in via /// Check `FeroxResponse::text` against what was requested from the site passed in via
/// --filter-similar-to /// --filter-similar-to
fn should_filter_response(&self, response: &FeroxResponse) -> bool { 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; 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 /// Simple implementor of FeroxFilter; used to filter out responses based on the length of a
/// Response body; specified using -S|--filter-size /// Response body; specified using -S|--filter-size
#[derive(Default, Debug, PartialEq, Serialize, Deserialize)] #[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SizeFilter { pub struct SizeFilter {
/// Overall length of a Response's body that should be filtered /// Overall length of a Response's body that should be filtered
pub content_length: u64, pub content_length: u64,

View File

@@ -2,7 +2,7 @@ use super::*;
/// Simple implementor of FeroxFilter; used to filter out status codes specified using /// Simple implementor of FeroxFilter; used to filter out status codes specified using
/// -C|--filter-status /// -C|--filter-status
#[derive(Default, Debug, PartialEq, Serialize, Deserialize)] #[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct StatusCodeFilter { pub struct StatusCodeFilter {
/// Status code that should not be displayed to the user /// Status code that should not be displayed to the user
pub filter_code: u16, 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 // 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 { Ok(SimilarityFilter {
hash, 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 /// `size` is size of the response that should be included with filters passed via runtime
/// configuration and any static wildcard lengths. /// configuration and any static wildcard lengths.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WildcardFilter { pub struct WildcardFilter {
/// size of the response that will later be combined with the length of the path of the url /// size of the response that will later be combined with the length of the path of the url
/// requested /// requested

View File

@@ -2,7 +2,7 @@ use super::*;
/// Simple implementor of FeroxFilter; used to filter out responses based on the number of words /// Simple implementor of FeroxFilter; used to filter out responses based on the number of words
/// in a Response body; specified using -W|--filter-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 { pub struct WordsFilter {
/// Number of words in a Response's body that should be filtered /// Number of words in a Response's body that should be filtered
pub word_count: usize, pub word_count: usize,

View File

@@ -91,7 +91,7 @@ impl HeuristicTests {
let mut ids = vec![]; let mut ids = vec![];
for _ in 0..length { for _ in 0..length {
ids.push(Uuid::new_v4().to_simple().to_string()); ids.push(Uuid::new_v4().as_simple().to_string());
} }
let unique_id = ids.join(""); let unique_id = ids.join("");

View File

@@ -76,6 +76,8 @@ pub const DEFAULT_WORDLIST: &str =
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub const DEFAULT_WORDLIST: &str = pub const DEFAULT_WORDLIST: &str =
".\\SecLists\\Discovery\\Web-Content\\raft-medium-directories.txt"; ".\\SecLists\\Discovery\\Web-Content\\raft-medium-directories.txt";
pub const SECONDARY_WORDLIST: &str =
"/usr/local/share/seclists/Discovery/Web-Content/raft-medium-directories.txt";
/// Number of milliseconds to wait between polls of `PAUSE_SCAN` when user pauses a scan /// Number of milliseconds to wait between polls of `PAUSE_SCAN` when user pauses a scan
pub(crate) const SLEEP_DURATION: u64 = 500; pub(crate) const SLEEP_DURATION: u64 = 500;

View File

@@ -65,7 +65,7 @@ pub fn initialize(config: Arc<Configuration>) -> Result<()> {
kind: "log".to_string(), 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 Some(buffered_file) = file.clone() {
if let Ok(mut unlocked) = buffered_file.write() { if let Ok(mut unlocked) = buffered_file.write() {

View File

@@ -17,7 +17,6 @@ use tokio::{
}; };
use tokio_util::codec::{FramedRead, LinesCodec}; use tokio_util::codec::{FramedRead, LinesCodec};
use feroxbuster::scan_manager::ScanType;
use feroxbuster::{ use feroxbuster::{
banner::{Banner, UPDATE_URL}, banner::{Banner, UPDATE_URL},
config::{Configuration, OutputLevel}, config::{Configuration, OutputLevel},
@@ -30,9 +29,10 @@ use feroxbuster::{
}, },
filters, heuristics, logger, filters, heuristics, logger,
progress::{PROGRESS_BAR, PROGRESS_PRINTER}, progress::{PROGRESS_BAR, PROGRESS_PRINTER},
scan_manager::{self}, scan_manager::{self, ScanType},
scanner, scanner,
utils::{fmt_err, slugify_filename}, utils::{fmt_err, slugify_filename},
SECONDARY_WORDLIST,
}; };
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
use feroxbuster::{utils::set_open_file_limit, DEFAULT_OPEN_FILE_LIMIT}; use feroxbuster::{utils::set_open_file_limit, DEFAULT_OPEN_FILE_LIMIT};
@@ -44,11 +44,12 @@ lazy_static! {
static ref PARALLEL_LIMITER: Semaphore = Semaphore::new(0); 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>>> { fn get_unique_words_from_wordlist(path: &str) -> Result<Arc<Vec<String>>> {
log::trace!("enter: get_unique_words_from_wordlist({})", path); 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); 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() { for line in reader.lines() {
line.map(|result| { line.map(|result| {
if !result.starts_with('#') && !result.is_empty() { 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(); .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!( log::trace!(
"exit: get_unique_words_from_wordlist -> Arc<wordlist[{} words...]>", "exit: get_unique_words_from_wordlist -> Arc<wordlist[{} words...]>",
words.len() words.len()
@@ -150,7 +160,7 @@ async fn get_targets(handles: Arc<Handles>) -> Result<Vec<String>> {
} }
// remove footgun that arises if a --dont-scan value matches on a base url // remove footgun that arises if a --dont-scan value matches on a base url
for target in &targets { for target in targets.iter_mut() {
for denier in &handles.config.regex_denylist { for denier in &handles.config.regex_denylist {
if denier.is_match(target) { if denier.is_match(target) {
bail!( bail!(
@@ -169,6 +179,11 @@ async fn get_targets(handles: Arc<Handles>) -> Result<Vec<String>> {
); );
} }
} }
if !target.starts_with("http") && !target.starts_with("https") {
// --url hackerone.com
*target = format!("https://{}", target);
}
} }
log::trace!("exit: get_targets -> {:?}", targets); log::trace!("exit: get_targets -> {:?}", targets);
@@ -195,7 +210,19 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
// cloning an Arc is cheap (it's basically a pointer into the heap) // cloning an Arc is cheap (it's basically a pointer into the heap)
// so that will allow for cheap/safe sharing of a single wordlist across multi-target scans // so that will allow for cheap/safe sharing of a single wordlist across multi-target scans
// as well as additional directories found as part of recursion // as well as additional directories found as part of recursion
let words = get_unique_words_from_wordlist(&config.wordlist)?; let words = match get_unique_words_from_wordlist(&config.wordlist) {
Ok(w) => w,
Err(err) => {
let secondary = Path::new(SECONDARY_WORDLIST);
if secondary.exists() {
eprintln!("Found wordlist in secondary location");
get_unique_words_from_wordlist(SECONDARY_WORDLIST)?
} else {
return Err(err);
}
}
};
if words.len() <= 1 { if words.len() <= 1 {
// the check is now <= 1 due to the initial empty string added in 2.6.0 // the check is now <= 1 due to the initial empty string added in 2.6.0
@@ -311,7 +338,7 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
let new_folder = slugify_filename(&base_name.to_string_lossy(), "", "logs"); 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 // create the directory or fail silently, assuming the reason for failure is that
// the path exists already // the path exists already

View File

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

View File

@@ -415,7 +415,18 @@ impl FeroxSerialize for FeroxResponse {
.get("Location") .get("Location")
.unwrap() // known Some() already .unwrap() // known Some() already
.to_str() .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 // prettify the redirect target
let loc = style(loc).yellow(); let loc = style(loc).yellow();

View File

@@ -32,6 +32,9 @@ pub struct FeroxScan {
/// The URL that to be scanned /// The URL that to be scanned
pub(super) url: String, pub(super) url: String,
/// A url used solely for comparison to other URLs
pub(super) normalized_url: String,
/// The type of scan /// The type of scan
pub scan_type: ScanType, pub scan_type: ScanType,
@@ -70,7 +73,7 @@ pub struct FeroxScan {
impl Default for FeroxScan { impl Default for FeroxScan {
/// Create a default FeroxScan, populates ID with a new UUID /// Create a default FeroxScan, populates ID with a new UUID
fn default() -> Self { fn default() -> Self {
let new_id = Uuid::new_v4().to_simple().to_string(); let new_id = Uuid::new_v4().as_simple().to_string();
FeroxScan { FeroxScan {
id: new_id, id: new_id,
@@ -79,6 +82,7 @@ impl Default for FeroxScan {
num_requests: 0, num_requests: 0,
scan_order: ScanOrder::Latest, scan_order: ScanOrder::Latest,
url: String::new(), url: String::new(),
normalized_url: String::new(),
progress_bar: Mutex::new(None), progress_bar: Mutex::new(None),
scan_type: ScanType::File, scan_type: ScanType::File,
output_level: Default::default(), output_level: Default::default(),
@@ -191,6 +195,7 @@ impl FeroxScan {
) -> Arc<Self> { ) -> Arc<Self> {
Arc::new(Self { Arc::new(Self {
url: url.to_string(), url: url.to_string(),
normalized_url: format!("{}/", url.trim_end_matches('/')),
scan_type, scan_type,
scan_order, scan_order,
num_requests, num_requests,
@@ -228,6 +233,14 @@ impl FeroxScan {
false 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 /// await a task's completion, similar to a thread's join; perform necessary bookkeeping
pub async fn join(&self) { pub async fn join(&self) {
log::trace!("enter join({:?})", self); log::trace!("enter join({:?})", self);
@@ -332,10 +345,11 @@ impl Serialize for FeroxScan {
where where
S: Serializer, 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("id", &self.id)?;
state.serialize_field("url", &self.url)?; 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("scan_type", &self.scan_type)?;
state.serialize_field("status", &self.status)?; state.serialize_field("status", &self.status)?;
state.serialize_field("num_requests", &self.num_requests)?; state.serialize_field("num_requests", &self.num_requests)?;
@@ -387,6 +401,11 @@ impl<'de> Deserialize<'de> for FeroxScan {
scan.url = url.to_string(); scan.url = url.to_string();
} }
} }
"normalized_url" => {
if let Some(normalized_url) = value.as_str() {
scan.normalized_url = normalized_url.to_string();
}
}
"num_requests" => { "num_requests" => {
if let Some(num_requests) = value.as_u64() { if let Some(num_requests) = value.as_u64() {
scan.num_requests = num_requests; scan.num_requests = num_requests;
@@ -480,6 +499,7 @@ mod tests {
let scan = FeroxScan { let scan = FeroxScan {
id: "".to_string(), id: "".to_string(),
url: "".to_string(), url: "".to_string(),
normalized_url: String::from("/"),
scan_type: ScanType::Directory, scan_type: ScanType::Directory,
scan_order: ScanOrder::Initial, scan_order: ScanOrder::Initial,
num_requests: 0, num_requests: 0,

View File

@@ -75,7 +75,7 @@ impl Serialize for FeroxScans {
let mut seq = serializer.serialize_seq(Some(scans.len() + 1))?; let mut seq = serializer.serialize_seq(Some(scans.len() + 1))?;
for scan in scans.iter() { for scan in scans.iter() {
seq.serialize_element(&*scan).unwrap_or_default(); seq.serialize_element(scan).unwrap_or_default();
} }
seq.end() seq.end()
} }
@@ -138,6 +138,15 @@ impl FeroxScans {
let mut deser_scan: FeroxScan = let mut deser_scan: FeroxScan =
serde_json::from_value(scan.clone()).unwrap_or_default(); 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 // 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 // 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 // and resumes the scan but adds -q, FeroxScan will not have the proper value
@@ -213,8 +222,10 @@ impl FeroxScans {
/// on the given URL /// on the given URL
pub fn contains(&self, url: &str) -> bool { pub fn contains(&self, url: &str) -> bool {
if let Ok(scans) = self.scans.read() { if let Ok(scans) = self.scans.read() {
let normalized = format!("{}/", url.trim_end_matches('/'));
for scan in scans.iter() { for scan in scans.iter() {
if scan.url == url { if scan.normalized_url == normalized {
return true; return true;
} }
} }
@@ -225,8 +236,10 @@ impl FeroxScans {
/// Find and return a `FeroxScan` based on the given URL /// Find and return a `FeroxScan` based on the given URL
pub fn get_scan_by_url(&self, url: &str) -> Option<Arc<FeroxScan>> { pub fn get_scan_by_url(&self, url: &str) -> Option<Arc<FeroxScan>> {
if let Ok(guard) = self.scans.read() { if let Ok(guard) = self.scans.read() {
let normalized = format!("{}/", url.trim_end_matches('/'));
for scan in guard.iter() { for scan in guard.iter() {
if scan.url == url { if scan.normalized_url == normalized {
return Some(scan.clone()); return Some(scan.clone());
} }
} }
@@ -589,7 +602,8 @@ impl FeroxScans {
/// ///
/// Also return a reference to the new `FeroxScan` /// Also return a reference to the new `FeroxScan`
pub fn add_directory_scan(&self, url: &str, scan_order: ScanOrder) -> (bool, Arc<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 /// 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, None,
); );
let fs_json = format!( 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 fs.id
); );
assert_eq!(fs_json, serde_json::to_string(&*fs).unwrap()); 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 = FeroxScans::default();
let ferox_scans_json = format!( 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_scan.id
); );
ferox_scans.scans.write().unwrap().push(ferox_scan); ferox_scans.scans.write().unwrap().push(ferox_scan);
@@ -556,6 +556,7 @@ fn feroxscan_display() {
let scan = FeroxScan { let scan = FeroxScan {
id: "".to_string(), id: "".to_string(),
url: String::from("http://localhost"), url: String::from("http://localhost"),
normalized_url: String::from("http://localhost/"),
scan_order: ScanOrder::Latest, scan_order: ScanOrder::Latest,
scan_type: Default::default(), scan_type: Default::default(),
num_requests: 0, num_requests: 0,
@@ -600,6 +601,7 @@ async fn ferox_scan_abort() {
let scan = FeroxScan { let scan = FeroxScan {
id: "".to_string(), id: "".to_string(),
url: String::from("http://localhost"), url: String::from("http://localhost"),
normalized_url: String::from("http://localhost/"),
scan_order: ScanOrder::Latest, scan_order: ScanOrder::Latest,
scan_type: Default::default(), scan_type: Default::default(),
num_requests: 0, num_requests: 0,

View File

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

View File

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

View File

@@ -526,7 +526,7 @@ pub fn slugify_filename(url: &str, prefix: &str, suffix: &str) -> String {
String::new() String::new()
}; };
let slug = url.replace("://", "_").replace('/', "_").replace('.', "_"); let slug = url.replace("://", "_").replace(['/', '.'], "_");
let filename = format!("{}{}-{}.{}", altered_prefix, slug, ts, suffix); 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| { let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE"); when.method(GET).path("/LICENSE");
then.status(200) 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| { let mock_two = srv.mock(|when, then| {
@@ -90,17 +90,17 @@ fn deny_list_works_during_recursion() {
let js_mock = srv.mock(|when, then| { let js_mock = srv.mock(|when, then| {
when.method(GET).path("/js"); 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| { let js_prod_mock = srv.mock(|when, then| {
when.method(GET).path("/js/prod"); 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| { let js_dev_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev"); 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| { 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| { let js_mock = srv.mock(|when, then| {
when.method(GET).path("/js"); 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| { 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| { let js_prod_mock = srv.mock(|when, then| {
when.method(GET).path("/js/prod"); 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| { let js_dev_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev"); 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| { 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| { let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE"); when.method(GET).path("/LICENSE");
then.status(200) 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| { let mock_two = srv.mock(|when, then| {
@@ -136,13 +136,13 @@ fn extractor_finds_same_relative_url_twice() {
let mock = srv.mock(|when, then| { let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE"); when.method(GET).path("/LICENSE");
then.status(200) 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| { let mock_two = srv.mock(|when, then| {
when.method(GET).path("/README"); when.method(GET).path("/README");
then.status(200) 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| { 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| { let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE"); when.method(GET).path("/LICENSE");
then.status(200) 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| { 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| { let mock_dir_redir = srv.mock(|when, then| {
when.method(GET).path("/misc"); 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| { let mock_dir = srv.mock(|when, then| {
when.method(GET).path("/misc/"); 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| { let mock_dir_redir = srv.mock(|when, then| {
when.method(GET).path("/misc"); 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| { let mock_dir = srv.mock(|when, then| {
when.method(GET).path("/misc/"); 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| { let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE"); when.method(GET).path("/LICENSE");
then.status(200) 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| { 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 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 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/ // 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()? { for entry in sub_dir.read_dir()? {
let entry = entry?; let entry = entry?;

View File

@@ -42,7 +42,13 @@ fn parser_incorrect_param_with_tack_h() {
.arg("-h") .arg("-h")
.assert() .assert()
.success() .success()
.stdout(predicate::str::contains( .stdout(
"[CAUTION] 4 -v's is probably too much", 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/ <- complete
// localhost:PORT/js <- will get scanned with /css and /stuff // localhost:PORT/js <- will get scanned with /css and /stuff
let complete_scan = format!( let complete_scan = format!(
r#"{{"id":"057016a14769414aac9a7a62707598cb","url":"{}","scan_type":"Directory","status":"Complete"}}"#, r#"{{"id":"057016a14769414aac9a7a62707598cb","url":"{}","normalized_url":"{}","scan_type":"Directory","status":"Complete"}}"#,
srv.url("/") srv.url("/"),
srv.url("/"),
); );
let incomplete_scan = format!( 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") srv.url("/js")
); );
let scans = format!(r#""scans":[{},{}]"#, complete_scan, incomplete_scan); 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| { let js_mock = srv.mock(|when, then| {
when.method(GET).path("/js"); 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| { let js_prod_mock = srv.mock(|when, then| {
when.method(GET).path("/js/prod"); 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| { let js_dev_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev"); 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| { 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| { let js_mock = srv.mock(|when, then| {
when.method(GET).path("/js/"); 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| { let js_prod_mock = srv.mock(|when, then| {
when.method(GET).path("/js/prod/"); 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| { let js_dev_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev/"); 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| { 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"); when.method(GET).path("/LICENSE");
then.status(301) then.status(301)
.body("this is a test") .body("this is a test")
.header("Location", &srv.url("/LICENSE")); .header("Location", srv.url("/LICENSE"));
}); });
let mock2 = srv.mock(|when, then| { let mock2 = srv.mock(|when, then| {

View File

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