Compare commits

...

310 Commits

Author SHA1 Message Date
epi
5d96658c79 Merge pull request #834 from epi052/827-load-wordlist-from-url
load wordlist from url; change some defaults/fix some bugs
2023-03-18 11:59:23 -05:00
epi
46d00507b0 removed cruft 2023-03-18 11:52:58 -05:00
epi
d561e59ec9 added test 2023-03-18 11:44:45 -05:00
epi
b786578c03 Merge pull request #824 from aancw/docs-package
Update alternative installation method for brew and chocolatey
2023-03-18 07:13:38 -05:00
epi
bd54ad0087 Merge branch 'main' into 827-load-wordlist-from-url 2023-03-18 07:09:14 -05:00
epi
d98c6a7457 bumped deps 2023-03-18 07:07:40 -05:00
epi
c493d001b5 fmt clippy etc 2023-03-18 07:02:45 -05:00
epi
bd4566fa7b updated parser text 2023-03-18 07:01:07 -05:00
epi
8fbf9d0274 -w accepts http/https urls 2023-03-18 06:59:19 -05:00
epi
d6b10c6476 reverted collect-backups change 2023-03-18 06:07:38 -05:00
epi
a5e845864c Merge branch 'main' of github.com:epi052/feroxbuster 2023-03-17 06:47:19 -05:00
epi
b02358678b added check for force-recursion to dirlisting check 2023-03-17 06:47:13 -05:00
epi
1b8fdcec17 hid old false defaults; added dont-* flags 2023-03-17 06:32:28 -05:00
epi
92cc2ab448 fixed test 2023-03-17 06:31:23 -05:00
epi
0b0e08ae4f updated extract-links and collect-backups default to true 2023-03-17 05:45:19 -05:00
epi
25762395b1 Merge pull request #833 from epi052/all-contributors/add-imBigo
docs: add imBigo as a contributor for bug
2023-03-16 21:30:12 -05:00
allcontributors[bot]
55b4034bd0 docs: update .all-contributorsrc [skip ci] 2023-03-17 02:29:52 +00:00
allcontributors[bot]
ffa409ca3d docs: update README.md [skip ci] 2023-03-17 02:29:51 +00:00
epi
bb4a335299 fixed divide by zero error 2023-03-16 21:23:39 -05:00
epi
1e0ec5c833 fixed divide by zero error 2023-03-16 21:21:05 -05:00
Aan
b5fa6b149e Update alternative installation method for brew and chocolatey 2023-03-12 22:05:05 +07:00
epi
04a43a0892 Merge pull request #823 from epi052/819-fix-resume-with-offset
fix resume with offset
2023-03-12 07:02:30 -05:00
epi
8a72e498e6 updated deps 2023-03-12 06:41:47 -05:00
epi
2987a84776 cleaned up another prog bar logic issue 2023-03-12 06:28:59 -05:00
epi
8add5599fb fixed the prog bar # issue 2023-03-12 06:28:22 -05:00
epi
9f557329eb fixed indexing out of bounds w/ extensions/methods on resume 2023-03-11 07:34:17 -06:00
epi
c04bf4a703 Merge pull request #807 from aancw/chocolatey
Adding feroxbuster as chocolatey package
2023-03-11 06:19:26 -06:00
epi
03e8625c6e Merge pull request #821 from epi052/816-fix-scan-mgt-menu-things
fix scan mgt menu things
2023-03-10 21:20:50 -06:00
epi
5d6b85fe12 clippy/fmt 2023-03-10 21:10:26 -06:00
epi
771041d225 added ability to stop previously unstoppable scans 2023-03-10 20:43:12 -06:00
epi
b5debed322 merged main 2023-03-10 19:42:44 -06:00
epi
30407cd338 fixed broken test 2023-03-10 16:19:52 -06:00
epi
ba4b26f2cd Update README.md 2023-03-10 16:15:23 -06:00
epi
4fdf558936 Merge pull request #820 from epi052/all-contributors/add-aancw
docs: add aancw as a contributor for ideas
2023-03-10 16:14:24 -06:00
allcontributors[bot]
2ffb0df516 docs: update .all-contributorsrc [skip ci] 2023-03-10 22:14:04 +00:00
allcontributors[bot]
10260f9db7 docs: update README.md [skip ci] 2023-03-10 22:14:03 +00:00
epi
4067be2f82 Merge pull request #813 from aancw/update-package
Implement auto update feature
2023-03-10 16:13:45 -06:00
Aan
7cb9c1c914 remove old commented code 2023-03-10 20:47:08 +07:00
Aan
99cbd657a5 Update parser, banner & test, exception handling, etc 2023-03-10 20:44:34 +07:00
Aan
703da383a7 Fix for fmt, clippy and nextest 2023-03-09 11:28:11 +07:00
Aan
aa83e40c4f Update README.md 2023-03-09 10:55:09 +07:00
Aan
a77c436e04 New feature checklist 2023-03-09 10:49:25 +07:00
Aan
c3455d123e Implement auto update feature 2023-03-09 10:06:17 +07:00
Aan
6431f01f12 Update iconUrl and copyright year in nuspec 2023-03-09 07:02:14 +07:00
epi
2d381e7e05 added logo for chocolatey packaging 2023-03-08 06:20:37 -06:00
epi
7d26f368f5 Merge pull request #808 from epi052/fix-wildcard-directory-redirect-v2
Fix wildcard directory redirect v2
2023-03-08 06:14:27 -06:00
epi
36970896ca Merge pull request #810 from epi052/all-contributors/add-aancw
docs: add aancw as a contributor for code, and infra
2023-03-08 05:54:56 -06:00
epi
39a75f0608 Merge pull request #804 from aancw/scanmanager-banner
Showing banner again after finish scan management menu
2023-03-08 05:54:26 -06:00
allcontributors[bot]
ab8537beeb docs: update .all-contributorsrc [skip ci] 2023-03-08 11:54:12 +00:00
allcontributors[bot]
9e907d37d5 docs: update README.md [skip ci] 2023-03-08 11:54:11 +00:00
epi
19e0a7f48b Merge branch 'main' into fix-wildcard-directory-redirect-v2 2023-03-08 05:50:29 -06:00
epi
5e93da0a65 fixed #809; thorough/smart bypassed mutual exclusion 2023-03-08 05:29:30 -06:00
Aan
fd0f31705d Update Copyright year in license 2023-03-08 14:49:35 +07:00
Aan
2704e33178 Update the code as requested in suggestion 2023-03-08 14:47:39 +07:00
epi
8392f6d26b fixed menu filter display; fixed wildcard filter comparison 2023-03-07 21:14:20 -06:00
epi
ca43a767d2 fixed failing test 2023-03-07 20:15:10 -06:00
epi
291ccedba3 clippy 2023-03-07 18:54:32 -06:00
epi
6d01bc8ec4 added a few tests taht were removed previously 2023-03-07 18:38:10 -06:00
epi
94aafccf8a bumped version 2023-03-07 06:30:53 -06:00
epi
8dd8871ae5 old tests pass 2023-03-07 06:27:24 -06:00
epi
ad0df8ccd3 updated deps 2023-03-07 06:00:00 -06:00
epi
31cdba64e4 fmt 2023-03-06 20:44:24 -06:00
epi
584fc940cd implemented fix for wildcard directories 2023-03-06 20:44:14 -06:00
Aan
5252587e65 Adding feroxbuster as chocolatey package 2023-03-06 21:56:44 +07:00
Aan
43116f9aab Showing banner again after finish scan management menu 2023-03-06 19:49:50 +07:00
Aan
aec083ea58 Showing banner again after finish scan management menu 2023-03-06 19:47:33 +07:00
epi
52d08e504d Merge pull request #801 from epi052/all-contributors/add-Luoooio
docs: add Luoooio as a contributor for ideas
2023-02-28 15:55:12 -06:00
allcontributors[bot]
a254574ce7 docs: update .all-contributorsrc [skip ci] 2023-02-28 21:55:03 +00:00
allcontributors[bot]
6cb7c8e342 docs: update README.md [skip ci] 2023-02-28 21:55:02 +00:00
epi
98670f367f Merge pull request #800 from epi052/all-contributors/add-xaeroborg
docs: add xaeroborg as a contributor for ideas
2023-02-28 15:53:42 -06:00
allcontributors[bot]
68913c9950 docs: update .all-contributorsrc [skip ci] 2023-02-28 21:53:34 +00:00
allcontributors[bot]
5901c75187 docs: update README.md [skip ci] 2023-02-28 21:53:33 +00:00
epi
8499901bfe Merge pull request #799 from epi052/all-contributors/add-pich4ya
docs: add pich4ya as a contributor for ideas
2023-02-28 15:52:00 -06:00
allcontributors[bot]
69dcb38360 docs: update .all-contributorsrc [skip ci] 2023-02-28 21:51:46 +00:00
allcontributors[bot]
eb8b70668d docs: update README.md [skip ci] 2023-02-28 21:51:45 +00:00
epi
f0702794b0 Merge pull request #796 from epi052/751-resume-scan-with-offset
resume scan starts from offset in wordlist
2023-02-28 06:32:17 -06:00
epi
367dcdbd72 fixed hanging test 2023-02-28 06:06:53 -06:00
epi
4b7a25c13b fixed ordering of dir bars / overall bar; fixed overall offset when resuming 2023-02-27 19:25:17 -06:00
epi
339189ff13 resume scan starts from offset in wordlist 2023-02-27 07:26:59 -06:00
epi
ed701c13b0 Merge pull request #794 from epi052/784-content-based-auto-filtering
Content-based auto filtering
2023-02-26 19:50:41 -06:00
epi
e034734df4 Merge branch 'main' into 784-content-based-auto-filtering 2023-02-26 13:21:55 -06:00
epi
73e2558404 fmt 2023-02-26 13:05:37 -06:00
epi
eb7ad68c01 all tests passing 2023-02-26 12:48:02 -06:00
epi
c61688f984 fmt 2023-02-26 07:31:03 -06:00
epi
6c96589ca5 fixed some tests 2023-02-26 07:30:41 -06:00
epi
0437c2baac updated deps harder 2023-02-26 07:30:28 -06:00
epi
0d689942eb updated deps 2023-02-26 06:54:23 -06:00
epi
74a1a8d597 bumped version to 2.8.0 2023-02-26 06:50:31 -06:00
epi
729d88a724 clippy 2023-02-26 06:49:48 -06:00
epi
ad38b56473 finalized new detections; removed wildcard filter and supporting code 2023-02-26 06:39:03 -06:00
epi
655364d9bd removed wildcard test, integrated into 404 detection 2023-02-25 20:58:28 -06:00
epi
ac7f59cd3f updated default status codes to all; adjusted banner entry 2023-02-25 07:28:27 -06:00
epi
0d64d28fe6 removed cruft 2023-02-25 06:28:14 -06:00
epi
89c29600c7 removed cruft 2023-02-25 06:23:39 -06:00
epi
96375e7734 added minhash algo when resp too short for ssdeep 2023-02-25 06:20:30 -06:00
epi
3531b8c74b added gaoya dependency for minhash algo 2023-02-25 06:20:08 -06:00
epi
e8f4438a52 fixed bug in dynamic wildcards; reorded 404-like id strat 2023-02-24 20:09:29 -06:00
epi
02b25dc553 incremental save for testing 2023-02-23 17:21:48 -06:00
epi
551cf065f3 Merge pull request #793 from epi052/all-contributors/add-f3rn0s
docs: add f3rn0s as a contributor for bug
2023-02-16 20:30:41 -06:00
allcontributors[bot]
c81885cf5e docs: update .all-contributorsrc [skip ci] 2023-02-17 02:30:28 +00:00
allcontributors[bot]
6a3d250e3b docs: update README.md [skip ci] 2023-02-17 02:30:27 +00:00
epi
259fbcca74 Merge pull request #790 from epi052/all-contributors/add-joaociocca
docs: add joaociocca as a contributor for bug, and ideas
2023-02-15 20:50:31 -06:00
allcontributors[bot]
f3c9f8ed20 docs: update .all-contributorsrc [skip ci] 2023-02-16 02:49:52 +00:00
allcontributors[bot]
be400ce971 docs: update README.md [skip ci] 2023-02-16 02:49:51 +00:00
epi
b62c76bce3 updated deps 2023-02-15 20:44:07 -06:00
epi
990a471d71 Merge pull request #779 from epi052/fix-some-visuals
Fix some visuals; update deps
2023-02-15 20:39:58 -06:00
epi
ec47d6f934 updated deps 2023-02-15 20:31:54 -06:00
epi
da509bd208 clippy 2023-02-15 19:39:27 -06:00
epi
8568b340a9 clippy 2023-02-15 19:38:23 -06:00
epi
7c9d8f529d tweaked auto-tune behavior to more aggressively move upward 2023-02-15 19:36:16 -06:00
epi
6d47b4b68b fixed a case where the --dont-filter message wasnt shown 2023-02-15 07:04:48 -06:00
epi
be3290572e fallback to body.len when content-length header missing 2023-02-15 06:40:35 -06:00
epi
4f13fd7974 Merge branch 'fix-some-visuals' of github.com:epi052/feroxbuster into fix-some-visuals 2023-02-07 05:33:50 -06:00
epi
57b8117015 fixed stale file reference 2023-02-05 20:33:53 -06:00
epi
7b4900fa07 caught a comparison bug 2023-02-01 18:51:29 -06:00
epi
d1e47b0025 added messaging about state of auto-tune/bail 2023-01-30 16:46:29 -06:00
epi
98612e2256 changed auto-tune emoji to align across different terminals 2023-01-30 16:46:03 -06:00
epi
f08023b813 Update README.md 2023-01-13 05:53:18 -06:00
epi
98254e3cac Update README.md 2023-01-13 05:52:55 -06:00
epi
46cc64325f Update README.md 2023-01-13 05:51:02 -06:00
epi
fc034f0720 Update README.md 2023-01-13 05:49:51 -06:00
epi
ef4a597cb1 Update README.md 2023-01-13 05:48:56 -06:00
epi
bb6b12d168 Merge branch 'main' of github.com:epi052/feroxbuster 2023-01-13 05:32:53 -06:00
epi
21a9de2d39 fixed code coverage workflow 2023-01-13 05:32:48 -06:00
epi
7d8f3b0305 Merge pull request #764 from epi052/all-contributors/add-aidanhall34
docs: add aidanhall34 as a contributor for code, and infra
2023-01-13 05:13:39 -06:00
allcontributors[bot]
6b3fe48b4f docs: update .all-contributorsrc [skip ci] 2023-01-13 11:12:49 +00:00
allcontributors[bot]
7a79000d96 docs: update README.md [skip ci] 2023-01-13 11:12:48 +00:00
epi
d164034d3e Merge pull request #762 from aidanhall34/NA-dockerfile
Fixes #761 | Updated Dockerfile and CONTRIBUTING docs
2023-01-13 05:12:25 -06:00
aidan.hall34
e4dc7da756 Remove edge branch, update alpine 2023-01-12 23:34:03 +00:00
aidan.hall34
6090cefa4f Updated Dockerfile and CONTRIBUTING docs 2023-01-12 14:16:58 +00:00
epi
ec05644854 Merge pull request #753 from epi052/all-contributors/add-duokebei
docs: add duokebei as a contributor for ideas
2022-12-29 20:31:42 -06:00
allcontributors[bot]
567f927884 docs: update .all-contributorsrc [skip ci] 2022-12-30 02:31:17 +00:00
allcontributors[bot]
176a6a6426 docs: update README.md [skip ci] 2022-12-30 02:31:16 +00:00
epi
c99f6146e3 Update README.md 2022-12-29 20:29:56 -06:00
epi
fb34817509 Merge pull request #752 from epi052/all-contributors/add-hakdogpinas
docs: add hakdogpinas as a contributor for ideas
2022-12-29 20:29:26 -06:00
epi
0c8e5d51f0 Update .all-contributorsrc 2022-12-29 20:27:05 -06:00
allcontributors[bot]
ac24e507ac docs: update .all-contributorsrc [skip ci] 2022-12-30 02:24:54 +00:00
allcontributors[bot]
808c749f63 docs: update README.md [skip ci] 2022-12-30 02:24:53 +00:00
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
epi
ccb10c1c68 Update README.md 2022-04-15 05:53:58 -05:00
epi
20ab0aade3 Update README.md 2022-04-15 05:52:14 -05:00
epi
02ad0b1d85 Update README.md 2022-04-15 05:49:58 -05:00
epi
09aad922c1 Update README.md 2022-04-15 05:48:49 -05:00
epi
697f947bfa Update README.md 2022-04-15 05:47:42 -05:00
epi
d300d68737 Update README.md 2022-04-15 05:46:39 -05:00
epi
c2c6854db4 Merge pull request #541 from epi052/all-contributors/add-Flangyver
docs: add Flangyver as a contributor for ideas
2022-04-15 05:45:26 -05:00
allcontributors[bot]
63be575d89 docs: update .all-contributorsrc [skip ci] 2022-04-15 10:45:17 +00:00
allcontributors[bot]
0d25fda11e docs: update README.md [skip ci] 2022-04-15 10:45:16 +00:00
epi
b0341c2432 Merge pull request #540 from epi052/all-contributors/add-0xdf223
docs: add 0xdf223 as a contributor for bug, ideas
2022-04-15 05:42:54 -05:00
allcontributors[bot]
62352db152 docs: update .all-contributorsrc [skip ci] 2022-04-15 10:42:41 +00:00
allcontributors[bot]
3090edc49c docs: update README.md [skip ci] 2022-04-15 10:42:40 +00:00
epi
85d686d1aa Merge pull request #539 from epi052/all-contributors/add-ThisLimn0
docs: add ThisLimn0 as a contributor for bug
2022-04-15 05:41:41 -05:00
allcontributors[bot]
17138f4ef7 docs: update .all-contributorsrc [skip ci] 2022-04-15 10:41:28 +00:00
allcontributors[bot]
1d30b7db31 docs: update README.md [skip ci] 2022-04-15 10:41:27 +00:00
epi
4c0d3c91a0 Merge pull request #536 from epi052/535-status-code-filter-overhaul
535 status code filter overhaul
2022-04-15 05:39:55 -05:00
epi
96fc6b232a update 2022-04-14 21:08:33 -05:00
epi
9b306aad34 update 2022-04-14 16:45:14 -05:00
epi
10eee184d0 update 2022-04-14 16:42:25 -05:00
epi
986161f05f update 2022-04-14 16:39:50 -05:00
epi
4a19dbfd7d update 2022-04-14 16:14:23 -05:00
epi
b8ceeaff0f update 2022-04-14 16:07:37 -05:00
epi
d04e58036e update 2022-04-14 13:10:56 -05:00
epi
d1a74207f4 one more again 2022-04-14 08:13:51 -05:00
epi
03a36f0b60 update 2022-04-14 07:54:43 -05:00
epi
f2d9269643 update 2022-04-14 07:44:35 -05:00
epi
bba7cba02e update 2022-04-14 06:46:26 -05:00
epi
fffd1e5c82 update 2022-04-14 06:44:51 -05:00
epi
3c6da0f782 update 2022-04-14 06:40:35 -05:00
epi
5ecd937c0e reverted heuristics tests 2022-04-14 06:25:55 -05:00
epi
9f6221daf6 removed more toolchain actions 2022-04-14 06:18:00 -05:00
epi
af49fd8e62 attempting to remove toolchain action 2022-04-14 06:15:14 -05:00
epi
d1daefd8ba removed allows from ci clippy 2022-04-14 06:12:42 -05:00
epi
3e8255d5b7 reverted ci back to normal 2022-04-14 06:11:38 -05:00
epi
5af18e83d8 ci tweak 2022-04-14 05:48:43 -05:00
epi
d1d0757d56 test troubleshoot 2022-04-14 05:28:20 -05:00
epi
f5f9344a81 test troubleshoot 2022-04-14 05:09:50 -05:00
epi
fd52e39188 reverted build to main only 2022-04-13 20:44:57 -05:00
epi
22377dc9a3 trying again with new ci configs 2022-04-13 20:43:34 -05:00
epi
1cf7dff734 trying again with new ci configs 2022-04-13 20:34:16 -05:00
epi
7c9eb900b7 reverted test action 2022-04-13 20:14:57 -05:00
epi
8480b3cc2c reverted coverage 2022-04-13 20:01:18 -05:00
epi
9d29142046 lint 2022-04-13 19:45:30 -05:00
epi
38c194b222 tweaked coverage action 2022-04-13 19:36:05 -05:00
epi
72dc14bf3d fixed nlp tests 2022-04-13 19:20:24 -05:00
epi
9a7c690c17 changed to SecLists 2022-04-13 17:55:33 -05:00
epi
de4514e381 test build windows 2022-04-13 17:27:15 -05:00
epi
2be8aaf2bf changed -w default when on windows host 2022-04-13 17:19:09 -05:00
epi
4db3a0b056 updated help a bit 2022-04-13 16:40:51 -05:00
epi
a9ef7f180f force-recursion and no-recursion are mutually exclusive 2022-04-13 16:34:52 -05:00
epi
ac7cb5d6b6 lint / tests 2022-04-13 06:25:35 -05:00
epi
f8e18abb48 added filter checking logic to collect-backups requests 2022-04-12 21:09:51 -05:00
epi
1d805aca5a tweaked pre-processing and selection criteria for nlp 2022-04-12 20:55:37 -05:00
epi
fa09266804 handles passed through to termhandler 2022-04-12 20:53:24 -05:00
epi
15592c3dfd added AddHandles command 2022-04-12 20:52:57 -05:00
epi
53c171aeb5 extract-links should also abide by forced recursion into found assets only 2022-04-12 07:15:40 -05:00
epi
5167d24c4b another deb builder fix attempt 2022-04-12 06:49:42 -05:00
epi
81ff62c53d force recursion should only happen on found assets 2022-04-12 06:45:37 -05:00
epi
433f68458e updated test runner to nextest 2022-04-12 06:11:09 -05:00
epi
d32720a90a fixed deb build, maybe 2022-04-12 06:04:35 -05:00
epi
0ceef975e6 updated ci coverage job 2022-04-12 05:52:03 -05:00
epi
6d7d6c4e7b added tests for --force-recursion 2022-04-11 20:14:41 -05:00
epi
5f21953bc1 added --force-recursion option 2022-04-11 20:13:10 -05:00
epi
6906ac0ee8 added force-recursion to example config 2022-04-11 20:12:58 -05:00
epi
cafe766d9e bumped version to 2.7.0 2022-04-11 20:12:38 -05:00
epi
98d0d177df updated parse extensions logic and banner to reflect status code changes 2022-04-11 17:37:36 -05:00
epi
23e10833d0 cicd test 2022-04-11 17:13:01 -05:00
epi
7f51d0f7bf initial stab at updated statuscode behavior 2022-04-11 17:10:53 -05:00
epi
9515a5da99 fixed bug related to methods/responses not being displayed 2022-04-09 11:08:46 -05:00
epi
b417ab41a5 fixed bug related to methods/responses not being displayed 2022-04-09 11:08:16 -05:00
epi
b946565c2f updated shell completions for new version 2022-04-09 06:46:14 -05:00
epi
714b054360 bumped version to 2.6.3 2022-04-09 06:24:48 -05:00
epi
30544eaf7d fixed bug in replay proxy related to #501 2022-04-09 06:18:50 -05:00
epi
99e2d46aa2 Merge pull request #534 from epi052/all-contributors/add-jhaddix
docs: add jhaddix as a contributor for ideas
2022-04-07 07:04:05 -05:00
allcontributors[bot]
82a4f16252 docs: update .all-contributorsrc [skip ci] 2022-04-07 12:02:25 +00:00
allcontributors[bot]
6e0fe1eced docs: update README.md [skip ci] 2022-04-07 12:02:24 +00:00
epi
1ffc93a337 Merge pull request #533 from epi052/527-add-filters-via-scan-menu
add and remove filters via scan management menu
2022-04-06 20:53:20 -05:00
epi
7ab0453eb7 lint 2022-04-06 20:13:07 -05:00
epi
50b29a2b74 added remove filter command to SMM 2022-04-06 19:16:30 -05:00
epi
bf78fea926 Merge pull request #528 from epi052/527-add-filters-via-scan-menu
add filters via scan menu
2022-04-04 19:18:07 -05:00
epi
56267726cc finalized tests 2022-04-04 19:03:51 -05:00
epi
01844cffd8 added filters::utils tests 2022-04-04 18:28:39 -05:00
epi
2abc0b78ee removed unnecessary async 2022-04-04 06:22:38 -05:00
epi
fdb8774bfd removed unnecessary async 2022-04-04 06:17:32 -05:00
epi
8d3cd2471b added docs to new util fns 2022-04-04 06:12:18 -05:00
epi
da22371c87 removed duplicate import 2022-04-04 06:06:24 -05:00
epi
ca494ca801 removed duplicate link-attr extractor 2022-04-04 06:01:01 -05:00
epi
376804aa59 added test for empty filter 2022-04-04 05:59:28 -05:00
epi
56a769c197 upgraded deps 2022-04-04 05:44:23 -05:00
epi
9922eb5124 added clippy to makefile 2022-04-03 20:43:12 -05:00
epi
7a58f8fcf8 clippy and tests 2022-04-03 20:42:55 -05:00
epi
8d1872fb3f initial stab at implementation 2022-04-03 20:20:08 -05:00
epi
e1e59e6e8e Merge pull request #515 from epi052/all-contributors/add-gtjamesa
docs: add gtjamesa as a contributor for bug
2022-03-08 05:50:02 -06:00
allcontributors[bot]
0c731c3639 docs: update .all-contributorsrc [skip ci] 2022-03-08 11:49:34 +00:00
allcontributors[bot]
b456215a00 docs: update README.md [skip ci] 2022-03-08 11:49:33 +00:00
epi
d38c66f290 Merge pull request #514 from epi052/513-fix-collect-backups-root-request
backups requested from proper dir
2022-03-08 05:46:37 -06:00
epi
6ba32d926c fixed 513; backups requested from proper dir 2022-03-08 05:43:40 -06:00
epi
86bcfd8fb1 Merge pull request #512 from epi052/all-contributors/add-ippsec
docs: add ippsec as a contributor for ideas
2022-03-05 08:15:33 -06:00
allcontributors[bot]
4a101e4ae5 docs: update .all-contributorsrc [skip ci] 2022-03-05 14:15:17 +00:00
allcontributors[bot]
1ae1430a11 docs: update README.md [skip ci] 2022-03-05 14:15:16 +00:00
87 changed files with 4626 additions and 1985 deletions

View File

@@ -294,10 +294,10 @@
]
},
{
"login": "narkopolo",
"name": "narkopolo",
"login": "n0kovo",
"name": "n0kovo",
"avatar_url": "https://avatars.githubusercontent.com/u/16690056?v=4",
"profile": "https://github.com/narkopolo",
"profile": "https://github.com/n0kovo",
"contributions": [
"ideas"
]
@@ -366,6 +366,202 @@
"contributions": [
"ideas"
]
},
{
"login": "ippsec",
"name": "ippsec",
"avatar_url": "https://avatars.githubusercontent.com/u/24677271?v=4",
"profile": "https://github.com/IppSec",
"contributions": [
"ideas"
]
},
{
"login": "gtjamesa",
"name": "James",
"avatar_url": "https://avatars.githubusercontent.com/u/2078364?v=4",
"profile": "https://github.com/gtjamesa",
"contributions": [
"bug"
]
},
{
"login": "jhaddix",
"name": "Jason Haddix",
"avatar_url": "https://avatars.githubusercontent.com/u/3488554?v=4",
"profile": "https://twitter.com/Jhaddix",
"contributions": [
"ideas",
"bug"
]
},
{
"login": "ThisLimn0",
"name": "Limn0",
"avatar_url": "https://avatars.githubusercontent.com/u/67125885?v=4",
"profile": "https://github.com/ThisLimn0",
"contributions": [
"bug"
]
},
{
"login": "0xdf223",
"name": "0xdf",
"avatar_url": "https://avatars.githubusercontent.com/u/76954092?v=4",
"profile": "https://github.com/0xdf223",
"contributions": [
"bug",
"ideas"
]
},
{
"login": "Flangyver",
"name": "Flangyver",
"avatar_url": "https://avatars.githubusercontent.com/u/59575870?v=4",
"profile": "https://github.com/Flangyver",
"contributions": [
"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"
]
},
{
"login": "hakdogpinas",
"name": "hakdogpinas",
"avatar_url": "https://avatars.githubusercontent.com/u/71529469?v=4",
"profile": "https://github.com/hakdogpinas",
"contributions": [
"ideas"
]
},
{
"login": "duokebei",
"name": "多可悲",
"avatar_url": "https://avatars.githubusercontent.com/u/75022552?v=4",
"profile": "https://github.com/duokebei",
"contributions": [
"ideas"
]
},
{
"login": "aidanhall34",
"name": "Aidan Hall",
"avatar_url": "https://avatars.githubusercontent.com/u/58670593?v=4",
"profile": "https://blog.ah34.net/",
"contributions": [
"code",
"infra"
]
},
{
"login": "joaociocca",
"name": "João Ciocca",
"avatar_url": "https://avatars.githubusercontent.com/u/6473725?v=4",
"profile": "https://hachyderm.io/@JohnnyCiocca",
"contributions": [
"bug",
"ideas"
]
},
{
"login": "f3rn0s",
"name": "f3rn0s",
"avatar_url": "https://avatars.githubusercontent.com/u/1351279?v=4",
"profile": "https://github.com/f3rn0s",
"contributions": [
"bug"
]
},
{
"login": "pich4ya",
"name": "LongCat",
"avatar_url": "https://avatars.githubusercontent.com/u/2099767?v=4",
"profile": "https://sth.sh",
"contributions": [
"ideas"
]
},
{
"login": "xaeroborg",
"name": "xaeroborg",
"avatar_url": "https://avatars.githubusercontent.com/u/33274680?v=4",
"profile": "https://github.com/xaeroborg",
"contributions": [
"ideas"
]
},
{
"login": "Luoooio",
"name": "Luoooio",
"avatar_url": "https://avatars.githubusercontent.com/u/26653157?v=4",
"profile": "https://github.com/Luoooio",
"contributions": [
"ideas"
]
},
{
"login": "aancw",
"name": "Aan",
"avatar_url": "https://avatars.githubusercontent.com/u/6284204?v=4",
"profile": "https://petruknisme.com",
"contributions": [
"code",
"infra",
"ideas"
]
},
{
"login": "imBigo",
"name": "Simon",
"avatar_url": "https://avatars.githubusercontent.com/u/54672433?v=4",
"profile": "https://github.com/imBigo",
"contributions": [
"bug"
]
}
],
"contributorsPerLine": 7,
@@ -373,5 +569,6 @@
"projectOwner": "epi052",
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": true
"skipCi": true,
"commitConvention": "angular"
}

View File

@@ -73,18 +73,22 @@ jobs:
name: ${{ matrix.name }}.tar.gz
path: ${{ matrix.name }}.tar.gz
build-deb:
needs: [build-nix]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Deb Build
uses: ebbflow-io/cargo-deb-amd64-ubuntu@1.0
- name: Upload Deb Artifact
uses: actions/upload-artifact@v2
with:
name: feroxbuster_amd64.deb
path: ./target/x86_64-unknown-linux-musl/debian/*
# build-deb:
# needs: [build-nix]
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@master
# - name: Install cargo-deb
# run: cargo install -f cargo-deb
# - name: Install musl toolchain
# run: rustup target add x86_64-unknown-linux-musl
# - name: Deb Build
# run: cargo deb --target=x86_64-unknown-linux-musl
# - name: Upload Deb Artifact
# uses: actions/upload-artifact@v2
# with:
# name: feroxbuster_amd64.deb
# path: ./target/x86_64-unknown-linux-musl/debian/*
build-macos:
env:

View File

@@ -8,11 +8,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
with:
command: check
@@ -22,26 +17,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
- name: Install latest nextest release
uses: taiki-e/install-action@nextest
- name: Test with latest nextest release
uses: actions-rs/cargo@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
with:
command: test
command: nextest
args: run --all-features --all-targets --retries 10
fmt:
name: Rust fmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
@@ -52,13 +40,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup component add clippy
- uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-targets --all-features -- -D warnings -A clippy::deref_addrof -A clippy::mutex-atomic
args: --all-targets --all-features -- -D warnings

View File

@@ -3,42 +3,22 @@ on: [push]
name: Code Coverage Pipeline
jobs:
upload-coverage:
coverage:
name: LLVM Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: nightly
override: true
- uses: actions-rs/cargo@v1
components: llvm-tools-preview
- name: Install cargo-llvm-cov and cargo-nextest
uses: taiki-e/install-action@v2
with:
command: clean
- uses: actions-rs/cargo@v1
tool: cargo-nextest,cargo-llvm-cov
- name: Generate code coverage
run: cargo llvm-cov nextest --all-features --no-fail-fast --lcov --output-path lcov.info
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
command: test
args: --all-features --no-fail-fast
env:
CARGO_INCREMENTAL: '0'
RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort'
RUSTDOCFLAGS: '-Cpanic=abort'
- uses: actions-rs/grcov@v0.1
- uses: actions/upload-artifact@v2
with:
name: lcov.info
path: lcov.info
- name: Convert lcov to xml
run: |
curl -O https://raw.githubusercontent.com/epi052/lcov-to-cobertura-xml/master/lcov_cobertura/lcov_cobertura.py
chmod +x lcov_cobertura.py
./lcov_cobertura.py ./lcov.info
- uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
name: codecov-umbrella
files: lcov.info
fail_ci_if_error: true
- uses: actions/upload-artifact@v2
with:
name: coverage.xml
path: ./coverage.xml

3
.gitignore vendored
View File

@@ -30,3 +30,6 @@ ferox-*.state
# python stuff cuz reasons
Pipfile*
# ignore choco_package generated nupkg
/choco_package/*.nupkg

View File

@@ -76,35 +76,35 @@ Now that you have a copy of your fork, there is work you will need to do to keep
Do this prior to every time you create a branch for a PR:
1. Make sure you are on the `master` branch
1. Make sure you are on the `main` branch
> ```sh
> $ git status
> On branch master
> Your branch is up-to-date with 'origin/master'.
> On branch main
> Your branch is up-to-date with 'origin/main'.
> ```
> If your aren't on `master`, resolve outstanding files and commits and checkout the `master` branch
> If your aren't on `main`, resolve outstanding files and commits and checkout the `main` branch
> ```sh
> $ git checkout master
> $ git checkout main
> ```
2. Do a pull with rebase against `upstream`
> ```sh
> $ git pull --rebase upstream master
> $ git pull --rebase upstream main
> ```
> This will pull down all of the changes to the official master branch, without making an additional commit in your local repo.
> This will pull down all of the changes to the official main branch, without making an additional commit in your local repo.
3. (_Optional_) Force push your updated master branch to your GitHub fork
3. (_Optional_) Force push your updated main branch to your GitHub fork
> ```sh
> $ git push origin master --force
> $ git push origin main --force
> ```
> This will overwrite the master branch of your fork.
> This will overwrite the main branch of your fork.
### Creating a branch
@@ -214,20 +214,20 @@ GitHub has a good guide on how to contribute to open source [here](https://opens
##### Editing via your local fork
1. Perform the maintenance step of rebasing `master`
2. Ensure you're on the `master` branch using `git status`:
1. Perform the maintenance step of rebasing `main`
2. Ensure you're on the `main` branch using `git status`:
```sh
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
On branch main
Your branch is up-to-date with 'origin/main'.
nothing to commit, working directory clean
```
1. If you're not on master or your working directory is not clean, resolve
any outstanding files/commits and checkout master `git checkout master`
2. Create a branch off of `master` with git: `git checkout -B
1. If you're not on main or your working directory is not clean, resolve
any outstanding files/commits and checkout main `git checkout main`
2. Create a branch off of `main` with git: `git checkout -B
branch/name-here`
3. Edit your file(s) locally with the editor of your choice
4. Check your `git status` to see unstaged files
@@ -239,8 +239,8 @@ nothing to commit, working directory clean
8. Push your commits to your GitHub Fork: `git push -u origin branch/name-here`
9. Once the edits have been committed, you will be prompted to create a pull
request on your fork's GitHub page
10. By default, all pull requests should be against the `master` branch
11. Submit a pull request from your branch to feroxbuster's `master` branch
10. By default, all pull requests should be against the `main` branch
11. Submit a pull request from your branch to feroxbuster's `main` branch
12. The title (also called the subject) of your PR should be descriptive of your
changes and succinctly indicate what is being fixed
- Examples: `Add test cases for Unicode support`; `Correct typo in overview documentation`

1777
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "feroxbuster"
version = "2.6.0"
version = "2.9.2"
authors = ["Ben 'epi' Risher (@epi052)"]
license = "MIT"
edition = "2021"
@@ -8,7 +8,13 @@ homepage = "https://github.com/epi052/feroxbuster"
repository = "https://github.com/epi052/feroxbuster"
description = "A fast, simple, recursive content discovery tool."
categories = ["command-line-utilities"]
keywords = ["pentest", "enumeration", "url-bruteforce", "content-discovery", "web"]
keywords = [
"pentest",
"enumeration",
"url-bruteforce",
"content-discovery",
"web",
]
exclude = [".github/*", "img/*", "check-coverage.sh"]
build = "build.rs"
@@ -16,46 +22,47 @@ build = "build.rs"
maintenance = { status = "actively-developed" }
[build-dependencies]
clap = { version = "3.1.5", features = ["wrap_help", "cargo"] }
clap_complete = "3.1.1"
regex = "1.5.4"
clap = { version = "4.1.8", features = ["wrap_help", "cargo"] }
clap_complete = "4.1.4"
regex = "1.5.5"
lazy_static = "1.4.0"
dirs = "4.0.0"
[dependencies]
scraper = "0.12.0"
futures = "0.3.21"
tokio = { version = "1.17.0", features = ["full"] }
tokio-util = { version = "0.7.0", features = ["codec"] }
log = "0.4.14"
env_logger = "0.9.0"
reqwest = { version = "0.11.9", features = ["socks"] }
scraper = "0.15.0"
futures = "0.3.26"
tokio = { version = "1.26.0", features = ["full"] }
tokio-util = { version = "0.7.7", features = ["codec"] }
log = "0.4.17"
env_logger = "0.10.0"
reqwest = { version = "0.11.10", features = ["socks"] }
# uses feature unification to add 'serde' to reqwest::Url
url = { version = "2.2.2", features = ["serde"] }
serde_regex = "1.1.0"
clap = { version = "3.1.5", features = ["wrap_help", "cargo"] }
clap = { version = "4.1.8", features = ["wrap_help", "cargo"] }
lazy_static = "1.4.0"
toml = "0.5.8"
serde = { version = "1.0.136", features = ["derive", "rc"] }
serde_json = "1.0.79"
uuid = { version = "0.8.2", features = ["v4"] }
toml = "0.7.2"
serde = { version = "1.0.137", features = ["derive", "rc"] }
serde_json = "1.0.94"
uuid = { version = "1.3.0", features = ["v4"] }
indicatif = "0.15"
console = "0.15.0"
openssl = { version = "0.10.38", features = ["vendored"] }
console = "0.15.2"
openssl = { version = "0.10", features = ["vendored"] }
dirs = "4.0.0"
regex = "1.5.4"
crossterm = "0.23.0"
rlimit = "0.7.0"
ctrlc = "3.2.1"
fuzzyhash = "0.2.1"
anyhow = "1.0.55"
leaky-bucket = "0.10.0" # todo: upgrade, will take a little work/thought since api changed
regex = "1.5.5"
crossterm = "0.26.0"
rlimit = "0.9.1"
ctrlc = "3.2.2"
anyhow = "1.0.69"
leaky-bucket = "0.12.1"
gaoya = "0.1.2"
self_update = {version = "0.36.0", features = ["archive-tar", "compression-flate2", "archive-zip", "compression-zip-deflate"]}
[dev-dependencies]
tempfile = "3.3.0"
httpmock = "0.6.6"
assert_cmd = "2.0.4"
predicates = "2.1.1"
predicates = "3.0.1"
[profile.release]
lto = true
@@ -67,9 +74,29 @@ section = "utility"
license-file = ["LICENSE", "4"]
conf-files = ["/etc/feroxbuster/ferox-config.toml"]
assets = [
["target/release/feroxbuster", "/usr/bin/", "755"],
["ferox-config.toml.example", "/etc/feroxbuster/ferox-config.toml", "644"],
["shell_completions/feroxbuster.bash", "/usr/share/bash-completion/completions/feroxbuster.bash", "644"],
["shell_completions/feroxbuster.fish", "/usr/share/fish/completions/feroxbuster.fish", "644"],
["shell_completions/_feroxbuster", "/usr/share/zsh/vendor-completions/_feroxbuster", "644"],
[
"target/release/feroxbuster",
"/usr/bin/",
"755",
],
[
"ferox-config.toml.example",
"/etc/feroxbuster/ferox-config.toml",
"644",
],
[
"shell_completions/feroxbuster.bash",
"/usr/share/bash-completion/completions/feroxbuster.bash",
"644",
],
[
"shell_completions/feroxbuster.fish",
"/usr/share/fish/completions/feroxbuster.fish",
"644",
],
[
"shell_completions/_feroxbuster",
"/usr/share/zsh/vendor-completions/_feroxbuster",
"644",
],
]

View File

@@ -1,10 +1,7 @@
# Image: alpine:3.14.2
FROM alpine@sha256:69704ef328d05a9f806b6b8502915e6a0a4faa4d72018dc42343f511490daf8a as build
FROM alpine:3.17.1 as build
LABEL maintainer="wfnintr@null.net"
RUN sed -i -e 's/v[[:digit:]]\..*\//edge\//g' /etc/apk/repositories \
&& apk upgrade --update-cache --available && apk add --update openssl
RUN apk upgrade --update-cache --available && apk add --update openssl
# Download latest release
RUN wget https://github.com/epi052/feroxbuster/releases/latest/download/x86_64-linux-feroxbuster.zip -qO feroxbuster.zip \
@@ -12,9 +9,7 @@ RUN wget https://github.com/epi052/feroxbuster/releases/latest/download/x86_64-l
&& chmod +x /tmp/feroxbuster \
&& wget https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/Web-Content/raft-medium-directories.txt -O /tmp/raft-medium-directories.txt
# Image: alpine:3.14.2
FROM alpine@sha256:69704ef328d05a9f806b6b8502915e6a0a4faa4d72018dc42343f511490daf8a as release
from alpine:3.17.1 as release
COPY --from=build /tmp/raft-medium-directories.txt /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
COPY --from=build /tmp/feroxbuster /usr/local/bin/feroxbuster

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020 epi
Copyright (c) 2020-2023 epi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -70,7 +70,8 @@ ifeq (1, $(VENDORED))
endif
$(TARGET)/$(BIN): extract
mkdir -p .cargo
mkdir -p .cargo debian
touch debian/cargo.config
cp debian/cargo.config .cargo/config.toml
cargo build $(ARGS)

View File

@@ -11,8 +11,22 @@ rm ferox-*.state
# dependency management
[tasks.upgrade-deps]
command = "cargo"
args = ["upgrade", "--exclude", "indicatif", "leaky-bucket"]
args = ["upgrade", "--exclude", "indicatif"]
[tasks.update]
command = "cargo"
args = ["update"]
# clippy / lint
[tasks.clippy]
clear = true
script = """
cargo clippy --all-targets --all-features -- -D warnings
"""
# tests
[tasks.test]
clear = true
script = """
cargo nextest run --all-features --all-targets --retries 10
"""

154
README.md
View File

@@ -8,7 +8,7 @@
<p align="center">
<a href="https://github.com/epi052/feroxbuster/actions?query=workflow%3A%22CI+Pipeline%22">
<img src="https://img.shields.io/github/workflow/status/epi052/feroxbuster/CI%20Pipeline/main?logo=github">
<img src="https://img.shields.io/github/actions/workflow/status/epi052/feroxbuster/.github/workflows/check.yml?branch=main&logo=github">
</a>
<a href="https://github.com/epi052/feroxbuster/releases">
@@ -101,6 +101,11 @@ sudo apt update && sudo apt install -y feroxbuster
curl -sL https://raw.githubusercontent.com/epi052/feroxbuster/master/install-nix.sh | bash
```
#### MacOS via Homebrew
```
brew install feroxbuster
```
#### Windows x86_64
@@ -110,10 +115,22 @@ Expand-Archive .\feroxbuster.zip
.\feroxbuster\feroxbuster.exe -V
```
#### Windows via Chocolatey
```
choco install feroxbuster
```
#### All others
Please refer the the [documentation](https://epi052.github.io/feroxbuster-docs/docs/).
### Updating feroxbuster (new in v2.9.1)
```
./feroxbuster --update
```
## 🧰 Example Usage
Here are a few brief examples to get you started. Please note, feroxbuster can do a **lot more** than what's listed below. As a result, there are **many more** examples, with **demonstration gifs** that highlight specific features, in the [documentation](https://epi052.github.io/feroxbuster-docs/docs/).
@@ -167,6 +184,8 @@ cat targets | ./feroxbuster --stdin --silent -s 200 301 302 --redirects -x js |
./feroxbuster -u http://127.1 --query token=0123456789ABCDEF
```
## 🚀 Documentation has **moved** 🚀
For realsies, there used to be over 1300 lines in this README, but it's all been moved to the [new documentation site](https://epi052.github.io/feroxbuster-docs/docs/). Go check it out!
@@ -181,57 +200,86 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://io.fi"><img src="https://avatars.githubusercontent.com/u/5235109?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Joona Hoikkala</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=joohoi" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/jsav0"><img src="https://avatars.githubusercontent.com/u/20546041?v=4?s=100" width="100px;" alt=""/><br /><sub><b>J Savage</b></sub></a><br /><a href="#infra-jsav0" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=jsav0" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.tgotwig.dev"><img src="https://avatars.githubusercontent.com/u/30773779?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Thomas Gotwig</b></sub></a><br /><a href="#infra-TGotwig" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=TGotwig" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/spikecodes"><img src="https://avatars.githubusercontent.com/u/19519553?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Spike</b></sub></a><br /><a href="#infra-spikecodes" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=spikecodes" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/evanrichter"><img src="https://avatars.githubusercontent.com/u/330292?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Evan Richter</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=evanrichter" title="Code">💻</a> <a href="https://github.com/epi052/feroxbuster/commits?author=evanrichter" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/mzpqnxow"><img src="https://avatars.githubusercontent.com/u/8016228?v=4?s=100" width="100px;" alt=""/><br /><sub><b>AG</b></sub></a><br /><a href="#ideas-mzpqnxow" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=mzpqnxow" title="Documentation">📖</a></td>
<td align="center"><a href="https://n-thumann.de/"><img src="https://avatars.githubusercontent.com/u/46975855?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nicolas Thumann</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=n-thumann" title="Code">💻</a> <a href="https://github.com/epi052/feroxbuster/commits?author=n-thumann" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/tomtastic"><img src="https://avatars.githubusercontent.com/u/302127?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tom Matthews</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=tomtastic" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/bsysop"><img src="https://avatars.githubusercontent.com/u/9998303?v=4?s=100" width="100px;" alt=""/><br /><sub><b>bsysop</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=bsysop" title="Documentation">📖</a></td>
<td align="center"><a href="http://bpsizemore.me"><img src="https://avatars.githubusercontent.com/u/11645898?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Brian Sizemore</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=bpsizemore" title="Code">💻</a></td>
<td align="center"><a href="https://pwn.by/noraj"><img src="https://avatars.githubusercontent.com/u/16578570?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alexandre ZANNI</b></sub></a><br /><a href="#infra-noraj" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=noraj" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/craig"><img src="https://avatars.githubusercontent.com/u/99729?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Craig</b></sub></a><br /><a href="#infra-craig" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://www.reddit.com/u/EONRaider"><img src="https://avatars.githubusercontent.com/u/15611424?v=4?s=100" width="100px;" alt=""/><br /><sub><b>EONRaider</b></sub></a><br /><a href="#infra-EONRaider" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/wtwver"><img src="https://avatars.githubusercontent.com/u/53866088?v=4?s=100" width="100px;" alt=""/><br /><sub><b>wtwver</b></sub></a><br /><a href="#infra-wtwver" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</tr>
<tr>
<td align="center"><a href="https://tib3rius.com"><img src="https://avatars.githubusercontent.com/u/48113936?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tib3rius</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ATib3rius" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/0xdf"><img src="https://avatars.githubusercontent.com/u/1489045?v=4?s=100" width="100px;" alt=""/><br /><sub><b>0xdf</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3A0xdf" title="Bug reports">🐛</a></td>
<td align="center"><a href="http://secure77.de"><img src="https://avatars.githubusercontent.com/u/31564517?v=4?s=100" width="100px;" alt=""/><br /><sub><b>secure-77</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asecure-77" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/sbrun"><img src="https://avatars.githubusercontent.com/u/7712154?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sophie Brun</b></sub></a><br /><a href="#infra-sbrun" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/black-A"><img src="https://avatars.githubusercontent.com/u/30686803?v=4?s=100" width="100px;" alt=""/><br /><sub><b>black-A</b></sub></a><br /><a href="#ideas-black-A" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/dinosn"><img src="https://avatars.githubusercontent.com/u/3851678?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nicolas Krassas</b></sub></a><br /><a href="#ideas-dinosn" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/N0ur5"><img src="https://avatars.githubusercontent.com/u/24260009?v=4?s=100" width="100px;" alt=""/><br /><sub><b>N0ur5</b></sub></a><br /><a href="#ideas-N0ur5" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/moscowchill"><img src="https://avatars.githubusercontent.com/u/72578879?v=4?s=100" width="100px;" alt=""/><br /><sub><b>mchill</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Amoscowchill" title="Bug reports">🐛</a></td>
<td align="center"><a href="http://BitThr3at.github.io"><img src="https://avatars.githubusercontent.com/u/45028933?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Naman</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ABitThr3at" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/Sicks3c"><img src="https://avatars.githubusercontent.com/u/32225186?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ayoub Elaich</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asicks3c" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/HenryHoggard"><img src="https://avatars.githubusercontent.com/u/1208121?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Henry</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AHenryHoggard" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/SleepiPanda"><img src="https://avatars.githubusercontent.com/u/6428561?v=4?s=100" width="100px;" alt=""/><br /><sub><b>SleepiPanda</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ASleepiPanda" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/uBadRequest"><img src="https://avatars.githubusercontent.com/u/47282747?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Bad Requests</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AuBadRequest" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://home.dnaka91.rocks"><img src="https://avatars.githubusercontent.com/u/36804488?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dominik Nakamura</b></sub></a><br /><a href="#infra-dnaka91" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/hunter0x8"><img src="https://avatars.githubusercontent.com/u/46222314?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Muhammad Ahsan</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Ahunter0x8" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/cortantief"><img src="https://avatars.githubusercontent.com/u/34527333?v=4?s=100" width="100px;" alt=""/><br /><sub><b>cortantief</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Acortantief" title="Bug reports">🐛</a> <a href="https://github.com/epi052/feroxbuster/commits?author=cortantief" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/dsaxton"><img src="https://avatars.githubusercontent.com/u/2658661?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Daniel Saxton</b></sub></a><br /><a href="#ideas-dsaxton" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=dsaxton" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/narkopolo"><img src="https://avatars.githubusercontent.com/u/16690056?v=4?s=100" width="100px;" alt=""/><br /><sub><b>narkopolo</b></sub></a><br /><a href="#ideas-narkopolo" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://ring0.lol"><img src="https://avatars.githubusercontent.com/u/1893909?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Justin Steven</b></sub></a><br /><a href="#ideas-justinsteven" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/7047payloads"><img src="https://avatars.githubusercontent.com/u/95562424?v=4?s=100" width="100px;" alt=""/><br /><sub><b>7047payloads</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=7047payloads" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/unkn0wnsyst3m"><img src="https://avatars.githubusercontent.com/u/21272239?v=4?s=100" width="100px;" alt=""/><br /><sub><b>unkn0wnsyst3m</b></sub></a><br /><a href="#ideas-unkn0wnsyst3m" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center"><a href="https://ironwort.me/"><img src="https://avatars.githubusercontent.com/u/15280042?v=4?s=100" width="100px;" alt=""/><br /><sub><b>0x08</b></sub></a><br /><a href="#ideas-its0x08" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/MD-Levitan"><img src="https://avatars.githubusercontent.com/u/12116508?v=4?s=100" width="100px;" alt=""/><br /><sub><b>kusok</b></sub></a><br /><a href="#ideas-MD-Levitan" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=MD-Levitan" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/godylockz"><img src="https://avatars.githubusercontent.com/u/81207744?v=4?s=100" width="100px;" alt=""/><br /><sub><b>godylockz</b></sub></a><br /><a href="#ideas-godylockz" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=godylockz" title="Code">💻</a></td>
<td align="center"><a href="http://ryanmontgomery.me"><img src="https://avatars.githubusercontent.com/u/44453666?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ryan Montgomery</b></sub></a><br /><a href="#ideas-0dayCTF" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><a href="https://github.com/mzpqnxow"><img src="https://avatars.githubusercontent.com/u/8016228?v=4?s=100" width="100px;" alt="AG"/><br /><sub><b>AG</b></sub></a><br /><a href="#ideas-mzpqnxow" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=mzpqnxow" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><a href="https://www.reddit.com/u/EONRaider"><img src="https://avatars.githubusercontent.com/u/15611424?v=4?s=100" width="100px;" alt="EONRaider"/><br /><sub><b>EONRaider</b></sub></a><br /><a href="#infra-EONRaider" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><a href="https://github.com/dinosn"><img src="https://avatars.githubusercontent.com/u/3851678?v=4?s=100" width="100px;" alt="Nicolas Krassas"/><br /><sub><b>Nicolas Krassas</b></sub></a><br /><a href="#ideas-dinosn" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><a href="https://github.com/uBadRequest"><img src="https://avatars.githubusercontent.com/u/47282747?v=4?s=100" width="100px;" alt="Bad Requests"/><br /><sub><b>Bad Requests</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AuBadRequest" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><a href="https://github.com/n0kovo"><img src="https://avatars.githubusercontent.com/u/16690056?v=4?s=100" width="100px;" alt="n0kovo"/><br /><sub><b>n0kovo</b></sub></a><br /><a href="#ideas-n0kovo" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><a href="https://github.com/7047payloads"><img src="https://avatars.githubusercontent.com/u/95562424?v=4?s=100" width="100px;" alt="7047payloads"/><br /><sub><b>7047payloads</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=7047payloads" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><a href="https://github.com/gtjamesa"><img src="https://avatars.githubusercontent.com/u/2078364?v=4?s=100" width="100px;" alt="James"/><br /><sub><b>James</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Agtjamesa" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><a href="https://github.com/0xdf223"><img src="https://avatars.githubusercontent.com/u/76954092?v=4?s=100" width="100px;" alt="0xdf"/><br /><sub><b>0xdf</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3A0xdf223" title="Bug reports">🐛</a> <a href="#ideas-0xdf223" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hakdogpinas"><img src="https://avatars.githubusercontent.com/u/71529469?v=4?s=100" width="100px;" alt="hakdogpinas"/><br /><sub><b>hakdogpinas</b></sub></a><br /><a href="#ideas-hakdogpinas" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/duokebei"><img src="https://avatars.githubusercontent.com/u/75022552?v=4?s=100" width="100px;" alt="多可悲"/><br /><sub><b>多可悲</b></sub></a><br /><a href="#ideas-duokebei" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://blog.ah34.net/"><img src="https://avatars.githubusercontent.com/u/58670593?v=4?s=100" width="100px;" alt="Aidan Hall"/><br /><sub><b>Aidan Hall</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=aidanhall34" title="Code">💻</a> <a href="#infra-aidanhall34" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://hachyderm.io/@JohnnyCiocca"><img src="https://avatars.githubusercontent.com/u/6473725?v=4?s=100" width="100px;" alt="João Ciocca"/><br /><sub><b>João Ciocca</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Ajoaociocca" title="Bug reports">🐛</a> <a href="#ideas-joaociocca" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/f3rn0s"><img src="https://avatars.githubusercontent.com/u/1351279?v=4?s=100" width="100px;" alt="f3rn0s"/><br /><sub><b>f3rn0s</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Af3rn0s" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://sth.sh"><img src="https://avatars.githubusercontent.com/u/2099767?v=4?s=100" width="100px;" alt="LongCat"/><br /><sub><b>LongCat</b></sub></a><br /><a href="#ideas-pich4ya" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xaeroborg"><img src="https://avatars.githubusercontent.com/u/33274680?v=4?s=100" width="100px;" alt="xaeroborg"/><br /><sub><b>xaeroborg</b></sub></a><br /><a href="#ideas-xaeroborg" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Luoooio"><img src="https://avatars.githubusercontent.com/u/26653157?v=4?s=100" width="100px;" alt="Luoooio"/><br /><sub><b>Luoooio</b></sub></a><br /><a href="#ideas-Luoooio" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://petruknisme.com"><img src="https://avatars.githubusercontent.com/u/6284204?v=4?s=100" width="100px;" alt="Aan"/><br /><sub><b>Aan</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=aancw" title="Code">💻</a> <a href="#infra-aancw" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#ideas-aancw" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/imBigo"><img src="https://avatars.githubusercontent.com/u/54672433?v=4?s=100" width="100px;" alt="Simon"/><br /><sub><b>Simon</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AimBigo" title="Bug reports">🐛</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
@@ -239,4 +287,4 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

View File

@@ -1,5 +1,5 @@
use std::fs::{copy, create_dir_all, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::io::{Read, Seek, Write};
use clap_complete::{generate_to, shells};
@@ -30,7 +30,7 @@ fn main() {
let mut bash_file = OpenOptions::new()
.read(true)
.write(true)
.open(format!("{}/feroxbuster.bash", outdir))
.open(format!("{outdir}/feroxbuster.bash"))
.expect("Couldn't open bash completion script");
bash_file
@@ -40,7 +40,7 @@ fn main() {
contents = contents.replace("default feroxbuster", "default -o plusdirs feroxbuster");
bash_file
.seek(SeekFrom::Start(0))
.rewind()
.expect("Couldn't seek to position 0 in bash completion script");
bash_file
@@ -59,15 +59,11 @@ fn main() {
if !config_dir.exists() {
// recursively create the feroxbuster directory and all of its parent components if
// they are missing
if !config_dir.exists() {
// recursively create the feroxbuster directory and all of its parent components if
// they are missing
if create_dir_all(&config_dir).is_err() {
// only copy the config file when we're not running in the CI/CD pipeline
// which fails with permission denied
eprintln!("Couldn't create one or more directories needed to copy the config file");
return;
}
if create_dir_all(&config_dir).is_err() {
// only copy the config file when we're not running in the CI/CD pipeline
// which fails with permission denied
eprintln!("Couldn't create one or more directories needed to copy the config file");
return;
}
}

View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
<metadata>
<id>feroxbuster</id>
<version>2.8.0</version>
<packageSourceUrl>https://github.com/epi052/feroxbuster/releases/</packageSourceUrl>
<owners>epi052</owners>
<title>feroxbuster (Install)</title>
<authors>epi052</authors>
<projectUrl>https://github.com/epi052/feroxbuster</projectUrl>
<iconUrl>https://rawcdn.githack.com/epi052/feroxbuster/2d381e7e057ce60c580b324dd36c9abaf30c2ec7/img/logo/logo.png</iconUrl>
<copyright>2020-2023</copyright>
<licenseUrl>https://github.com/epi052/feroxbuster/blob/main/LICENSE</licenseUrl>
<requireLicenseAcceptance>true</requireLicenseAcceptance>
<projectSourceUrl>https://github.com/epi052/feroxbuster</projectSourceUrl>
<docsUrl>https://epi052.github.io/feroxbuster-docs/docs/</docsUrl>
<!--<mailingListUrl></mailingListUrl>-->
<bugTrackerUrl>https://github.com/epi052/feroxbuster/issues</bugTrackerUrl>
<tags>content-discovery pentesting-tool url-bruteforcer</tags>
<summary>A simple, fast, recursive content discovery tool written in Rust</summary>
<description>
A simple, fast, recursive content discovery tool written in Rust
[![Feroxbuster](https://github.com/epi052/feroxbuster/raw/main/img/logo/default-cropped.png)](https://github.com/epi052/feroxbuster)
## What the heck is a ferox anyway?
Ferox is short for Ferric Oxide. Ferric Oxide, simply put, is rust. The name rustbuster was taken, so I decided on a
variation.
## What's it do tho?
`feroxbuster` is a tool designed to perform [Forced Browsing](https://owasp.org/www-community/attacks/Forced_browsing).
Forced browsing is an attack where the aim is to enumerate and access resources that are not referenced by the web
application, but are still accessible by an attacker.
`feroxbuster` uses brute force combined with a wordlist to search for unlinked content in target directories. These
resources may store sensitive information about web applications and operational systems, such as source code,
credentials, internal network addressing, etc...
This attack is also known as Predictable Resource Location, File Enumeration, Directory Enumeration, and Resource
Enumeration.
## Quick Start
This section will cover the minimum amount of information to get up and running with feroxbuster. Please refer the the [documentation](https://epi052.github.io/feroxbuster-docs/docs/), as it's much more comprehensive.
### Installation
There are quite a few other [installation methods](https://epi052.github.io/feroxbuster-docs/docs/installation/), but these snippets should cover the majority of users.
#### All others Docs
Please refer the the [documentation](https://epi052.github.io/feroxbuster-docs/docs/).
## Example Usage
Here are a few brief examples to get you started. Please note, feroxbuster can do a **lot more** than what's listed below. As a result, there are **many more** examples, with **demonstration gifs** that highlight specific features, in the [documentation](https://epi052.github.io/feroxbuster-docs/docs/).
### Multiple Values
Options that take multiple values are very flexible. Consider the following ways of specifying extensions:
```
./feroxbuster -u http://127.1 -x pdf -x js,html -x php txt json,docx
```
The command above adds .pdf, .js, .html, .php, .txt, .json, and .docx to each url
All of the methods above (multiple flags, space separated, comma separated, etc...) are valid and interchangeable. The
same goes for urls, headers, status codes, queries, and size filters.
</description>
<!-- <releaseNotes>__REPLACE_OR_REMOVE__MarkDown_Okay</releaseNotes> -->
</metadata>
<files>
<!-- this section controls what actually gets packaged into the Chocolatey package -->
<file src="tools\**" target="tools" />
</files>
</package>

View File

@@ -0,0 +1,26 @@
From: https://github.com/epi052/feroxbuster/blob/main/LICENSE
LICENSE
MIT License
Copyright (c) 2020-2023 epi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,5 @@
VERIFICATION
checksum -t sha512 -f .\x86-windows-feroxbuster.exe.zip
checksum -t sha512 -f .\x86_64-windows-feroxbuster.exe.zip

View File

@@ -0,0 +1,27 @@
$ErrorActionPreference = 'Stop'
$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
$version = '2.8.0'
$url = "https://github.com/epi052/feroxbuster/releases/download/v$version/x86-windows-feroxbuster.exe.zip"
$url64 = "https://github.com/epi052/feroxbuster/releases/download/v$version/x86_64-windows-feroxbuster.exe.zip"
$packageArgs = @{
packageName = $env:ChocolateyPackageName
unzipLocation = $toolsDir
fileType = 'exe' #only one of these: exe, msi, msu
url = $url
url64bit = $url64
#file = $fileLocation
softwareName = 'feroxbuster*'
# Checksums are now required as of 0.10.0.
# To determine checksums, you can get that from the original site if provided.
# You can also use checksum.exe (choco install checksum) and use it
# e.g. checksum -t sha256 -f path\to\file
checksum = 'e5cac59c737260233903a17706a68bac11fe0d7a15169e1c5a9637cc221e7230fd6ddbfc1a7243833dde6472ad053c033449ca8338164654f7354363da54ba88'
checksumType = 'sha512'
checksum64 = 'cce58d6eacef7e12c31076f5a00fee9742a4e3fdfc69d807d98736200e50469f77359978e137ecafd87b14460845c65c6808d1f8b23ae561f7e7c637e355dee3'
checksumType64= 'sha512'
}
Install-ChocolateyZipPackage @packageArgs # https://docs.chocolatey.org/en-us/create/functions/install-chocolateyzippackage

View File

@@ -0,0 +1,47 @@
$ErrorActionPreference = 'Stop' # stop on all errors
$packageArgs = @{
packageName = $env:ChocolateyPackageName
softwareName = 'feroxbuster*' #part or all of the Display Name as you see it in Programs and Features. It should be enough to be unique
fileType = 'exe' #only one of these: MSI or EXE (ignore MSU for now)
}
# Get-UninstallRegistryKey is new to 0.9.10, if supporting 0.9.9.x and below,
# take a dependency on "chocolatey-core.extension" in your nuspec file.
# This is only a fuzzy search if $softwareName includes '*'. Otherwise it is
# exact. In the case of versions in key names, we recommend removing the version
# and using '*'.
[array]$key = Get-UninstallRegistryKey -SoftwareName $packageArgs['softwareName']
if ($key.Count -eq 1) {
$key | % {
$packageArgs['file'] = "$($_.UninstallString)" #NOTE: You may need to split this if it contains spaces, see below
if ($packageArgs['fileType'] -eq 'MSI') {
# The Product Code GUID is all that should be passed for MSI, and very
# FIRST, because it comes directly after /x, which is already set in the
# Uninstall-ChocolateyPackage msiargs (facepalm).
$packageArgs['silentArgs'] = "$($_.PSChildName) $($packageArgs['silentArgs'])"
# Don't pass anything for file, it is ignored for msi (facepalm number 2)
# Alternatively if you need to pass a path to an msi, determine that and
# use it instead of the above in silentArgs, still very first
$packageArgs['file'] = ''
} else {
# NOTES:
# - You probably will need to sanitize $packageArgs['file'] as it comes from the registry and could be in a variety of fun but unusable formats
# - Split args from exe in $packageArgs['file'] and pass those args through $packageArgs['silentArgs'] or ignore them
# - Ensure you don't pass double quotes in $file (aka $packageArgs['file']) - otherwise you will get "Illegal characters in path when you attempt to run this"
# - Review the code for auto-uninstaller for all of the fun things it does in sanitizing - https://github.com/chocolatey/choco/blob/bfe351b7d10c798014efe4bfbb100b171db25099/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs#L142-L192
}
Uninstall-ChocolateyPackage @packageArgs
}
} elseif ($key.Count -eq 0) {
Write-Warning "$packageName has already been uninstalled by other means."
} elseif ($key.Count -gt 1) {
Write-Warning "$($key.Count) matches found!"
Write-Warning "To prevent accidental data loss, no programs will be uninstalled."
Write-Warning "Please alert package maintainer the following keys were matched:"
$key | % {Write-Warning "- $($_.DisplayName)"}
}

View File

@@ -45,6 +45,7 @@
# dont_filter = true
# extract_links = true
# depth = 1
# force_recursion = true
# filter_size = [5174]
# filter_regex = ["^ignore me$"]
# filter_similar = ["https://somesite.com/soft404"]

BIN
img/logo/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -24,8 +24,8 @@ _feroxbuster() {
'--replay-proxy=[Send only unfiltered requests through a Replay Proxy, instead of all requests]:REPLAY_PROXY:_urls' \
'*-R+[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \
'*--replay-codes=[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \
'-a+[Sets the User-Agent (default: feroxbuster/2.6.0)]:USER_AGENT: ' \
'--user-agent=[Sets the User-Agent (default: feroxbuster/2.6.0)]:USER_AGENT: ' \
'-a+[Sets the User-Agent (default: feroxbuster/2.9.2)]:USER_AGENT: ' \
'--user-agent=[Sets the User-Agent (default: feroxbuster/2.9.2)]:USER_AGENT: ' \
'*-x+[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \
'*--extensions=[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \
'*-m+[Which HTTP request method(s) should be sent (default: GET)]:HTTP_METHODS: ' \
@@ -46,11 +46,11 @@ _feroxbuster() {
'*--filter-words=[Filter out messages of a particular word count (ex: -W 312 -W 91,82)]:WORDS: ' \
'*-N+[Filter out messages of a particular line count (ex: -N 20 -N 31,30)]:LINES: ' \
'*--filter-lines=[Filter out messages of a particular line count (ex: -N 20 -N 31,30)]:LINES: ' \
'*-C+[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \
'*--filter-status=[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \
'(-s --status-codes)*-C+[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \
'(-s --status-codes)*--filter-status=[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \
'*--filter-similar-to=[Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)]:UNWANTED_PAGE:_urls' \
'*-s+[Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)]:STATUS_CODE: ' \
'*--status-codes=[Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)]:STATUS_CODE: ' \
'*-s+[Status Codes to include (allow list) (default: All Status Codes)]:STATUS_CODE: ' \
'*--status-codes=[Status Codes to include (allow list) (default: All Status Codes)]:STATUS_CODE: ' \
'-T+[Number of seconds before a client'\''s request times out (default: 7)]:SECONDS: ' \
'--timeout=[Number of seconds before a client'\''s request times out (default: 7)]:SECONDS: ' \
'-t+[Number of concurrent threads (default: 50)]:THREADS: ' \
@@ -62,22 +62,18 @@ _feroxbuster() {
'--parallel=[Run parallel feroxbuster instances (one child process per url passed via stdin)]:PARALLEL_SCANS: ' \
'(--auto-tune)--rate-limit=[Limit number of requests per second (per directory) (default: 0, i.e. no limit)]:RATE_LIMIT: ' \
'--time-limit=[Limit total run time of all scans (ex: --time-limit 10m)]:TIME_SPEC: ' \
'-w+[Path to the wordlist]:FILE:_files' \
'--wordlist=[Path to the wordlist]:FILE:_files' \
'-w+[Path or URL of the wordlist]:FILE:_files' \
'--wordlist=[Path or URL of the wordlist]:FILE:_files' \
'*-I+[File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)]:FILE_EXTENSION: ' \
'*--dont-collect=[File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)]:FILE_EXTENSION: ' \
'-o+[Output file to write results to (use w/ --json for JSON entries)]:FILE:_files' \
'--output=[Output file to write results to (use w/ --json for JSON entries)]:FILE:_files' \
'--debug-log=[Output file to write log entries (use w/ --json for JSON entries)]:FILE:_files' \
'-h[Print help information]' \
'--help[Print help information]' \
'-V[Print version information]' \
'--version[Print version information]' \
'(-u --url)--stdin[Read url(s) from STDIN]' \
'(-p --proxy -k --insecure --burp-replay)--burp[Set --proxy to http://127.0.0.1:8080 and set --insecure to true]' \
'(-P --replay-proxy -k --insecure)--burp-replay[Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true]' \
'--smart[Set --extract-links, --auto-tune, --collect-words, and --collect-backups to true]' \
'--thorough[Use the same settings as --smart and set --collect-extensions to true]' \
'(--rate-limit --auto-bail)--smart[Set --auto-tune, --collect-words, and --collect-backups to true]' \
'(--rate-limit --auto-bail)--thorough[Use the same settings as --smart and set --collect-extensions to true]' \
'-A[Use a random User-Agent]' \
'--random-agent[Use a random User-Agent]' \
'-f[Append / to each request'\''s URL]' \
@@ -88,8 +84,10 @@ _feroxbuster() {
'--insecure[Disables TLS certificate validation in the client]' \
'-n[Do not scan recursively]' \
'--no-recursion[Do not scan recursively]' \
'-e[Extract links from response body (html, javascript, etc...); make new requests based on findings]' \
'--extract-links[Extract links from response body (html, javascript, etc...); make new requests based on findings]' \
'(-n --no-recursion)--force-recursion[Force recursion attempts on all '\''found'\'' endpoints (still respects recursion depth)]' \
'-e[Extract links from response body (html, javascript, etc...); make new requests based on findings (default: true)]' \
'--extract-links[Extract links from response body (html, javascript, etc...); make new requests based on findings (default: true)]' \
'--dont-extract-links[Don'\''t extract links from response body (html, javascript, etc...)]' \
'(--auto-bail)--auto-tune[Automatically lower scan rate when an excessive amount of errors are encountered]' \
'--auto-bail[Automatically stop scanning when an excessive amount of errors are encountered]' \
'-D[Don'\''t auto-filter wildcard responses]' \
@@ -107,6 +105,12 @@ _feroxbuster() {
'--quiet[Hide progress bars and banner (good for tmux windows w/ notifications)]' \
'--json[Emit JSON logs to --output and --debug-log instead of normal text]' \
'--no-state[Disable state output file (*.state)]' \
'-U[Update feroxbuster to the latest version]' \
'--update[Update feroxbuster to the latest version]' \
'-h[Print help (see more with '\''--help'\'')]' \
'--help[Print help (see more with '\''--help'\'')]' \
'-V[Print version]' \
'--version[Print version]' \
&& ret=0
}
@@ -116,4 +120,8 @@ _feroxbuster_commands() {
_describe -t commands 'feroxbuster commands' commands "$@"
}
_feroxbuster "$@"
if [ "$funcstack[1]" = "_feroxbuster" ]; then
_feroxbuster "$@"
else
compdef _feroxbuster feroxbuster
fi

View File

@@ -30,8 +30,8 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('--replay-proxy', 'replay-proxy', [CompletionResultType]::ParameterName, 'Send only unfiltered requests through a Replay Proxy, instead of all requests')
[CompletionResult]::new('-R', 'R', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)')
[CompletionResult]::new('--replay-codes', 'replay-codes', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)')
[CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.6.0)')
[CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.6.0)')
[CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.9.2)')
[CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.9.2)')
[CompletionResult]::new('-x', 'x', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)')
[CompletionResult]::new('--extensions', 'extensions', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)')
[CompletionResult]::new('-m', 'm', [CompletionResultType]::ParameterName, 'Which HTTP request method(s) should be sent (default: GET)')
@@ -55,8 +55,8 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('-C', 'C', [CompletionResultType]::ParameterName, 'Filter out status codes (deny list) (ex: -C 200 -C 401)')
[CompletionResult]::new('--filter-status', 'filter-status', [CompletionResultType]::ParameterName, 'Filter out status codes (deny list) (ex: -C 200 -C 401)')
[CompletionResult]::new('--filter-similar-to', 'filter-similar-to', [CompletionResultType]::ParameterName, 'Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)')
[CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)')
[CompletionResult]::new('--status-codes', 'status-codes', [CompletionResultType]::ParameterName, 'Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)')
[CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Status Codes to include (allow list) (default: All Status Codes)')
[CompletionResult]::new('--status-codes', 'status-codes', [CompletionResultType]::ParameterName, 'Status Codes to include (allow list) (default: All Status Codes)')
[CompletionResult]::new('-T', 'T', [CompletionResultType]::ParameterName, 'Number of seconds before a client''s request times out (default: 7)')
[CompletionResult]::new('--timeout', 'timeout', [CompletionResultType]::ParameterName, 'Number of seconds before a client''s request times out (default: 7)')
[CompletionResult]::new('-t', 't', [CompletionResultType]::ParameterName, 'Number of concurrent threads (default: 50)')
@@ -68,21 +68,17 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('--parallel', 'parallel', [CompletionResultType]::ParameterName, 'Run parallel feroxbuster instances (one child process per url passed via stdin)')
[CompletionResult]::new('--rate-limit', 'rate-limit', [CompletionResultType]::ParameterName, 'Limit number of requests per second (per directory) (default: 0, i.e. no limit)')
[CompletionResult]::new('--time-limit', 'time-limit', [CompletionResultType]::ParameterName, 'Limit total run time of all scans (ex: --time-limit 10m)')
[CompletionResult]::new('-w', 'w', [CompletionResultType]::ParameterName, 'Path to the wordlist')
[CompletionResult]::new('--wordlist', 'wordlist', [CompletionResultType]::ParameterName, 'Path to the wordlist')
[CompletionResult]::new('-w', 'w', [CompletionResultType]::ParameterName, 'Path or URL of the wordlist')
[CompletionResult]::new('--wordlist', 'wordlist', [CompletionResultType]::ParameterName, 'Path or URL of the wordlist')
[CompletionResult]::new('-I', 'I', [CompletionResultType]::ParameterName, 'File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)')
[CompletionResult]::new('--dont-collect', 'dont-collect', [CompletionResultType]::ParameterName, 'File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)')
[CompletionResult]::new('-o', 'o', [CompletionResultType]::ParameterName, 'Output file to write results to (use w/ --json for JSON entries)')
[CompletionResult]::new('--output', 'output', [CompletionResultType]::ParameterName, 'Output file to write results to (use w/ --json for JSON entries)')
[CompletionResult]::new('--debug-log', 'debug-log', [CompletionResultType]::ParameterName, 'Output file to write log entries (use w/ --json for JSON entries)')
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information')
[CompletionResult]::new('--stdin', 'stdin', [CompletionResultType]::ParameterName, 'Read url(s) from STDIN')
[CompletionResult]::new('--burp', 'burp', [CompletionResultType]::ParameterName, 'Set --proxy to http://127.0.0.1:8080 and set --insecure to true')
[CompletionResult]::new('--burp-replay', 'burp-replay', [CompletionResultType]::ParameterName, 'Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true')
[CompletionResult]::new('--smart', 'smart', [CompletionResultType]::ParameterName, 'Set --extract-links, --auto-tune, --collect-words, and --collect-backups to true')
[CompletionResult]::new('--smart', 'smart', [CompletionResultType]::ParameterName, 'Set --auto-tune, --collect-words, and --collect-backups to true')
[CompletionResult]::new('--thorough', 'thorough', [CompletionResultType]::ParameterName, 'Use the same settings as --smart and set --collect-extensions to true')
[CompletionResult]::new('-A', 'A', [CompletionResultType]::ParameterName, 'Use a random User-Agent')
[CompletionResult]::new('--random-agent', 'random-agent', [CompletionResultType]::ParameterName, 'Use a random User-Agent')
@@ -94,8 +90,10 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('--insecure', 'insecure', [CompletionResultType]::ParameterName, 'Disables TLS certificate validation in the client')
[CompletionResult]::new('-n', 'n', [CompletionResultType]::ParameterName, 'Do not scan recursively')
[CompletionResult]::new('--no-recursion', 'no-recursion', [CompletionResultType]::ParameterName, 'Do not scan recursively')
[CompletionResult]::new('-e', 'e', [CompletionResultType]::ParameterName, 'Extract links from response body (html, javascript, etc...); make new requests based on findings')
[CompletionResult]::new('--extract-links', 'extract-links', [CompletionResultType]::ParameterName, 'Extract links from response body (html, javascript, etc...); make new requests based on findings')
[CompletionResult]::new('--force-recursion', 'force-recursion', [CompletionResultType]::ParameterName, 'Force recursion attempts on all ''found'' endpoints (still respects recursion depth)')
[CompletionResult]::new('-e', 'e', [CompletionResultType]::ParameterName, 'Extract links from response body (html, javascript, etc...); make new requests based on findings (default: true)')
[CompletionResult]::new('--extract-links', 'extract-links', [CompletionResultType]::ParameterName, 'Extract links from response body (html, javascript, etc...); make new requests based on findings (default: true)')
[CompletionResult]::new('--dont-extract-links', 'dont-extract-links', [CompletionResultType]::ParameterName, 'Don''t extract links from response body (html, javascript, etc...)')
[CompletionResult]::new('--auto-tune', 'auto-tune', [CompletionResultType]::ParameterName, 'Automatically lower scan rate when an excessive amount of errors are encountered')
[CompletionResult]::new('--auto-bail', 'auto-bail', [CompletionResultType]::ParameterName, 'Automatically stop scanning when an excessive amount of errors are encountered')
[CompletionResult]::new('-D', 'D', [CompletionResultType]::ParameterName, 'Don''t auto-filter wildcard responses')
@@ -113,6 +111,12 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('--quiet', 'quiet', [CompletionResultType]::ParameterName, 'Hide progress bars and banner (good for tmux windows w/ notifications)')
[CompletionResult]::new('--json', 'json', [CompletionResultType]::ParameterName, 'Emit JSON logs to --output and --debug-log instead of normal text')
[CompletionResult]::new('--no-state', 'no-state', [CompletionResultType]::ParameterName, 'Disable state output file (*.state)')
[CompletionResult]::new('-U', 'U', [CompletionResultType]::ParameterName, 'Update feroxbuster to the latest version')
[CompletionResult]::new('--update', 'update', [CompletionResultType]::ParameterName, 'Update feroxbuster to the latest version')
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version')
break
}
})

View File

@@ -1,5 +1,5 @@
_feroxbuster() {
local i cur prev opts cmds
local i cur prev opts cmd
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
@@ -8,8 +8,8 @@ _feroxbuster() {
for i in ${COMP_WORDS[@]}
do
case "${i}" in
"$1")
case "${cmd},${i}" in
",$1")
cmd="feroxbuster"
;;
*)
@@ -19,7 +19,7 @@ _feroxbuster() {
case "${cmd}" in
feroxbuster)
opts="-h -V -u -p -P -R -a -A -x -m -H -b -Q -f -S -X -W -N -C -s -T -r -k -t -n -d -e -L -w -D -E -B -g -I -v -q -o --help --version --url --stdin --resume-from --burp --burp-replay --smart --thorough --proxy --replay-proxy --replay-codes --user-agent --random-agent --extensions --methods --data --headers --cookies --query --add-slash --dont-scan --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --status-codes --timeout --redirects --insecure --threads --no-recursion --depth --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 -U -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 --dont-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 --update --help --version"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0

View File

@@ -27,8 +27,8 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand --replay-proxy 'Send only unfiltered requests through a Replay Proxy, instead of all requests'
cand -R 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)'
cand --replay-codes 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)'
cand -a 'Sets the User-Agent (default: feroxbuster/2.6.0)'
cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.6.0)'
cand -a 'Sets the User-Agent (default: feroxbuster/2.9.2)'
cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.9.2)'
cand -x 'File extension(s) to search for (ex: -x php -x pdf js)'
cand --extensions 'File extension(s) to search for (ex: -x php -x pdf js)'
cand -m 'Which HTTP request method(s) should be sent (default: GET)'
@@ -52,8 +52,8 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand -C 'Filter out status codes (deny list) (ex: -C 200 -C 401)'
cand --filter-status 'Filter out status codes (deny list) (ex: -C 200 -C 401)'
cand --filter-similar-to 'Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)'
cand -s 'Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)'
cand --status-codes 'Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)'
cand -s 'Status Codes to include (allow list) (default: All Status Codes)'
cand --status-codes 'Status Codes to include (allow list) (default: All Status Codes)'
cand -T 'Number of seconds before a client''s request times out (default: 7)'
cand --timeout 'Number of seconds before a client''s request times out (default: 7)'
cand -t 'Number of concurrent threads (default: 50)'
@@ -65,21 +65,17 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand --parallel 'Run parallel feroxbuster instances (one child process per url passed via stdin)'
cand --rate-limit 'Limit number of requests per second (per directory) (default: 0, i.e. no limit)'
cand --time-limit 'Limit total run time of all scans (ex: --time-limit 10m)'
cand -w 'Path to the wordlist'
cand --wordlist 'Path to the wordlist'
cand -w 'Path or URL of the wordlist'
cand --wordlist 'Path or URL of the wordlist'
cand -I 'File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)'
cand --dont-collect 'File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)'
cand -o 'Output file to write results to (use w/ --json for JSON entries)'
cand --output 'Output file to write results to (use w/ --json for JSON entries)'
cand --debug-log 'Output file to write log entries (use w/ --json for JSON entries)'
cand -h 'Print help information'
cand --help 'Print help information'
cand -V 'Print version information'
cand --version 'Print version information'
cand --stdin 'Read url(s) from STDIN'
cand --burp 'Set --proxy to http://127.0.0.1:8080 and set --insecure to true'
cand --burp-replay 'Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true'
cand --smart 'Set --extract-links, --auto-tune, --collect-words, and --collect-backups to true'
cand --smart 'Set --auto-tune, --collect-words, and --collect-backups to true'
cand --thorough 'Use the same settings as --smart and set --collect-extensions to true'
cand -A 'Use a random User-Agent'
cand --random-agent 'Use a random User-Agent'
@@ -91,8 +87,10 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand --insecure 'Disables TLS certificate validation in the client'
cand -n 'Do not scan recursively'
cand --no-recursion 'Do not scan recursively'
cand -e 'Extract links from response body (html, javascript, etc...); make new requests based on findings'
cand --extract-links 'Extract links from response body (html, javascript, etc...); make new requests based on findings'
cand --force-recursion 'Force recursion attempts on all ''found'' endpoints (still respects recursion depth)'
cand -e 'Extract links from response body (html, javascript, etc...); make new requests based on findings (default: true)'
cand --extract-links 'Extract links from response body (html, javascript, etc...); make new requests based on findings (default: true)'
cand --dont-extract-links 'Don''t extract links from response body (html, javascript, etc...)'
cand --auto-tune 'Automatically lower scan rate when an excessive amount of errors are encountered'
cand --auto-bail 'Automatically stop scanning when an excessive amount of errors are encountered'
cand -D 'Don''t auto-filter wildcard responses'
@@ -110,6 +108,12 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand --quiet 'Hide progress bars and banner (good for tmux windows w/ notifications)'
cand --json 'Emit JSON logs to --output and --debug-log instead of normal text'
cand --no-state 'Disable state output file (*.state)'
cand -U 'Update feroxbuster to the latest version'
cand --update 'Update feroxbuster to the latest version'
cand -h 'Print help (see more with ''--help'')'
cand --help 'Print help (see more with ''--help'')'
cand -V 'Print version'
cand --version 'Print version'
}
]
$completions[$command]

View File

@@ -3,7 +3,7 @@ use crate::{
config::Configuration,
event_handlers::Handles,
utils::{logged_request, status_colorizer},
DEFAULT_IGNORED_EXTENSIONS, DEFAULT_METHOD, VERSION,
DEFAULT_IGNORED_EXTENSIONS, DEFAULT_METHOD, DEFAULT_STATUS_CODES, VERSION,
};
use anyhow::{bail, Result};
use console::{style, Emoji};
@@ -163,6 +163,9 @@ pub struct Banner {
/// represents Configuration.collect_words
collect_words: BannerEntry,
/// represents Configuration.collect_words
force_recursion: BannerEntry,
}
/// implementation of Banner
@@ -201,12 +204,25 @@ impl Banner {
));
}
let mut codes = vec![];
for code in &config.status_codes {
codes.push(status_colorizer(&code.to_string()))
}
let status_codes =
BannerEntry::new("👌", "Status Codes", &format!("[{}]", codes.join(", ")));
// the +2 is for the 2 experimental status codes we add to the default list manually
let status_codes = if config.status_codes.len() == DEFAULT_STATUS_CODES.len() + 2 {
let all_str = format!(
"{} {} {}{}",
style("All").cyan(),
style("Status").green(),
style("Codes").yellow(),
style("!").red()
);
BannerEntry::new("👌", "Status Codes", &all_str)
} else {
let mut codes = vec![];
for code in &config.status_codes {
codes.push(status_colorizer(&code.to_string()))
}
BannerEntry::new("👌", "Status Codes", &format!("[{}]", codes.join(", ")))
};
for code in &config.filter_status {
code_filters.push(status_colorizer(&code.to_string()))
@@ -230,7 +246,7 @@ impl Banner {
headers.push(BannerEntry::new(
"🤯",
"Header",
&format!("{}: {}", name, value),
&format!("{name}: {value}"),
));
}
@@ -300,9 +316,11 @@ impl Banner {
&config.scan_limit.to_string(),
);
let force_recursion =
BannerEntry::new("🤘", "Force Recursion", &config.force_recursion.to_string());
let replay_proxy = BannerEntry::new("🎥", "Replay Proxy", &config.replay_proxy);
let auto_tune = BannerEntry::new("🎶", "Auto Tune", &config.auto_tune.to_string());
let auto_bail = BannerEntry::new("🪣", "Auto Bail", &config.auto_bail.to_string());
let auto_bail = BannerEntry::new("🙅", "Auto Bail", &config.auto_bail.to_string());
let cfg = BannerEntry::new("💉", "Config File", &config.config);
let proxy = BannerEntry::new("💎", "Proxy", &config.proxy);
let threads = BannerEntry::new("🚀", "Threads", &config.threads.to_string());
@@ -409,6 +427,7 @@ impl Banner {
no_recursion,
rate_limit,
scan_limit,
force_recursion,
time_limit,
url_denylist,
collect_extensions,
@@ -435,7 +454,7 @@ by Ben "epi" Risher {} ver: {}"#,
let top = "───────────────────────────┬──────────────────────";
format!("{}\n{}", artwork, top)
format!("{artwork}\n{top}")
}
/// get a fancy footer for the banner
@@ -449,7 +468,7 @@ by Ben "epi" Risher {} ver: {}"#,
style("Scan Management Menu").bright().yellow(),
);
format!("{}\n{}\n{}", bottom, instructions, addl_section)
format!("{bottom}\n{instructions}\n{addl_section}")
}
/// Makes a request to the given url, expecting to receive a JSON response that contains a field
@@ -502,20 +521,21 @@ by Ben "epi" Risher {} ver: {}"#,
// begin with always printed items
for target in &self.targets {
writeln!(&mut writer, "{}", target)?;
writeln!(&mut writer, "{target}")?;
}
for denied_url in &self.url_denylist {
writeln!(&mut writer, "{}", denied_url)?;
writeln!(&mut writer, "{denied_url}")?;
}
writeln!(&mut writer, "{}", self.threads)?;
writeln!(&mut writer, "{}", self.wordlist)?;
writeln!(&mut writer, "{}", self.status_codes)?;
if !config.filter_status.is_empty() {
// exception here for an optional print in the middle of always printed values is due
// to me wanting the allows and denys to be printed one after the other
if config.filter_status.is_empty() {
// -C and -s are mutually exclusive, and -s meaning changes when -C is used
// so only print one or the other
writeln!(&mut writer, "{}", self.status_codes)?;
} else {
writeln!(&mut writer, "{}", self.filter_status)?;
}
@@ -544,27 +564,27 @@ by Ben "epi" Risher {} ver: {}"#,
}
for header in &self.headers {
writeln!(&mut writer, "{}", header)?;
writeln!(&mut writer, "{header}")?;
}
for filter in &self.filter_size {
writeln!(&mut writer, "{}", filter)?;
writeln!(&mut writer, "{filter}")?;
}
for filter in &self.filter_similar {
writeln!(&mut writer, "{}", filter)?;
writeln!(&mut writer, "{filter}")?;
}
for filter in &self.filter_word_count {
writeln!(&mut writer, "{}", filter)?;
writeln!(&mut writer, "{filter}")?;
}
for filter in &self.filter_line_count {
writeln!(&mut writer, "{}", filter)?;
writeln!(&mut writer, "{filter}")?;
}
for filter in &self.filter_regex {
writeln!(&mut writer, "{}", filter)?;
writeln!(&mut writer, "{filter}")?;
}
if config.extract_links {
@@ -576,7 +596,7 @@ by Ben "epi" Risher {} ver: {}"#,
}
for query in &self.queries {
writeln!(&mut writer, "{}", query)?;
writeln!(&mut writer, "{query}")?;
}
if !config.output.is_empty() {
@@ -642,6 +662,10 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(&mut writer, "{}", self.no_recursion)?;
if config.force_recursion {
writeln!(&mut writer, "{}", self.force_recursion)?;
}
if config.scan_limit > 0 {
writeln!(&mut writer, "{}", self.scan_limit)?;
}
@@ -664,7 +688,7 @@ by Ben "epi" Risher {} ver: {}"#,
"New Version Available",
"https://github.com/epi052/feroxbuster/releases/latest",
);
writeln!(&mut writer, "{}", update)?;
writeln!(&mut writer, "{update}")?;
}
writeln!(&mut writer, "{}", self.footer())?;

View File

@@ -1,6 +1,7 @@
use super::utils::{
depth, ignored_extensions, methods, report_and_exit, save_state, serialized_type, status_codes,
threads, timeout, user_agent, wordlist, OutputLevel, RequesterPolicy,
depth, extract_links, ignored_extensions, methods, report_and_exit, save_state,
serialized_type, status_codes, threads, timeout, user_agent, wordlist, OutputLevel,
RequesterPolicy,
};
use crate::config::determine_output_level;
use crate::config::utils::determine_requester_policy;
@@ -9,7 +10,7 @@ use crate::{
DEFAULT_CONFIG_NAME,
};
use anyhow::{anyhow, Context, Result};
use clap::ArgMatches;
use clap::{parser::ValueSource, ArgMatches};
use regex::Regex;
use reqwest::{Client, Method, StatusCode, Url};
use serde::{Deserialize, Serialize};
@@ -22,15 +23,10 @@ use std::{
/// macro helper to abstract away repetitive configuration updates
macro_rules! update_config_if_present {
($conf_val:expr, $matches:ident, $arg_name:expr) => {
match $matches.value_of_t($arg_name) {
Ok(value) => *$conf_val = value, // Update value
Err(err) => {
if !matches!(err.kind(), clap::ErrorKind::ArgumentNotFound) {
// Do nothing if argument not found
err.exit() // Exit with error on any other parse error
}
}
($conf_val:expr, $matches:ident, $arg_name:expr, $arg_type:ty) => {
match $matches.get_one::<$arg_type>($arg_name) {
Some(value) => *$conf_val = value.to_owned(), // Update value
None => {}
}
};
}
@@ -44,6 +40,35 @@ macro_rules! update_if_not_default {
};
}
/// macro helper to abstract away repetitive checks to see if the user has specified a value
/// for a given argument from the commandline or if we just had a default value in the parser
macro_rules! came_from_cli {
($matches:ident, $arg_name:expr) => {
matches!(
$matches.value_source($arg_name),
Some(ValueSource::CommandLine)
)
};
}
/// macro helper to abstract away repetitive if not default: update checks, specifically for
/// values that are number types, i.e. usize, u64, etc
macro_rules! update_config_with_num_type_if_present {
($conf_val:expr, $matches:ident, $arg_name:expr, $arg_type:ty) => {
if let Some(val) = $matches.get_one::<String>($arg_name) {
match val.parse::<$arg_type>() {
Ok(v) => *$conf_val = v,
Err(_) => {
report_and_exit(&format!(
"Invalid value for --{}, must be a positive integer",
$arg_name
));
}
}
}
};
}
/// Represents the final, global configuration of the program.
///
/// This struct is the combination of the following:
@@ -190,7 +215,7 @@ pub struct Configuration {
pub no_recursion: bool,
/// Extract links from html/javscript
#[serde(default)]
#[serde(default = "extract_links")]
pub extract_links: bool,
/// Append / to each request
@@ -281,6 +306,14 @@ pub struct Configuration {
/// Automatically discover important words from within responses and add them to the wordlist
#[serde(default)]
pub collect_words: bool,
/// override recursion logic to always attempt recursion, still respects --depth
#[serde(default)]
pub force_recursion: bool,
/// Auto update app feature
#[serde(skip)]
pub update_app: bool,
}
impl Default for Configuration {
@@ -296,6 +329,7 @@ impl Default for Configuration {
let kind = serialized_type();
let output_level = OutputLevel::Default;
let requester_policy = RequesterPolicy::Default;
let extract_links = extract_links();
Configuration {
kind,
@@ -304,6 +338,7 @@ impl Default for Configuration {
user_agent,
replay_codes,
status_codes,
extract_links,
replay_client,
requester_policy,
dont_filter: false,
@@ -323,12 +358,13 @@ impl Default for Configuration {
insecure: false,
redirects: false,
no_recursion: false,
extract_links: false,
random_agent: false,
collect_extensions: false,
collect_backups: false,
collect_words: false,
save_state: true,
force_recursion: false,
update_app: false,
proxy: String::new(),
config: String::new(),
output: String::new(),
@@ -364,7 +400,7 @@ impl Configuration {
///
/// - **timeout**: `5` seconds
/// - **redirects**: `false`
/// - **extract-links**: `false`
/// - **extract_links**: `true`
/// - **wordlist**: [`DEFAULT_WORDLIST`](constant.DEFAULT_WORDLIST.html)
/// - **config**: `None`
/// - **threads**: `50`
@@ -405,12 +441,14 @@ impl Configuration {
/// - **json**: `false`
/// - **dont_filter**: `false` (auto filter wildcard responses)
/// - **depth**: `4` (maximum recursion depth)
/// - **force_recursion**: `false` (still respects recursion depth)
/// - **scan_limit**: `0` (no limit on concurrent scans imposed)
/// - **parallel**: `0` (no limit on parallel scans imposed)
/// - **rate_limit**: `0` (no limit on requests per second imposed)
/// - **time_limit**: `None` (no limit on length of scan imposed)
/// - **replay_proxy**: `None` (no limit on concurrent scans imposed)
/// - **replay_codes**: [`DEFAULT_RESPONSE_CODES`](constant.DEFAULT_RESPONSE_CODES.html)
/// - **update_app**: `false`
///
/// After which, any values defined in a
/// [ferox-config.toml](constant.DEFAULT_CONFIG_NAME.html) config file will override the
@@ -454,7 +492,7 @@ impl Configuration {
// --resume-from used, need to first read the Configuration from disk, and then
// merge the cli_config into the resumed config
if let Some(filename) = args.value_of("resume_from") {
if let Some(filename) = args.get_one::<String>("resume_from") {
// when resuming a scan, instead of normal configuration loading, we just
// load the config from disk by calling resume_scan
let mut previous_config = resume_scan(filename);
@@ -540,18 +578,21 @@ impl Configuration {
fn parse_cli_args(args: &ArgMatches) -> Self {
let mut config = Configuration::default();
update_config_if_present!(&mut config.threads, args, "threads");
update_config_if_present!(&mut config.depth, args, "depth");
update_config_if_present!(&mut config.scan_limit, args, "scan_limit");
update_config_if_present!(&mut config.parallel, args, "parallel");
update_config_if_present!(&mut config.rate_limit, args, "rate_limit");
update_config_if_present!(&mut config.wordlist, args, "wordlist");
update_config_if_present!(&mut config.output, args, "output");
update_config_if_present!(&mut config.debug_log, args, "debug_log");
update_config_if_present!(&mut config.time_limit, args, "time_limit");
update_config_if_present!(&mut config.resume_from, args, "resume_from");
update_config_with_num_type_if_present!(&mut config.threads, args, "threads", usize);
update_config_with_num_type_if_present!(&mut config.parallel, args, "parallel", usize);
update_config_with_num_type_if_present!(&mut config.depth, args, "depth", usize);
update_config_with_num_type_if_present!(&mut config.scan_limit, args, "scan_limit", usize);
update_config_with_num_type_if_present!(&mut config.rate_limit, args, "rate_limit", usize);
update_config_if_present!(&mut config.wordlist, args, "wordlist", String);
update_config_if_present!(&mut config.output, args, "output", String);
update_config_if_present!(&mut config.debug_log, args, "debug_log", String);
update_config_if_present!(&mut config.resume_from, args, "resume_from", String);
if let Some(arg) = args.values_of("status_codes") {
if let Ok(Some(inner)) = args.try_get_one::<String>("time_limit") {
config.time_limit = inner.to_owned();
}
if let Some(arg) = args.get_many::<String>("status_codes") {
config.status_codes = arg
.map(|code| {
StatusCode::from_bytes(code.as_bytes())
@@ -561,7 +602,7 @@ impl Configuration {
.collect();
}
if let Some(arg) = args.values_of("replay_codes") {
if let Some(arg) = args.get_many::<String>("replay_codes") {
// replay codes passed in by the user
config.replay_codes = arg
.map(|code| {
@@ -575,7 +616,7 @@ impl Configuration {
config.replay_codes = config.status_codes.clone();
}
if let Some(arg) = args.values_of("filter_status") {
if let Some(arg) = args.get_many::<String>("filter_status") {
config.filter_status = arg
.map(|code| {
StatusCode::from_bytes(code.as_bytes())
@@ -585,15 +626,17 @@ impl Configuration {
.collect();
}
if let Some(arg) = args.values_of("extensions") {
config.extensions = arg.map(|val| val.to_string()).collect();
if let Some(arg) = args.get_many::<String>("extensions") {
config.extensions = arg
.map(|val| val.trim_start_matches('.').to_string())
.collect();
}
if let Some(arg) = args.values_of("dont_collect") {
if let Some(arg) = args.get_many::<String>("dont_collect") {
config.dont_collect = arg.map(|val| val.to_string()).collect();
}
if let Some(arg) = args.values_of("methods") {
if let Some(arg) = args.get_many::<String>("methods") {
config.methods = arg
.map(|val| {
// Check methods if they are correct
@@ -605,7 +648,7 @@ impl Configuration {
.collect();
}
if let Some(arg) = args.value_of("data") {
if let Some(arg) = args.get_one::<String>("data") {
if let Some(stripped) = arg.strip_prefix('@') {
config.data =
std::fs::read(stripped).unwrap_or_else(|e| report_and_exit(&e.to_string()));
@@ -614,13 +657,13 @@ impl Configuration {
}
}
if args.is_present("stdin") {
if came_from_cli!(args, "stdin") {
config.stdin = true;
} else if let Some(url) = args.value_of("url") {
config.target_url = String::from(url);
} else if let Some(url) = args.get_one::<String>("url") {
config.target_url = url.into();
}
if let Some(arg) = args.values_of("url_denylist") {
if let Some(arg) = args.get_many::<String>("url_denylist") {
// compile all regular expressions and absolute urls used for --dont-scan
//
// when --dont-scan is used, the should_deny_url function is called at least once per
@@ -664,15 +707,15 @@ impl Configuration {
}
}
if let Some(arg) = args.values_of("filter_regex") {
if let Some(arg) = args.get_many::<String>("filter_regex") {
config.filter_regex = arg.map(|val| val.to_string()).collect();
}
if let Some(arg) = args.values_of("filter_similar") {
if let Some(arg) = args.get_many::<String>("filter_similar") {
config.filter_similar = arg.map(|val| val.to_string()).collect();
}
if let Some(arg) = args.values_of("filter_size") {
if let Some(arg) = args.get_many::<String>("filter_size") {
config.filter_size = arg
.map(|size| {
size.parse::<u64>()
@@ -681,7 +724,7 @@ impl Configuration {
.collect();
}
if let Some(arg) = args.values_of("filter_words") {
if let Some(arg) = args.get_many::<String>("filter_words") {
config.filter_word_count = arg
.map(|size| {
size.parse::<usize>()
@@ -690,7 +733,7 @@ impl Configuration {
.collect();
}
if let Some(arg) = args.values_of("filter_lines") {
if let Some(arg) = args.get_many::<String>("filter_lines") {
config.filter_line_count = arg
.map(|size| {
size.parse::<usize>()
@@ -699,7 +742,7 @@ impl Configuration {
.collect();
}
if args.is_present("silent") {
if came_from_cli!(args, "silent") {
// the reason this is protected by an if statement:
// consider a user specifying silent = true in ferox-config.toml
// if the line below is outside of the if, we'd overwrite true with
@@ -708,102 +751,112 @@ impl Configuration {
config.output_level = OutputLevel::Silent;
}
if args.is_present("quiet") {
if came_from_cli!(args, "quiet") {
config.quiet = true;
config.output_level = OutputLevel::Quiet;
}
if args.is_present("auto_tune") || args.is_present("smart") || args.is_present("thorough") {
if came_from_cli!(args, "auto_tune")
|| came_from_cli!(args, "smart")
|| came_from_cli!(args, "thorough")
{
config.auto_tune = true;
config.requester_policy = RequesterPolicy::AutoTune;
}
if args.is_present("auto_bail") {
if came_from_cli!(args, "auto_bail") {
config.auto_bail = true;
config.requester_policy = RequesterPolicy::AutoBail;
}
if args.is_present("no_state") {
if came_from_cli!(args, "no_state") {
config.save_state = false;
}
if args.is_present("dont_filter") {
if came_from_cli!(args, "dont_filter") {
config.dont_filter = true;
}
if args.is_present("collect_extensions") || args.is_present("thorough") {
if came_from_cli!(args, "collect_extensions") || came_from_cli!(args, "thorough") {
config.collect_extensions = true;
}
if args.is_present("collect_backups")
|| args.is_present("smart")
|| args.is_present("thorough")
if came_from_cli!(args, "collect_backups")
|| came_from_cli!(args, "smart")
|| came_from_cli!(args, "thorough")
{
config.collect_backups = true;
}
if args.is_present("collect_words")
|| args.is_present("smart")
|| args.is_present("thorough")
if came_from_cli!(args, "collect_words")
|| came_from_cli!(args, "smart")
|| came_from_cli!(args, "thorough")
{
config.collect_words = true;
}
if args.occurrences_of("verbosity") > 0 {
if args.get_count("verbosity") > 0 {
// occurrences_of returns 0 if none are found; this is protected in
// an if block for the same reason as the quiet option
config.verbosity = args.occurrences_of("verbosity") as u8;
config.verbosity = args.get_count("verbosity");
}
if args.is_present("no_recursion") {
if came_from_cli!(args, "no_recursion") {
config.no_recursion = true;
}
if args.is_present("add_slash") {
if came_from_cli!(args, "add_slash") {
config.add_slash = true;
}
if args.is_present("extract_links")
|| args.is_present("smart")
|| args.is_present("thorough")
{
config.extract_links = true;
if came_from_cli!(args, "dont_extract_links") {
config.extract_links = false;
}
if args.is_present("json") {
if came_from_cli!(args, "json") {
config.json = true;
}
if came_from_cli!(args, "force_recursion") {
config.force_recursion = true;
}
if came_from_cli!(args, "update_app") {
config.update_app = true;
}
////
// organizational breakpoint; all options below alter the Client configuration
////
update_config_if_present!(&mut config.proxy, args, "proxy");
update_config_if_present!(&mut config.replay_proxy, args, "replay_proxy");
update_config_if_present!(&mut config.user_agent, args, "user_agent");
update_config_if_present!(&mut config.timeout, args, "timeout");
update_config_if_present!(&mut config.proxy, args, "proxy", String);
update_config_if_present!(&mut config.replay_proxy, args, "replay_proxy", String);
update_config_if_present!(&mut config.user_agent, args, "user_agent", String);
update_config_with_num_type_if_present!(&mut config.timeout, args, "timeout", u64);
if args.is_present("burp") {
if came_from_cli!(args, "burp") {
config.proxy = String::from("http://127.0.0.1:8080");
}
if args.is_present("burp_replay") {
if came_from_cli!(args, "burp_replay") {
config.replay_proxy = String::from("http://127.0.0.1:8080");
}
if args.is_present("random_agent") {
if came_from_cli!(args, "random_agent") {
config.random_agent = true;
}
if args.is_present("redirects") {
if came_from_cli!(args, "redirects") {
config.redirects = true;
}
if args.is_present("insecure") || args.is_present("burp") || args.is_present("burp_replay")
if came_from_cli!(args, "insecure")
|| came_from_cli!(args, "burp")
|| came_from_cli!(args, "burp_replay")
{
config.insecure = true;
}
if let Some(headers) = args.values_of("headers") {
if let Some(headers) = args.get_many::<String>("headers") {
for val in headers {
let mut split_val = val.split(':');
@@ -817,7 +870,7 @@ impl Configuration {
}
}
if let Some(cookies) = args.values_of("cookies") {
if let Some(cookies) = args.get_many::<String>("cookies") {
config.headers.insert(
// we know the header name is always "cookie"
"Cookie".to_string(),
@@ -833,7 +886,7 @@ impl Configuration {
);
}
if let Some(queries) = args.values_of("queries") {
if let Some(queries) = args.get_many::<String>("queries") {
for val in queries {
// same basic logic used as reading in the headers HashMap above
let mut split_val = val.split('=');
@@ -942,11 +995,13 @@ impl Configuration {
update_if_not_default!(&mut conf.output, new.output, "");
update_if_not_default!(&mut conf.redirects, new.redirects, false);
update_if_not_default!(&mut conf.insecure, new.insecure, false);
update_if_not_default!(&mut conf.extract_links, new.extract_links, false);
update_if_not_default!(&mut conf.force_recursion, new.force_recursion, false);
update_if_not_default!(&mut conf.extract_links, new.extract_links, extract_links());
update_if_not_default!(&mut conf.extensions, new.extensions, Vec::<String>::new());
update_if_not_default!(&mut conf.methods, new.methods, Vec::<String>::new());
update_if_not_default!(&mut conf.methods, new.methods, methods());
update_if_not_default!(&mut conf.data, new.data, Vec::<u8>::new());
update_if_not_default!(&mut conf.url_denylist, new.url_denylist, Vec::<Url>::new());
update_if_not_default!(&mut conf.update_app, new.update_app, false);
if !new.regex_denylist.is_empty() {
// cant use the update_if_not_default macro due to the following error
//
@@ -1017,7 +1072,17 @@ impl Configuration {
/// uses serde to deserialize the toml into a `Configuration` struct
pub(super) fn parse_config(config_file: PathBuf) -> Result<Self> {
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)
}
}

View File

@@ -45,10 +45,11 @@ fn setup_config_test() -> Configuration {
add_slash = true
stdin = true
dont_filter = true
extract_links = true
extract_links = false
json = true
save_state = false
depth = 1
force_recursion = true
filter_size = [4120]
filter_regex = ["^ignore me$"]
filter_similar = ["https://somesite.com/soft404"]
@@ -95,8 +96,9 @@ fn default_configuration() {
assert!(config.save_state);
assert!(!config.stdin);
assert!(!config.add_slash);
assert!(!config.force_recursion);
assert!(!config.redirects);
assert!(!config.extract_links);
assert!(config.extract_links);
assert!(!config.insecure);
assert!(!config.collect_extensions);
assert!(!config.collect_backups);
@@ -208,6 +210,13 @@ fn config_reads_silent() {
assert!(config.silent);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_force_recursion() {
let config = setup_config_test();
assert!(config.force_recursion);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_quiet() {
@@ -296,7 +305,7 @@ fn config_reads_add_slash() {
/// parse the test config and see that the value parsed is correct
fn config_reads_extract_links() {
let config = setup_config_test();
assert!(config.extract_links);
assert!(!config.extract_links);
}
#[test]
@@ -473,7 +482,7 @@ fn config_report_and_exit_works() {
fn as_str_returns_string_with_newline() {
let config = Configuration::new().unwrap();
let config_str = config.as_str();
println!("{}", config_str);
println!("{config_str}");
assert!(config_str.starts_with("Configuration {"));
assert!(config_str.ends_with("}\n"));
assert!(config_str.contains("replay_codes:"));

View File

@@ -49,6 +49,10 @@ pub(super) fn status_codes() -> Vec<u16> {
DEFAULT_STATUS_CODES
.iter()
.map(|code| code.as_u16())
// add experimental codes not found in reqwest
// - 103 - EARLY_HINTS
// - 425 - TOO_EARLY
.chain([103, 425])
.collect()
}
@@ -72,7 +76,7 @@ pub(super) fn wordlist() -> String {
/// default user-agent
pub(super) fn user_agent() -> String {
format!("feroxbuster/{}", VERSION)
format!("feroxbuster/{VERSION}")
}
/// default recursion depth
@@ -80,8 +84,13 @@ pub(super) fn depth() -> usize {
4
}
/// default extract links
pub(super) fn extract_links() -> bool {
true
}
/// enum representing the three possible states for informational output (not logging verbosity)
#[derive(Debug, Copy, Clone, PartialEq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum OutputLevel {
/// normal scan, no --quiet|--silent
Default,
@@ -116,7 +125,7 @@ pub fn determine_output_level(quiet: bool, silent: bool) -> OutputLevel {
}
/// represents actions the Requester should take in certain situations
#[derive(Debug, PartialEq, Copy, Clone)]
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum RequesterPolicy {
/// automatically try to lower request rate in order to reduce errors
AutoTune,

View File

@@ -5,6 +5,7 @@ use tokio::sync::oneshot::Sender;
use crate::response::FeroxResponse;
use crate::{
event_handlers::Handles,
message::FeroxMessage,
statistics::{StatError, StatField},
traits::FeroxFilter,
@@ -23,7 +24,9 @@ pub enum Command {
AddStatus(StatusCode),
/// Create the progress bar (`BarType::Total`) that is updated from the stats thread
CreateBar,
///
/// the u64 value is the offset at which to start the progress bar (can be 0)
CreateBar(u64),
/// Add to a `Stats` field that corresponds to the given `StatField` by the given `usize` value
AddToUsizeField(StatField, usize),
@@ -43,6 +46,9 @@ pub enum Command {
/// Add a `FeroxFilter` implementor to `FilterHandler`'s instance of `FeroxFilters`
AddFilter(Box<dyn FeroxFilter>),
/// Remove a set of `FeroxFilter` implementors from `FeroxFilters` by index
RemoveFilters(Vec<usize>),
/// Send a `FeroxResponse` to the output handler for reporting
Report(Box<FeroxResponse>),
@@ -75,4 +81,8 @@ pub enum Command {
/// Break out of the (infinite) mpsc receive loop
Exit,
/// Give a handler access to an Arc<Handles> instance after the handler has
/// already been initialized
AddHandles(Arc<Handles>),
}

View File

@@ -160,13 +160,17 @@ impl Handles {
/// number of extensions plus the number of request method types plus any dynamically collected
/// extensions
pub fn expected_num_requests_multiplier(&self) -> usize {
let multiplier = self.config.extensions.len()
+ self.config.methods.len()
+ self.num_collected_extensions();
let mut multiplier = self.config.extensions.len().max(1);
// methods should always have at least 1 member, likely making this .max call unneeded
// but leaving it for 'just in case' reasons
multiplier.max(1)
if multiplier > 1 {
// when we have more than one extension, we need to account for the fact that we'll
// be making a request for each extension and the base word (e.g. /foo.html and /foo)
multiplier += 1;
}
multiplier *= self.config.methods.len().max(1) * self.num_collected_extensions().max(1);
multiplier
}
/// Helper to easily get the (locked) underlying FeroxScans object

View File

@@ -1,4 +1,5 @@
use super::*;
use crate::filters::EmptyFilter;
use crate::{filters::FeroxFilters, CommandSender, FeroxChannel, Joiner};
use anyhow::Result;
use std::sync::Arc;
@@ -84,8 +85,12 @@ impl FiltersHandler {
while let Some(command) = self.receiver.recv().await {
match command {
Command::AddFilter(filter) => {
self.data.push(filter)?;
if filter.as_any().downcast_ref::<EmptyFilter>().is_none() {
// don't add an empty filter
self.data.push(filter)?;
}
}
Command::RemoveFilters(mut indices) => self.data.remove(&mut indices),
Command::Sync(sender) => {
log::debug!("filters: {:?}", self);
sender.send(true).unwrap_or_default();
@@ -99,3 +104,41 @@ impl FiltersHandler {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::filters::WordsFilter;
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn empty_filter_skipped() {
let data = Arc::new(FeroxFilters::default());
let (tx, rx): FeroxChannel<Command> = mpsc::unbounded_channel();
let mut handler = FiltersHandler::new(data.clone(), rx);
let event_handle = FiltersHandle::new(data, tx);
let _task = tokio::spawn(async move { handler.start().await });
event_handle
.send(Command::AddFilter(Box::new(EmptyFilter {})))
.unwrap();
let (tx, rx) = oneshot::channel::<bool>();
event_handle.send(Command::Sync(tx)).unwrap();
rx.await.unwrap();
assert!(event_handle.data.filters.read().unwrap().is_empty());
event_handle
.send(Command::AddFilter(Box::new(WordsFilter { word_count: 1 })))
.unwrap();
let (tx, rx) = oneshot::channel::<bool>();
event_handle.send(Command::Sync(tx)).unwrap();
rx.await.unwrap();
assert_eq!(event_handle.data.filters.read().unwrap().len(), 1);
}
}

View File

@@ -98,12 +98,16 @@ impl TermInputHandler {
handles.config.clone(),
&RESPONSES,
handles.stats.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?;
write_to(&state, &mut buffered_file, true)?;
let mut buffered_file = state_file?;
write_to(&state, &mut buffered_file, true)?;
}
log::trace!("exit: sigint_handler (end of program)");
std::process::exit(1);

View File

@@ -5,14 +5,13 @@ use anyhow::{Context, Result};
use futures::future::{BoxFuture, FutureExt};
use tokio::sync::{mpsc, oneshot};
use crate::statistics::StatField::TotalExpected;
use crate::{
config::Configuration,
progress::PROGRESS_PRINTER,
response::FeroxResponse,
scanner::RESPONSES,
send_command, skip_fail,
statistics::StatField::ResourcesDiscovered,
statistics::StatField::{ResourcesDiscovered, TotalExpected},
traits::FeroxSerialize,
utils::{ferox_print, fmt_err, make_request, open_file, write_to},
CommandReceiver, CommandSender, Joiner,
@@ -144,6 +143,9 @@ pub struct TermOutHandler {
/// pointer to "global" configuration struct
config: Arc<Configuration>,
/// handles instance
handles: Option<Arc<Handles>>,
}
/// implementation of TermOutHandler
@@ -161,6 +163,7 @@ impl TermOutHandler {
tx_file,
file_task,
config,
handles: None,
}
}
@@ -212,6 +215,9 @@ impl TermOutHandler {
Command::Sync(sender) => {
sender.send(true).unwrap_or_default();
}
Command::AddHandles(handles) => {
self.handles = Some(handles);
}
Command::Exit => {
if self.file_task.is_some() && self.tx_file.send(Command::Exit).is_ok() {
self.file_task.as_mut().unwrap().await??; // wait for death
@@ -236,9 +242,26 @@ impl TermOutHandler {
log::trace!("enter: process_response({:?}, {:?})", resp, call_type);
async move {
let contains_sentry = self.config.status_codes.contains(&resp.status().as_u16());
let should_filter = self
.handles
.as_ref()
.unwrap()
.filters
.data
.should_filter_response(&resp, tx_stats.clone());
let contains_sentry = if !self.config.filter_status.is_empty() {
// -C was used, meaning -s was not and we should ignore the defaults
// https://github.com/epi052/feroxbuster/issues/535
// -C indicates that we should filter that status code, but allow all others
!self.config.filter_status.contains(&resp.status().as_u16())
} else {
// -C wasn't used, so, we defer to checking the -s values
self.config.status_codes.contains(&resp.status().as_u16())
};
let unknown_sentry = !RESPONSES.contains(&resp); // !contains == unknown
let should_process_response = contains_sentry && unknown_sentry;
let should_process_response = contains_sentry && unknown_sentry && !should_filter;
if should_process_response {
// print to stdout
@@ -251,7 +274,7 @@ impl TermOutHandler {
self.tx_file
.send(Command::Report(resp.clone()))
.with_context(|| {
fmt_err(&format!("Could not send {} to file handler", resp))
fmt_err(&format!("Could not send {resp} to file handler"))
})?;
}
}
@@ -284,7 +307,7 @@ impl TermOutHandler {
&& matches!(call_type, ProcessResponseCall::Recursive)
{
// --collect-backups was used; the response is one we care about, and the function
// call came from the loop in `.start` (i.e. recursive was specified
// call came from the loop in `.start` (i.e. recursive was specified)
let backup_urls = self.generate_backup_urls(&resp).await;
// need to manually adjust stats
@@ -341,9 +364,9 @@ impl TermOutHandler {
/// internal helper to stay DRY
fn add_new_url_to_vec(&self, url: &Url, new_name: &str, urls: &mut Vec<Url>) {
let mut new_url = url.clone();
new_url.set_path(new_name);
urls.push(new_url);
if let Ok(joined) = url.join(new_name) {
urls.push(joined);
}
}
/// given a `FeroxResponse`, generate either 6 or 7 urls that are likely backups of the
@@ -371,11 +394,11 @@ impl TermOutHandler {
if !filename.is_empty() {
// append rules
for suffix in ["~", ".bak", ".bak2", ".old", ".1"] {
self.add_new_url_to_vec(url, &format!("{}{}", filename, suffix), &mut urls);
self.add_new_url_to_vec(url, &format!("{filename}{suffix}"), &mut urls);
}
// vim swap rule
self.add_new_url_to_vec(url, &format!(".{}.swp", filename), &mut urls);
self.add_new_url_to_vec(url, &format!(".{filename}.swp"), &mut urls);
// replace original extension rule
let parts: Vec<_> = filename
@@ -398,6 +421,7 @@ impl TermOutHandler {
#[cfg(test)]
mod tests {
use super::*;
use crate::event_handlers::Command;
#[test]
/// try to hit struct field coverage of FileOutHandler
@@ -408,7 +432,7 @@ mod tests {
config,
receiver: rx,
};
println!("{:?}", foh);
println!("{foh:?}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
@@ -417,15 +441,17 @@ mod tests {
let (tx, rx) = mpsc::unbounded_channel::<Command>();
let (tx_file, _) = mpsc::unbounded_channel::<Command>();
let config = Arc::new(Configuration::new().unwrap());
let handles = Arc::new(Handles::for_testing(None, None).0);
let toh = TermOutHandler {
config,
file_task: None,
receiver: rx,
tx_file,
handles: Some(handles),
};
println!("{:?}", toh);
println!("{toh:?}");
tx.send(Command::Exit).unwrap();
}
@@ -435,12 +461,14 @@ mod tests {
let (tx, rx) = mpsc::unbounded_channel::<Command>();
let (tx_file, _) = mpsc::unbounded_channel::<Command>();
let config = Arc::new(Configuration::new().unwrap());
let handles = Arc::new(Handles::for_testing(None, None).0);
let toh = TermOutHandler {
config,
file_task: None,
receiver: rx,
tx_file,
handles: Some(handles),
};
let expected: Vec<_> = vec![
@@ -478,12 +506,14 @@ mod tests {
let (tx, rx) = mpsc::unbounded_channel::<Command>();
let (tx_file, _) = mpsc::unbounded_channel::<Command>();
let config = Arc::new(Configuration::new().unwrap());
let handles = Arc::new(Handles::for_testing(None, None).0);
let toh = TermOutHandler {
config,
file_task: None,
receiver: rx,
tx_file,
handles: Some(handles),
};
let expected: Vec<_> = vec![
@@ -513,4 +543,47 @@ mod tests {
tx.send(Command::Exit).unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
/// test to ensure that backups are requested from the directory in which they were found
/// re: issue #513
async fn generate_backup_urls_creates_correct_urls_when_not_at_root() {
let (tx, rx) = mpsc::unbounded_channel::<Command>();
let (tx_file, _) = mpsc::unbounded_channel::<Command>();
let config = Arc::new(Configuration::new().unwrap());
let handles = Arc::new(Handles::for_testing(None, None).0);
let toh = TermOutHandler {
config,
file_task: None,
receiver: rx,
tx_file,
handles: Some(handles),
};
let expected: Vec<_> = vec![
"http://localhost/wordpress/derp.php~",
"http://localhost/wordpress/derp.php.bak",
"http://localhost/wordpress/derp.php.bak2",
"http://localhost/wordpress/derp.php.old",
"http://localhost/wordpress/derp.php.1",
"http://localhost/wordpress/.derp.php.swp",
"http://localhost/wordpress/derp.bak",
];
let mut fr = FeroxResponse::default();
fr.set_url("http://localhost/wordpress/derp.php");
let urls = toh.generate_backup_urls(&fr).await;
let url_strs: Vec<_> = urls.iter().map(|url| url.as_str()).collect();
assert_eq!(urls.len(), 7);
for url_str in url_strs {
assert!(expected.contains(&url_str));
}
tx.send(Command::Exit).unwrap();
}
}

View File

@@ -6,7 +6,7 @@ use tokio::sync::{mpsc, Semaphore};
use crate::{
response::FeroxResponse,
scan_manager::{FeroxScan, FeroxScans, ScanOrder},
scanner::FeroxScanner,
scanner::{FeroxScanner, RESPONSES},
statistics::StatField::TotalScans,
url::FeroxUrl,
utils::should_deny_url,
@@ -218,11 +218,11 @@ impl ScanHandler {
// current number of requests expected per scan
// ExpectedPerScan and TotalExpected are a += action, so we need the wordlist length to
// update them while the other updates use expected_num_requests_per_dir
let num_words = self.get_wordlist()?.len();
let num_words = self.get_wordlist(0)?.len();
let current_expectation = self.handles.expected_num_requests_per_dir() as u64;
// used in the calculation of bar width down below, see explanation there
let divisor = self.handles.expected_num_requests_multiplier() as u64 - 1;
let divisor = (self.handles.expected_num_requests_multiplier() as u64 - 1).max(1);
// add another `wordlist.len` to the expected per scan tracker in the statistics handler
self.handles
@@ -290,10 +290,14 @@ impl ScanHandler {
}
/// Helper to easily get the (locked) underlying wordlist
pub fn get_wordlist(&self) -> Result<Arc<Vec<String>>> {
pub fn get_wordlist(&self, offset: usize) -> Result<Arc<Vec<String>>> {
if let Ok(guard) = self.wordlist.lock().as_ref() {
if let Some(list) = guard.as_ref() {
return Ok(list.clone());
return if offset > 0 {
Ok(Arc::new(list[offset..].to_vec()))
} else {
Ok(list.clone())
};
}
}
@@ -328,7 +332,18 @@ impl ScanHandler {
continue;
}
let list = self.get_wordlist()?;
let divisor = self.handles.expected_num_requests_multiplier();
let list = if divisor > 1 && scan.requests() > 0 {
// if there were extensions provided and/or more than a single method used, and some
// number of requests have already been sent, we need to adjust the offset into the
// wordlist to ensure we don't index out of bounds
let adjusted = scan.requests_made_so_far() as f64 / (divisor as f64 - 1.0).max(1.0);
self.get_wordlist(adjusted as usize)?
} else {
self.get_wordlist(scan.requests_made_so_far() as usize)?
};
log::info!("scan handler received {} - beginning scan", target);
@@ -368,8 +383,8 @@ impl ScanHandler {
async fn try_recursion(&mut self, response: Box<FeroxResponse>) -> Result<()> {
log::trace!("enter: try_recursion({:?})", response,);
if !response.is_directory() {
// not a directory, quick exit
if !self.handles.config.force_recursion && !response.is_directory() {
// not a directory and --force-recursion wasn't used, quick exit
return Ok(());
}
@@ -386,6 +401,58 @@ impl ScanHandler {
return Ok(());
}
if let Ok(responses) = RESPONSES.responses.read() {
for maybe_wild in responses.iter() {
if !maybe_wild.wildcard() || !maybe_wild.is_directory() {
// if the stored response isn't a wildcard, skip it
// if the stored response isn't a directory, skip it
// we're only interested in preventing recursion into wildcard directories
continue;
}
if maybe_wild.method() != response.method() {
// methods don't match, skip it
continue;
}
// methods match and is a directory wildcard
// need to check the wildcard's parent directory
// for equality with the incoming response's parent directory
//
// if the parent directories match, we need to prevent recursion
// into the wildcard directory
match (
maybe_wild.url().path_segments(),
response.url().path_segments(),
) {
// both urls must have path segments
(Some(mut maybe_wild_segments), Some(mut response_segments)) => {
match (
maybe_wild_segments.nth_back(1),
response_segments.nth_back(1),
) {
// both urls must have at least 2 path segments, the next to last being the parent
(Some(maybe_wild_parent), Some(response_parent)) => {
if maybe_wild_parent == response_parent {
// the parent directories match, so we need to prevent recursion
return Ok(());
}
}
_ => {
// we couldn't get the parent directory, so we'll skip this
continue;
}
}
}
_ => {
// we couldn't get the path segments, so we'll skip this
continue;
}
}
}
}
let targets = vec![response.url().to_string()];
self.ordered_scan_url(targets, ScanOrder::Latest).await?;

View File

@@ -115,8 +115,9 @@ impl StatsHandler {
}
}
Command::AddToF64Field(field, value) => self.stats.update_f64_field(field, value),
Command::CreateBar => {
Command::CreateBar(offset) => {
self.bar = add_bar("", self.stats.total_expected() as u64, BarType::Total);
self.bar.set_position(offset);
}
Command::LoadStats(filename) => {
self.stats.merge_from(&filename)?;
@@ -147,7 +148,12 @@ impl StatsHandler {
);
self.bar.set_message(&msg);
self.bar.inc(1);
if self.bar.position() < self.stats.total_expected() as u64 {
// don't run off the end when we're a few requests over the expected total
// due to the heuristics tests
self.bar.inc(1);
}
}
/// Initialize new `Stats` object and the sc side of an mpsc channel that is responsible for

View File

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

View File

@@ -2,7 +2,6 @@ use super::*;
use crate::{
client,
event_handlers::{
Command,
Command::{AddError, AddToUsizeField},
Handles,
},
@@ -12,14 +11,13 @@ use crate::{
StatField::{LinksExtracted, TotalExpected},
},
url::FeroxUrl,
utils::{logged_request, make_request, should_deny_url},
utils::{logged_request, make_request, send_try_recursion_command, should_deny_url},
ExtractionResult, DEFAULT_METHOD,
};
use anyhow::{bail, Context, Result};
use reqwest::{Client, StatusCode, Url};
use scraper::{Html, Selector};
use std::collections::HashSet;
use tokio::sync::oneshot;
use std::{borrow::Cow, collections::HashSet};
/// Whether an active scan is recursive or not
#[derive(Debug)]
@@ -40,6 +38,9 @@ pub struct Extractor<'a> {
/// `ROBOTS_TXT_REGEX` as a regex::Regex type
pub(super) robots_regex: Regex,
/// regex to validate a url
pub(super) url_regex: Regex,
/// Response from which to extract links
pub(super) response: Option<&'a FeroxResponse>,
@@ -186,11 +187,21 @@ impl<'a> Extractor<'a> {
resp.set_url(&format!("{}/", resp.url()));
}
self.handles
.send_scan_command(Command::TryRecursion(Box::new(resp)))?;
let (tx, rx) = oneshot::channel::<bool>();
self.handles.send_scan_command(Command::Sync(tx))?;
rx.await?;
if self.handles.config.filter_status.is_empty() {
// -C wasn't used, so -s is the only 'filter' left to account for
if self
.handles
.config
.status_codes
.contains(&resp.status().as_u16())
{
send_try_recursion_command(self.handles.clone(), resp).await?;
}
} else {
// -C was used, that means the filters above would have removed
// those responses, and anything else should be let through
send_try_recursion_command(self.handles.clone(), resp).await?;
}
}
}
log::trace!("exit: request_links");
@@ -212,7 +223,7 @@ impl<'a> Extractor<'a> {
self.extract_links_by_attr(resp_url, links, html, "div", "src");
self.extract_links_by_attr(resp_url, links, html, "frame", "src");
self.extract_links_by_attr(resp_url, links, html, "embed", "src");
self.extract_links_by_attr(resp_url, links, html, "script", "src");
self.extract_links_by_attr(resp_url, links, html, "link", "href");
}
/// Given the body of a `reqwest::Response`, perform the following actions
@@ -325,8 +336,9 @@ impl<'a> Extractor<'a> {
let normalized_path = self.normalize_url_path(path);
// filter out any empty strings caused by .split
let mut parts: Vec<&str> = normalized_path
let mut parts: Vec<Cow<_>> = normalized_path
.split('/')
.map(|s| self.url_regex.replace_all(s, ""))
.filter(|s| !s.is_empty())
.collect();
@@ -350,7 +362,7 @@ impl<'a> Extractor<'a> {
// this isn't the last index of the parts array
// ex: /buried/misc/stupidfile.php
// this block skips the file but sees all parent folders
possible_path = format!("{}/", possible_path);
possible_path = format!("{possible_path}/");
}
paths.push(possible_path); // good sub-path found
@@ -383,7 +395,18 @@ impl<'a> Extractor<'a> {
let new_url = old_url
.join(link)
.with_context(|| format!("Could not join {} with {}", old_url, link))?;
.with_context(|| format!("Could not join {old_url} with {link}"))?;
if old_url.domain() != new_url.domain() || old_url.host() != new_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());
@@ -406,7 +429,7 @@ impl<'a> Extractor<'a> {
let scanned_urls = self.handles.ferox_scans()?;
if scanned_urls.get_scan_by_url(&new_url.to_string()).is_some() {
if scanned_urls.get_scan_by_url(new_url.as_ref()).is_some() {
//we've seen the url before and don't need to scan again
log::trace!("exit: request_link -> None");
bail!("previously seen url");

View File

@@ -1,4 +1,4 @@
use super::builder::{LINKFINDER_REGEX, ROBOTS_TXT_REGEX};
use super::builder::{LINKFINDER_REGEX, ROBOTS_TXT_REGEX, URL_CHARS_REGEX};
use super::*;
use crate::config::{Configuration, OutputLevel};
use crate::scan_manager::ScanOrder;
@@ -273,6 +273,7 @@ async fn extractor_get_links_with_absolute_url_that_differs_from_target_domain()
let extractor = Extractor {
links_regex: Regex::new(LINKFINDER_REGEX).unwrap(),
robots_regex: Regex::new(ROBOTS_TXT_REGEX).unwrap(),
url_regex: Regex::new(URL_CHARS_REGEX).unwrap(),
response: Some(&ferox_response),
url: String::new(),
target: ExtractionTarget::ResponseBody,
@@ -301,6 +302,7 @@ async fn request_robots_txt_without_proxy() -> Result<()> {
let extractor = Extractor {
links_regex: Regex::new(LINKFINDER_REGEX).unwrap(),
robots_regex: Regex::new(ROBOTS_TXT_REGEX).unwrap(),
url_regex: Regex::new(URL_CHARS_REGEX).unwrap(),
response: None,
url: srv.url("/api/users/stuff/things"),
target: ExtractionTarget::RobotsTxt,
@@ -310,7 +312,7 @@ async fn request_robots_txt_without_proxy() -> Result<()> {
let resp = extractor.make_extract_request("/robots.txt").await?;
assert!(matches!(resp.status(), &StatusCode::OK));
println!("{}", resp);
println!("{resp}");
assert_eq!(resp.content_length(), 14);
assert_eq!(mock.hits(), 1);
Ok(())

View File

@@ -1,27 +1,30 @@
use std::sync::Mutex;
use std::sync::RwLock;
use anyhow::Result;
use serde::{ser::SerializeSeq, Serialize, Serializer};
use crate::response::FeroxResponse;
use super::{
FeroxFilter, LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter,
WildcardFilter, WordsFilter,
};
use crate::{
event_handlers::Command::AddToUsizeField, statistics::StatField::WildcardsFiltered,
CommandSender,
};
use super::{FeroxFilter, WildcardFilter};
/// Container around a collection of `FeroxFilters`s
#[derive(Debug, Default)]
pub struct FeroxFilters {
/// collection of `FeroxFilters`
pub filters: Mutex<Vec<Box<dyn FeroxFilter>>>,
pub filters: RwLock<Vec<Box<dyn FeroxFilter>>>,
}
/// implementation of FeroxFilter collection
impl FeroxFilters {
/// add a single FeroxFilter to the collection
pub fn push(&self, filter: Box<dyn FeroxFilter>) -> Result<()> {
if let Ok(mut guard) = self.filters.lock() {
if let Ok(mut guard) = self.filters.write() {
if guard.contains(&filter) {
return Ok(());
}
@@ -31,6 +34,37 @@ impl FeroxFilters {
Ok(())
}
/// remove items from the underlying collection by their index
///
/// note: indexes passed in should be index-to-remove+1. This is built for the scan mgt menu
/// so indexes aren't 0-based whehn the user enters them.
///
pub fn remove(&self, indices: &mut [usize]) {
// since we're removing by index, indices must be sorted and then reversed.
// this allows us to iterate over the collection from the rear, allowing any shifting
// of the vector to happen on sections that we no longer care about, as we're moving
// in the opposite direction
indices.sort_unstable();
indices.reverse();
if let Ok(mut guard) = self.filters.write() {
for index in indices {
// numbering of the menu starts at 1, so we'll need to reduce the index by 1
// to account for that. if they've provided 0 as an offset, we'll set the
// result to a gigantic number and skip it in the loop with a bounds check
let reduced_idx = index.checked_sub(1).unwrap_or(usize::MAX);
// check if number provided is out of range
if reduced_idx >= guard.len() {
// usize can't be negative, just need to handle exceeding bounds
continue;
}
guard.remove(reduced_idx);
}
}
}
/// Simple helper to stay DRY; determines whether or not a given `FeroxResponse` should be reported
/// to the user or not.
pub fn should_filter_response(
@@ -38,10 +72,11 @@ impl FeroxFilters {
response: &FeroxResponse,
tx_stats: CommandSender,
) -> bool {
if let Ok(filters) = self.filters.lock() {
if let Ok(filters) = self.filters.read() {
for filter in filters.iter() {
// wildcard.should_filter goes here
if filter.should_filter_response(response) {
log::debug!("filtering response due to: {:?}", filter);
if filter.as_any().downcast_ref::<WildcardFilter>().is_some() {
tx_stats
.send(AddToUsizeField(WildcardsFiltered, 1))
@@ -54,3 +89,43 @@ impl FeroxFilters {
false
}
}
impl Serialize for FeroxFilters {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Ok(guard) = self.filters.read() {
let mut seq = serializer.serialize_seq(Some(guard.len()))?;
for filter in &*guard {
if let Some(line_filter) = filter.as_any().downcast_ref::<LinesFilter>() {
seq.serialize_element(line_filter).unwrap_or_default();
} else if let Some(word_filter) = filter.as_any().downcast_ref::<WordsFilter>() {
seq.serialize_element(word_filter).unwrap_or_default();
} else if let Some(size_filter) = filter.as_any().downcast_ref::<SizeFilter>() {
seq.serialize_element(size_filter).unwrap_or_default();
} else if let Some(wildcard_filter) =
filter.as_any().downcast_ref::<WildcardFilter>()
{
seq.serialize_element(wildcard_filter).unwrap_or_default();
} else if let Some(status_filter) =
filter.as_any().downcast_ref::<StatusCodeFilter>()
{
seq.serialize_element(status_filter).unwrap_or_default();
} else if let Some(regex_filter) = filter.as_any().downcast_ref::<RegexFilter>() {
seq.serialize_element(regex_filter).unwrap_or_default();
} else if let Some(similarity_filter) =
filter.as_any().downcast_ref::<SimilarityFilter>()
{
seq.serialize_element(similarity_filter).unwrap_or_default();
}
}
seq.end()
} else {
// if for some reason we can't unlock the mutex, just write an empty list
let seq = serializer.serialize_seq(Some(0))?;
seq.end()
}
}
}

22
src/filters/empty.rs Normal file
View File

@@ -0,0 +1,22 @@
use super::*;
/// Dummy filter for internal shenanigans
#[derive(Default, Debug, PartialEq, Eq)]
pub struct EmptyFilter {}
impl FeroxFilter for EmptyFilter {
/// `EmptyFilter` always returns false
fn should_filter_response(&self, _response: &FeroxResponse) -> bool {
false
}
/// Compare one EmptyFilter to another
fn box_eq(&self, other: &dyn Any) -> bool {
other.downcast_ref::<Self>().map_or(false, |a| self == a)
}
/// Return self as Any for dynamic dispatch purposes
fn as_any(&self) -> &dyn Any {
self
}
}

View File

@@ -1,18 +1,10 @@
use super::{
LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter, WordsFilter,
};
use crate::{
event_handlers::Handles,
response::FeroxResponse,
skip_fail,
utils::{fmt_err, logged_request},
Command::AddFilter,
DEFAULT_METHOD, SIMILARITY_THRESHOLD,
utils::create_similarity_filter, LinesFilter, RegexFilter, SizeFilter, StatusCodeFilter,
WordsFilter,
};
use crate::{event_handlers::Handles, skip_fail, utils::fmt_err, Command::AddFilter};
use anyhow::Result;
use fuzzyhash::FuzzyHash;
use regex::Regex;
use reqwest::Url;
use std::sync::Arc;
/// add all user-supplied filters to the (already started) filters handler
@@ -68,32 +60,7 @@ pub async fn initialize(handles: Arc<Handles>) -> Result<()> {
// add any similarity filters to filters handler's FeroxFilters (--filter-similar-to)
for similarity_filter in &handles.config.filter_similar {
// url as-is based on input, ignores user-specified url manipulation options (add-slash etc)
let url = skip_fail!(Url::parse(similarity_filter));
// attempt to request the given url
let resp = skip_fail!(logged_request(&url, DEFAULT_METHOD, None, handles.clone()).await);
// if successful, create a filter based on the response's body
let mut fr = FeroxResponse::from(
resp,
similarity_filter,
DEFAULT_METHOD,
handles.config.output_level,
)
.await;
if handles.config.collect_extensions {
fr.parse_extension(handles.clone())?;
}
// hash the response body and store the resulting hash in the filter object
let hash = FuzzyHash::new(&fr.text()).to_string();
let filter = SimilarityFilter {
text: hash,
threshold: SIMILARITY_THRESHOLD,
};
let filter = skip_fail!(create_similarity_filter(similarity_filter, handles.clone()).await);
let boxed_filter = Box::new(filter);
skip_fail!(handles.filters.send(AddFilter(boxed_filter)));

View File

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

View File

@@ -1,21 +1,23 @@
//! contains all of feroxbuster's filters
use serde::{Deserialize, Serialize};
use std::any::Any;
use std::fmt::Debug;
use crate::response::FeroxResponse;
use crate::traits::{FeroxFilter, FeroxSerialize};
use crate::traits::FeroxFilter;
pub use self::container::FeroxFilters;
pub(crate) use self::empty::EmptyFilter;
pub use self::init::initialize;
pub use self::lines::LinesFilter;
pub use self::regex::RegexFilter;
pub use self::similarity::SimilarityFilter;
pub use self::similarity::{SimilarityFilter, SIM_HASHER};
pub use self::size::SizeFilter;
pub use self::status_code::StatusCodeFilter;
pub(crate) use self::utils::{create_similarity_filter, filter_lookup};
pub use self::wildcard::WildcardFilter;
pub use self::words::WordsFilter;
mod wildcard;
mod status_code;
mod words;
mod lines;
@@ -26,3 +28,6 @@ mod container;
#[cfg(test)]
mod tests;
mod init;
mod utils;
mod wildcard;
mod empty;

View File

@@ -3,15 +3,25 @@ use ::regex::Regex;
/// Simple implementor of FeroxFilter; used to filter out responses based on a given regular
/// expression; specified using -X|--filter-regex
#[derive(Debug)]
#[derive(Debug, Serialize, Deserialize)]
pub struct RegexFilter {
/// Regular expression to be applied to the response body for filtering, compiled
#[serde(with = "serde_regex")]
pub compiled: Regex,
/// Regular expression as passed in on the command line, not compiled
pub raw_string: String,
}
impl Default for RegexFilter {
fn default() -> Self {
Self {
compiled: Regex::new("").unwrap(),
raw_string: String::new(),
}
}
}
/// implementation of FeroxFilter for RegexFilter
impl FeroxFilter for RegexFilter {
/// Check `expression` against the response body, if the expression matches, the response

View File

@@ -1,15 +1,29 @@
use super::*;
use fuzzyhash::FuzzyHash;
use crate::nlp::preprocess;
use gaoya::simhash::{SimHash, SimHashBits, SimSipHasher64};
use lazy_static::lazy_static;
lazy_static! {
/// single instance of the sip hasher used in similarity filtering
pub static ref SIM_HASHER: SimHash<SimSipHasher64, u64, 64> =
SimHash::<SimSipHasher64, u64, 64>::new(SimSipHasher64::new(1, 2));
}
/// maximum hamming distance allowed between two signatures
///
/// ref: https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/33026.pdf
/// section: 4.1 Choice of Parameters
const MAX_HAMMING_DISTANCE: usize = 3;
/// Simple implementor of FeroxFilter; used to filter out responses based on the similarity of a
/// Response body with a known response; specified using --filter-similar-to
#[derive(Default, Debug, PartialEq)]
#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SimilarityFilter {
/// Response's body to be used for comparison for similarity
pub text: String,
/// Hash of Response's body to be used during similarity comparison
pub hash: u64,
/// Percentage of similarity at which a page is determined to be a near-duplicate of another
pub threshold: u32,
/// Url originally requested for the similarity filter
pub original_url: String,
}
/// implementation of FeroxFilter for SimilarityFilter
@@ -17,20 +31,15 @@ impl FeroxFilter for SimilarityFilter {
/// Check `FeroxResponse::text` against what was requested from the site passed in via
/// --filter-similar-to
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
let other = FuzzyHash::new(&response.text());
if let Ok(result) = FuzzyHash::compare(&self.text, &other.to_string()) {
return result >= self.threshold;
}
// couldn't hash the response, don't filter
log::warn!("Could not hash body from {}", response.as_str());
false
let other = SIM_HASHER.create_signature(preprocess(response.text()).iter());
self.hash.hamming_distance(&other) <= MAX_HAMMING_DISTANCE
}
/// Compare one SimilarityFilter to another
fn box_eq(&self, other: &dyn Any) -> bool {
other.downcast_ref::<Self>().map_or(false, |a| self == a)
other
.downcast_ref::<Self>()
.map_or(false, |a| self.hash == a.hash)
}
/// Return self as Any for dynamic dispatch purposes

View File

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

View File

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

View File

@@ -1,26 +1,38 @@
use super::*;
use ::fuzzyhash::FuzzyHash;
use crate::nlp::preprocess;
use crate::DEFAULT_METHOD;
use ::regex::Regex;
#[test]
/// simply test the default values for wildcardfilter, expect 0, 0
/// simply test the default values for wildcardfilter
fn wildcard_filter_default() {
let wcf = WildcardFilter::default();
assert_eq!(wcf.size, u64::MAX);
assert_eq!(wcf.dynamic, u64::MAX);
assert_eq!(wcf.content_length, None);
assert_eq!(wcf.line_count, None);
assert_eq!(wcf.word_count, None);
assert_eq!(wcf.method, DEFAULT_METHOD.to_string());
assert_eq!(wcf.status_code, 0);
assert!(!wcf.dont_filter);
}
#[test]
/// just a simple test to increase code coverage by hitting as_any and the inner value
fn wildcard_filter_as_any() {
let filter = WildcardFilter::default();
let mut filter = WildcardFilter::default();
let filter2 = WildcardFilter::default();
assert!(filter.box_eq(filter2.as_any()));
assert_eq!(
*filter.as_any().downcast_ref::<WildcardFilter>().unwrap(),
filter
filter2
);
filter.content_length = Some(1);
assert_ne!(
*filter.as_any().downcast_ref::<WildcardFilter>().unwrap(),
filter2
);
}
@@ -111,18 +123,21 @@ fn regex_filter_as_any() {
#[test]
/// test should_filter on WilcardFilter where static logic matches
fn wildcard_should_filter_when_static_wildcard_found() {
let body =
"pellentesque diam volutpat commodo sed egestas egestas fringilla phasellus faucibus";
let mut resp = FeroxResponse::default();
resp.set_wildcard(true);
resp.set_url("http://localhost");
resp.set_text(
"pellentesque diam volutpat commodo sed egestas egestas fringilla phasellus faucibus",
);
resp.set_text(body);
let filter = WildcardFilter {
size: 83,
dynamic: 0,
content_length: Some(body.len() as u64),
line_count: Some(1),
word_count: Some(10),
method: DEFAULT_METHOD.to_string(),
status_code: 200,
dont_filter: false,
method: "GET".to_owned(),
};
assert!(filter.should_filter_response(&resp));
@@ -136,7 +151,14 @@ fn wildcard_should_filter_when_static_wildcard_len_is_zero() {
resp.set_url("http://localhost");
// default WildcardFilter is used in the code that executes when response.content_length() == 0
let filter = WildcardFilter::new(false);
let filter = WildcardFilter {
content_length: Some(0),
line_count: Some(0),
word_count: Some(0),
method: DEFAULT_METHOD.to_string(),
status_code: 200,
dont_filter: false,
};
assert!(filter.should_filter_response(&resp));
}
@@ -150,17 +172,16 @@ fn wildcard_should_filter_when_dynamic_wildcard_found() {
resp.set_text("pellentesque diam volutpat commodo sed egestas egestas fringilla");
let filter = WildcardFilter {
size: 0,
dynamic: 59, // content-length - 5 (len('stuff'))
content_length: None,
line_count: None,
word_count: Some(8),
method: DEFAULT_METHOD.to_string(),
status_code: 200,
dont_filter: false,
method: "GET".to_owned(),
};
println!("resp: {:?}: filter: {:?}", resp, filter);
assert!(filter.should_filter_response(&resp));
}
#[test]
/// test should_filter on RegexFilter where regex matches body
fn regexfilter_should_filter_when_regex_matches_on_response_body() {
@@ -186,23 +207,23 @@ fn similarity_filter_is_accurate() {
resp.set_text("sitting");
let mut filter = SimilarityFilter {
text: FuzzyHash::new("kitten").to_string(),
threshold: 95,
hash: SIM_HASHER.create_signature(["kitten"].iter()),
original_url: "".to_string(),
};
// kitten/sitting is 57% similar, so a threshold of 95 should not be filtered
assert!(!filter.should_filter_response(&resp));
resp.set_text("");
filter.text = String::new();
filter.threshold = 100;
filter.hash = SIM_HASHER.create_signature([""].iter());
// two empty strings are the same, however ssdeep doesn't accept empty strings, expect false
// two empty strings are the same
assert!(!filter.should_filter_response(&resp));
resp.set_text("some data to hash for the purposes of running a test");
filter.text = FuzzyHash::new("some data to hash for the purposes of running a te").to_string();
filter.threshold = 17;
resp.set_text("some data hash purposes running test");
filter.hash = SIM_HASHER.create_signature(
preprocess("some data to hash for the purposes of running a test").iter(),
);
assert!(filter.should_filter_response(&resp));
}
@@ -211,20 +232,55 @@ fn similarity_filter_is_accurate() {
/// just a simple test to increase code coverage by hitting as_any and the inner value
fn similarity_filter_as_any() {
let filter = SimilarityFilter {
text: String::from("stuff"),
threshold: 95,
hash: 1,
original_url: "".to_string(),
};
let filter2 = SimilarityFilter {
text: String::from("stuff"),
threshold: 95,
hash: 1,
original_url: "".to_string(),
};
assert!(filter.box_eq(filter2.as_any()));
assert_eq!(filter.text, "stuff");
assert_eq!(
*filter.as_any().downcast_ref::<SimilarityFilter>().unwrap(),
filter
);
}
#[test]
/// test correctness of FeroxFilters::remove
fn remove_function_works_as_expected() {
let data = FeroxFilters::default();
assert!(data.filters.read().unwrap().is_empty());
(0..8).for_each(|i| {
data.push(Box::new(WordsFilter { word_count: i })).unwrap();
});
// remove removes index-1 from the vec, zero is skipped, and out-of-bounds indices are skipped
data.remove(&mut [0]);
assert_eq!(data.filters.read().unwrap().len(), 8);
data.remove(&mut [10000]);
assert_eq!(data.filters.read().unwrap().len(), 8);
// removing 0, 2, 4
data.remove(&mut [1, 3, 5]);
assert_eq!(data.filters.read().unwrap().len(), 5);
let expected = vec![
WordsFilter { word_count: 1 },
WordsFilter { word_count: 3 },
WordsFilter { word_count: 5 },
WordsFilter { word_count: 6 },
WordsFilter { word_count: 7 },
];
for filter in data.filters.read().unwrap().iter() {
let downcast = filter.as_any().downcast_ref::<WordsFilter>().unwrap();
assert!(expected.contains(downcast));
}
}

200
src/filters/utils.rs Normal file
View File

@@ -0,0 +1,200 @@
use super::FeroxFilter;
use super::SimilarityFilter;
use crate::event_handlers::Handles;
use crate::filters::similarity::SIM_HASHER;
use crate::nlp::preprocess;
use crate::response::FeroxResponse;
use crate::utils::logged_request;
use crate::DEFAULT_METHOD;
use anyhow::Result;
use regex::Regex;
use reqwest::Url;
use std::sync::Arc;
/// wrapper around logic necessary to create a SimilarityFilter
///
/// - parses given url
/// - makes request to the parsed url
/// - gathers extensions from the url, if configured to do so
/// - computes hash of response body
/// - creates filter with hash
pub(crate) async fn create_similarity_filter(
similarity_filter: &str,
handles: Arc<Handles>,
) -> Result<SimilarityFilter> {
// url as-is based on input, ignores user-specified url manipulation options (add-slash etc)
let url = Url::parse(similarity_filter)?;
// attempt to request the given url
let resp = logged_request(&url, DEFAULT_METHOD, None, handles.clone()).await?;
// if successful, create a filter based on the response's body
let mut fr = FeroxResponse::from(
resp,
similarity_filter,
DEFAULT_METHOD,
handles.config.output_level,
)
.await;
if handles.config.collect_extensions {
fr.parse_extension(handles.clone())?;
}
let hash = SIM_HASHER.create_signature(preprocess(fr.text()).iter());
Ok(SimilarityFilter {
hash,
original_url: similarity_filter.to_string(),
})
}
/// used in conjunction with the Scan Management Menu
///
/// when a user uses the n[ew-filter] command in the menu, the two params are passed here for
/// processing.
///
/// an example command may be `new-filter lines 40`. `lines` and `40` are passed here as &str's
///
/// once here, the type and value are used to create an appropriate FeroxFilter. If anything
/// goes wrong during creation, None is returned.
pub(crate) fn filter_lookup(filter_type: &str, filter_value: &str) -> Option<Box<dyn FeroxFilter>> {
match filter_type {
"status" => {
if let Ok(parsed) = filter_value.parse() {
return Some(Box::new(super::StatusCodeFilter {
filter_code: parsed,
}));
}
}
"lines" => {
if let Ok(parsed) = filter_value.parse() {
return Some(Box::new(super::LinesFilter { line_count: parsed }));
}
}
"size" => {
if let Ok(parsed) = filter_value.parse() {
return Some(Box::new(super::SizeFilter {
content_length: parsed,
}));
}
}
"words" => {
if let Ok(parsed) = filter_value.parse() {
return Some(Box::new(super::WordsFilter { word_count: parsed }));
}
}
"regex" => {
if let Ok(parsed) = Regex::new(filter_value) {
return Some(Box::new(super::RegexFilter {
compiled: parsed,
raw_string: filter_value.to_string(),
}));
}
}
"similarity" => {
return Some(Box::new(SimilarityFilter {
hash: 0,
original_url: filter_value.to_string(),
}));
}
_ => (),
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Configuration;
use crate::filters::{LinesFilter, RegexFilter, SizeFilter, StatusCodeFilter, WordsFilter};
use crate::scan_manager::FeroxScans;
use httpmock::Method::GET;
use httpmock::MockServer;
#[test]
/// filter_lookup returns correct filters
fn filter_lookup_returns_correct_filters() {
let filter = filter_lookup("status", "200").unwrap();
assert_eq!(
filter.as_any().downcast_ref::<StatusCodeFilter>().unwrap(),
&StatusCodeFilter { filter_code: 200 }
);
let filter = filter_lookup("lines", "10").unwrap();
assert_eq!(
filter.as_any().downcast_ref::<LinesFilter>().unwrap(),
&LinesFilter { line_count: 10 }
);
let filter = filter_lookup("size", "20").unwrap();
assert_eq!(
filter.as_any().downcast_ref::<SizeFilter>().unwrap(),
&SizeFilter { content_length: 20 }
);
let filter = filter_lookup("words", "30").unwrap();
assert_eq!(
filter.as_any().downcast_ref::<WordsFilter>().unwrap(),
&WordsFilter { word_count: 30 }
);
let filter = filter_lookup("regex", "stuff.*").unwrap();
let compiled = Regex::new("stuff.*").unwrap();
let raw_string = String::from("stuff.*");
assert_eq!(
filter.as_any().downcast_ref::<RegexFilter>().unwrap(),
&RegexFilter {
compiled,
raw_string
}
);
let filter = filter_lookup("similarity", "http://localhost").unwrap();
assert_eq!(
filter.as_any().downcast_ref::<SimilarityFilter>().unwrap(),
&SimilarityFilter {
hash: 0,
original_url: "http://localhost".to_string()
}
);
assert!(filter_lookup("non-existent", "").is_none());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
/// ensure create_similarity_filter correctness of return value and side-effects
async fn create_similarity_filter_is_correct() {
let srv = MockServer::start();
let mock = srv.mock(|when, then| {
when.method(GET).path("/");
then.status(200).body("this is a test");
});
let scans = FeroxScans::default();
let config = Configuration {
collect_extensions: true,
..Default::default()
};
let (test_handles, _) = Handles::for_testing(Some(Arc::new(scans)), Some(Arc::new(config)));
let handles = Arc::new(test_handles);
let filter = create_similarity_filter(&srv.url("/"), handles.clone())
.await
.unwrap();
assert_eq!(mock.hits(), 1);
assert_eq!(
filter,
SimilarityFilter {
hash: 14897447612059286329,
original_url: srv.url("/")
}
);
}
}

View File

@@ -1,28 +1,29 @@
use console::style;
use super::*;
use crate::{url::FeroxUrl, DEFAULT_METHOD};
use crate::utils::create_report_string;
use crate::{config::OutputLevel, DEFAULT_METHOD};
/// Data holder for two pieces of data needed when auto-filtering out wildcard responses
///
/// `dynamic` is the size of the response that will later be combined with the length
/// of the path of the url requested and used to determine interesting pages from custom
/// 404s where the requested url is reflected back in the response
///
/// `size` is size of the response that should be included with filters passed via runtime
/// configuration and any static wildcard lengths.
#[derive(Debug, Clone, PartialEq)]
/// Data holder for all relevant data needed when auto-filtering out wildcard responses
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WildcardFilter {
/// size of the response that will later be combined with the length of the path of the url
/// requested
pub dynamic: u64,
/// The content-length of this response, if known
pub content_length: Option<u64>,
/// size of the response that should be included with filters passed via runtime configuration
pub size: u64,
/// The number of lines contained in the body of this response, if known
pub line_count: Option<usize>,
/// The number of words contained in the body of this response, if known
pub word_count: Option<usize>,
/// method used in request that should be included with filters passed via runtime configuration
pub method: String,
/// the status code returned in the response
pub status_code: u16,
/// whether or not the user passed -D on the command line
pub(super) dont_filter: bool,
pub dont_filter: bool,
}
/// implementation of WildcardFilter
@@ -36,22 +37,23 @@ impl WildcardFilter {
}
}
/// implement default that populates both values with u64::MAX
/// implement default that populates `method` with its default value
impl Default for WildcardFilter {
/// populate both values with u64::MAX
fn default() -> Self {
Self {
content_length: None,
line_count: None,
word_count: None,
method: DEFAULT_METHOD.to_string(),
status_code: 0,
dont_filter: false,
size: u64::MAX,
method: DEFAULT_METHOD.to_owned(),
dynamic: u64::MAX,
}
}
}
/// implementation of FeroxFilter for WildcardFilter
impl FeroxFilter for WildcardFilter {
/// Examine size, dynamic, and content_len to determine whether or not the response received
/// Examine size/words/lines and method to determine whether or not the response received
/// is a wildcard response and therefore should be filtered out
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
log::trace!("enter: should_filter_response({:?} {})", self, response);
@@ -64,44 +66,78 @@ impl FeroxFilter for WildcardFilter {
return false;
}
if self.size != u64::MAX
&& self.size == response.content_length()
&& self.method == response.method().as_str()
{
// static wildcard size found during testing
// size isn't default, size equals response length, and auto-filter is on
log::debug!("static wildcard: filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
if self.method != response.method().as_str() {
// method's don't match, so this response should not be filtered out
log::trace!("exit: should_filter_response -> false");
return false;
}
if self.size == u64::MAX
&& response.content_length() == 0
&& self.method == response.method().as_str()
{
// static wildcard size found during testing
// but response length was zero; pointed out by @Tib3rius
log::debug!("static wildcard: filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
if self.status_code != response.status().as_u16() {
// status codes don't match, so this response should not be filtered out
log::trace!("exit: should_filter_response -> false");
return false;
}
if self.dynamic != u64::MAX {
// dynamic wildcard offset found during testing
// methods and status codes match at this point, just need to check the other fields
// I'm about to manually split this url path instead of using reqwest::Url's
// builtin parsing. The reason is that they call .split() on the url path
// except that I don't want an empty string taking up the last index in the
// event that the url ends with a forward slash. It's ugly enough to be split
// into its own function for readability.
let url_len = FeroxUrl::path_length_of_url(response.url());
if url_len + self.dynamic == response.content_length() {
log::debug!("dynamic wildcard: filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
match (self.content_length, self.word_count, self.line_count) {
(Some(cl), Some(wc), Some(lc)) => {
if cl == response.content_length()
&& wc == response.word_count()
&& lc == response.line_count()
{
log::debug!("filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
}
}
(Some(cl), Some(wc), None) => {
if cl == response.content_length() && wc == response.word_count() {
log::debug!("filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
}
}
(Some(cl), None, Some(lc)) => {
if cl == response.content_length() && lc == response.line_count() {
log::debug!("filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
}
}
(None, Some(wc), Some(lc)) => {
if wc == response.word_count() && lc == response.line_count() {
log::debug!("filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
}
}
(Some(cl), None, None) => {
if cl == response.content_length() {
log::debug!("filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
}
}
(None, Some(wc), None) => {
if wc == response.word_count() {
log::debug!("filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
}
}
(None, None, Some(lc)) => {
if lc == response.line_count() {
log::debug!("filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
}
}
(None, None, None) => {
unreachable!("wildcard filter without any filters set");
}
}
log::trace!("exit: should_filter_response -> false");
false
}
@@ -116,3 +152,29 @@ impl FeroxFilter for WildcardFilter {
self
}
}
impl std::fmt::Display for WildcardFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let msg = create_report_string(
self.status_code.to_string().as_str(),
self.method.as_str(),
&self
.line_count
.map_or_else(|| "-".to_string(), |x| x.to_string()),
&self
.word_count
.map_or_else(|| "-".to_string(), |x| x.to_string()),
&self
.content_length
.map_or_else(|| "-".to_string(), |x| x.to_string()),
&format!(
"{} found {}-like response and created new filter; toggle off with {}",
style("Auto-filtering").bright().green(),
style("404").red(),
style("--dont-filter").yellow()
),
OutputLevel::Default,
);
write!(f, "{}", msg)
}
}

View File

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

View File

@@ -1,43 +1,24 @@
use std::sync::Arc;
use anyhow::{bail, Result};
use console::style;
use scraper::{Html, Selector};
use uuid::Uuid;
use crate::filters::{SimilarityFilter, WildcardFilter, SIM_HASHER};
use crate::message::FeroxMessage;
use crate::nlp::preprocess;
use crate::scanner::RESPONSES;
use crate::{
config::OutputLevel,
event_handlers::{Command, Handles},
filters::WildcardFilter,
progress::PROGRESS_PRINTER,
response::FeroxResponse,
skip_fail,
url::FeroxUrl,
utils::{ferox_print, fmt_err, logged_request, status_colorizer},
utils::{ferox_print, fmt_err, logged_request},
DEFAULT_METHOD,
};
/// length of a standard UUID, used when determining wildcard responses
const UUID_LENGTH: u64 = 32;
/// wrapper around ugly string formatting
macro_rules! format_template {
($template:expr, $method:expr, $length:expr) => {
format!(
$template,
status_colorizer("WLD"),
$method,
"-",
"-",
"-",
style("auto-filtering").yellow(),
style($length).cyan(),
style("--dont-filter").yellow()
)
};
}
/// enum representing the different servers that `parse_html` can detect when directory listing is
/// enabled
#[derive(Copy, Debug, Clone)]
@@ -68,6 +49,16 @@ pub struct DirListingResult {
pub response: FeroxResponse,
}
/// wrapper around the results of running a wildcard detection against a target web page
#[derive(Copy, Debug, Clone)]
pub enum WildcardResult {
/// variant that represents a wildcard directory
WildcardDirectory(usize),
/// variant that represents the presence of a 404-like response
FourOhFourLike(usize),
}
/// container for heuristics related info
pub struct HeuristicTests {
/// Handles object for event handler interaction
@@ -91,7 +82,7 @@ impl HeuristicTests {
let mut ids = vec![];
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("");
@@ -100,171 +91,6 @@ impl HeuristicTests {
unique_id
}
/// wrapper for sending a filter to the filters event handler
fn send_filter(&self, filter: WildcardFilter) -> Result<()> {
self.handles
.filters
.send(Command::AddFilter(Box::new(filter)))
}
/// Tests the given url to see if it issues a wildcard response
///
/// In the event that url returns a wildcard response, a
/// [WildcardFilter](struct.WildcardFilter.html) is created and sent to the filters event
/// handler.
///
/// Returns the number of times to increment the caller's progress bar
pub async fn wildcard(&self, target_url: &str) -> Result<u64> {
log::trace!("enter: wildcard_test({:?})", target_url);
if self.handles.config.dont_filter {
// early return, dont_filter scans don't need tested
log::trace!("exit: wildcard_test -> 0");
return Ok(0);
}
let data = match self.handles.config.data.is_empty() {
true => None,
false => Some(self.handles.config.data.as_slice()),
};
let ferox_url = FeroxUrl::from_string(target_url, self.handles.clone());
for method in self.handles.config.methods.iter() {
let ferox_response = self
.make_wildcard_request(&ferox_url, method.as_str(), data, 1)
.await?;
// found a wildcard response
let mut wildcard = WildcardFilter::new(self.handles.config.dont_filter);
let wc_length = ferox_response.content_length();
if wc_length == 0 {
log::trace!("exit: wildcard_test -> 1");
self.send_filter(wildcard)?;
return Ok(1);
}
// content length of wildcard is non-zero, perform additional tests:
// make a second request, with a known-sized (64) longer request
let resp_two = self
.make_wildcard_request(&ferox_url, method.as_str(), data, 3)
.await?;
let wc2_length = resp_two.content_length();
wildcard.method = resp_two.method().as_str().to_owned();
if wc2_length == wc_length + (UUID_LENGTH * 2) {
// second length is what we'd expect to see if the requested url is
// reflected in the response along with some static content; aka custom 404
let url_len = ferox_url.path_length()?;
wildcard.dynamic = wc_length - url_len;
if matches!(
self.handles.config.output_level,
OutputLevel::Default | OutputLevel::Quiet
) {
let msg = format_template!("{} {:>8} {:>9} {:>9} {:>9} Wildcard response is dynamic; {} ({} + url length) responses; toggle this behavior by using {}\n", method, wildcard.dynamic);
ferox_print(&msg, &PROGRESS_PRINTER);
}
} else if wc_length == wc2_length {
wildcard.size = wc_length;
if matches!(
self.handles.config.output_level,
OutputLevel::Default | OutputLevel::Quiet
) {
let msg = format_template!("{} {:>8} {:>9} {:>9} {:>9} Wildcard response is static; {} {} responses; toggle this behavior by using {}\n", method, wildcard.size);
ferox_print(&msg, &PROGRESS_PRINTER);
}
}
self.send_filter(wildcard)?;
}
log::trace!("exit: wildcard_test");
Ok(2)
}
/// Generates a uuid and appends it to the given target url. The reasoning is that the randomly
/// generated unique string should not exist on and be served by the target web server.
///
/// Once the unique url is created, the request is sent to the server. If the server responds
/// back with a valid status code, the response is considered to be a wildcard response. If that
/// wildcard response has a 3xx status code, that redirection location is displayed to the user.
async fn make_wildcard_request(
&self,
target: &FeroxUrl,
method: &str,
data: Option<&[u8]>,
length: usize,
) -> Result<FeroxResponse> {
log::trace!("enter: make_wildcard_request({}, {})", target, length);
let unique_str = self.unique_string(length);
// To take care of slash when needed
let slash = if self.handles.config.add_slash {
Some("/")
} else {
None
};
let nonexistent_url = target.format(&unique_str, slash)?;
let response = logged_request(
&nonexistent_url.to_owned(),
method,
data,
self.handles.clone(),
)
.await?;
if self
.handles
.config
.status_codes
.contains(&response.status().as_u16())
{
// found a wildcard response
let mut ferox_response = FeroxResponse::from(
response,
&target.target,
method,
self.handles.config.output_level,
)
.await;
ferox_response.set_wildcard(true);
if self
.handles
.filters
.data
.should_filter_response(&ferox_response, self.handles.stats.tx.clone())
{
bail!("filtered response")
}
if matches!(
self.handles.config.output_level,
OutputLevel::Default | OutputLevel::Quiet
) {
let boxed = Box::new(ferox_response.clone());
self.handles.output.send(Command::Report(boxed))?;
}
log::trace!("exit: make_wildcard_request -> {}", ferox_response);
return Ok(ferox_response);
}
log::trace!("exit: make_wildcard_request -> Err");
bail!("uninteresting status code")
}
/// Simply tries to connect to all given sites before starting to scan
///
/// In the event that no sites can be reached, the program will exit.
@@ -292,12 +118,12 @@ impl HeuristicTests {
) {
if e.to_string().contains(":SSL") {
ferox_print(
&format!("Could not connect to {} due to SSL errors (run with -k to ignore), skipping...", target_url),
&format!("Could not connect to {target_url} due to SSL errors (run with -k to ignore), skipping..."),
&PROGRESS_PRINTER,
);
} else {
ferox_print(
&format!("Could not connect to {}, skipping...", target_url),
&format!("Could not connect to {target_url}, skipping..."),
&PROGRESS_PRINTER,
);
}
@@ -325,7 +151,7 @@ impl HeuristicTests {
// so, instead of `directory_listing("http://localhost") -> None` we get
// `directory_listing("http://localhost/") -> Some(DirListingResult)` if there is
// directory listing beyond the redirect
format!("{}/", target_url)
format!("{target_url}/")
} else {
target_url.to_string()
};
@@ -419,6 +245,268 @@ impl HeuristicTests {
log::trace!("exit: detect_directory_listing -> None");
None
}
/// given a target's base url, attempt to automatically detect its 404 response
/// pattern(s), and then set filters that will exclude those patterns from future
/// responses
pub async fn detect_404_like_responses(
&self,
target_url: &str,
) -> Result<Option<WildcardResult>> {
log::trace!("enter: detect_404_like_responses({:?})", target_url);
if self.handles.config.dont_filter {
// early return, dont_filter scans don't need tested
log::trace!("exit: detect_404_like_responses -> dont_filter is true");
return Ok(None);
}
let mut req_counter = 0;
let data = if self.handles.config.data.is_empty() {
None
} else {
Some(self.handles.config.data.as_slice())
};
// To take care of slash when needed
let slash = if self.handles.config.add_slash {
Some("/")
} else {
None
};
// 4 is due to the array in the nested for loop below
let mut responses = Vec::with_capacity(4);
// for every method, attempt to id its 404 response
//
// a good example of one where the GET/POST differ is on hackthebox:
// - http://prd.m.rendering-api.interface.htb/api
for method in self.handles.config.methods.iter() {
for (prefix, length) in [("", 1), ("", 3), (".htaccess", 1), ("admin", 1)] {
let path = format!("{prefix}{}", self.unique_string(length));
let ferox_url = FeroxUrl::from_string(target_url, self.handles.clone());
let nonexistent_url = ferox_url.format(&path, slash)?;
// example requests:
// - http://localhost/2fc1077836ad43ab98b7a31c2ca28fea
// - http://localhost/92969beae6bf4beb855d1622406d87e395c87387a9ad432e8a11245002b709b03cf609d471004154b83bcc1c6ec49f6f
// - http://localhost/.htaccessa005a2131e68449aa26e99029c914c09
// - http://localhost/adminf1d2541e73c44dcb9d1fb7d93334b280
let response =
logged_request(&nonexistent_url, method, data, self.handles.clone()).await;
req_counter += 1;
// continue to next on error
let response = skip_fail!(response);
if !self
.handles
.config
.status_codes
.contains(&response.status().as_u16())
{
// if the response code isn't one that's accepted via -s values, then skip to the next
//
// the default value for -s is all status codes, so unless the user says otherwise
// this won't fire
continue;
}
let ferox_response = FeroxResponse::from(
response,
&ferox_url.target,
method,
self.handles.config.output_level,
)
.await;
responses.push(ferox_response);
}
if responses.len() < 2 {
// don't have enough responses to make a determination, continue to next method
responses.clear();
continue;
}
// Command::AddFilter, &str (bytes/words/lines), usize (i.e. length associated with the type)
let Some(filter) = self.examine_404_like_responses(&responses) else {
// no match was found during analysis of responses
responses.clear();
continue;
};
// report to the user, if appropriate
if matches!(
self.handles.config.output_level,
OutputLevel::Default | OutputLevel::Quiet
) {
// sentry value to control whether or not to print the filter
// used because we only want to print the same filter once
let mut print_sentry = true;
if let Ok(filters) = self.handles.filters.data.filters.read() {
for other in filters.iter() {
if let Some(other_wildcard) =
other.as_any().downcast_ref::<WildcardFilter>()
{
if &*filter == other_wildcard {
print_sentry = false;
break;
}
}
}
}
if print_sentry {
ferox_print(&format!("{}", filter), &PROGRESS_PRINTER);
}
}
// create the new filter
self.handles.filters.send(Command::AddFilter(filter))?;
// if we're here, we've detected a 404-like response pattern, and we're already filtering for size/word/line
//
// in addition, we'll create a similarity filter as a fallback
let hash = SIM_HASHER.create_signature(preprocess(responses[0].text()).iter());
let sim_filter = SimilarityFilter {
hash,
original_url: responses[0].url().to_string(),
};
self.handles
.filters
.send(Command::AddFilter(Box::new(sim_filter)))?;
if responses[0].is_directory() {
// response is either a 3XX with a Location header that matches url + '/'
// or it's a 2XX that ends with a '/'
// or it's a 403 that ends with a '/'
// set the wildcard flag to true, so we can check it when preventing
// recursion in event_handlers/scans.rs
responses[0].set_wildcard(true);
// add the response to the global list of responses
RESPONSES.insert(responses[0].clone());
// function-internal magic number, indicates that we've detected a wildcard directory
req_counter += 100;
}
// reset the responses for the next method, if it exists
responses.clear();
}
log::trace!("exit: detect_404_like_responses");
let retval = if req_counter > 100 {
WildcardResult::WildcardDirectory(req_counter)
} else {
WildcardResult::FourOhFourLike(req_counter)
};
Ok(Some(retval))
}
/// for all responses, examine chars/words/lines
/// if all responses respective lengths match each other, we can assume
/// that will remain true for subsequent non-existent urls
///
/// values are examined from most to least specific (content length, word count, line count)
fn examine_404_like_responses(
&self,
responses: &[FeroxResponse],
) -> Option<Box<WildcardFilter>> {
let mut size_sentry = true;
let mut word_sentry = true;
let mut line_sentry = true;
let method = responses[0].method();
let status_code = responses[0].status();
let content_length = responses[0].content_length();
let word_count = responses[0].word_count();
let line_count = responses[0].line_count();
for response in &responses[1..] {
// if any of the responses differ in length, that particular
// response length type is no longer a candidate for filtering
if response.content_length() != content_length {
size_sentry = false;
}
if response.word_count() != word_count {
word_sentry = false;
}
if response.line_count() != line_count {
line_sentry = false;
}
}
if !size_sentry && !word_sentry && !line_sentry {
// none of the response lengths match, so we can't filter on any of them
return None;
}
let mut wildcard = WildcardFilter {
content_length: None,
line_count: None,
word_count: None,
method: method.to_string(),
status_code: status_code.as_u16(),
dont_filter: self.handles.config.dont_filter,
};
match (size_sentry, word_sentry, line_sentry) {
(true, true, true) => {
// all three types of length match, so we can't filter on any of them
wildcard.content_length = Some(content_length);
wildcard.word_count = Some(word_count);
wildcard.line_count = Some(line_count);
}
(true, true, false) => {
// content length and word count match, so we can filter on either
wildcard.content_length = Some(content_length);
wildcard.word_count = Some(word_count);
}
(true, false, true) => {
// content length and line count match, so we can filter on either
wildcard.content_length = Some(content_length);
wildcard.line_count = Some(line_count);
}
(false, true, true) => {
// word count and line count match, so we can filter on either
wildcard.word_count = Some(word_count);
wildcard.line_count = Some(line_count);
}
(true, false, false) => {
// content length matches, so we can filter on that
wildcard.content_length = Some(content_length);
}
(false, true, false) => {
// word count matches, so we can filter on that
wildcard.word_count = Some(word_count);
}
(false, false, true) => {
// line count matches, so we can filter on that
wildcard.line_count = Some(line_count);
}
(false, false, false) => {
// none of the length types match, so we can't filter on any of them
unreachable!("no wildcard size matches; handled by the if statement above");
}
};
Some(Box::new(wildcard))
}
}
#[cfg(test)]

View File

@@ -52,9 +52,6 @@ pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION");
/// Maximum number of file descriptors that can be opened during a scan
pub const DEFAULT_OPEN_FILE_LIMIT: u64 = 8192;
/// Default value used to determine near-duplicate web pages (equivalent to 95%)
pub const SIMILARITY_THRESHOLD: u32 = 95;
/// Default set of extensions to Ignore when auto-collecting extensions during scans
pub(crate) const DEFAULT_IGNORED_EXTENSIONS: [&str; 38] = [
"tif", "tiff", "ico", "cur", "bmp", "webp", "svg", "png", "jpg", "jpeg", "jfif", "gif", "avif",
@@ -65,10 +62,19 @@ pub(crate) const DEFAULT_IGNORED_EXTENSIONS: [&str; 38] = [
/// Default wordlist to use when `-w|--wordlist` isn't specified and not `wordlist` isn't set
/// in a [ferox-config.toml](constant.DEFAULT_CONFIG_NAME.html) config file.
///
/// defaults to kali's default install location:
/// defaults to kali's default install location on linux:
/// - `/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt`
///
/// and to the current directory on windows
/// - `.\seclists\Discovery\Web-Content\raft-medium-directories.txt`
#[cfg(not(target_os = "windows"))]
pub const DEFAULT_WORDLIST: &str =
"/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt";
#[cfg(target_os = "windows")]
pub const DEFAULT_WORDLIST: &str =
".\\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
pub(crate) const SLEEP_DURATION: u64 = 500;
@@ -76,29 +82,73 @@ pub(crate) const SLEEP_DURATION: u64 = 500;
/// The percentage of requests as errors it takes to be deemed too high
pub const HIGH_ERROR_RATIO: f64 = 0.90;
/// Default list of status codes to report
///
/// * 200 Ok
/// * 204 No Content
/// * 301 Moved Permanently
/// * 302 Found
/// * 307 Temporary Redirect
/// * 308 Permanent Redirect
/// * 401 Unauthorized
/// * 403 Forbidden
/// * 405 Method Not Allowed
/// * 500 Internal Server Error
pub const DEFAULT_STATUS_CODES: [StatusCode; 10] = [
/// Default list of status codes to report (all of them)
pub const DEFAULT_STATUS_CODES: [StatusCode; 60] = [
// all 1XX response codes
StatusCode::CONTINUE,
StatusCode::SWITCHING_PROTOCOLS,
StatusCode::PROCESSING,
// all 2XX response codes
StatusCode::OK,
StatusCode::CREATED,
StatusCode::ACCEPTED,
StatusCode::NON_AUTHORITATIVE_INFORMATION,
StatusCode::NO_CONTENT,
StatusCode::RESET_CONTENT,
StatusCode::PARTIAL_CONTENT,
StatusCode::MULTI_STATUS,
StatusCode::ALREADY_REPORTED,
StatusCode::IM_USED,
// all 3XX response codes
StatusCode::MULTIPLE_CHOICES,
StatusCode::MOVED_PERMANENTLY,
StatusCode::FOUND,
StatusCode::SEE_OTHER,
StatusCode::NOT_MODIFIED,
StatusCode::USE_PROXY,
StatusCode::TEMPORARY_REDIRECT,
StatusCode::PERMANENT_REDIRECT,
// all 4XX response codes
StatusCode::BAD_REQUEST,
StatusCode::UNAUTHORIZED,
StatusCode::PAYMENT_REQUIRED,
StatusCode::FORBIDDEN,
StatusCode::NOT_FOUND,
StatusCode::METHOD_NOT_ALLOWED,
StatusCode::NOT_ACCEPTABLE,
StatusCode::PROXY_AUTHENTICATION_REQUIRED,
StatusCode::REQUEST_TIMEOUT,
StatusCode::CONFLICT,
StatusCode::GONE,
StatusCode::LENGTH_REQUIRED,
StatusCode::PRECONDITION_FAILED,
StatusCode::PAYLOAD_TOO_LARGE,
StatusCode::URI_TOO_LONG,
StatusCode::UNSUPPORTED_MEDIA_TYPE,
StatusCode::RANGE_NOT_SATISFIABLE,
StatusCode::EXPECTATION_FAILED,
StatusCode::IM_A_TEAPOT,
StatusCode::MISDIRECTED_REQUEST,
StatusCode::UNPROCESSABLE_ENTITY,
StatusCode::LOCKED,
StatusCode::FAILED_DEPENDENCY,
StatusCode::UPGRADE_REQUIRED,
StatusCode::PRECONDITION_REQUIRED,
StatusCode::TOO_MANY_REQUESTS,
StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE,
StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS,
// all 5XX response codes
StatusCode::INTERNAL_SERVER_ERROR,
StatusCode::NOT_IMPLEMENTED,
StatusCode::BAD_GATEWAY,
StatusCode::SERVICE_UNAVAILABLE,
StatusCode::GATEWAY_TIMEOUT,
StatusCode::HTTP_VERSION_NOT_SUPPORTED,
StatusCode::VARIANT_ALSO_NEGOTIATES,
StatusCode::INSUFFICIENT_STORAGE,
StatusCode::LOOP_DETECTED,
StatusCode::NOT_EXTENDED,
StatusCode::NETWORK_AUTHENTICATION_REQUIRED,
];
/// Default method for requests

View File

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

View File

@@ -1,11 +1,14 @@
use std::io::stdin;
use std::{
env::args,
env::{
args,
consts::{ARCH, OS},
},
fs::{create_dir, remove_file, File},
io::{stderr, BufRead, BufReader},
ops::Index,
path::Path,
process::Command,
process::{exit, Command},
sync::{atomic::Ordering, Arc},
};
@@ -17,36 +20,40 @@ use tokio::{
};
use tokio_util::codec::{FramedRead, LinesCodec};
use feroxbuster::scan_manager::ScanType;
use feroxbuster::{
banner::{Banner, UPDATE_URL},
config::{Configuration, OutputLevel},
event_handlers::{
Command::{CreateBar, Exit, JoinTasks, LoadStats, ScanInitialUrls, UpdateWordlist},
Command::{
AddHandles, CreateBar, Exit, JoinTasks, LoadStats, ScanInitialUrls, UpdateWordlist,
},
FiltersHandler, Handles, ScanHandler, StatsHandler, Tasks, TermInputHandler,
TermOutHandler, SCAN_COMPLETE,
},
filters, heuristics, logger,
progress::{PROGRESS_BAR, PROGRESS_PRINTER},
scan_manager::{self},
scan_manager::{self, ScanType},
scanner,
utils::{fmt_err, slugify_filename},
SECONDARY_WORDLIST,
};
#[cfg(not(target_os = "windows"))]
use feroxbuster::{utils::set_open_file_limit, DEFAULT_OPEN_FILE_LIMIT};
use lazy_static::lazy_static;
use regex::Regex;
use self_update::cargo_crate_version;
lazy_static! {
/// Limits the number of parallel scans active at any given time when using --parallel
static ref PARALLEL_LIMITER: Semaphore = Semaphore::new(0);
}
/// Create a HashSet of Strings from the given wordlist then stores it inside an Arc
/// Create a Vec of Strings from the given wordlist then stores it inside an Arc
fn get_unique_words_from_wordlist(path: &str) -> Result<Arc<Vec<String>>> {
log::trace!("enter: get_unique_words_from_wordlist({})", path);
let mut trimmed_word = false;
let file = File::open(&path).with_context(|| format!("Could not open {}", path))?;
let file = File::open(path).with_context(|| format!("Could not open {path}"))?;
let reader = BufReader::new(file);
@@ -59,12 +66,21 @@ fn get_unique_words_from_wordlist(path: &str) -> Result<Arc<Vec<String>>> {
for line in reader.lines() {
line.map(|result| {
if !result.starts_with('#') && !result.is_empty() {
words.push(result);
if result.starts_with('/') {
words.push(result.trim_start_matches('/').to_string());
trimmed_word = true;
} else {
words.push(result);
}
}
})
.ok();
}
if trimmed_word {
log::warn!("Some words in the wordlist started with a leading forward-slash; those words were trimmed (i.e. /word -> word)");
}
log::trace!(
"exit: get_unique_words_from_wordlist -> Arc<wordlist[{} words...]>",
words.len()
@@ -90,8 +106,20 @@ async fn scan(targets: Vec<String>, handles: Arc<Handles>) -> Result<()> {
// having been set, makes it so the progress bar doesn't flash as full before anything has
// even happened
if matches!(handles.config.output_level, OutputLevel::Default) {
let mut total_offset = 0;
if let Ok(guard) = handles.scans.read() {
if let Some(handle) = &*guard {
if let Ok(scans) = handle.data.scans.read() {
for scan in scans.iter() {
total_offset += scan.requests_made_so_far();
}
}
}
}
// only create the bar if no --silent|--quiet
handles.stats.send(CreateBar)?;
handles.stats.send(CreateBar(total_offset))?;
// blocks until the bar is created / avoids race condition in first two bars
handles.stats.sync().await?;
@@ -148,7 +176,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
for target in &targets {
for target in targets.iter_mut() {
for denier in &handles.config.regex_denylist {
if denier.is_match(target) {
bail!(
@@ -167,6 +195,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);
@@ -190,10 +223,67 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
PROGRESS_BAR.join().unwrap();
});
// 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
// as well as additional directories found as part of recursion
let words = get_unique_words_from_wordlist(&config.wordlist)?;
// check if update_app is true
if config.update_app {
match update_app().await {
Err(e) => eprintln!("\n[ERROR] {}", e),
Ok(self_update::Status::UpToDate(version)) => {
eprintln!("\nFeroxbuster {} is up to date", version)
}
Ok(self_update::Status::Updated(version)) => {
eprintln!("\nFeroxbuster updated to {} version", version)
}
}
exit(0);
}
let words = if config.wordlist.starts_with("http") {
// found a url scheme, attempt to download the wordlist
let response = config.client.get(&config.wordlist).send().await?;
if !response.status().is_success() {
// status code isn't a 200, bail
bail!(
"[{}] Unable to download wordlist from url: {}",
response.status().as_str(),
config.wordlist
);
}
// attempt to get the filename from the url's path
let Some(path_segments) = response
.url()
.path_segments() else {
bail!("Unable to parse path from url: {}", response.url());
};
let Some(filename) = path_segments.last() else {
bail!("Unable to parse filename from url's path: {}", response.url().path());
};
let filename = filename.to_string();
// read the body and write it to disk, then use existing code to read the wordlist
let body = response.text().await?;
std::fs::write(&filename, body)?;
get_unique_words_from_wordlist(&filename)?
} else {
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 {
// the check is now <= 1 due to the initial empty string added in 2.6.0
@@ -220,6 +310,7 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
let (scan_task, scan_handle) = ScanHandler::initialize(handles.clone());
handles.set_scan_handle(scan_handle); // must be done after Handles initialization
handles.output.send(AddHandles(handles.clone()))?;
filters::initialize(handles.clone()).await?; // send user-supplied filters to the handler
@@ -247,7 +338,7 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
let from_here = config.resume_from.clone();
// populate FeroxScans object with previously seen scans
scanned_urls.add_serialized_scans(&from_here)?;
scanned_urls.add_serialized_scans(&from_here, handles.clone())?;
// populate Stats object with previously known statistics
handles.stats.send(LoadStats(from_here))?;
@@ -308,7 +399,7 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
let new_folder = slugify_filename(&base_name.to_string_lossy(), "", "logs");
let final_path = output_path.with_file_name(&new_folder);
let final_path = output_path.with_file_name(new_folder);
// create the directory or fail silently, assuming the reason for failure is that
// the path exists already
@@ -439,7 +530,7 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
Ok(_) => {}
Err(e) => {
clean_up(handles, tasks).await?;
bail!(fmt_err(&format!("Failed while scanning: {}", e)));
bail!(fmt_err(&format!("Failed while scanning: {e}")));
}
}
@@ -484,6 +575,24 @@ async fn clean_up(handles: Arc<Handles>, tasks: Tasks) -> Result<()> {
Ok(())
}
async fn update_app() -> Result<self_update::Status, Box<dyn ::std::error::Error>> {
let target_os = format!("{}-{}", ARCH, OS);
let status = tokio::task::spawn_blocking(move || {
self_update::backends::github::Update::configure()
.repo_owner("epi052")
.repo_name("feroxbuster")
.bin_name("feroxbuster")
.target(target_os.as_str())
.show_download_progress(true)
.current_version(cargo_crate_version!())
.build()?
.update()
})
.await??;
Ok(status)
}
fn main() -> Result<()> {
let config = Arc::new(Configuration::new().with_context(|| "Could not create Configuration")?);
@@ -506,7 +615,7 @@ fn main() -> Result<()> {
{
let future = wrapped_main(config.clone());
if let Err(e) = runtime.block_on(future) {
eprintln!("{}", e);
eprintln!("{e}");
// the code below is to facilitate testing tests/test_banner entries. Since it's an
// integration test, normal test detection (cfg!(test), etc...) won't work. So, in

View File

@@ -23,7 +23,7 @@ impl Document {
document.number_of_terms += processed.len();
for normalized in processed {
if normalized.len() > 2 {
if normalized.len() >= 2 {
document.add_term(&normalized)
}
}
@@ -85,7 +85,7 @@ impl Document {
// at this point, we have a non-empty Text element with a non-script|style parent;
// now we can return the trimmed up string
return Some(format!("{} ", trimmed));
return Some(format!("{trimmed} "));
}
// not an Element node

View File

@@ -8,3 +8,4 @@ mod utils;
pub(crate) use self::document::Document;
pub(crate) use self::model::TfIdf;
pub(crate) use self::utils::preprocess;

View File

@@ -4,7 +4,7 @@ use std::borrow::Cow;
/// pre-processing pipeline wrapper that removes punctuation, normalizes word case (utf-8 included)
/// to lowercase, and remove stop words
pub(super) fn preprocess(text: &str) -> Vec<String> {
pub(crate) fn preprocess(text: &str) -> Vec<String> {
let text = remove_punctuation(text);
let text = normalize_case(text);
let text = remove_stop_words(&text);
@@ -38,19 +38,16 @@ fn normalize_case<'a, S: Into<Cow<'a, str>>>(input: S) -> Cow<'a, str> {
}
}
/// remove ascii and some utf-8 punctuation characters from the given string
/// replace ascii and some utf-8 punctuation characters with ' ' (space) in the given string
fn remove_punctuation(text: &str) -> String {
// non-separator type chars can be replaced with an empty string, while separators are replaced
// with a space. This attempts to keep things like
// 'aboutblogfaqcontactpresstermslexicondisclosure' from happening
text.replace(
[
'!', '\\', '"', '#', '$', '%', '&', '(', ')', '*', '+', ':', ';', '<', '=', '>', '?',
'@', '[', ']', '^', '{', '}', '|', '~', ',', '\'', '“', '”', '', '', '', '',
'@', '[', ']', '^', '{', '}', '|', '~', ',', '\'', '“', '”', '', '', '', '', '/',
'', '—', '.',
],
"",
" ",
)
.replace(['/', '', '—', '.'], " ")
}
/// remove stop words from the given string
@@ -86,7 +83,10 @@ mod tests {
fn test_remove_punctuation() {
let tester = "!\\\"#$%&()*+/:;<=>?@[]^{}|~,.'“”’‘–—\n";
// the `" \n"` is because of the things like / getting replaced with a space
assert_eq!(remove_punctuation(tester), " \n");
assert_eq!(
remove_punctuation(tester),
" \n "
);
}
#[test]
@@ -115,7 +115,7 @@ mod tests {
/// ensure preprocess
fn test_preprocess_results() {
let tester = "WHY are Y'all YELLing?";
assert_eq!(&preprocess(tester), &["yall", "yelling"]);
assert_eq!(&preprocess(tester), &["y", "all", "yelling"]);
}
#[test]

View File

@@ -1,3 +1,4 @@
use clap::ArgAction;
use clap::{
crate_authors, crate_description, crate_name, crate_version, Arg, ArgGroup, Command, ValueHint,
};
@@ -25,7 +26,7 @@ lazy_static! {
}
/// Create and return an instance of [clap::App](https://docs.rs/clap/latest/clap/struct.App.html), i.e. the Command Line Interface's configuration
pub fn initialize() -> Command<'static> {
pub fn initialize() -> Command {
let app = Command::new(crate_name!())
.version(crate_version!())
.author(crate_authors!())
@@ -39,7 +40,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("url")
.short('u')
.long("url")
.required_unless_present_any(&["stdin", "resume_from"])
.required_unless_present_any(["stdin", "resume_from", "update_app"])
.help_heading("Target selection")
.value_name("URL")
.use_value_delimiter(true)
@@ -50,7 +51,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("stdin")
.long("stdin")
.help_heading("Target selection")
.takes_value(false)
.num_args(0)
.help("Read url(s) from STDIN")
.conflicts_with("url")
)
@@ -62,7 +63,7 @@ pub fn initialize() -> Command<'static> {
.help_heading("Target selection")
.help("State file from which to resume a partially complete scan (ex. --resume-from ferox-1606586780.state)")
.conflicts_with("url")
.takes_value(true),
.num_args(1),
);
/////////////////////////////////////////////////////////////////////
@@ -72,26 +73,33 @@ pub fn initialize() -> Command<'static> {
.arg(
Arg::new("burp")
.long("burp")
.num_args(0)
.help_heading("Composite settings")
.conflicts_with_all(&["proxy", "insecure", "burp_replay"])
.conflicts_with_all(["proxy", "insecure", "burp_replay"])
.help("Set --proxy to http://127.0.0.1:8080 and set --insecure to true"),
)
.arg(
Arg::new("burp_replay")
.long("burp-replay")
.num_args(0)
.help_heading("Composite settings")
.conflicts_with_all(&["replay_proxy", "insecure"])
.conflicts_with_all(["replay_proxy", "insecure"])
.help("Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true"),
)
.arg(
Arg::new("smart")
.long("smart")
.num_args(0)
.help_heading("Composite settings")
.help("Set --extract-links, --auto-tune, --collect-words, and --collect-backups to true"),
).arg(
.conflicts_with_all(["rate_limit", "auto_bail"])
.help("Set --auto-tune, --collect-words, and --collect-backups to true"),
)
.arg(
Arg::new("thorough")
.long("thorough")
.num_args(0)
.help_heading("Composite settings")
.conflicts_with_all(["rate_limit", "auto_bail"])
.help("Use the same settings as --smart and set --collect-extensions to true"),
);
@@ -103,7 +111,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("proxy")
.short('p')
.long("proxy")
.takes_value(true)
.num_args(1)
.value_name("PROXY")
.value_hint(ValueHint::Url)
.help_heading("Proxy settings")
@@ -115,7 +123,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("replay_proxy")
.short('P')
.long("replay-proxy")
.takes_value(true)
.num_args(1)
.value_hint(ValueHint::Url)
.value_name("REPLAY_PROXY")
.help_heading("Proxy settings")
@@ -128,9 +136,8 @@ pub fn initialize() -> Command<'static> {
.short('R')
.long("replay-codes")
.value_name("REPLAY_CODE")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.requires("replay_proxy")
.help_heading("Proxy settings")
@@ -148,7 +155,7 @@ pub fn initialize() -> Command<'static> {
.short('a')
.long("user-agent")
.value_name("USER_AGENT")
.takes_value(true)
.num_args(1)
.help_heading("Request settings")
.help(&**DEFAULT_USER_AGENT),
)
@@ -156,7 +163,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("random_agent")
.short('A')
.long("random-agent")
.takes_value(false)
.num_args(0)
.help_heading("Request settings")
.help("Use a random User-Agent"),
)
@@ -165,9 +172,8 @@ pub fn initialize() -> Command<'static> {
.short('x')
.long("extensions")
.value_name("FILE_EXTENSION")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Request settings")
.help(
@@ -179,9 +185,8 @@ pub fn initialize() -> Command<'static> {
.short('m')
.long("methods")
.value_name("HTTP_METHODS")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Request settings")
.help(
@@ -192,7 +197,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("data")
.long("data")
.value_name("DATA")
.takes_value(true)
.num_args(1)
.help_heading("Request settings")
.help(
"Request's Body; can read data from a file if input starts with an @ (ex: @post.bin)",
@@ -203,10 +208,9 @@ pub fn initialize() -> Command<'static> {
.short('H')
.long("headers")
.value_name("HEADER")
.takes_value(true)
.num_args(1..)
.action(ArgAction::Append)
.help_heading("Request settings")
.multiple_values(true)
.multiple_occurrences(true)
.use_value_delimiter(true)
.help(
"Specify HTTP headers to be used in each request (ex: -H Header:val -H 'stuff: things')",
@@ -217,9 +221,8 @@ pub fn initialize() -> Command<'static> {
.short('b')
.long("cookies")
.value_name("COOKIE")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Request settings")
.help(
@@ -231,9 +234,8 @@ pub fn initialize() -> Command<'static> {
.short('Q')
.long("query")
.value_name("QUERY")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Request settings")
.help(
@@ -245,7 +247,7 @@ pub fn initialize() -> Command<'static> {
.short('f')
.long("add-slash")
.help_heading("Request settings")
.takes_value(false)
.num_args(0)
.help("Append / to each request's URL")
);
@@ -256,9 +258,8 @@ pub fn initialize() -> Command<'static> {
Arg::new("url_denylist")
.long("dont-scan")
.value_name("URL")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Request filters")
.help("URL(s) or Regex Pattern(s) to exclude from recursion/scans"),
@@ -273,9 +274,8 @@ pub fn initialize() -> Command<'static> {
.short('S')
.long("filter-size")
.value_name("SIZE")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Response filters")
.help(
@@ -287,9 +287,8 @@ pub fn initialize() -> Command<'static> {
.short('X')
.long("filter-regex")
.value_name("REGEX")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Response filters")
.help(
@@ -301,9 +300,8 @@ pub fn initialize() -> Command<'static> {
.short('W')
.long("filter-words")
.value_name("WORDS")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Response filters")
.help(
@@ -315,9 +313,8 @@ pub fn initialize() -> Command<'static> {
.short('N')
.long("filter-lines")
.value_name("LINES")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Response filters")
.help(
@@ -329,10 +326,10 @@ pub fn initialize() -> Command<'static> {
.short('C')
.long("filter-status")
.value_name("STATUS_CODE")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.conflicts_with("status_codes")
.help_heading("Response filters")
.help(
"Filter out status codes (deny list) (ex: -C 200 -C 401)",
@@ -342,9 +339,8 @@ pub fn initialize() -> Command<'static> {
Arg::new("filter_similar")
.long("filter-similar-to")
.value_name("UNWANTED_PAGE")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.value_hint(ValueHint::Url)
.use_value_delimiter(true)
.help_heading("Response filters")
@@ -357,13 +353,12 @@ pub fn initialize() -> Command<'static> {
.short('s')
.long("status-codes")
.value_name("STATUS_CODE")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Response filters")
.help(
"Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)",
"Status Codes to include (allow list) (default: All Status Codes)",
),
);
@@ -376,7 +371,7 @@ pub fn initialize() -> Command<'static> {
.short('T')
.long("timeout")
.value_name("SECONDS")
.takes_value(true)
.num_args(1)
.help_heading("Client settings")
.help("Number of seconds before a client's request times out (default: 7)"),
)
@@ -384,7 +379,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("redirects")
.short('r')
.long("redirects")
.takes_value(false)
.num_args(0)
.help_heading("Client settings")
.help("Allow client to follow redirects"),
)
@@ -392,7 +387,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("insecure")
.short('k')
.long("insecure")
.takes_value(false)
.num_args(0)
.help_heading("Client settings")
.help("Disables TLS certificate validation in the client"),
);
@@ -406,7 +401,7 @@ pub fn initialize() -> Command<'static> {
.short('t')
.long("threads")
.value_name("THREADS")
.takes_value(true)
.num_args(1)
.help_heading("Scan settings")
.help("Number of concurrent threads (default: 50)"),
)
@@ -414,7 +409,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("no_recursion")
.short('n')
.long("no-recursion")
.takes_value(false)
.num_args(0)
.help_heading("Scan settings")
.help("Do not scan recursively"),
)
@@ -423,23 +418,38 @@ pub fn initialize() -> Command<'static> {
.short('d')
.long("depth")
.value_name("RECURSION_DEPTH")
.takes_value(true)
.num_args(1)
.help_heading("Scan settings")
.help("Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)"),
).arg(
Arg::new("force_recursion")
.long("force-recursion")
.num_args(0)
.conflicts_with("no_recursion")
.help_heading("Scan settings")
.help("Force recursion attempts on all 'found' endpoints (still respects recursion depth)"),
).arg(
Arg::new("extract_links")
.short('e')
.long("extract-links")
.takes_value(false)
.num_args(0)
.help_heading("Scan settings")
.help("Extract links from response body (html, javascript, etc...); make new requests based on findings")
.hide(true)
.help("Extract links from response body (html, javascript, etc...); make new requests based on findings (default: true)")
)
.arg(
Arg::new("dont_extract_links")
.long("dont-extract-links")
.num_args(0)
.help_heading("Scan settings")
.help("Don't extract links from response body (html, javascript, etc...)")
)
.arg(
Arg::new("scan_limit")
.short('L')
.long("scan-limit")
.value_name("SCAN_LIMIT")
.takes_value(true)
.num_args(1)
.help_heading("Scan settings")
.help("Limit total number of concurrent scans (default: 0, i.e. no limit)")
)
@@ -447,7 +457,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("parallel")
.long("parallel")
.value_name("PARALLEL_SCANS")
.takes_value(true)
.num_args(1)
.requires("stdin")
.help_heading("Scan settings")
.help("Run parallel feroxbuster instances (one child process per url passed via stdin)")
@@ -456,7 +466,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("rate_limit")
.long("rate-limit")
.value_name("RATE_LIMIT")
.takes_value(true)
.num_args(1)
.conflicts_with("auto_tune")
.help_heading("Scan settings")
.help("Limit number of requests per second (per directory) (default: 0, i.e. no limit)")
@@ -465,8 +475,8 @@ pub fn initialize() -> Command<'static> {
Arg::new("time_limit")
.long("time-limit")
.value_name("TIME_SPEC")
.takes_value(true)
.validator(valid_time_spec)
.num_args(1)
.value_parser(valid_time_spec)
.help_heading("Scan settings")
.help("Limit total run time of all scans (ex: --time-limit 10m)")
)
@@ -476,13 +486,13 @@ pub fn initialize() -> Command<'static> {
.long("wordlist")
.value_hint(ValueHint::FilePath)
.value_name("FILE")
.help("Path to the wordlist")
.help("Path or URL of the wordlist")
.help_heading("Scan settings")
.takes_value(true),
.num_args(1),
).arg(
Arg::new("auto_tune")
.long("auto-tune")
.takes_value(false)
.num_args(0)
.conflicts_with("auto_bail")
.help_heading("Scan settings")
.help("Automatically lower scan rate when an excessive amount of errors are encountered")
@@ -490,35 +500,36 @@ pub fn initialize() -> Command<'static> {
.arg(
Arg::new("auto_bail")
.long("auto-bail")
.takes_value(false)
.num_args(0)
.help_heading("Scan settings")
.help("Automatically stop scanning when an excessive amount of errors are encountered")
).arg(
Arg::new("dont_filter")
.short('D')
.long("dont-filter")
.takes_value(false)
.num_args(0)
.help_heading("Scan settings")
.help("Don't auto-filter wildcard responses")
).arg(
Arg::new("collect_extensions")
.short('E')
.long("collect-extensions")
.takes_value(false)
.num_args(0)
.help_heading("Dynamic collection settings")
.help("Automatically discover extensions and add them to --extensions (unless they're in --dont-collect)")
).arg(
Arg::new("collect_backups")
.short('B')
.long("collect-backups")
.takes_value(false)
.num_args(0)
.help_heading("Dynamic collection settings")
.help("Automatically request likely backup extensions for \"found\" urls")
).arg(
)
.arg(
Arg::new("collect_words")
.short('g')
.long("collect-words")
.takes_value(false)
.num_args(0)
.help_heading("Dynamic collection settings")
.help("Automatically discover important words from within responses and add them to the wordlist")
).arg(
@@ -526,9 +537,8 @@ pub fn initialize() -> Command<'static> {
.short('I')
.long("dont-collect")
.value_name("FILE_EXTENSION")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Dynamic collection settings")
.help(
@@ -544,15 +554,15 @@ pub fn initialize() -> Command<'static> {
Arg::new("verbosity")
.short('v')
.long("verbosity")
.takes_value(false)
.multiple_occurrences(true)
.num_args(0)
.action(ArgAction::Count)
.conflicts_with("silent")
.help_heading("Output settings")
.help("Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v's is probably too much)"),
).arg(
Arg::new("silent")
.long("silent")
.takes_value(false)
.num_args(0)
.conflicts_with("quiet")
.help_heading("Output settings")
.help("Only print URLs + turn off logging (good for piping a list of urls to other commands)")
@@ -561,7 +571,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("quiet")
.short('q')
.long("quiet")
.takes_value(false)
.num_args(0)
.help_heading("Output settings")
.help("Hide progress bars and banner (good for tmux windows w/ notifications)")
)
@@ -569,7 +579,7 @@ pub fn initialize() -> Command<'static> {
.arg(
Arg::new("json")
.long("json")
.takes_value(false)
.num_args(0)
.requires("output_files")
.help_heading("Output settings")
.help("Emit JSON logs to --output and --debug-log instead of normal text")
@@ -581,7 +591,7 @@ pub fn initialize() -> Command<'static> {
.value_name("FILE")
.help_heading("Output settings")
.help("Output file to write results to (use w/ --json for JSON entries)")
.takes_value(true),
.num_args(1),
)
.arg(
Arg::new("debug_log")
@@ -590,12 +600,12 @@ pub fn initialize() -> Command<'static> {
.value_hint(ValueHint::FilePath)
.help_heading("Output settings")
.help("Output file to write log entries (use w/ --json for JSON entries)")
.takes_value(true),
.num_args(1),
)
.arg(
Arg::new("no_state")
.long("no-state")
.takes_value(false)
.num_args(0)
.help_heading("Output settings")
.help("Disable state output file (*.state)")
);
@@ -606,9 +616,18 @@ pub fn initialize() -> Command<'static> {
let mut app = app
.group(
ArgGroup::new("output_files")
.args(&["debug_log", "output"])
.args(["debug_log", "output"])
.multiple(true),
)
.arg(
Arg::new("update_app")
.short('U')
.long("update")
.exclusive(true)
.num_args(0)
.help_heading("Update settings")
.help("Update feroxbuster to the latest version"),
)
.after_long_help(EPILOGUE);
/////////////////////////////////////////////////////////////////////
@@ -634,13 +653,12 @@ pub fn initialize() -> Command<'static> {
}
/// Validate that a string is formatted as a number followed by s, m, h, or d (10d, 30s, etc...)
fn valid_time_spec(time_spec: &str) -> Result<(), String> {
fn valid_time_spec(time_spec: &str) -> Result<String, String> {
match TIMESPEC_REGEX.is_match(time_spec) {
true => Ok(()),
true => Ok(time_spec.to_string()),
false => {
let msg = format!(
"Expected a non-negative, whole number followed by s, m, h, or d (case insensitive); received {}",
time_spec
"Expected a non-negative, whole number followed by s, m, h, or d (case insensitive); received {time_spec}"
);
Err(msg)
}
@@ -668,7 +686,7 @@ EXAMPLES:
cat targets | ./feroxbuster --stdin --silent -s 200 301 302 --redirects -x js | fff -s 200 -o js-files
Proxy traffic through Burp
./feroxbuster -u http://127.1 --insecure --proxy http://127.0.0.1:8080
./feroxbuster -u http://127.1 --burp
Proxy traffic through a SOCKS proxy
./feroxbuster -u http://127.1 --proxy socks5://127.0.0.1:9050
@@ -676,11 +694,8 @@ EXAMPLES:
Pass auth token via query parameter
./feroxbuster -u http://127.1 --query token=0123456789ABCDEF
Find links in javascript/html and make additional requests based on results
./feroxbuster -u http://127.1 --extract-links
Ludicrous speed... go!
./feroxbuster -u http://127.1 -threads 200
./feroxbuster -u http://127.1 --threads 200
Limit to a total of 60 active requests at any given time (threads * scan limit)
./feroxbuster -u http://127.1 --threads 30 --scan-limit 2

View File

@@ -223,6 +223,14 @@ impl FeroxResponse {
.with_context(|| "Could not parse body from response")
.unwrap_or_default();
// in the event that the content_length was 0, we can try to get the length
// of the body we just parsed. At worst, it's still 0; at best we've accounted
// for sites that reply without a content-length header and yet still have
// contents in the body.
//
// thanks to twitter use @f3rn0s for pointing out the possibility
let content_length = content_length.max(text.len() as u64);
let line_count = text.lines().count();
let word_count = text.lines().map(|s| s.split_whitespace().count()).sum();
@@ -279,7 +287,9 @@ impl FeroxResponse {
if handles
.config
.status_codes
.contains(&self.status().as_u16())
.contains(&self.status().as_u16()) // in -s list
// or -C was used, and -s should be all responses that aren't filtered
|| !handles.config.filter_status.is_empty()
{
// only add extensions to those responses that pass our checks; filtered out
// status codes are handled by should_filter, but we need to still check against
@@ -413,7 +423,18 @@ impl FeroxSerialize for FeroxResponse {
.get("Location")
.unwrap() // known Some() already
.to_str()
.unwrap_or("Unknown");
.unwrap_or("Unknown")
.to_string();
let loc = if loc.starts_with('/') {
if let Ok(joined) = self.url().join(&loc) {
joined.to_string()
} else {
loc
}
} else {
loc
};
// prettify the redirect target
let loc = style(loc).yellow();
@@ -432,7 +453,7 @@ impl FeroxSerialize for FeroxResponse {
// create the base message
let mut message = format!(
"{} {:>8} {:>8}l {:>8}w {:>8}c Got {} for {} (url length: {})\n",
"{} {:>8} {:>8}l {:>8}w {:>8}c Got {} for {}\n",
wild_status,
method,
lines,
@@ -440,7 +461,6 @@ impl FeroxSerialize for FeroxResponse {
chars,
status_colorizer(status),
self.url(),
FeroxUrl::path_length_of_url(&self.url)
);
if self.status().is_redirection() {

View File

@@ -1,4 +1,6 @@
use crate::filters::filter_lookup;
use crate::progress::PROGRESS_BAR;
use crate::traits::FeroxFilter;
use console::{measure_text_width, pad_str, style, Alignment, Term};
use indicatif::ProgressDrawTarget;
use regex::Regex;
@@ -7,10 +9,16 @@ use regex::Regex;
#[derive(Debug)]
pub enum MenuCmd {
/// user wants to add a url to be scanned
Add(String),
AddUrl(String),
/// user wants to cancel one or more active scans
Cancel(Vec<usize>, bool),
/// user wants to create a new filter
AddFilter(Box<dyn FeroxFilter>),
/// user wants to remove one or more active filters
RemoveFilter(Vec<usize>),
}
/// Data container for a command result to be used internally by the ferox_scanner
@@ -21,6 +29,9 @@ pub enum MenuCmdResult {
/// Number of scans that were actually cancelled, can be 0
NumCancelled(usize),
/// Filter to be added to current list of `FeroxFilters`
Filter(Box<dyn FeroxFilter>),
}
/// Interactive scan cancellation menu
@@ -32,6 +43,9 @@ pub(super) struct Menu {
/// footer: instructions surrounded by separators
footer: String,
/// unicode line border, matched to longest displayed line
border: String,
/// target for output
pub(super) term: Term,
}
@@ -57,16 +71,44 @@ impl Menu {
);
let canx_cmd = format!(
" {}[{}] [-f] SCAN_ID[-SCAN_ID[,...]] (ex: {} 1-4,8,9-13 or {} -f 3)",
" {}[{}] [-f] SCAN_ID[-SCAN_ID[,...]] (ex: {} 1-4,8,9-13 or {} -f 3)\n",
style("c").red(),
style("ancel").red(),
style("cancel").red(),
style("c").red(),
);
let mut commands = String::from("Commands:\n");
let new_filter_cmd = format!(
" {}[{}] FILTER_TYPE FILTER_VALUE (ex: {} lines 40)\n",
style("n").green(),
style("ew-filter").green(),
style("n").green(),
);
let valid_filters = format!(
" FILTER_TYPEs: {}, {}, {}, {}, {}, {}\n",
style("status").yellow(),
style("lines").yellow(),
style("size").yellow(),
style("words").yellow(),
style("regex").yellow(),
style("similarity").yellow()
);
let rm_filter_cmd = format!(
" {}[{}] FILTER_ID[-FILTER_ID[,...]] (ex: {} 1-4,8,9-13 or {} 3)",
style("r").red(),
style("m-filter").red(),
style("rm-filter").red(),
style("r").red(),
);
let mut commands = format!("{}:\n", style("Commands").bright().blue());
commands.push_str(&add_cmd);
commands.push_str(&canx_cmd);
commands.push_str(&new_filter_cmd);
commands.push_str(&valid_filters);
commands.push_str(&rm_filter_cmd);
let longest = measure_text_width(&canx_cmd).max(measure_text_width(&name));
@@ -74,12 +116,13 @@ impl Menu {
let padded_name = pad_str(&name, longest, Alignment::Center, None);
let header = format!("{}\n{}\n{}", border, padded_name, border);
let footer = format!("{}\n{}\n{}", border, commands, border);
let header = format!("{border}\n{padded_name}\n{border}");
let footer = format!("{commands}\n{border}");
Self {
header,
footer,
border,
term: Term::stderr(),
}
}
@@ -89,6 +132,11 @@ impl Menu {
self.println(&self.header);
}
/// print menu unicode border line
pub(super) fn print_border(&self) {
self.println(&self.border);
}
/// print menu footer
pub(super) fn print_footer(&self) {
self.println(&self.footer);
@@ -126,7 +174,7 @@ impl Menu {
.to_string()
.parse::<usize>()
.unwrap_or_else(|e| {
self.println(&format!("Found non-numeric input: {}: {:?}", e, value));
self.println(&format!("Found non-numeric input: {e}: {value:?}"));
0
})
}
@@ -150,7 +198,7 @@ impl Menu {
if range.len() != 2 {
// expecting [1, 4] or similar, if a 0 was used, we'd be left with a vec of size 1
self.println(&format!("Found invalid range of scans: {}", value));
self.println(&format!("Found invalid range of scans: {value}"));
continue;
}
@@ -162,10 +210,14 @@ impl Menu {
}
});
} else {
if value.is_empty() {
continue;
}
let value = self.str_to_usize(value);
if value != 0 && !nums.contains(&value) {
// the zeroth scan is always skipped, skip already known values
if !nums.contains(&value) {
// skip already known values
nums.push(value);
}
}
@@ -198,7 +250,39 @@ impl Menu {
let re = Regex::new(r"^[aA][dD]*").unwrap();
let line = re.replace(line, "").to_string().trim().to_string();
Some(MenuCmd::Add(line))
Some(MenuCmd::AddUrl(line))
}
'n' => {
// new filter command
let mut line = line.split_whitespace();
line.next(); // 'n' or 'new-filter'
if let Some(filter_type) = line.next() {
// have a string in the filter_type position
if let Some(filter_value) = line.next() {
// have a string in the filter_value position
if let Some(result) = filter_lookup(filter_type, filter_value) {
// lookup was successful, return the new filter
return Some(MenuCmd::AddFilter(result));
}
}
}
None
}
'r' => {
// remove filter command
// remove r[m-filter] from the command so it can be passed to the number
// splitter
let re = Regex::new(r"^[rR][mfilterMFILTER-]*").unwrap();
// we don't respect a -f or lack thereof in this command, but in case the user
// doesn't realize / thinks its the same as cancel -f, just remove it
let line = line.replace("-f", "");
let line = re.replace(&line, "").to_string();
let indices = self.split_to_nums(&line);
Some(MenuCmd::RemoveFilter(indices))
}
_ => {
// invalid input
@@ -210,8 +294,7 @@ impl Menu {
/// Given a url, confirm with user that we should cancel
pub(super) fn confirm_cancellation(&self, url: &str) -> char {
self.println(&format!(
"You sure you wanna cancel this scan: {}? [Y/n]",
url
"You sure you wanna cancel this scan: {url}? [Y/n]"
));
self.term.read_char().unwrap_or('n')

View File

@@ -45,7 +45,7 @@ impl FeroxResponses {
pub fn contains(&self, other: &FeroxResponse) -> bool {
if let Ok(responses) = self.responses.read() {
for response in responses.iter() {
if response.url() == other.url() {
if response.url() == other.url() && response.method() == other.method() {
return true;
}
}

View File

@@ -32,15 +32,25 @@ pub struct FeroxScan {
/// The URL that to be scanned
pub(super) url: String,
/// A url used solely for comparison to other URLs
pub(super) normalized_url: String,
/// The type of scan
pub scan_type: ScanType,
/// The order in which the scan was received
#[allow(dead_code)] // not entirely sure this isn't used somewhere
pub(crate) scan_order: ScanOrder,
/// Number of requests to populate the progress bar with
pub(super) num_requests: u64,
/// Number of requests made so far, only used during deserialization
///
/// serialization: saves self.requests() to this field
/// deserialization: sets self.requests_made_so_far to this field
pub(super) requests_made_so_far: u64,
/// Status of this scan
pub status: Mutex<ScanStatus>,
@@ -70,15 +80,17 @@ pub struct FeroxScan {
impl Default for FeroxScan {
/// Create a default FeroxScan, populates ID with a new UUID
fn default() -> Self {
let new_id = Uuid::new_v4().to_simple().to_string();
let new_id = Uuid::new_v4().as_simple().to_string();
FeroxScan {
id: new_id,
task: sync::Mutex::new(None), // tokio mutex
status: Mutex::new(ScanStatus::default()),
num_requests: 0,
requests_made_so_far: 0,
scan_order: ScanOrder::Latest,
url: String::new(),
normalized_url: String::new(),
progress_bar: Mutex::new(None),
scan_type: ScanType::File,
output_level: Default::default(),
@@ -118,6 +130,11 @@ impl FeroxScan {
&self.url
}
/// getter for number of requests made during previously saved scans (i.e. --resume-from used)
pub fn requests_made_so_far(&self) -> u64 {
self.requests_made_so_far
}
/// small wrapper to set the JoinHandle
pub async fn set_task(&self, task: JoinHandle<()>) -> Result<()> {
let mut guard = self.task.lock().await;
@@ -137,7 +154,13 @@ impl FeroxScan {
pub(super) fn stop_progress_bar(&self) {
if let Ok(guard) = self.progress_bar.lock() {
if guard.is_some() {
(*guard).as_ref().unwrap().finish_at_current_pos()
let pb = (*guard).as_ref().unwrap();
if pb.position() > self.num_requests {
pb.finish()
} else {
pb.finish_at_current_pos()
}
}
}
}
@@ -158,6 +181,8 @@ impl FeroxScan {
let pb = add_bar(&self.url, self.num_requests, bar_type);
pb.reset_elapsed();
pb.set_position(self.requests_made_so_far);
let _ = std::mem::replace(&mut *guard, Some(pb.clone()));
pb
@@ -191,6 +216,7 @@ impl FeroxScan {
) -> Arc<Self> {
Arc::new(Self {
url: url.to_string(),
normalized_url: format!("{}/", url.trim_end_matches('/')),
scan_type,
scan_order,
num_requests,
@@ -228,6 +254,14 @@ impl FeroxScan {
false
}
/// small wrapper to inspect ScanStatus and see if it's Cancelled
pub fn is_cancelled(&self) -> bool {
if let Ok(guard) = self.status.lock() {
return matches!(*guard, ScanStatus::Cancelled);
}
false
}
/// await a task's completion, similar to a thread's join; perform necessary bookkeeping
pub async fn join(&self) {
log::trace!("enter join({:?})", self);
@@ -264,6 +298,7 @@ impl FeroxScan {
PolicyTrigger::Status403 => self.status_403s(),
PolicyTrigger::Status429 => self.status_429s(),
PolicyTrigger::Errors => self.errors(),
PolicyTrigger::TryAdjustUp => 0,
}
}
@@ -332,13 +367,15 @@ impl Serialize for FeroxScan {
where
S: Serializer,
{
let mut state = serializer.serialize_struct("FeroxScan", 4)?;
let mut state = serializer.serialize_struct("FeroxScan", 6)?;
state.serialize_field("id", &self.id)?;
state.serialize_field("url", &self.url)?;
state.serialize_field("normalized_url", &self.normalized_url)?;
state.serialize_field("scan_type", &self.scan_type)?;
state.serialize_field("status", &self.status)?;
state.serialize_field("num_requests", &self.num_requests)?;
state.serialize_field("requests_made_so_far", &self.requests())?;
state.end()
}
@@ -387,11 +424,21 @@ impl<'de> Deserialize<'de> for FeroxScan {
scan.url = url.to_string();
}
}
"normalized_url" => {
if let Some(normalized_url) = value.as_str() {
scan.normalized_url = normalized_url.to_string();
}
}
"num_requests" => {
if let Some(num_requests) = value.as_u64() {
scan.num_requests = num_requests;
}
}
"requests_made_so_far" => {
if let Some(requests_made_so_far) = value.as_u64() {
scan.requests_made_so_far = requests_made_so_far;
}
}
_ => {}
}
}
@@ -480,9 +527,11 @@ mod tests {
let scan = FeroxScan {
id: "".to_string(),
url: "".to_string(),
normalized_url: String::from("/"),
scan_type: ScanType::Directory,
scan_order: ScanOrder::Initial,
num_requests: 0,
requests_made_so_far: 0,
status: Mutex::new(ScanStatus::Running),
task: Default::default(),
progress_bar: Mutex::new(None),

View File

@@ -1,15 +1,24 @@
use super::scan::ScanType;
use super::*;
use crate::event_handlers::Handles;
use crate::filters::{
EmptyFilter, LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter,
WildcardFilter, WordsFilter,
};
use crate::traits::FeroxFilter;
use crate::Command::AddFilter;
use crate::{
banner::Banner,
config::OutputLevel,
progress::PROGRESS_PRINTER,
progress::{add_bar, BarType},
scan_manager::{MenuCmd, MenuCmdResult},
scanner::RESPONSES,
traits::FeroxSerialize,
SLEEP_DURATION,
Command, SLEEP_DURATION,
};
use anyhow::Result;
use console::style;
use reqwest::StatusCode;
use serde::{ser::SerializeSeq, Serialize, Serializer};
use std::{
@@ -67,7 +76,7 @@ impl Serialize for FeroxScans {
let mut seq = serializer.serialize_seq(Some(scans.len() + 1))?;
for scan in scans.iter() {
seq.serialize_element(&*scan).unwrap_or_default();
seq.serialize_element(scan).unwrap_or_default();
}
seq.end()
}
@@ -117,7 +126,7 @@ impl FeroxScans {
}
/// load serialized FeroxScan(s) and any previously collected extensions into this FeroxScans
pub fn add_serialized_scans(&self, filename: &str) -> Result<()> {
pub fn add_serialized_scans(&self, filename: &str, handles: Arc<Handles>) -> Result<()> {
log::trace!("enter: add_serialized_scans({})", filename);
let file = File::open(filename)?;
@@ -130,6 +139,15 @@ impl FeroxScans {
let mut deser_scan: FeroxScan =
serde_json::from_value(scan.clone()).unwrap_or_default();
if deser_scan.is_cancelled() {
// if the scan was cancelled by the user, mark it as complete. This will
// prevent the scan from being resumed as well as prevent the wordlist
// from requesting it again
if let Ok(mut guard) = deser_scan.status.lock() {
*guard = ScanStatus::Complete;
}
}
// FeroxScans gets -q value from config as usual; the FeroxScans themselves
// rely on that value being passed in. If the user starts a scan without -q
// and resumes the scan but adds -q, FeroxScan will not have the proper value
@@ -154,6 +172,49 @@ impl FeroxScans {
}
}
if let Some(filters) = state.get("filters") {
if let Some(arr_filters) = filters.as_array() {
for filter in arr_filters {
let final_filter: Box<dyn FeroxFilter> = if let Ok(deserialized) =
serde_json::from_value::<RegexFilter>(filter.clone())
{
Box::new(deserialized)
} else if let Ok(deserialized) =
serde_json::from_value::<WordsFilter>(filter.clone())
{
Box::new(deserialized)
} else if let Ok(deserialized) =
serde_json::from_value::<WildcardFilter>(filter.clone())
{
Box::new(deserialized)
} else if let Ok(deserialized) =
serde_json::from_value::<SizeFilter>(filter.clone())
{
Box::new(deserialized)
} else if let Ok(deserialized) =
serde_json::from_value::<LinesFilter>(filter.clone())
{
Box::new(deserialized)
} else if let Ok(deserialized) =
serde_json::from_value::<SimilarityFilter>(filter.clone())
{
Box::new(deserialized)
} else if let Ok(deserialized) =
serde_json::from_value::<StatusCodeFilter>(filter.clone())
{
Box::new(deserialized)
} else {
Box::new(EmptyFilter {})
};
handles
.filters
.send(AddFilter(final_filter))
.unwrap_or_default();
}
}
}
log::trace!("exit: add_serialized_scans");
Ok(())
}
@@ -162,8 +223,10 @@ impl FeroxScans {
/// on the given URL
pub fn contains(&self, url: &str) -> bool {
if let Ok(scans) = self.scans.read() {
let normalized = format!("{}/", url.trim_end_matches('/'));
for scan in scans.iter() {
if scan.url == url {
if scan.normalized_url == normalized {
return true;
}
}
@@ -174,8 +237,10 @@ impl FeroxScans {
/// Find and return a `FeroxScan` based on the given URL
pub fn get_scan_by_url(&self, url: &str) -> Option<Arc<FeroxScan>> {
if let Ok(guard) = self.scans.read() {
let normalized = format!("{}/", url.trim_end_matches('/'));
for scan in guard.iter() {
if scan.url == url {
if scan.normalized_url == normalized {
return Some(scan.clone());
}
}
@@ -207,7 +272,7 @@ impl FeroxScans {
for (idx, _) in &matches {
for scan in guard.iter() {
let slice = url.index(0..*idx);
if slice == scan.url || format!("{}/", slice).as_str() == scan.url {
if slice == scan.url || format!("{slice}/").as_str() == scan.url {
log::trace!("enter: get_base_scan_by_url -> {}", scan);
return Some(scan.clone());
}
@@ -257,19 +322,25 @@ impl FeroxScans {
.clone()
};
for (i, scan) in scans.iter().enumerate() {
if matches!(scan.scan_order, ScanOrder::Initial) || scan.task.try_lock().is_err() {
// original target passed in via either -u or --stdin
continue;
}
let mut printed = 0;
for (i, scan) in scans.iter().enumerate() {
if matches!(scan.scan_type, ScanType::Directory) {
if printed == 0 {
self.menu
.println(&format!("{}:", style("Scans").bright().blue()));
}
// we're only interested in displaying directory scans, as those are
// the only ones that make sense to be stopped
let scan_msg = format!("{:3}: {}", i, scan);
let scan_msg = format!("{i:3}: {scan}");
self.menu.println(&scan_msg);
printed += 1;
}
}
if printed > 0 {
self.menu.print_border();
}
}
/// Given a list of indexes, cancel their associated FeroxScans
@@ -285,7 +356,7 @@ impl FeroxScans {
if num >= u_scans.len() {
// usize can't be negative, just need to handle exceeding bounds
self.menu
.println(&format!("The number {} is not a valid choice.", num));
.println(&format!("The number {num} is not a valid choice."));
sleep(menu_pause_duration);
continue;
}
@@ -302,14 +373,13 @@ impl FeroxScans {
if input == 'y' || input == '\n' {
self.menu.println(&format!("Stopping {}...", selected.url));
selected
.abort()
.await
.unwrap_or_else(|e| log::warn!("Could not cancel task: {}", e));
let pb = selected.progress_bar();
num_cancelled += pb.length() as usize - pb.position() as usize
num_cancelled += pb.length() as usize - pb.position() as usize;
} else {
self.menu.println("Ok, doing nothing...");
}
@@ -320,12 +390,34 @@ impl FeroxScans {
num_cancelled
}
fn display_filters(&self, handles: Arc<Handles>) {
let mut printed = 0;
if let Ok(guard) = handles.filters.data.filters.read() {
for (i, filter) in guard.iter().enumerate() {
if i == 0 {
self.menu
.println(&format!("{}:", style("Filters").bright().blue()));
}
let filter_msg = format!("{:3}: {}", i + 1, filter);
self.menu.println(&filter_msg);
printed += 1;
}
if printed > 0 {
self.menu.print_border();
}
}
}
/// CLI menu that allows for interactive cancellation of recursed-into directories
async fn interactive_menu(&self) -> Option<MenuCmdResult> {
async fn interactive_menu(&self, handles: Arc<Handles>) -> Option<MenuCmdResult> {
self.menu.hide_progress_bars();
self.menu.clear_screen();
self.menu.print_header();
self.display_scans().await;
self.display_filters(handles.clone());
self.menu.print_footer();
let menu_cmd = if let Ok(line) = self.menu.term.read_line() {
@@ -340,13 +432,53 @@ impl FeroxScans {
let num_cancelled = self.cancel_scans(indices, should_force).await;
Some(MenuCmdResult::NumCancelled(num_cancelled))
}
Some(MenuCmd::Add(url)) => Some(MenuCmdResult::Url(url)),
Some(MenuCmd::AddUrl(url)) => Some(MenuCmdResult::Url(url)),
Some(MenuCmd::AddFilter(filter)) => Some(MenuCmdResult::Filter(filter)),
Some(MenuCmd::RemoveFilter(indices)) => {
handles
.filters
.send(Command::RemoveFilters(indices))
.unwrap_or_default();
None
}
None => None,
};
self.menu.clear_screen();
let banner = Banner::new(&[handles.config.target_url.clone()], &handles.config);
banner
.print_to(&self.menu.term, handles.config.clone())
.unwrap_or_default();
self.menu.show_progress_bars();
let has_active_scans = if let Ok(guard) = self.scans.read() {
guard.iter().any(|s| s.is_active())
} else {
// if we can't tell for sure, we'll let it ride
//
// i'm not sure which is the better option here:
// either return true and let it potentially hang, or
// return false and exit, so just going with not
// abruptly exiting for maybe no reason
true
};
if !has_active_scans {
// the last active scan was cancelled, so we can exit
self.menu.println(&format!(
" 😱 no more active scans... {}",
style("exiting").red()
));
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
handles
.send_scan_command(Command::JoinTasks(tx))
.unwrap_or_default();
rx.await.unwrap_or_default();
}
result
}
@@ -395,7 +527,11 @@ impl FeroxScans {
///
/// When the value stored in `PAUSE_SCAN` becomes `false`, the function returns, exiting the busy
/// loop
pub async fn pause(&self, get_user_input: bool) -> Option<MenuCmdResult> {
pub async fn pause(
&self,
get_user_input: bool,
handles: Arc<Handles>,
) -> Option<MenuCmdResult> {
// function uses tokio::time, not std
// local testing showed a pretty slow increase (less than linear) in CPU usage as # of
@@ -407,7 +543,7 @@ impl FeroxScans {
INTERACTIVE_BARRIER.fetch_add(1, Ordering::Relaxed);
if get_user_input {
command_result = self.interactive_menu().await;
command_result = self.interactive_menu(handles).await;
PAUSE_SCAN.store(false, Ordering::Relaxed);
self.print_known_responses();
}
@@ -493,7 +629,8 @@ impl FeroxScans {
///
/// Also return a reference to the new `FeroxScan`
pub fn add_directory_scan(&self, url: &str, scan_order: ScanOrder) -> (bool, Arc<FeroxScan>) {
self.add_scan(url, ScanType::Directory, scan_order)
let normalized = format!("{}/", url.trim_end_matches('/'));
self.add_scan(&normalized, ScanType::Directory, scan_order)
}
/// Given a url, create a new `FeroxScan` and add it to `FeroxScans` as a File Scan

View File

@@ -1,4 +1,5 @@
use super::*;
use crate::filters::FeroxFilters;
use crate::{config::Configuration, statistics::Stats, traits::FeroxSerialize, utils::fmt_err};
use anyhow::{Context, Result};
use serde::Serialize;
@@ -22,6 +23,9 @@ pub struct FeroxState {
/// collected extensions
collected_extensions: HashSet<String>,
/// runtime filters, as they may differ from original config
filters: Arc<FeroxFilters>,
}
/// implementation of FeroxState
@@ -32,6 +36,7 @@ impl FeroxState {
config: Arc<Configuration>,
responses: &'static FeroxResponses,
statistics: Arc<Stats>,
filters: Arc<FeroxFilters>,
) -> Self {
let collected_extensions = match scans.collected_extensions.read() {
Ok(extensions) => extensions.clone(),
@@ -44,6 +49,7 @@ impl FeroxState {
responses,
statistics,
collected_extensions,
filters,
}
}
}
@@ -52,7 +58,7 @@ impl FeroxState {
impl FeroxSerialize for FeroxState {
/// Simply return debug format of FeroxState to satisfy as_str
fn as_str(&self) -> String {
format!("{:?}", self)
format!("{self:?}")
}
/// Simple call to produce a JSON string using the given FeroxState

View File

@@ -1,4 +1,8 @@
use super::*;
use crate::filters::{
FeroxFilters, LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter,
WordsFilter,
};
use crate::{
config::{Configuration, OutputLevel},
event_handlers::Handles,
@@ -10,6 +14,7 @@ use crate::{
};
use indicatif::ProgressBar;
use predicates::prelude::*;
use regex::Regex;
use std::sync::{atomic::Ordering, Arc};
use std::thread::sleep;
use std::time::Instant;
@@ -31,6 +36,7 @@ fn default_scantype_is_file() {
async fn scanner_pause_scan_with_finished_spinner() {
let now = time::Instant::now();
let urls = FeroxScans::default();
let handles = Arc::new(Handles::for_testing(None, None).0);
PAUSE_SCAN.store(true, Ordering::Relaxed);
@@ -41,7 +47,7 @@ async fn scanner_pause_scan_with_finished_spinner() {
PAUSE_SCAN.store(false, Ordering::Relaxed);
});
urls.pause(false).await;
urls.pause(false, handles).await;
assert!(now.elapsed() > expected);
}
@@ -218,7 +224,7 @@ fn ferox_scan_get_progress_bar_when_none_is_set() {
/// given a JSON entry representing a FeroxScan, test that it deserializes into the proper type
/// with the right attributes
fn ferox_scan_deserialize() {
let fs_json = r#"{"id":"057016a14769414aac9a7a62707598cb","url":"https://spiritanimal.com","scan_type":"Directory","status":"Complete"}"#;
let fs_json = r#"{"id":"057016a14769414aac9a7a62707598cb","url":"https://spiritanimal.com","scan_type":"Directory","status":"Complete","requests_made_so_far":500}"#;
let fs_json_two = r#"{"id":"057016a14769414aac9a7a62707598cb","url":"https://spiritanimal.com","scan_type":"Not Correct","status":"Cancelled"}"#;
let fs_json_three = r#"{"id":"057016a14769414aac9a7a62707598cb","url":"https://spiritanimal.com","scan_type":"Not Correct","status":"","num_requests":42}"#;
@@ -240,9 +246,13 @@ fn ferox_scan_deserialize() {
ScanType::File => {}
}
match *fs.progress_bar.lock().unwrap() {
None => {}
Some(_) => {
match fs.progress_bar.lock() {
Ok(guard) => {
if guard.is_some() {
panic!();
}
}
Err(_) => {
panic!();
}
}
@@ -271,7 +281,7 @@ fn ferox_scan_serialize() {
None,
);
let fs_json = format!(
r#"{{"id":"{}","url":"https://spiritanimal.com","scan_type":"Directory","status":"NotStarted","num_requests":0}}"#,
r#"{{"id":"{}","url":"https://spiritanimal.com","normalized_url":"https://spiritanimal.com/","scan_type":"Directory","status":"NotStarted","num_requests":0,"requests_made_so_far":0}}"#,
fs.id
);
assert_eq!(fs_json, serde_json::to_string(&*fs).unwrap());
@@ -290,7 +300,7 @@ fn ferox_scans_serialize() {
);
let ferox_scans = FeroxScans::default();
let ferox_scans_json = format!(
r#"[{{"id":"{}","url":"https://spiritanimal.com","scan_type":"Directory","status":"NotStarted","num_requests":0}}]"#,
r#"[{{"id":"{}","url":"https://spiritanimal.com","normalized_url":"https://spiritanimal.com/","scan_type":"Directory","status":"NotStarted","num_requests":0,"requests_made_so_far":0}}]"#,
ferox_scan.id
);
ferox_scans.scans.write().unwrap().push(ferox_scan);
@@ -311,7 +321,7 @@ fn ferox_responses_serialize() {
// responses has a response now
// serialized should be a list of responses
let expected = format!("[{}]", json_response);
let expected = format!("[{json_response}]");
let serialized = serde_json::to_string(&responses).unwrap();
assert_eq!(expected, serialized);
@@ -370,7 +380,41 @@ fn feroxstates_feroxserialize_implementation() {
let response: FeroxResponse = serde_json::from_str(json_response).unwrap();
RESPONSES.insert(response);
let ferox_state = FeroxState::new(Arc::new(ferox_scans), Arc::new(config), &RESPONSES, stats);
let filters = FeroxFilters::default();
filters
.push(Box::new(StatusCodeFilter { filter_code: 100 }))
.unwrap();
filters
.push(Box::new(WordsFilter { word_count: 200 }))
.unwrap();
filters
.push(Box::new(SizeFilter {
content_length: 300,
}))
.unwrap();
filters
.push(Box::new(LinesFilter { line_count: 400 }))
.unwrap();
filters
.push(Box::new(RegexFilter {
raw_string: ".*".to_string(),
compiled: Regex::new(".*").unwrap(),
}))
.unwrap();
filters
.push(Box::new(SimilarityFilter {
hash: 1,
original_url: "http://localhost:12345/".to_string(),
}))
.unwrap();
let ferox_state = FeroxState::new(
Arc::new(ferox_scans),
Arc::new(config),
&RESPONSES,
stats,
Arc::new(filters),
);
let expected_strs = predicates::str::contains("scans: FeroxScans").and(
predicate::str::contains("config: Configuration")
@@ -385,11 +429,11 @@ fn feroxstates_feroxserialize_implementation() {
let json_state = ferox_state.as_json().unwrap();
println!("echo '{}'|jq", json_state); // for debugging, if the test fails, can see what's going on
println!("echo '{json_state}'|jq"); // for debugging, if the test fails, can see what's going on
for expected in [
r#""scans""#,
&format!(r#""id":"{}""#, saved_id),
&format!(r#""id":"{saved_id}""#),
r#""url":"https://spiritanimal.com""#,
r#""scan_type":"Directory""#,
r#""status":"NotStarted""#,
@@ -401,8 +445,8 @@ fn feroxstates_feroxserialize_implementation() {
r#""proxy":"""#,
r#""replay_proxy":"""#,
r#""target_url":"""#,
r#""status_codes":[200,204,301,302,307,308,401,403,405,500]"#,
r#""replay_codes":[200,204,301,302,307,308,401,403,405,500]"#,
r#""status_codes":[100,101,102,200,201,202,203,204,205,206,207,208,226,300,301,302,303,304,305,307,308,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,421,422,423,424,426,428,429,431,451,500,501,502,503,504,505,506,507,508,510,511,103,425]"#,
r#""replay_codes":[100,101,102,200,201,202,203,204,205,206,207,208,226,300,301,302,303,304,305,307,308,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,421,422,423,424,426,428,429,431,451,500,501,502,503,504,505,506,507,508,510,511,103,425]"#,
r#""filter_status":[]"#,
r#""threads":50"#,
r#""timeout":7"#,
@@ -411,10 +455,11 @@ fn feroxstates_feroxserialize_implementation() {
r#""quiet":false"#,
r#""auto_bail":false"#,
r#""auto_tune":false"#,
r#""force_recursion":false"#,
r#""json":false"#,
r#""output":"""#,
r#""debug_log":"""#,
&format!(r#""user_agent":"feroxbuster/{}""#, VERSION),
&format!(r#""user_agent":"feroxbuster/{VERSION}""#),
r#""random_agent":false"#,
r#""redirects":false"#,
r#""insecure":false"#,
@@ -424,7 +469,7 @@ fn feroxstates_feroxserialize_implementation() {
r#""headers""#,
r#""queries":[]"#,
r#""no_recursion":false"#,
r#""extract_links":false"#,
r#""extract_links":true"#,
r#""add_slash":false"#,
r#""stdin":false"#,
r#""depth":4"#,
@@ -457,6 +502,7 @@ fn feroxstates_feroxserialize_implementation() {
r#""collect_extensions":true"#,
r#""collect_backups":false"#,
r#""collect_words":false"#,
r#""filters":[{"filter_code":100},{"word_count":200},{"content_length":300},{"line_count":400},{"compiled":".*","raw_string":".*"},{"hash":1,"original_url":"http://localhost:12345/"}]"#,
r#""collected_extensions":["php"]"#,
r#""dont_collect":["tif","tiff","ico","cur","bmp","webp","svg","png","jpg","jpeg","jfif","gif","avif","apng","pjpeg","pjp","mov","wav","mpg","mpeg","mp3","mp4","m4a","m4p","m4v","ogg","webm","ogv","oga","flac","aac","3gp","css","zip","xls","xml","gz","tgz"]"#,
]
@@ -513,9 +559,11 @@ fn feroxscan_display() {
let scan = FeroxScan {
id: "".to_string(),
url: String::from("http://localhost"),
normalized_url: String::from("http://localhost/"),
scan_order: ScanOrder::Latest,
scan_type: Default::default(),
num_requests: 0,
requests_made_so_far: 0,
start_time: Instant::now(),
output_level: OutputLevel::Default,
status_403s: Default::default(),
@@ -526,26 +574,26 @@ fn feroxscan_display() {
errors: Default::default(),
};
let not_started = format!("{}", scan);
let not_started = format!("{scan}");
assert!(predicate::str::contains("not started")
.and(predicate::str::contains("localhost"))
.eval(&not_started));
scan.set_status(ScanStatus::Complete).unwrap();
let complete = format!("{}", scan);
let complete = format!("{scan}");
assert!(predicate::str::contains("complete")
.and(predicate::str::contains("localhost"))
.eval(&complete));
scan.set_status(ScanStatus::Cancelled).unwrap();
let cancelled = format!("{}", scan);
let cancelled = format!("{scan}");
assert!(predicate::str::contains("cancelled")
.and(predicate::str::contains("localhost"))
.eval(&cancelled));
scan.set_status(ScanStatus::Running).unwrap();
let running = format!("{}", scan);
let running = format!("{scan}");
assert!(predicate::str::contains("running")
.and(predicate::str::contains("localhost"))
.eval(&running));
@@ -557,9 +605,11 @@ async fn ferox_scan_abort() {
let scan = FeroxScan {
id: "".to_string(),
url: String::from("http://localhost"),
normalized_url: String::from("http://localhost/"),
scan_order: ScanOrder::Latest,
scan_type: Default::default(),
num_requests: 0,
requests_made_so_far: 0,
start_time: Instant::now(),
output_level: OutputLevel::Default,
status_403s: Default::default(),
@@ -587,14 +637,11 @@ async fn ferox_scan_abort() {
/// and their correctness can be verified easily manually; just calling for now
fn menu_print_header_and_footer() {
let menu = Menu::new();
let menu_cmd_1 = MenuCmd::Add(String::from("http://localhost"));
let menu_cmd_1 = MenuCmd::AddUrl(String::from("http://localhost"));
let menu_cmd_2 = MenuCmd::Cancel(vec![0], false);
let menu_cmd_res_1 = MenuCmdResult::Url(String::from("http://localhost"));
let menu_cmd_res_2 = MenuCmdResult::NumCancelled(2);
println!(
"{:?}{:?}{:?}{:?}",
menu_cmd_1, menu_cmd_2, menu_cmd_res_1, menu_cmd_res_2
);
println!("{menu_cmd_1:?}{menu_cmd_2:?}{menu_cmd_res_1:?}{menu_cmd_res_2:?}");
menu.clear_screen();
menu.print_header();
menu.print_footer();
@@ -602,8 +649,8 @@ fn menu_print_header_and_footer() {
menu.show_progress_bars();
}
#[test]
/// ensure command parsing from user input results int he correct MenuCmd returned
#[test]
fn menu_get_command_input_from_user_returns_cancel() {
let menu = Menu::new();
@@ -611,9 +658,9 @@ fn menu_get_command_input_from_user_returns_cancel() {
let force = idx % 2 == 0;
let full_cmd = if force {
format!("{} -f {}\n", cmd, idx)
format!("{cmd} -f {idx}\n")
} else {
format!("{} {}\n", cmd, idx)
format!("{cmd} {idx}\n")
};
let result = menu.get_command_input_from_user(&full_cmd).unwrap();
@@ -621,30 +668,26 @@ fn menu_get_command_input_from_user_returns_cancel() {
assert!(matches!(result, MenuCmd::Cancel(_, _)));
if let MenuCmd::Cancel(canx_list, ret_force) = result {
if idx == 0 {
assert!(canx_list.is_empty());
} else {
assert_eq!(canx_list, vec![idx]);
}
assert_eq!(canx_list, vec![idx]);
assert_eq!(force, ret_force);
}
}
}
#[test]
/// ensure command parsing from user input results int he correct MenuCmd returned
#[test]
fn menu_get_command_input_from_user_returns_add() {
let menu = Menu::new();
for cmd in ["add", "Addd", "a", "A", "None"] {
let test_url = "http://happyfuntimes.commmm";
let full_cmd = format!("{} {}\n", cmd, test_url);
let full_cmd = format!("{cmd} {test_url}\n");
if cmd != "None" {
let result = menu.get_command_input_from_user(&full_cmd).unwrap();
assert!(matches!(result, MenuCmd::Add(_)));
assert!(matches!(result, MenuCmd::AddUrl(_)));
if let MenuCmd::Add(url) = result {
if let MenuCmd::AddUrl(url) = result {
assert_eq!(url, test_url);
}
} else {

View File

@@ -41,7 +41,7 @@ pub async fn start_max_time_thread(handles: Arc<Handles>) {
log::trace!("exit: start_max_time_thread");
#[cfg(test)]
panic!("{:?}", handles);
panic!("{handles:?}");
#[cfg(not(test))]
let _ = TermInputHandler::sigint_handler(handles.clone());
}

View File

@@ -1,3 +1,4 @@
use std::fmt::Write as _;
use std::sync::atomic::AtomicBool;
use std::{ops::Deref, sync::atomic::Ordering, sync::Arc, time::Instant};
@@ -8,6 +9,9 @@ use indicatif::ProgressBar;
use lazy_static::lazy_static;
use tokio::sync::Semaphore;
use crate::filters::{create_similarity_filter, EmptyFilter, SimilarityFilter};
use crate::heuristics::WildcardResult;
use crate::Command::AddFilter;
use crate::{
event_handlers::{
Command::{AddError, AddToF64Field, AddToUsizeField, SubtractFromUsizeField},
@@ -47,7 +51,7 @@ async fn check_for_user_input(
// todo write a test or two for this function at some point...
if pause_flag.load(Ordering::Acquire) {
match scanned_urls.pause(true).await {
match scanned_urls.pause(true, handles.clone()).await {
Some(MenuCmdResult::Url(url)) => {
// user wants to add a new url to be scanned, need to send
// it over to the event handler for processing
@@ -63,6 +67,38 @@ async fn check_for_user_input(
.unwrap_or_else(|e| log::warn!("Could not update overall scan bar: {}", e));
}
}
Some(MenuCmdResult::Filter(mut filter)) => {
let url = if let Some(SimilarityFilter { original_url, .. }) =
filter.as_any().downcast_ref::<SimilarityFilter>()
{
original_url.to_owned()
} else {
String::new()
};
if !url.is_empty() {
// filter was a SimilarityFilter and now we have a url to request.
//
// The reason for this janky structure is that `filter.as_any().downcast_ref`
// isn't Send so we can't call create_similarity_filter(...).await, within
// the if let Some ipso-facto, janky code /shrug
let real_filter = create_similarity_filter(&url, handles.clone())
.await
.unwrap_or_default();
if real_filter.original_url.is_empty() {
// failed to create filter
filter = Box::new(EmptyFilter {});
} else {
filter = Box::new(real_filter)
}
}
handles
.filters
.send(AddFilter(filter))
.unwrap_or_else(|e| log::warn!("Could not add new filter: {}", e));
}
_ => {}
}
}
@@ -147,6 +183,7 @@ impl FeroxScanner {
Err(e) => {
log::warn!("error awaiting a response: {}", e);
self.handles.stats.send(AddError(Other)).unwrap_or_default();
std::process::exit(1);
}
}
});
@@ -208,13 +245,9 @@ impl FeroxScanner {
}
{
// heuristics test block
// heuristics test block:
let test = heuristics::HeuristicTests::new(self.handles.clone());
if let Ok(num_reqs) = test.wildcard(&self.target_url).await {
progress_bar.inc(num_reqs);
}
if let Ok(dirlist_result) = test.directory_listing(&self.target_url).await {
if dirlist_result.is_some() {
let dirlist_result = dirlist_result.unwrap();
@@ -250,18 +283,44 @@ impl FeroxScanner {
let mut message = format!("=> {}", style("Directory listing").blue().bright());
if !self.handles.config.extract_links {
message
.push_str(&format!(" (add {} to scan)", style("-e").bright().yellow()))
write!(message, " (add {} to scan)", style("-e").bright().yellow())?;
}
progress_bar.reset_eta();
progress_bar.finish_with_message(&message);
if !self.handles.config.force_recursion {
progress_bar.reset_eta();
progress_bar.finish_with_message(&message);
ferox_scan.finish()?;
ferox_scan.finish()?;
return Ok(());
return Ok(()); // nothing left to do if we found a dir listing
}
}
}
// now that we haven't found a directory listing, we'll attempt to derive whatever
// the server is using to respond to resources that don't exist (could be a
// traditional 404, or a custom response)
//
// `detect_404_like_responses` will make the requests that the wildcard test used to
// perform pre-2.8 in addition to new detection techniques, superseding the old
// wildcard test
let num_reqs_made = test.detect_404_like_responses(&self.target_url).await?;
match num_reqs_made {
Some(WildcardResult::WildcardDirectory(num_reqs)) => {
let message = format!(
"=> {} dir! {} recursion",
style("Wildcard").blue().bright(),
style("stopped").red()
);
progress_bar.set_message(&message);
progress_bar.inc(num_reqs as u64);
}
Some(WildcardResult::FourOhFourLike(num_reqs)) => {
progress_bar.inc(num_reqs as u64);
}
_ => {}
}
}
// Arc clones to be passed around to the various scans
@@ -294,7 +353,7 @@ impl FeroxScanner {
log::info!(
"requesting {} collected words: {:?}...",
new_words_len,
&new_words[..new_words_len.min(3) as usize]
&new_words[..new_words_len.min(3)]
);
self.stream_requests(

View File

@@ -41,7 +41,7 @@ impl Debug for LimitHeap {
"LimitHeap {{ original: {}, current: {}, inner: [{}...] }}",
self.original, self.current, self.inner[0]
);
write!(f, "{}", msg)
write!(f, "{msg}")
}
}

View File

@@ -84,10 +84,6 @@ impl PolicyData {
if heap.has_parent() && heap.parent_value() > current {
// all nodes except 0th node (root)
heap.move_up();
} else if !heap.has_parent() {
// been here enough that we can try resuming the scan to its original
// speed (no limiting at all)
atomic_store!(self.remove_limit, true);
}
}
} else if heap.has_children() {
@@ -103,6 +99,12 @@ impl PolicyData {
heap.move_up();
}
}
if !heap.has_parent() {
// been here enough that we can try resuming the scan to its original
// speed (no limiting at all)
atomic_store!(self.remove_limit, true);
}
self.set_limit(heap.value() as usize);
}
}

View File

@@ -1,14 +1,19 @@
use std::{
cmp::max,
collections::HashSet,
sync::{self, atomic::Ordering, Arc, Mutex},
sync::{
self,
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
};
use anyhow::Result;
use console::style;
use lazy_static::lazy_static;
use leaky_bucket::LeakyBucket;
use leaky_bucket::RateLimiter;
use tokio::{
sync::{oneshot, RwLock},
sync::RwLock,
time::{sleep, Duration},
};
@@ -16,7 +21,7 @@ use crate::{
atomic_load, atomic_store,
config::RequesterPolicy,
event_handlers::{
Command::{self, AddError, SubtractFromUsizeField},
Command::{AddError, SubtractFromUsizeField},
Handles,
},
extractor::{ExtractionTarget, ExtractorBuilder},
@@ -25,7 +30,7 @@ use crate::{
scan_manager::{FeroxScan, ScanStatus},
statistics::{StatError::Other, StatField::TotalExpected},
url::FeroxUrl,
utils::{logged_request, should_deny_url},
utils::{logged_request, send_try_recursion_command, should_deny_url},
HIGH_ERROR_RATIO,
};
@@ -45,7 +50,7 @@ pub(super) struct Requester {
target_url: String,
/// limits requests per second if present
rate_limiter: RwLock<Option<LeakyBucket>>,
rate_limiter: RwLock<Option<RateLimiter>>,
/// data regarding policy and metadata about last enforced trigger etc...
policy_data: PolicyData,
@@ -64,6 +69,8 @@ pub(super) struct Requester {
/// seen; this will satisfy the non-mut self constraint (due to us being behind an Arc, and
/// the need for a counter)
tuning_lock: Mutex<usize>,
policy_triggered: AtomicBool,
}
/// Requester implementation
@@ -91,21 +98,22 @@ impl Requester {
handles: scanner.handles.clone(),
target_url: scanner.target_url.to_owned(),
tuning_lock: Mutex::new(0),
policy_triggered: AtomicBool::new(false),
})
}
/// build a LeakyBucket, given a rate limit (as requests per second)
fn build_a_bucket(limit: usize) -> Result<LeakyBucket> {
/// build a RateLimiter, given a rate limit (as requests per second)
fn build_a_bucket(limit: usize) -> Result<RateLimiter> {
let refill = max((limit as f64 / 10.0).round() as usize, 1); // minimum of 1 per second
let tokens = max((limit as f64 / 2.0).round() as usize, 1);
let interval = if refill == 1 { 1000 } else { 100 }; // 1 second if refill is 1
Ok(LeakyBucket::builder()
.refill_interval(Duration::from_millis(interval)) // add tokens every 0.1s
.refill_amount(refill) // ex: 100 req/s -> 10 tokens per 0.1s
.tokens(tokens) // reduce initial burst, 2 is arbitrary, but felt good
Ok(RateLimiter::builder()
.interval(Duration::from_millis(interval)) // add tokens every 0.1s
.refill(refill) // ex: 100 req/s -> 10 tokens per 0.1s
.initial(tokens) // reduce initial burst, 2 is arbitrary, but felt good
.max(limit)
.build()?)
.build())
}
/// sleep and set a flag that can be checked by other threads
@@ -118,19 +126,19 @@ impl Requester {
atomic_store!(self.policy_data.cooling_down, true, Ordering::SeqCst);
sleep(Duration::from_millis(self.policy_data.wait_time)).await;
self.ferox_scan.progress_bar().set_message("");
atomic_store!(self.policy_data.cooling_down, false, Ordering::SeqCst);
}
/// limit the number of requests per second
pub async fn limit(&self) -> Result<()> {
self.rate_limiter
.read()
.await
.as_ref()
.unwrap()
.acquire_one()
.await?;
let guard = self.rate_limiter.read().await;
if guard.is_some() {
guard.as_ref().unwrap().acquire_one().await;
}
Ok(())
}
@@ -204,18 +212,34 @@ impl Requester {
*guard = 0; // reset streak counter to 0
if atomic_load!(self.policy_data.errors) != 0 {
self.policy_data.adjust_down();
let styled_direction = style("reduced").red();
self.ferox_scan
.progress_bar()
.set_message(&format!("=> 🚦 {styled_direction} scan speed",));
}
self.policy_data.set_errors(scan_errors);
} else {
// errors can only be incremented, so an else is sufficient
*guard += 1;
self.policy_data.adjust_up(&*guard);
self.policy_data.adjust_up(&guard);
let styled_direction = style("increased").green();
self.ferox_scan
.progress_bar()
.set_message(&format!("=> 🚦 {styled_direction} scan speed",));
}
}
if atomic_load!(self.policy_data.remove_limit) {
self.set_rate_limiter(None).await?;
atomic_store!(self.policy_data.remove_limit, false);
self.ferox_scan
.progress_bar()
.set_message("=> 🚦 removed rate limiter 🚀");
} else if create_limiter {
// create_limiter is really just used for unit testing situations, it's true anytime
// during actual execution
@@ -254,8 +278,15 @@ impl Requester {
let reqs_sec = self.ferox_scan.requests_per_second() as usize;
self.policy_data.set_reqs_sec(reqs_sec);
// set the flag to indicate that we have triggered the rate limiter
// at least once
atomic_store!(self.policy_triggered, true);
let new_limit = self.policy_data.get_limit();
self.set_rate_limiter(Some(new_limit)).await?;
self.ferox_scan
.progress_bar()
.set_message(&format!("=> 🚦 set rate limit ({new_limit}/s)"));
}
self.adjust_limit(trigger, true).await?;
@@ -292,6 +323,14 @@ impl Requester {
let pb = self.ferox_scan.progress_bar();
let num_skipped = pb.length().saturating_sub(pb.position()) as usize;
let styled_trigger = style(format!("{trigger:?}")).red();
pb.set_message(&format!(
"=> 💀 too many {} ({}) 💀 bailing",
styled_trigger,
self.ferox_scan.num_errors(trigger),
));
// update the overall scan bar by subtracting the number of skipped requests from
// the total
self.handles
@@ -358,6 +397,9 @@ impl Requester {
RequesterPolicy::AutoTune => {
if let Some(trigger) = self.should_enforce_policy() {
self.tune(trigger).await?;
} else if atomic_load!(self.policy_triggered) {
self.adjust_limit(PolicyTrigger::TryAdjustUp, true).await?;
self.cool_down().await;
}
}
RequesterPolicy::AutoBail => {
@@ -379,14 +421,14 @@ impl Requester {
.await;
// do recursion if appropriate
if !self.handles.config.no_recursion {
self.handles
.send_scan_command(Command::TryRecursion(Box::new(
ferox_response.clone(),
)))?;
let (tx, rx) = oneshot::channel::<bool>();
self.handles.send_scan_command(Command::Sync(tx))?;
rx.await?;
if !self.handles.config.no_recursion && !self.handles.config.force_recursion {
// to support --force-recursion, we want to limit recursive calls to only
// 'found' assets. That means we need to either gate or delay the call.
//
// this branch will retain the 'old' behavior by checking that
// --force-recursion isn't turned on
send_try_recursion_command(self.handles.clone(), ferox_response.clone())
.await?;
}
// purposefully doing recursion before filtering. the thought process is that
@@ -400,6 +442,33 @@ impl Requester {
continue;
}
if !self.handles.config.no_recursion && self.handles.config.force_recursion {
// in this branch, we're saying that both recursion AND force recursion
// are turned on. It comes after should_filter_response, so those cases
// are handled. Now we need to account for -s/-C options.
if self.handles.config.filter_status.is_empty() {
// -C wasn't used, so -s is the only 'filter' left to account for
if self
.handles
.config
.status_codes
.contains(&ferox_response.status().as_u16())
{
send_try_recursion_command(
self.handles.clone(),
ferox_response.clone(),
)
.await?;
}
} else {
// -C was used, that means the filters above would have removed
// those responses, and anything else should be let through
send_try_recursion_command(self.handles.clone(), ferox_response.clone())
.await?;
}
}
if self.handles.config.collect_extensions {
ferox_response.parse_extension(self.handles.clone())?;
}
@@ -469,6 +538,7 @@ mod tests {
use crate::{
config::Configuration,
config::OutputLevel,
event_handlers::Command::AddStatus,
event_handlers::{FiltersHandler, ScanHandler, StatsHandler, Tasks, TermOutHandler},
filters,
scan_manager::{ScanOrder, ScanType},
@@ -509,10 +579,7 @@ mod tests {
/// helper to stay DRY
async fn increment_errors(handles: Arc<Handles>, scan: Arc<FeroxScan>, num_errors: usize) {
for _ in 0..num_errors {
handles
.stats
.send(Command::AddError(StatError::Other))
.unwrap();
handles.stats.send(AddError(StatError::Other)).unwrap();
scan.add_error();
}
@@ -524,7 +591,7 @@ mod tests {
let scans = handles.ferox_scans().unwrap();
for _ in 0..num_errors {
scans.increment_error(format!("{}/", url).as_str());
scans.increment_error(format!("{url}/").as_str());
}
}
@@ -537,7 +604,7 @@ mod tests {
) {
let scans = handles.ferox_scans().unwrap();
for _ in 0..num_errors {
scans.increment_status_code(format!("{}/", url).as_str(), code);
scans.increment_status_code(format!("{url}/").as_str(), code);
}
}
@@ -549,7 +616,7 @@ mod tests {
code: StatusCode,
) {
for _ in 0..num_codes {
handles.stats.send(Command::AddStatus(code)).unwrap();
handles.stats.send(AddStatus(code)).unwrap();
if code == StatusCode::FORBIDDEN {
scan.add_403();
} else {
@@ -603,6 +670,7 @@ mod tests {
PolicyTrigger::Errors => {
increment_scan_errors(handles.clone(), url, num_errors).await;
}
_ => {}
}
assert_eq!(scan.num_errors(trigger), num_errors);
@@ -623,6 +691,7 @@ mod tests {
ferox_scan: Arc::new(FeroxScan::default()),
rate_limiter: RwLock::new(None),
policy_data: Default::default(),
policy_triggered: AtomicBool::new(false),
};
let ferox_scan = Arc::new(FeroxScan::default());
@@ -651,6 +720,7 @@ mod tests {
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None),
policy_data: Default::default(),
policy_triggered: AtomicBool::new(false),
};
increment_errors(requester.handles.clone(), ferox_scan.clone(), 25).await;
@@ -676,6 +746,7 @@ mod tests {
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None),
policy_data: Default::default(),
policy_triggered: AtomicBool::new(false),
};
increment_status_codes(
@@ -716,6 +787,7 @@ mod tests {
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None),
policy_data: Default::default(),
policy_triggered: AtomicBool::new(false),
};
increment_status_codes(
@@ -771,6 +843,7 @@ mod tests {
target_url: "http://one/one/stuff.php".to_string(),
rate_limiter: RwLock::new(None),
policy_data: Default::default(),
policy_triggered: AtomicBool::new(false),
};
requester.bail(PolicyTrigger::Errors).await.unwrap();
@@ -805,6 +878,7 @@ mod tests {
target_url: "http://one/one/stuff.php".to_string(),
rate_limiter: RwLock::new(None),
policy_data: Default::default(),
policy_triggered: AtomicBool::new(false),
};
let result = requester.bail(PolicyTrigger::Status403).await;
@@ -827,6 +901,7 @@ mod tests {
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None),
policy_data: Default::default(),
policy_triggered: AtomicBool::new(false),
};
requester
@@ -850,6 +925,7 @@ mod tests {
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
policy_triggered: AtomicBool::new(false),
});
let start = Instant::now();
@@ -880,6 +956,7 @@ mod tests {
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
policy_triggered: AtomicBool::new(false),
};
requester.policy_data.set_reqs_sec(400);
@@ -901,10 +978,10 @@ mod tests {
/// decrease the scan rate
async fn adjust_limit_resets_streak_counter_on_downward_movement() {
let (handles, _) = setup_requester_test(None).await;
let mut buckets = leaky_bucket::LeakyBuckets::new();
let coordinator = buckets.coordinate().unwrap();
tokio::spawn(async move { coordinator.await.expect("coordinator errored") });
let limiter = buckets.rate_limiter().max(200).build().unwrap();
let limiter = RateLimiter::builder()
.interval(Duration::from_secs(1))
.max(200)
.build();
let scan = FeroxScan::default();
scan.add_error();
@@ -918,14 +995,16 @@ mod tests {
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(Some(limiter)),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
policy_triggered: AtomicBool::new(false),
};
requester.policy_data.set_reqs_sec(400);
requester.policy_data.set_errors(1);
let mut guard = requester.tuning_lock.lock().unwrap();
*guard = 2;
drop(guard);
{
let mut guard = requester.tuning_lock.lock().unwrap();
*guard = 2;
}
requester
.adjust_limit(PolicyTrigger::Errors, false)
@@ -954,6 +1033,7 @@ mod tests {
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
policy_triggered: AtomicBool::new(false),
};
requester.policy_data.set_reqs_sec(400);
@@ -982,6 +1062,7 @@ mod tests {
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
policy_triggered: AtomicBool::new(false),
};
assert!(!requester.too_many_status_errors(PolicyTrigger::Errors));
@@ -1012,10 +1093,10 @@ mod tests {
/// set_rate_limiter should exit early when new limit equals the current bucket's max
async fn set_rate_limiter_early_exit() {
let (handles, _) = setup_requester_test(None).await;
let mut buckets = leaky_bucket::LeakyBuckets::new();
let coordinator = buckets.coordinate().unwrap();
tokio::spawn(async move { coordinator.await.expect("coordinator errored") });
let limiter = buckets.rate_limiter().max(200).build().unwrap();
let limiter = RateLimiter::builder()
.interval(Duration::from_secs(1))
.max(200)
.build();
let requester = Requester {
handles,
@@ -1025,6 +1106,7 @@ mod tests {
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(Some(limiter)),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
policy_triggered: AtomicBool::new(false),
};
requester.set_rate_limiter(Some(200)).await.unwrap();
@@ -1044,10 +1126,10 @@ mod tests {
async fn tune_sets_expected_values_and_then_waits() {
let (handles, _) = setup_requester_test(None).await;
let mut buckets = leaky_bucket::LeakyBuckets::new();
let coordinator = buckets.coordinate().unwrap();
tokio::spawn(async move { coordinator.await.expect("coordinator errored") });
let limiter = buckets.rate_limiter().max(200).build().unwrap();
let limiter = RateLimiter::builder()
.interval(Duration::from_secs(1))
.max(200)
.build();
let scan = FeroxScan::new(
"http://localhost",
@@ -1068,6 +1150,7 @@ mod tests {
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(Some(limiter)),
policy_data: PolicyData::new(RequesterPolicy::AutoTune, 4),
policy_triggered: AtomicBool::new(false),
};
let start = Instant::now();

View File

@@ -1,4 +1,4 @@
#[derive(Copy, Clone, PartialEq, Debug)]
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
/// represents different situations where different criteria can trigger auto-tune/bail behavior
pub enum PolicyTrigger {
/// excessive 403 trigger
@@ -9,4 +9,7 @@ pub enum PolicyTrigger {
/// excessive general errors
Errors,
/// dummy error for upward rate adjustment
TryAdjustUp,
}

View File

@@ -667,8 +667,8 @@ impl Stats {
///
/// This is only ever called when resuming a scan from disk
pub fn merge_from(&self, filename: &str) -> Result<()> {
let file = File::open(filename)
.with_context(|| fmt_err(&format!("Could not open {}", filename)))?;
let file =
File::open(filename).with_context(|| fmt_err(&format!("Could not open {filename}")))?;
let reader = BufReader::new(file);
let state: serde_json::Value = serde_json::from_reader(reader)?;

View File

@@ -1,9 +1,15 @@
//! collection of all traits used
use crate::filters::{
LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter, WildcardFilter,
WordsFilter,
};
use crate::response::FeroxResponse;
use crate::utils::status_colorizer;
use anyhow::Result;
use crossterm::style::{style, Stylize};
use serde::Serialize;
use std::any::Any;
use std::fmt::Debug;
use std::fmt::{self, Debug, Display, Formatter};
// references:
// https://dev.to/magnusstrale/rust-trait-objects-in-a-vector-non-trivial-4co5
@@ -22,6 +28,68 @@ pub trait FeroxFilter: Debug + Send + Sync {
fn as_any(&self) -> &dyn Any;
}
impl Display for dyn FeroxFilter {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
if let Some(filter) = self.as_any().downcast_ref::<LinesFilter>() {
write!(f, "Line count: {}", style(filter.line_count).cyan())
} else if let Some(filter) = self.as_any().downcast_ref::<WordsFilter>() {
write!(f, "Word count: {}", style(filter.word_count).cyan())
} else if let Some(filter) = self.as_any().downcast_ref::<SizeFilter>() {
write!(f, "Response size: {}", style(filter.content_length).cyan())
} else if let Some(filter) = self.as_any().downcast_ref::<RegexFilter>() {
write!(f, "Regex: {}", style(&filter.raw_string).cyan())
} else if let Some(filter) = self.as_any().downcast_ref::<WildcardFilter>() {
let mut msg = format!(
"{} requests with {} responses ",
style(&filter.method).cyan(),
status_colorizer(&filter.status_code.to_string())
);
match (filter.content_length, filter.word_count, filter.line_count) {
(None, None, None) => {
unreachable!("wildcard filter without any filters set");
}
(None, None, Some(lc)) => {
msg.push_str(&format!("containing {} lines", lc));
}
(None, Some(wc), None) => {
msg.push_str(&format!("containing {} words", wc));
}
(None, Some(wc), Some(lc)) => {
msg.push_str(&format!("containing {} words and {} lines", wc, lc));
}
(Some(cl), None, None) => {
msg.push_str(&format!("containing {} bytes", cl));
}
(Some(cl), None, Some(lc)) => {
msg.push_str(&format!("containing {} bytes and {} lines", cl, lc));
}
(Some(cl), Some(wc), None) => {
msg.push_str(&format!("containing {} bytes and {} words", cl, wc));
}
(Some(cl), Some(wc), Some(lc)) => {
msg.push_str(&format!(
"containing {} bytes, {} words, and {} lines",
cl, wc, lc
));
}
}
write!(f, "{}", msg)
} else if let Some(filter) = self.as_any().downcast_ref::<StatusCodeFilter>() {
write!(f, "Status code: {}", style(filter.filter_code).cyan())
} else if let Some(filter) = self.as_any().downcast_ref::<SimilarityFilter>() {
write!(
f,
"Pages similar to: {}",
style(&filter.original_url).cyan()
)
} else {
write!(f, "Filter: {self:?}")
}
}
}
/// implementation of PartialEq, necessary long-form due to "trait cannot be made into an object"
/// error when attempting to derive PartialEq on the trait itself
impl PartialEq for Box<dyn FeroxFilter> {

View File

@@ -2,7 +2,7 @@ use crate::{event_handlers::Handles, statistics::StatError::UrlFormat, Command::
use anyhow::{anyhow, bail, Result};
use reqwest::Url;
use std::collections::HashSet;
use std::{convert::TryInto, fmt, sync::Arc};
use std::{fmt, sync::Arc};
/// abstraction around target urls; collects all Url related shenanigans in one place
#[derive(Debug)]
@@ -90,7 +90,7 @@ impl FeroxUrl {
//
// in order to resolve the issue, we check if the word from the wordlist is a parsable URL
// and if so, don't do any further processing
let message = format!("word ({}) from wordlist is a URL, skipping...", word);
let message = format!("word ({word}) from wordlist is a URL, skipping...");
log::warn!("{}", message);
log::trace!("exit: format -> Err({})", message);
bail!(message);
@@ -122,9 +122,9 @@ impl FeroxUrl {
// We handle the special case of forward slash
// That allow us to treat it as an extension with a particular format
if ext == "/" {
format!("{}/", word)
format!("{word}/")
} else {
format!("{}.{}", word, ext)
format!("{word}.{ext}")
}
} else {
String::from(word)
@@ -157,47 +157,6 @@ impl FeroxUrl {
}
}
/// Gets the length of a url's path
pub fn path_length(&self) -> Result<u64> {
let parsed = Url::parse(&self.target)?;
Ok(FeroxUrl::path_length_of_url(&parsed))
}
/// Gets the length of a url's path
///
/// example: http://localhost/stuff -> 5
pub fn path_length_of_url(url: &Url) -> u64 {
log::trace!("enter: get_path_length({})", url);
let path = url.path();
let segments = if let Some(split) = path.strip_prefix('/') {
split.split_terminator('/')
} else {
log::trace!("exit: get_path_length -> 0");
return 0;
};
if let Some(last) = segments.last() {
// failure on conversion should be very unlikely. While a usize can absolutely overflow a
// u64, the generally accepted maximum for the length of a url is ~2000. so the value we're
// putting into the u64 should never realistically be anywhere close to producing an
// overflow.
// usize max: 18,446,744,073,709,551,615
// u64 max: 9,223,372,036,854,775,807
let url_len: u64 = last
.len()
.try_into()
.expect("Failed usize -> u64 conversion");
log::trace!("exit: get_path_length -> {}", url_len);
return url_len;
}
log::trace!("exit: get_path_length -> 0");
0
}
/// Simple helper to abstract away adding a forward-slash to a url if not present
///
/// used mostly for deduplication purposes and url state tracking
@@ -483,7 +442,7 @@ mod tests {
let handles = Arc::new(Handles::for_testing(None, None).0);
let url = FeroxUrl::from_string("http://localhost", handles);
for ext in ["rocks", "fun"] {
let to_check = format!("http://localhost/upload/ferox.{}", ext);
let to_check = format!("http://localhost/upload/ferox.{ext}");
assert_eq!(
url.format("//upload/ferox", Some(ext)).unwrap(),
reqwest::Url::parse(&to_check[..]).unwrap()

View File

@@ -12,16 +12,17 @@ use std::{
time::Duration,
time::{SystemTime, UNIX_EPOCH},
};
use tokio::sync::mpsc::UnboundedSender;
use tokio::sync::{mpsc::UnboundedSender, oneshot};
use crate::config::Configuration;
use crate::{
config::Configuration,
config::OutputLevel,
event_handlers::{
Command::{self, AddError, AddStatus},
Handles,
},
progress::PROGRESS_PRINTER,
response::FeroxResponse,
send_command,
statistics::StatError::{Connection, Other, Redirection, Request, Timeout},
traits::FeroxSerialize,
@@ -40,7 +41,7 @@ pub fn open_file(filename: &str) -> Result<BufWriter<fs::File>> {
.create(true)
.append(true)
.open(filename)
.with_context(|| fmt_err(&format!("Could not open {}", filename)))?;
.with_context(|| fmt_err(&format!("Could not open {filename}")))?;
let writer = BufWriter::new(file); // std io
@@ -67,6 +68,20 @@ pub fn fmt_err(msg: &str) -> String {
format!("{}: {}", status_colorizer("ERROR"), msg)
}
/// given a FeroxResponse, send a TryRecursion command
///
/// moved to utils to allow for calls from extractor and scanner
pub(crate) async fn send_try_recursion_command(
handles: Arc<Handles>,
response: FeroxResponse,
) -> Result<()> {
handles.send_scan_command(Command::TryRecursion(Box::new(response.clone())))?;
let (tx, rx) = oneshot::channel::<bool>();
handles.send_scan_command(Command::Sync(tx))?;
rx.await?;
Ok(())
}
/// Takes in a string and colors it using console::style
///
/// mainly putting this here in case i want to change the color later, making any changes easy
@@ -89,7 +104,7 @@ pub fn ferox_print(msg: &str, bar: &ProgressBar) {
bar.println(msg);
} else {
let stripped = strip_ansi_codes(msg);
println!("{}", stripped);
println!("{stripped}");
}
}
@@ -146,7 +161,7 @@ pub async fn make_request(
let mut request = client.request(Method::from_bytes(method.as_bytes())?, url.to_owned());
if (!config.proxy.is_empty() || config.replay_proxy.is_empty())
if (!config.proxy.is_empty() || !config.replay_proxy.is_empty())
&& data.is_none()
&& ["post", "put", "patch"].contains(&method.to_ascii_lowercase().as_str())
{
@@ -250,14 +265,19 @@ pub fn create_report_string(
) -> String {
if matches!(output_level, OutputLevel::Silent) {
// --silent used, just need the url
format!("{}\n", url)
format!("{url}\n")
} else {
// normal printing with status and sizes
let color_status = status_colorizer(status);
format!(
"{} {:>8} {:>8}l {:>8}w {:>8}c {}\n",
color_status, method, line_count, word_count, content_length, url
)
if status.contains("MSG") {
format!(
"{color_status} {method:>8} {line_count:>9} {word_count:>9} {content_length:>9} {url}\n"
)
} else {
format!(
"{color_status} {method:>8} {line_count:>8}l {word_count:>8}w {content_length:>8}c {url}\n"
)
}
}
}
@@ -401,7 +421,7 @@ fn should_deny_absolute(url_to_test: &Url, denier: &Url, handles: Arc<Handles>)
// to a scanned url that is also a parent of the given url
for ferox_scan in handles.ferox_scans()?.get_active_scans() {
let scanner = Url::parse(ferox_scan.url().trim_end_matches('/'))
.with_context(|| format!("Could not parse {} as a url", ferox_scan))?;
.with_context(|| format!("Could not parse {ferox_scan} as a url"))?;
if let Some(scan_host) = scanner.host() {
// same domain/ip check we perform on the denier above
@@ -499,14 +519,14 @@ pub fn slugify_filename(url: &str, prefix: &str, suffix: &str) -> String {
.as_secs();
let altered_prefix = if !prefix.is_empty() {
format!("{}-", prefix)
format!("{prefix}-")
} else {
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}");
log::trace!("exit: slugify -> {}", filename);
filename

View File

@@ -784,7 +784,6 @@ fn banner_prints_filter_status() {
.and(predicate::str::contains("http://localhost"))
.and(predicate::str::contains("Threads"))
.and(predicate::str::contains("Wordlist"))
.and(predicate::str::contains("Status Codes"))
.and(predicate::str::contains("Timeout (secs)"))
.and(predicate::str::contains("User-Agent"))
.and(predicate::str::contains("Status Code Filters"))
@@ -1394,3 +1393,42 @@ fn banner_prints_all_composite_settings_burp_replay() {
.and(predicate::str::contains("─┴─")),
);
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + force recursion
fn banner_prints_force_recursion() {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("--force-recursion")
.arg("--wordlist")
.arg("/definitely/doesnt/exist/0cd7fed0-47f4-4b18-a1b0-ac39708c1676")
.assert()
.success()
.stderr(
predicate::str::contains("─┬─")
.and(predicate::str::contains("Target Url"))
.and(predicate::str::contains("http://localhost"))
.and(predicate::str::contains("Threads"))
.and(predicate::str::contains("Wordlist"))
.and(predicate::str::contains("Status Codes"))
.and(predicate::str::contains("Timeout (secs)"))
.and(predicate::str::contains("User-Agent"))
.and(predicate::str::contains("Force Recursion"))
.and(predicate::str::contains("─┴─")),
);
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + force recursion
fn banner_prints_update_app() {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--update")
.assert()
.success()
.stdout(predicate::str::contains("Checking target-arch..."));
}

View File

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

View File

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

View File

@@ -180,68 +180,18 @@ fn test_static_wildcard_request_found() -> Result<(), Box<dyn std::error::Error>
teardown_tmp_directory(tmp_dir);
cmd.assert().success().stdout(
predicate::str::contains("WLD")
.and(predicate::str::contains("Got"))
predicate::str::contains("GET")
.and(predicate::str::contains(
"Auto-filtering found 404-like response and created new filter",
))
.and(predicate::str::contains("200"))
.and(predicate::str::contains("(url length: 32)")),
.and(predicate::str::contains("1l")),
);
assert_eq!(mock.hits(), 1);
Ok(())
}
#[test]
/// test finds a dynamic wildcard and reports as much to stdout and a file
fn test_dynamic_wildcard_request_found() {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
let outfile = tmp_dir.path().join("outfile");
let mock = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
let mock2 = srv.mock(|when, then| {
when.method(GET).path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
then.status(200).body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--add-slash")
.arg("--output")
.arg(outfile.as_os_str())
.unwrap();
let contents = std::fs::read_to_string(outfile).unwrap();
teardown_tmp_directory(tmp_dir);
assert!(contents.contains("WLD"));
assert!(contents.contains("Got"));
assert!(contents.contains("200"));
assert!(contents.contains("(url length: 32)"));
assert!(contents.contains("(url length: 96)"));
cmd.assert().success().stdout(
predicate::str::contains("WLD")
.and(predicate::str::contains("Got"))
.and(predicate::str::contains("200"))
.and(predicate::str::contains("(url length: 32)"))
.and(predicate::str::contains("(url length: 96)")),
);
assert_eq!(mock.hits(), 1);
assert_eq!(mock2.hits(), 1);
}
#[test]
/// uses dont_filter, so the normal wildcard test should never happen
fn heuristics_static_wildcard_request_with_dont_filter() -> Result<(), Box<dyn std::error::Error>> {
@@ -269,51 +219,53 @@ fn heuristics_static_wildcard_request_with_dont_filter() -> Result<(), Box<dyn s
Ok(())
}
#[test]
/// test finds a static wildcard and reports as much to stdout
fn heuristics_wildcard_test_with_two_static_wildcards() {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
// #[test]
// /// test finds a static wildcard and reports as much to stdout
// fn heuristics_wildcard_test_with_two_static_wildcards() {
// let srv = MockServer::start();
// let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
let mock = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
// let mock = srv.mock(|when, then| {
// when.method(GET)
// .path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
// then.status(200)
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
// });
let mock2 = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
// let mock2 = srv.mock(|when, then| {
// when.method(GET)
// .path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
// then.status(200)
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
// });
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--add-slash")
.unwrap();
// let cmd = Command::cargo_bin("feroxbuster")
// .unwrap()
// .arg("--url")
// .arg(srv.url("/"))
// .arg("--wordlist")
// .arg(file.as_os_str())
// .arg("--add-slash")
// .arg("--threads")
// .arg("1")
// .unwrap();
teardown_tmp_directory(tmp_dir);
// teardown_tmp_directory(tmp_dir);
cmd.assert().success().stdout(
predicate::str::contains("WLD")
.and(predicate::str::contains("Got"))
.and(predicate::str::contains("200"))
.and(predicate::str::contains("(url length: 32)"))
.and(predicate::str::contains("(url length: 96)"))
.and(predicate::str::contains(
"Wildcard response is static; auto-filtering 46",
)),
);
// cmd.assert().success().stdout(
// predicate::str::contains("WLD")
// .and(predicate::str::contains("Got"))
// .and(predicate::str::contains("200"))
// .and(predicate::str::contains("(url length: 32)"))
// .and(predicate::str::contains("(url length: 96)"))
// .and(predicate::str::contains(
// "Wildcard response is static; auto-filtering 46",
// )),
// );
assert_eq!(mock.hits(), 1);
assert_eq!(mock2.hits(), 1);
}
// assert_eq!(mock.hits(), 1);
// assert_eq!(mock2.hits(), 1);
// }
#[test]
/// test finds a static wildcard and reports nothing to stdout
@@ -324,14 +276,14 @@ fn heuristics_wildcard_test_with_two_static_wildcards_with_silent_enabled(
let mock = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
.path_matches(Regex::new("/.?[a-zA-Z0-9]{32,}").unwrap());
then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
let mock2 = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
.path_matches(Regex::new("/LICENSE").unwrap());
then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
@@ -342,132 +294,142 @@ fn heuristics_wildcard_test_with_two_static_wildcards_with_silent_enabled(
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--add-slash")
.arg("--silent")
.arg("--threads")
.arg("1")
.unwrap();
teardown_tmp_directory(tmp_dir);
cmd.assert().success().stdout(predicate::str::is_empty());
cmd.assert()
.success()
.stdout(predicate::str::contains(srv.url("/")));
assert_eq!(mock.hits(), 1);
assert_eq!(mock.hits(), 4);
assert_eq!(mock2.hits(), 1);
Ok(())
}
#[test]
/// test finds a static wildcard and reports as much to stdout and a file
fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file() {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
let outfile = tmp_dir.path().join("outfile");
// #[test]
// /// test finds a static wildcard and reports as much to stdout and a file
// fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file() {
// let srv = MockServer::start();
// let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
// let outfile = tmp_dir.path().join("outfile");
let mock = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
// let mock = srv.mock(|when, then| {
// when.method(GET)
// .path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
// then.status(200)
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
// });
let mock2 = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
// let mock2 = srv.mock(|when, then| {
// when.method(GET)
// .path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
// then.status(200)
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
// });
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--add-slash")
.arg("--output")
.arg(outfile.as_os_str())
.unwrap();
// let cmd = Command::cargo_bin("feroxbuster")
// .unwrap()
// .arg("--url")
// .arg(srv.url("/"))
// .arg("--wordlist")
// .arg(file.as_os_str())
// .arg("--add-slash")
// .arg("--output")
// .arg(outfile.as_os_str())
// .arg("--threads")
// .arg("1")
// .unwrap();
let contents = std::fs::read_to_string(outfile).unwrap();
// let contents = std::fs::read_to_string(outfile).unwrap();
teardown_tmp_directory(tmp_dir);
// teardown_tmp_directory(tmp_dir);
assert!(contents.contains("WLD"));
assert!(contents.contains("Got"));
assert!(contents.contains("200"));
assert!(contents.contains("(url length: 32)"));
assert!(contents.contains("(url length: 96)"));
// assert!(contents.contains("WLD"));
// assert!(contents.contains("Got"));
// assert!(contents.contains("200"));
// assert!(contents.contains("(url length: 32)"));
// assert!(contents.contains("(url length: 96)"));
cmd.assert().success().stdout(
predicate::str::contains("WLD")
.and(predicate::str::contains("Got"))
.and(predicate::str::contains("200"))
.and(predicate::str::contains("(url length: 32)"))
.and(predicate::str::contains("(url length: 96)"))
.and(predicate::str::contains(
"Wildcard response is static; auto-filtering 46",
)),
);
// cmd.assert().success().stdout(
// predicate::str::contains("WLD")
// .and(predicate::str::contains("Got"))
// .and(predicate::str::contains("200"))
// .and(predicate::str::contains("(url length: 32)"))
// .and(predicate::str::contains("(url length: 96)"))
// .and(predicate::str::contains(
// "Wildcard response is static; auto-filtering 46",
// )),
// );
assert_eq!(mock.hits(), 1);
assert_eq!(mock2.hits(), 1);
}
// assert_eq!(mock.hits(), 1);
// assert_eq!(mock2.hits(), 1);
// }
#[test]
/// test finds a static wildcard that returns 3xx, expect redirects to => in response as well as
/// in the output file
fn heuristics_wildcard_test_with_redirect_as_response_code(
) -> Result<(), Box<dyn std::error::Error>> {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
let outfile = tmp_dir.path().join("outfile");
// #[test]
// /// test finds a static wildcard that returns 3xx, expect redirects to => in response as well as
// /// in the output file
// fn heuristics_wildcard_test_with_redirect_as_response_code(
// ) -> Result<(), Box<dyn std::error::Error>> {
// let srv = MockServer::start();
let mock = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
then.status(301)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
// let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
// let outfile = tmp_dir.path().join("outfile");
let mock2 = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
then.status(301)
.header("Location", &srv.url("/some-redirect"))
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
// let mock = srv.mock(|when, then| {
// when.method(GET)
// .path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
// then.status(301)
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
// });
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--add-slash")
.arg("--output")
.arg(outfile.as_os_str())
.unwrap();
// let mock2 = srv.mock(|when, then| {
// when.method(GET)
// .path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
// then.status(301)
// .header("Location", &srv.url("/some-redirect"))
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
// });
let contents = std::fs::read_to_string(outfile).unwrap();
// let cmd = Command::cargo_bin("feroxbuster")
// .unwrap()
// .arg("--url")
// .arg(srv.url("/"))
// .arg("--wordlist")
// .arg(file.as_os_str())
// .arg("--add-slash")
// .arg("--output")
// .arg(outfile.as_os_str())
// .arg("--threads")
// .arg("1")
// .unwrap();
teardown_tmp_directory(tmp_dir);
// let contents = std::fs::read_to_string(outfile).unwrap();
assert!(contents.contains("WLD"));
assert!(contents.contains("301"));
assert!(contents.contains("/some-redirect"));
assert!(contents.contains(" => "));
assert!(contents.contains(&srv.url("/")));
assert!(contents.contains("(url length: 32)"));
// teardown_tmp_directory(tmp_dir);
cmd.assert().success().stdout(
predicate::str::contains(" => ")
.and(predicate::str::contains("/some-redirect"))
.and(predicate::str::contains("301"))
.and(predicate::str::contains(srv.url("/")))
.and(predicate::str::contains("(url length: 32)"))
.and(predicate::str::contains("WLD")),
);
// assert!(contents.contains("WLD"));
// assert!(contents.contains("301"));
// assert!(contents.contains("/some-redirect"));
// assert!(contents.contains(" => "));
// assert!(contents.contains(&srv.url("/")));
// assert!(contents.contains("(url length: 32)"));
assert_eq!(mock.hits(), 1);
assert_eq!(mock2.hits(), 1);
Ok(())
}
// cmd.assert().success().stdout(
// predicate::str::contains(" => ")
// .and(predicate::str::contains("/some-redirect"))
// .and(predicate::str::contains("301"))
// .and(predicate::str::contains(srv.url("/")))
// .and(predicate::str::contains("(url length: 32)"))
// .and(predicate::str::contains("WLD")),
// );
// assert_eq!(mock.hits(), 1);
// assert_eq!(mock2.hits(), 1);
// Ok(())
// }
// todo figure out why ci hates these tests

View File

@@ -127,7 +127,7 @@ fn main_parallel_spawns_children() -> Result<(), Box<dyn std::error::Error>> {
);
let contents = read_to_string(outfile).unwrap();
println!("contents: {}", contents);
println!("contents: {contents}");
assert!(contents.contains("parallel branch && wrapped main")); // exits parallel branch
@@ -196,10 +196,10 @@ fn main_parallel_creates_output_directory() -> Result<(), Box<dyn std::error::Er
let file_regex = Regex::new("ferox-[a-zA-Z_:0-9]+-[0-9]+.log").unwrap();
let dir_regex = Regex::new("output-file-[0-9]+.logs").unwrap();
let sub_dir = output_dir.as_ref().join(&sub_dir);
let sub_dir = output_dir.as_ref().join(sub_dir);
// created directory like output-file-1627845741.logs/
assert!(dir_regex.is_match(&sub_dir.to_string_lossy().to_string()));
assert!(dir_regex.is_match(&sub_dir.to_string_lossy()));
for entry in sub_dir.read_dir()? {
let entry = entry?;
@@ -218,3 +218,46 @@ fn main_parallel_creates_output_directory() -> Result<(), Box<dyn std::error::Er
Ok(())
}
#[test]
/// download a wordlist from a url
fn main_download_wordlist_from_url() -> Result<(), Box<dyn std::error::Error>> {
let srv = MockServer::start();
let (tmp_dir, _) = setup_tmp_directory(&["a".to_string()], "wordlist")?;
let mock1 = srv.mock(|when, then| {
when.method(GET).path("/derp");
then.status(200).body("stuff\nthings");
});
// serve endpoints stuff and things
let mock2 = srv.mock(|when, then| {
when.method(GET).path("/stuff");
then.status(200);
});
let mock3 = srv.mock(|when, then| {
when.method(GET).path("/things");
then.status(200);
});
Command::cargo_bin("feroxbuster")
.unwrap()
.current_dir(&tmp_dir)
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(srv.url("/derp"))
.assert()
.success()
.stderr(predicate::str::contains(srv.url("/derp")));
teardown_tmp_directory(tmp_dir);
assert_eq!(mock1.hits(), 1); // downloaded wordlist
assert_eq!(mock2.hits(), 1); // found stuff from wordlist
assert_eq!(mock3.hits(), 1); // found things from wordlist
Ok(())
}

View File

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

View File

@@ -92,7 +92,7 @@ fn auto_bail_cancels_scan_with_timeouts() {
.parse::<usize>()
.unwrap();
println!("expected: {}", total_expected);
println!("expected: {total_expected}");
// without bailing, should be 6180; after bail decreases significantly
assert!(total_expected < 5000);
}
@@ -161,7 +161,7 @@ fn auto_bail_cancels_scan_with_403s() {
let str_msg = message.as_str().unwrap_or_default().to_string();
if str_msg.starts_with("Stats") {
println!("{}", str_msg);
println!("{str_msg}");
let re = Regex::new("total_expected: ([0-9]+),").unwrap();
assert!(re.is_match(&str_msg));
let total_expected = re
@@ -171,7 +171,7 @@ fn auto_bail_cancels_scan_with_403s() {
.map_or("", |m| m.as_str())
.parse::<usize>()
.unwrap();
println!("total_expected: {}", total_expected);
println!("total_expected: {total_expected}");
assert!(total_expected < 5000);
}
}
@@ -243,7 +243,7 @@ fn auto_bail_cancels_scan_with_429s() {
let str_msg = message.as_str().unwrap_or_default().to_string();
if str_msg.starts_with("Stats") {
println!("{}", str_msg);
println!("{str_msg}");
let re = Regex::new("total_expected: ([0-9]+),").unwrap();
assert!(re.is_match(&str_msg));
let total_expected = re
@@ -253,7 +253,7 @@ fn auto_bail_cancels_scan_with_429s() {
.map_or("", |m| m.as_str())
.parse::<usize>()
.unwrap();
println!("total_expected: {}", total_expected);
println!("total_expected: {total_expected}");
assert!(total_expected < 5000);
}
}

View File

@@ -20,14 +20,16 @@ fn resume_scan_works() {
// localhost:PORT/ <- complete
// localhost:PORT/js <- will get scanned with /css and /stuff
let complete_scan = format!(
r#"{{"id":"057016a14769414aac9a7a62707598cb","url":"{}","scan_type":"Directory","status":"Complete"}}"#,
srv.url("/")
r#"{{"id":"057016a14769414aac9a7a62707598cb","url":"{}","normalized_url":"{}","scan_type":"Directory","status":"Complete","num_requests":4174,"requests_made_so_far":0}}"#,
srv.url("/"),
srv.url("/"),
);
let incomplete_scan = format!(
r#"{{"id":"400b2323a16f43468a04ffcbbeba34c6","url":"{}","scan_type":"Directory","status":"NotStarted"}}"#,
r#"{{"id":"400b2323a16f43468a04ffcbbeba34c6","url":"{}","normalized_url":"{}/","scan_type":"Directory","status":"NotStarted","num_requests":4174,"requests_made_so_far":0}}"#,
srv.url("/js"),
srv.url("/js")
);
let scans = format!(r#""scans":[{},{}]"#, complete_scan, incomplete_scan);
let scans = format!(r#""scans":[{complete_scan},{incomplete_scan}]"#);
let config = format!(
r#""config": {{"type":"configuration","wordlist":"{}","config":"","proxy":"","replay_proxy":"","target_url":"{}","status_codes":[200,204,301,302,307,308,401,403,405],"replay_codes":[200,204,301,302,307,308,401,403,405],"filter_status":[],"threads":50,"timeout":7,"verbosity":0,"silent":false,"quiet":false,"json":false,"output":"","debug_log":"","user_agent":"feroxbuster/1.9.0","redirects":false,"insecure":false,"extensions":[],"headers":{{}},"queries":[],"no_recursion":false,"extract_links":false,"add_slash":false,"stdin":false,"depth":2,"scan_limit":1,"filter_size":[],"filter_line_count":[],"filter_word_count":[],"filter_regex":[],"dont_filter":false}}"#,
@@ -40,7 +42,7 @@ fn resume_scan_works() {
r#"{{"type":"response","url":"{}","path":"/js/css","wildcard":true,"status":301,"content_length":173,"line_count":10,"word_count":16,"headers":{{"server":"nginx/1.16.1"}}}}"#,
srv.url("/js/css")
);
let responses = format!(r#""responses":[{}]"#, response);
let responses = format!(r#""responses":[{response}]"#);
// not scanned because /js is not complete, and /js/stuff response is not known
let not_scanned_yet = srv.mock(|when, then| {
@@ -61,11 +63,13 @@ fn resume_scan_works() {
then.status(200).body("two words");
});
let state_file_contents = format!("{{{},{},{}}}", scans, config, responses);
let state_file_contents = format!("{{{scans},{config},{responses}}}");
let (tmp_dir2, state_file) = setup_tmp_directory(&[state_file_contents], "state-file").unwrap();
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("-vvv")
.arg("--resume-from")
.arg(state_file.as_os_str())
.assert()

View File

@@ -53,17 +53,17 @@ fn scanner_recursive_request_scan() -> Result<(), Box<dyn std::error::Error>> {
let js_mock = srv.mock(|when, then| {
when.method(GET).path("/js");
then.status(301).header("Location", &srv.url("/js/"));
then.status(301).header("Location", srv.url("/js/"));
});
let js_prod_mock = srv.mock(|when, then| {
when.method(GET).path("/js/prod");
then.status(301).header("Location", &srv.url("/js/prod/"));
then.status(301).header("Location", srv.url("/js/prod/"));
});
let js_dev_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev");
then.status(301).header("Location", &srv.url("/js/dev/"));
then.status(301).header("Location", srv.url("/js/dev/"));
});
let js_dev_file_mock = srv.mock(|when, then| {
@@ -116,17 +116,17 @@ fn scanner_recursive_request_scan_using_only_success_responses(
let js_mock = srv.mock(|when, then| {
when.method(GET).path("/js/");
then.status(200).header("Location", &srv.url("/js/"));
then.status(200).header("Location", srv.url("/js/"));
});
let js_prod_mock = srv.mock(|when, then| {
when.method(GET).path("/js/prod/");
then.status(200).header("Location", &srv.url("/js/prod/"));
then.status(200).header("Location", srv.url("/js/prod/"));
});
let js_dev_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev/");
then.status(200).header("Location", &srv.url("/js/dev/"));
then.status(200).header("Location", srv.url("/js/dev/"));
});
let js_dev_file_mock = srv.mock(|when, then| {
@@ -454,7 +454,7 @@ fn scanner_single_request_scan_with_debug_logging() {
.unwrap();
let contents = std::fs::read_to_string(outfile).unwrap();
println!("{}", contents);
println!("{contents}");
assert!(contents.starts_with("Configuration {"));
assert!(contents.contains("TRC"));
assert!(contents.contains("DBG"));
@@ -492,7 +492,7 @@ fn scanner_single_request_scan_with_debug_logging_as_json() {
.unwrap();
let contents = std::fs::read_to_string(outfile).unwrap();
println!("{}", contents);
println!("{contents}");
assert!(contents.starts_with("{\"type\":\"configuration\""));
assert!(contents.contains("\"level\":\"TRACE\""));
assert!(contents.contains("\"level\":\"DEBUG\""));
@@ -587,9 +587,12 @@ fn scanner_recursion_works_with_403_directories() {
cmd.assert().success().stdout(
predicate::str::contains("/LICENSE")
.count(2)
.and(predicate::str::contains("200").count(2))
.and(predicate::str::contains("403"))
.and(predicate::str::contains("53c"))
.and(predicate::str::contains("200"))
.and(predicate::str::contains("404"))
.and(predicate::str::contains("53c Auto-filtering"))
.and(predicate::str::contains(
"Auto-filtering found 404-like response and created new filter;",
))
.and(predicate::str::contains("14c"))
.and(predicate::str::contains("0c"))
.and(predicate::str::contains("ignored").count(2))
@@ -651,7 +654,7 @@ fn add_discovered_extension_updates_bars_and_stats() {
)
.unwrap();
srv.mock(|when, then| {
let mock = srv.mock(|when, then| {
when.method(GET).path("/stuff.php");
then.status(200).body("cool... coolcoolcool");
});
@@ -675,10 +678,11 @@ fn add_discovered_extension_updates_bars_and_stats() {
.assert()
.success();
mock.assert_hits(1);
let contents = std::fs::read_to_string(file_path).unwrap();
println!("{}", contents);
println!("{contents}");
assert!(contents.contains("discovered new extension: php"));
assert!(contents.contains("extensions_collected: 1"));
// assert!(contents.contains("extensions_collected: 1")); // this is racy
assert!(contents.contains("expected_per_scan: 6"));
}
@@ -852,3 +856,66 @@ fn collect_words_makes_appropriate_requests() {
teardown_tmp_directory(tmp_dir);
}
#[test]
/// send a request to an endpoint that has abnormal redirect logic, ala fast-api
fn scanner_forced_recursion_ignores_normal_redirect_logic() -> Result<(), Box<dyn std::error::Error>>
{
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
let mock1 = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(301)
.body("this is a test")
.header("Location", srv.url("/LICENSE"));
});
let mock2 = srv.mock(|when, then| {
when.method(GET).path("/LICENSE/LICENSE");
then.status(404);
});
let mock3 = srv.mock(|when, then| {
when.method(GET).path("/LICENSE/LICENSE/LICENSE");
then.status(404);
});
let mock4 = srv.mock(|when, then| {
when.method(GET).path("/LICENSE/LICENSE/LICENSE/LICENSE");
then.status(404);
});
let outfile = tmp_dir.path().join("output");
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--force-recursion")
.arg("--dont-filter")
.arg("--status-codes")
.arg("301")
.arg("200")
.arg("-o")
.arg(outfile.as_os_str())
.unwrap();
let contents = std::fs::read_to_string(outfile)?;
println!("{contents}");
assert!(contents.contains("/LICENSE"));
assert!(contents.contains("301"));
assert!(contents.contains("14"));
assert_eq!(mock1.hits(), 2);
assert_eq!(mock2.hits(), 1);
assert_eq!(mock3.hits(), 0);
assert_eq!(mock4.hits(), 0);
teardown_tmp_directory(tmp_dir);
Ok(())
}

View File

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