Compare commits

...

507 Commits
v2.6.3 ... main

Author SHA1 Message Date
allcontributors[bot]
378d75964c docs: add aldamd as a contributor for code (#1309)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2026-04-15 08:31:37 -04:00
Daniel Aldam
ffdf871abe fix: support LF-only line endings in --request-file parsing (#1306)
* fix: support LF-only line endings in --request-file parsing
* preserve raw body when parsing request file
* Added CRLFCRLF/LFLF request-file parsing tests
* better invalid UTF-8 in request file header error message
* strengthened request-file mixed CRLF LF headers test
* added request-file explicit binary body test
* cargo fmt
* updated request-file header-body separation logic to choose first occurrence & added testing
2026-04-15 08:30:39 -04:00
epi
bedf4d3f8e visual update for readme 2026-04-12 08:02:37 -04:00
epi
7787c83e1e updated readme with pro domain 2026-04-12 07:47:56 -04:00
allcontributors[bot]
242b134a3d docs: add ghsdpolley as a contributor for bug (#1300)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2026-02-08 09:37:34 -05:00
epi
b4ceaef08d removed is_file check from path extraction (#1299) 2026-02-08 09:36:53 -05:00
allcontributors[bot]
143d5710fc docs: add redacean as a contributor for bug (#1296)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2026-02-06 09:25:18 -05:00
epi
0efb0684b5 1293 fix parser unwrap (#1295)
* removed help unwraps

* clippy

* refixed clippy fix
2026-02-06 09:23:37 -05:00
allcontributors[bot]
c7ed9c9899 docs: add Antonio-R1 as a contributor for code, and bug (#1291)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2026-01-21 20:55:27 -05:00
Antonio
510bad0473 Improve the robots.txt regex (#1290)
Co-authored-by: sbiotto <54334833+sbiotto@users.noreply.github.com>
2026-01-21 20:54:36 -05:00
allcontributors[bot]
23661d17c9 docs: add OpenSourceKyle as a contributor for doc, and bug (#1288)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2026-01-17 06:52:48 -05:00
epi
097d54f384 Add security notice for domain impersonation
Added a security notice regarding domain impersonation and official download sources.
2026-01-17 06:47:48 -05:00
epi
970ce73ac4 Merge branch 'main' of github.com:epi052/feroxbuster 2025-12-24 05:46:10 -05:00
epi
5bb42c4004 updated integration tests to use cargo_bin! macro 2025-12-24 05:45:10 -05:00
allcontributors[bot]
0732ee11ef docs: add sebastiaanspeck as a contributor for bug, and doc (#1286)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-12-24 05:04:04 -05:00
epi
47b4efdd1b updated links to new docs 2025-12-24 05:01:48 -05:00
epi
e50e150fb9 Update links in README for example usage and documentation 2025-12-24 04:47:31 -05:00
epi
84aef80cea Update copyright year in LICENSE file 2025-12-15 19:35:28 -05:00
epi
9fe5bfd622 re-added dockerhub verification 2025-12-13 09:18:43 -05:00
epi
ddd04dac7f Revert workflow changes 2025-12-13 09:17:21 -05:00
epi
aa8e133580 fixed missing artifacts bug 2025-12-13 09:09:44 -05:00
epi
2ec7cda0d4 reverted coverage changes to workflows 2025-12-13 08:57:49 -05:00
epi
ec3d439aaf automated release process 2025-12-13 08:41:53 -05:00
epi
2847b624ab clippy and fixed failing doctest 2025-12-13 06:52:10 -05:00
allcontributors[bot]
b88c11f9a2 docs: add pg9051 as a contributor for doc (#1283)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-12-13 06:42:38 -05:00
allcontributors[bot]
94d03a82bc docs: add mzember as a contributor for bug (#1282)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-12-13 06:38:23 -05:00
allcontributors[bot]
b9798ab223 docs: add auk0x01 as a contributor for code (#1281)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-12-13 06:35:13 -05:00
Adnan Ullah Khan (auk0x01)
328f858696 Added web fonts to ignored extensions list (#1274) 2025-12-13 06:33:38 -05:00
epi
c8bcfb8f01 fixed rate limiting
* fixed requests/sec for small values

* ensured limit var is never 0 in build_a_bucket, not just refill

* removed unnecessary cooldown flag manipulation in cool_down func

* removed minor toctou in should_enforce_policy

* added new flag releases before returns from should_enforce_policy

* cleaned up how limitheap is initialized from tune func

* added (more) safety/bounds checks to limitheap

* capped timeout to 30sec; added lock error logging

* added per-trigger error tracking to policy data

* updated requester to use new policy data per-trigger errors

* fixed race condition in progress bar message display; fixed tests

* touched up a few minor issues in nlp

* fixed req/sec test

* fixed more tests

* added new test suite for tuning; fixed more tests

* clippy/fmt

* fixed possible deadlock in error path for tune/bail

* fixed a handful of minor correctness issues

* removed unnecessary array allocation for error tracking

* --rate-limit now serves as a hard cap, in general and on --auto-tune if both are provided together

* renamed test file

* bumped version to 2.13.1

* added new dirlisting detection heuristics

* clippy

* nitpickery
2025-12-13 05:55:37 -05:00
allcontributors[bot]
2f608c505f docs: add 0x7274 as a contributor for bug, ideas, and doc (#1273)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-10-10 16:19:48 -04:00
allcontributors[bot]
def88cc529 docs: add lidorelias3 as a contributor for ideas (#1272)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-10-10 16:18:18 -04:00
epi
a4f873269b 1267 add scope option
* bumped version
* added cli option to parser
* added banner entry
* fixed state file with colon on windows
* tweaked banner name for scoped url
* fixed test with new In-Scope Url banner name
* added STATE_FILENAME env var to control state file name/location
* added ferox config example
* initial implementation complete
* updated ci/cd to add components to fmt/clippy configs
* clippy
* made subdomain detection a bit more robust
* --request-file correctly sets scope values
* added debug windows build
* fixed failing test
2025-10-10 16:17:29 -04:00
allcontributors[bot]
449e301915 docs: add 4FunAndProfit as a contributor for ideas (#1266)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-08-31 19:29:18 -04:00
allcontributors[bot]
93bd25fe2f docs: add 0x7274 as a contributor for bug (#1265)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-08-31 19:28:51 -04:00
allcontributors[bot]
877fdddbf3 docs: add HenriBom as a contributor for bug (#1264)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-08-31 19:28:24 -04:00
allcontributors[bot]
0b7e232546 docs: add wilco375 as a contributor for bug (#1263)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-08-31 19:27:30 -04:00
allcontributors[bot]
aff367101d docs: add s0i37 as a contributor for ideas (#1262)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-08-31 19:25:46 -04:00
allcontributors[bot]
0d536a0d1a docs: add h121h as a contributor for ideas (#1261)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-08-31 19:25:10 -04:00
epi
a9dc872071 v2.12.0 meta branch (#1253)
* updated deps
* bumped version
* increase scan limit via scan management menu (#1254)
* increase scan limit via SMM implemented
* figured out subtracting limits; implemented set-limit in SMM
* removed unneeded to_string; changed SMM header slightly
* removed debugging log statement

* 817 scan limit via scan mgmt menu (#1255)

* added waiting as a scan status for vis in smm

* 635/1240 unique responses (#1256)

* added --unique boilerplate
* implemented --unique logic
* added unit tests

* added unique to scan mgmt menu

* fixed tests using termouthandler

* added integration tests

* changed implementation to simhash with hamming dist=1

* cleaned up code; fixed tests

* tweaked docstring for config

* removed toggleunique logic

* removed toggleunique logic

* removed old unique logic

* moved hamming distance constants out to lib.rs

* updated filter to use self.cuttof instead of constant

* fixed bug filed under issue #1077 (#1257)

* updated linkfinder regex

* improve ssl error message (#1258)

* improved ssl error message (again)

* removed unnecessary type statement

* add max size read option (#1260)

* implemented --response-size-limit, need tests and docs

* added tests
* fmt
2025-08-31 19:24:16 -04:00
allcontributors[bot]
33fe6350bc docs: add karanabe as a contributor for code (#1252)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-08-03 09:18:50 -04:00
karanabe
1f7214f617 fix clippy errors when denying warnings (#1247)
* fix clippy errors when denying warnings

* fixed additional clippy errors

---------

Co-authored-by: epi <43392618+epi052@users.noreply.github.com>
2025-08-03 09:17:01 -04:00
allcontributors[bot]
8fae4f136b docs: add karanabe as a contributor for doc (#1251)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-08-02 17:03:34 -04:00
karanabe
f4092e947c docs: replace -Zprofile with instrument-coverage (#1245)
* docs: replace -Zprofile with instrument-coverage

* docs: update coverage instructions to include submodules and cleanup profraw

* gitignore drop profraw patterns
2025-08-02 17:03:02 -04:00
epi
3fe21b22ae Fix tests aug 25 (#1250)
* moved doctests to tests module
* fixed auto-bail on timeout tests
2025-08-02 17:02:21 -04:00
epi
29b8a4a9a0 updated deps 2025-08-02 08:22:52 -04:00
allcontributors[bot]
a9ff23be84 docs: add zar3bski as a contributor for code, and ideas (#1249)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-08-02 08:12:24 -04:00
zar3bski
1b576fc7e6 Feat/1117 auto content type (#1234)
* feat: Content-Type set with composite options

* feat: content-type auto, file handling

* fix: log before logger has config for init

* docs: config::utils::ContentType

* fix: use eprintln for preconfig logger
2025-08-02 08:11:45 -04:00
epi
e321a4e0e6 Merge branch 'main' of github.com:epi052/feroxbuster 2025-04-05 14:44:53 -04:00
epi
5ccc190de6 fixed cookie parsing bug 2025-04-05 14:44:42 -04:00
allcontributors[bot]
af3dcdf6a2 docs: add zer0x64 as a contributor for code (#1230)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-03-10 20:49:28 -04:00
zer0x64
d4fd06418b fix typo to regen fish completions (#1229) 2025-03-10 20:48:47 -04:00
epi
3b0e530fb4 changed winget action version 2024-09-15 07:33:23 -04:00
epi
64113b8da4 Merge branch 'migrate-build-pipeline-to-artifact-v4' 2024-09-15 06:54:37 -04:00
epi
e827fd02ad updated lints reported by docker ci 2024-09-15 06:53:06 -04:00
epi
988fa744b5 update all ci-cd workflows (#1200)
* test AUv2 with debug build

* updated AU to v4 for build pipeline; test 1

* upgraded checkout from v2 to v4; test 1

* migrating from actions-rs to dtolnay stuff; test 1

* migrating from actions-rs to dtolnay stuff; test 2

* migrating from actions-rs to dtolnay stuff; test 3

* migrating from actions-rs to dtolnay stuff; matrix build; test 1

* migrating from actions-rs to dtolnay stuff; matrix build; test 2

* migrating from actions-rs to dtolnay stuff; matrix build; test 3

* migrating from actions-rs to dtolnay stuff; matrix build; test 4

* migrating from actions-rs to dtolnay stuff; matrix build; test 5

* migrating from actions-rs to dtolnay stuff; matrix build; test 6

* migrating from actions-rs to dtolnay stuff; matrix build; test 7

* updated ci pipeline

* updated ci pipeline; test 2

* remove cruft

* updated ci pipeline; test 3

* updated ci pipeline; test 4

* updated the rest

* updated the rest; test 2

* updated codecov action
2024-09-15 05:49:17 -04:00
epi
56d0ebaa59 updated codecov action 2024-09-15 05:37:02 -04:00
epi
0cee8d4a7d updated the rest; test 2 2024-09-15 05:24:23 -04:00
epi
34a9eb236b updated the rest 2024-09-14 22:57:49 -04:00
epi
5acaf47db4 updated ci pipeline; test 4 2024-09-14 22:53:49 -04:00
epi
c0243475e4 updated ci pipeline; test 3 2024-09-14 22:50:54 -04:00
epi
08b3534c33 remove cruft 2024-09-14 22:48:06 -04:00
epi
30877cadb8 updated ci pipeline; test 2 2024-09-14 22:47:47 -04:00
epi
05d550c7f8 updated ci pipeline 2024-09-14 22:45:15 -04:00
epi
a0a836695f migrating from actions-rs to dtolnay stuff; matrix build; test 7 2024-09-14 22:34:55 -04:00
epi
4f959f926d migrating from actions-rs to dtolnay stuff; matrix build; test 6 2024-09-14 22:25:15 -04:00
epi
4b613b716c migrating from actions-rs to dtolnay stuff; matrix build; test 5 2024-09-14 22:22:37 -04:00
epi
24617a63ac migrating from actions-rs to dtolnay stuff; matrix build; test 4 2024-09-14 22:02:24 -04:00
epi
5bbbcc87b0 migrating from actions-rs to dtolnay stuff; matrix build; test 3 2024-09-14 14:57:25 -04:00
epi
fd58223d24 migrating from actions-rs to dtolnay stuff; matrix build; test 2 2024-09-14 14:55:28 -04:00
epi
1206ca835e migrating from actions-rs to dtolnay stuff; matrix build; test 1 2024-09-14 14:52:21 -04:00
epi
8daada6690 migrating from actions-rs to dtolnay stuff; test 3 2024-09-14 14:46:40 -04:00
epi
8599c87174 migrating from actions-rs to dtolnay stuff; test 2 2024-09-14 14:44:10 -04:00
epi
4f83b30424 migrating from actions-rs to dtolnay stuff; test 1 2024-09-14 14:39:02 -04:00
epi
f96466d5f0 upgraded checkout from v2 to v4; test 1 2024-09-14 14:26:00 -04:00
epi
36081fd6eb updated AU to v4 for build pipeline; test 1 2024-09-14 14:16:13 -04:00
epi
a8d8b655a5 test AUv2 with debug build 2024-09-14 14:15:12 -04:00
allcontributors[bot]
ae0bcfab14 docs: add Raymond-JV as a contributor for ideas (#1199)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-09-14 14:07:59 -04:00
allcontributors[bot]
a8dac70ba1 docs: add Tib3rius as a contributor for ideas (#1198)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-09-14 14:06:40 -04:00
allcontributors[bot]
53162bae85 docs: add libklein as a contributor for ideas (#1197)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-09-14 14:05:20 -04:00
allcontributors[bot]
98b2268aa9 docs: add L1-0 as a contributor for ideas (#1196)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-09-14 14:03:43 -04:00
epi
762bfc4e78 907 dont skip dir listings (#1192)
* bumped version to 2.11.0

* updated deps

* new cli options

* added --request-file, --protocol, --scan-dir-listings

* added tests / clippy

* removed errant module definition

* implemented visible bar limiter

* many fixes; feature implemented i believe

* added banner test for limit-bars

* beginning troubleshooting of recursion panic

* put a bandaid on trace-level logging bug

* clippy
2024-09-14 14:00:14 -04:00
epi
b44c52f0ea url now conflicts with parallel (#1174) 2024-06-19 09:34:45 -04:00
Ryan
27061eb1b5 Fix workflow_dispatch trigger for Winget workflow (#1169) 2024-06-17 05:31:53 -04:00
epi
49e54f5722 Update winget.yml 2024-06-16 16:25:28 -04:00
epi
bb01fadd5a Update winget.yml 2024-06-16 16:13:44 -04:00
allcontributors[bot]
70ae679b50 docs: add swordfish0x0 as a contributor for ideas (#1168)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-06-16 15:56:56 -04:00
epi
01da38fa6d Merge branch 'main' of github.com:epi052/feroxbuster 2024-06-16 09:32:44 -04:00
epi
22586f3835 updated regex filter help 2024-06-16 09:32:40 -04:00
allcontributors[bot]
0510cb91aa docs: add sa7mon as a contributor for ideas (#1167)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-06-16 09:28:44 -04:00
allcontributors[bot]
4663ec4cea docs: add L1-0 as a contributor for bug (#1166)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-06-16 09:28:22 -04:00
allcontributors[bot]
e8a98a54d8 docs: add wikamp-collaborator as a contributor for ideas, and infra (#1165)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-06-16 09:28:02 -04:00
allcontributors[bot]
fa42c72ac5 docs: add JulianGR as a contributor for infra, doc, and ideas (#1164)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-06-16 09:27:34 -04:00
allcontributors[bot]
4ce77b5012 docs: add sitiom as a contributor for infra, and doc (#1163)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-06-16 09:27:11 -04:00
epi
72c09854fc upgrade deps 2024-06-16 09:19:01 -04:00
Ryan
17a3d8af9f ci: add winget releaser workflow (#1155)
* ci: add winget releaser workflow
* docs(readme): add winget installation reference
2024-06-16 09:10:28 -04:00
Julián Gómez
b67f1399b3 fixes #1151 (#1152)
fixes issue https://github.com/epi052/feroxbuster/issues/1151
2024-06-16 08:59:44 -04:00
epi
57db4adb69 added headers to regex filtering (#1142)
* added headers to regex filtering

* added regex header test

* added pipeline for mac arm

* bumped version; updated deps; updated .cargo/config to .toml

* -b more robust

* fixed overall prog bar showing 0 eta too early

* fixed ssl error test

* added time estimate to SMM
2024-06-16 08:59:17 -04:00
allcontributors[bot]
87b6589f51 docs: add soutzis as a contributor for bug (#1133)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-04-26 20:18:37 -04:00
allcontributors[bot]
f36897431e docs: add JulianGR as a contributor for ideas (#1132)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-04-26 20:18:03 -04:00
allcontributors[bot]
3c89721f54 docs: add Spidle as a contributor for ideas (#1131)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-04-26 20:17:38 -04:00
allcontributors[bot]
9193614f3c docs: add deadloot as a contributor for ideas (#1130)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-04-26 20:17:01 -04:00
epi
8eb41f40a0 1105 - improve json logs for post processing (#1114)
* added timestamp field to responses
* added targets field to stats object
* clippy
* write config to output file
* --data implies post request if no other method provided
* fixed issue with multiple leading spaces
* ignoring flaky test
* upgraded deps
* bumped version to 2.10.3
2024-04-26 20:15:00 -04:00
epi
f3d6d185cd default to silent when parallel used 2024-03-01 07:38:53 -05:00
allcontributors[bot]
df7b6ab6f9 docs: add rew1nter as a contributor for ideas (#1093)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-02-28 07:11:16 -05:00
allcontributors[bot]
22bed3c9e7 docs: add tritoke as a contributor for code (#1092)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-02-28 07:07:14 -05:00
Sam Leonard
fe0f7d6f3c query fontconfig to determine if Noto Color Emoji is installed (#1083) 2024-02-28 07:06:50 -05:00
allcontributors[bot]
3b0d787ca7 docs: add NotoriousRebel as a contributor for ideas (#1091)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-02-28 07:05:03 -05:00
allcontributors[bot]
eba35b205e docs: add FieldOfRice as a contributor for infra (#1090)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-02-28 07:04:16 -05:00
allcontributors[bot]
ecdd1bce81 docs: add dirhamgithub as a contributor for bug (#1089)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-02-28 07:03:11 -05:00
allcontributors[bot]
0771407939 docs: add amiremami as a contributor for bug (#1088)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-02-28 07:02:28 -05:00
allcontributors[bot]
2ea6b97c86 docs: add n0kovo as a contributor for bug (#1087)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-02-28 07:01:16 -05:00
epi
9ff0253deb parallel time limit enforced individually instead of main thread (#1072)
* parallel time limit enforced individually instead of main thread
* added ability to select silent/quiet when using --parallel
* fixed robots.txt error -> hang
* build pipeline back to main
* fixed up tests
* removed retries from nextest
* clippy
2024-02-28 06:59:43 -05:00
allcontributors[bot]
423889b142 docs: add ArthurMuraro as a contributor for bug (#1069)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-01-24 06:55:30 -05:00
epi
595665cc04 fixed issue where --silent included too much info on found dir (#1067) 2024-01-24 06:50:27 -05:00
allcontributors[bot]
a583e2ff38 docs: add manugramm as a contributor for bug (#1061)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-01-20 16:23:47 -05:00
epi
539851e3e8 headers with commas from cli should parse correctly 2024-01-20 16:19:23 -05:00
allcontributors[bot]
c1e7c5ff59 docs: add Mister7F as a contributor for ideas (#1036)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2023-11-25 09:51:50 -05:00
epi
38a1ed3f63 custom backup extension list (#1035)
* --collect-backups flag changed to accept values
* added default set of backup extensions constant
* tweaked cli parsing to account for changes to --collect-backups
* changed backup collection to use new cli values; fixed bug that could hang ferox on exit
* fmt
* bumped version to 2.10.2
* underped cli logic
2023-11-25 09:51:07 -05:00
allcontributors[bot]
0d55fe2502 docs: add stuhlmann as a contributor for bug (#1034)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2023-11-25 06:36:44 -05:00
epi
a714825d09 remove scan info from update check (#1033)
* removed scan info from github update check
* added build-debug pipeline job
2023-11-25 06:35:20 -05:00
epi
d805e46474 fixed bug with url parsing; re: devx00 2023-11-13 08:44:27 -05:00
allcontributors[bot]
fe71f288e3 docs: add RavySena as a contributor for ideas (#1025)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2023-11-08 21:24:35 -05:00
allcontributors[bot]
a38a0444fe docs: add ocervell as a contributor for ideas (#1024)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2023-11-08 21:23:26 -05:00
allcontributors[bot]
2938094c73 docs: add devx00 as a contributor for bug (#1023)
* docs: update README.md [skip ci]
* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2023-11-08 21:22:40 -05:00
epi
55c67358d6 allow --json in conjunction with --silent (#1022)
* updated parser to allow --silent with --json
* updated config parsing with new silentjson output variant
* added new silentjson output variant
* updated outputlevel usage to include new variant
2023-11-08 21:18:53 -05:00
epi
c3c6fc6753 add http/2 support (#1020)
* added http/2 support
* updated deps
2023-11-08 06:22:00 -05:00
epi
a28ff857ca added test for robots/--dont-extract-links 2023-11-03 06:38:55 -04:00
epi
6c0fe90909 fixed collect backups filtering (#1016)
* fixed collect backups filtering and clippy
* added test for filtered backups
2023-11-03 06:28:09 -04:00
epi
bc486ac8d3 nitpickery; added success msg 2023-10-04 21:43:17 -04:00
epi
fa9d42554f Merge pull request #1001 from epi052/all-contributors/add-N0ur5
docs: add N0ur5 as a contributor for bug
2023-10-04 21:45:11 -04:00
allcontributors[bot]
b78dbe6cc4 docs: update .all-contributorsrc [skip ci] 2023-10-05 01:45:01 +00:00
allcontributors[bot]
29f616f51a docs: update README.md [skip ci] 2023-10-05 01:45:00 +00:00
epi
c1ba5cf942 fixed unwritable cwd bug 2023-10-04 20:53:49 -04:00
epi
e3ec3aee3a Merge pull request #980 from epi052/all-contributors/add-sawmj
docs: add sawmj as a contributor for bug
2023-09-11 21:18:35 -04:00
allcontributors[bot]
52db396aa9 docs: update .all-contributorsrc [skip ci] 2023-09-12 01:17:22 +00:00
allcontributors[bot]
e1066cd5c7 docs: update README.md [skip ci] 2023-09-12 01:17:21 +00:00
epi
d90ee38aad added error message in response to issue #977 2023-09-11 21:10:42 -04:00
epi
a3501ac494 clippy 2023-09-11 08:02:35 -04:00
epi
23827a1d45 fmt 2023-09-11 07:57:54 -04:00
epi
a2b04b2b5e Merge pull request #978 from epi052/all-contributors/add-andreademurtas
docs: add andreademurtas as a contributor for code
2023-09-11 08:00:45 -04:00
epi
362633bc63 Merge pull request #976 from andreademurtas/extensions-from-file
Enable reading extensions from file
2023-09-11 08:00:26 -04:00
allcontributors[bot]
08c5b2bf67 docs: update .all-contributorsrc [skip ci] 2023-09-11 12:00:21 +00:00
allcontributors[bot]
ccef4fd713 docs: update README.md [skip ci] 2023-09-11 12:00:20 +00:00
Andrea De Murtas
4afe0cf95c Update src/config/container.rs
Co-authored-by: epi <43392618+epi052@users.noreply.github.com>
2023-09-10 18:28:48 +02:00
Andrea De Murtas
564686bc5a Update src/config/container.rs
Co-authored-by: epi <43392618+epi052@users.noreply.github.com>
2023-09-10 18:28:42 +02:00
Andrea De Murtas
83f90529e9 Update src/config/container.rs
Co-authored-by: epi <43392618+epi052@users.noreply.github.com>
2023-09-10 18:28:35 +02:00
Andrea De Murtas
ad49320968 enable reading extensions from file 2023-09-07 19:27:31 +02:00
epi
70946ad916 Merge pull request #938 from epi052/all-contributors/add-ktecv2000
docs: add ktecv2000 as a contributor for bug
2023-07-11 05:35:21 -05:00
allcontributors[bot]
fd5c5af5fa docs: update .all-contributorsrc [skip ci] 2023-07-11 10:35:12 +00:00
allcontributors[bot]
ff32aba1db docs: update README.md [skip ci] 2023-07-11 10:35:11 +00:00
epi
cbf028a8ac Merge pull request #937 from epi052/all-contributors/add-sectroyer
docs: add sectroyer as a contributor for bug, and ideas
2023-07-11 05:34:39 -05:00
allcontributors[bot]
8bf80f4eda docs: update .all-contributorsrc [skip ci] 2023-07-11 10:34:19 +00:00
allcontributors[bot]
7c2d09cc22 docs: update README.md [skip ci] 2023-07-11 10:34:18 +00:00
epi
0fb682c121 Merge pull request #936 from epi052/935-fix-scan-menu-range-issue
935 fix scan menu range issue
2023-07-11 05:32:59 -05:00
epi
bcfd8b6eef fixed unwrap in nlp::document 2023-07-11 06:23:18 -04:00
epi
1c9235a56b dont show cancelled scans in scan menu 2023-07-11 06:04:34 -04:00
epi
4d787f08d0 kept indicatif pinned to 17.3 due to bug crashing scan menu 2023-07-11 06:04:09 -04:00
epi
0c7520f5ee clippy 2023-07-11 05:25:53 -04:00
epi
83b55aaf10 updated deps 2023-07-11 05:22:11 -04:00
epi
aea64324f7 cancel scans ignores scantypes of file 2023-07-11 05:14:33 -04:00
epi
8d0614b1a5 Merge pull request #898 from epi052/all-contributors/add-AkechiShiro
docs: add AkechiShiro as a contributor for ideas
2023-05-06 06:47:16 -05:00
allcontributors[bot]
d19cf58af3 docs: update .all-contributorsrc [skip ci] 2023-05-06 11:46:58 +00:00
allcontributors[bot]
bd44bacf95 docs: update README.md [skip ci] 2023-05-06 11:46:57 +00:00
epi
2bf5dc5e6f updated deps 2023-05-06 06:44:18 -05:00
epi
e5fadde073 Merge pull request #892 from lavafroth/client_ssl_cert
Adds certificate management options (unknown servers & mTLS)
2023-05-06 06:38:32 -05:00
epi
ac3fdb1975 added mutual auth testing server and cert generating script 2023-05-06 06:21:08 -05:00
epi
ff40549140 added a little more context to connection errors 2023-05-06 05:54:05 -05:00
epi
0cd25eedfc added client init tests; removed extension requirement 2023-05-06 05:47:22 -05:00
epi
328d1d2ec9 bumped version to 2.10.0 2023-05-06 04:54:17 -05:00
epi
68cc6bc748 nitpickery 2023-05-06 04:53:28 -05:00
epi
f44f320a49 added banner tests 2023-05-06 04:53:10 -05:00
Himadri Bhattacharjee
0965379b9a cargo fmt --all 2023-05-06 09:24:31 +05:30
Himadri Bhattacharjee
4afbf77631 do not use option for server_certs since next() on an empty iterator yields None 2023-05-06 09:14:19 +05:30
epi
5385ce5e99 bumped version to 2.9.6 2023-05-05 18:48:58 -05:00
epi
c8a50d9c0c added new config values to ferox-config example 2023-05-05 18:47:56 -05:00
epi
a3c887f2d7 added config tests for new values 2023-05-05 18:47:01 -05:00
epi
e094dab4a4 key requires cert; accept multiple server certs 2023-05-05 07:07:06 -05:00
epi
c307e6d56d fixed missing client cert issues 2023-05-05 06:32:46 -05:00
Himadri Bhattacharjee
d27cb57d66 simplify certificate and key parsing logic 2023-05-04 08:52:19 +05:30
Himadri Bhattacharjee
372f7c5cd4 add client_key flag to separated PEM key file for identity from_pkcs8_pem 2023-05-04 08:35:25 +05:30
Himadri Bhattacharjee
4986ebdaae cargo clippy 2023-05-02 12:47:47 +05:30
Himadri Bhattacharjee
4198a019d3 added functionality to use SSL key as client identity for mutual authentication 2023-05-02 12:43:45 +05:30
Himadri Bhattacharjee
3b8c6f6ba9 added doc comments for certificate field in Configuration struct 2023-04-30 08:36:47 +05:30
Himadri Bhattacharjee
39f8259f31 documentation for changes in client 2023-04-30 08:36:40 +05:30
Himadri Bhattacharjee
3f5ff1ad3e create Some or None variants for proxy and certificate ahead of time to avoid multiple initializations. 2023-04-30 08:30:17 +05:30
Himadri Bhattacharjee
12206e668f added basic functionality to add root cert to client 2023-04-29 21:07:08 +05:30
Himadri Bhattacharjee
1796e4eeb2 added cert flag 2023-04-29 18:20:04 +05:30
epi
70a8d0f5df Merge pull request #889 from epi052/all-contributors/add-lavafroth
docs: add lavafroth as a contributor for code, and ideas
2023-04-26 19:35:58 -05:00
allcontributors[bot]
11831a3ab5 docs: update .all-contributorsrc [skip ci] 2023-04-27 00:35:40 +00:00
allcontributors[bot]
9356b058eb docs: update README.md [skip ci] 2023-04-27 00:35:39 +00:00
epi
df490f6224 update 2023-04-26 19:35:28 -05:00
epi
4079551c96 update 2023-04-26 19:34:18 -05:00
epi
6d0658a635 update 2023-04-26 19:33:14 -05:00
epi
890519f39c Merge pull request #888 from epi052/all-contributors/add-aroly
docs: add aroly as a contributor for ideas, and code
2023-04-26 19:32:09 -05:00
allcontributors[bot]
abf18b0481 docs: update .all-contributorsrc [skip ci] 2023-04-27 00:29:40 +00:00
allcontributors[bot]
409844ed05 docs: update README.md [skip ci] 2023-04-27 00:29:39 +00:00
epi
1cf37e38a2 Merge pull request #884 from epi052/878-support-raw-urls
878 support raw urls
2023-04-26 06:59:04 -05:00
epi
9876759606 nitpickery 2023-04-26 06:45:13 -05:00
epi
4150b61a42 fixed windows logic 2023-04-26 06:33:43 -05:00
epi
16d34bbee0 bumped version to 2.9.5 2023-04-25 07:10:48 -05:00
epi
f1fd2fc379 updated Url::parse callsites to use the new utility function 2023-04-25 07:09:56 -05:00
epi
3dd070a0db fmt 2023-04-24 06:20:14 -05:00
epi
a3dc6c97a0 added workaround to add partial support for raw urls 2023-04-24 06:19:21 -05:00
epi
ec78ec3049 added ability to specify install directory for install-nix.sh 2023-04-19 17:15:50 -05:00
epi
960536e918 Merge pull request #879 from epi052/all-contributors/add-DrorDvash
docs: add DrorDvash as a contributor for bug
2023-04-19 08:05:15 -05:00
allcontributors[bot]
fdae9aa9d6 docs: update .all-contributorsrc [skip ci] 2023-04-19 13:03:50 +00:00
allcontributors[bot]
5c73c3fb23 docs: update README.md [skip ci] 2023-04-19 13:03:49 +00:00
epi
02ef6d7e3f Merge pull request #877 from epi052/update-indicatif-finally
Random improvements
2023-04-19 07:59:47 -05:00
epi
3378246820 updated arm release names for --update fix 2023-04-19 07:46:43 -05:00
epi
692db93048 clippy/tests and added logic to wait for link extraction if done 2023-04-19 06:57:36 -05:00
epi
233cf99907 made link extraction req/resp async 2023-04-19 06:56:52 -05:00
epi
8cd9918b76 upgraded deps 2023-04-19 06:55:23 -05:00
epi
66bcbfc2f2 bumped version to 2.9.4 2023-04-19 06:51:35 -05:00
epi
8b127c0093 made 404-like req/resp async 2023-04-17 06:37:28 -05:00
epi
94de58d855 removed response body from mpsc traversal 2023-04-17 06:36:47 -05:00
epi
2b95b7be69 updated indicatif to 0.17.3 2023-04-17 06:26:59 -05:00
epi
e77c1314b1 Merge pull request #869 from epi052/auto-filtering-account-for-extensions
added extensions and status codes into auto filtering decision calculus
2023-04-11 19:07:53 -05:00
epi
1ced3b5d77 modified msg when dir listing is found with dont-extract 2023-04-11 18:48:18 -05:00
epi
b5472f5341 updated deps 2023-04-11 18:39:28 -05:00
epi
ea81600850 clippy 2023-04-11 18:36:37 -05:00
epi
4f679592b8 bumped version to 2.9.3 2023-04-11 18:34:02 -05:00
epi
b375893461 nitpickery 2023-04-11 18:32:56 -05:00
epi
e110f86f39 added extensions and status codes into auto filtering decision calculus 2023-04-11 18:29:12 -05:00
epi
c7498a7695 Merge pull request #839 from epi052/all-contributors/add-acut3
docs: add acut3 as a contributor for bug
2023-03-18 12:23:34 -05:00
allcontributors[bot]
f973baaba8 docs: update .all-contributorsrc [skip ci] 2023-03-18 17:23:25 +00:00
allcontributors[bot]
148982cdc4 docs: update README.md [skip ci] 2023-03-18 17:23:24 +00:00
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
124 changed files with 16211 additions and 3804 deletions

View File

@@ -144,7 +144,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/48113936?v=4",
"profile": "https://tib3rius.com",
"contributions": [
"bug"
"bug",
"ideas"
]
},
{
@@ -198,7 +199,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/24260009?v=4",
"profile": "https://github.com/N0ur5",
"contributions": [
"ideas"
"ideas",
"bug"
]
},
{
@@ -294,12 +296,13 @@
]
},
{
"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"
"ideas",
"bug"
]
},
{
@@ -391,8 +394,659 @@
"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"
]
},
{
"login": "acut3",
"name": "Nicolas Christin",
"avatar_url": "https://avatars.githubusercontent.com/u/17295243?v=4",
"profile": "https://acut3.github.io/",
"contributions": [
"bug"
]
},
{
"login": "DrorDvash",
"name": "DrDv",
"avatar_url": "https://avatars.githubusercontent.com/u/8413651?v=4",
"profile": "https://github.com/DrorDvash",
"contributions": [
"bug"
]
},
{
"login": "aroly",
"name": "Antoine Roly",
"avatar_url": "https://avatars.githubusercontent.com/u/1257705?v=4",
"profile": "https://github.com/aroly",
"contributions": [
"ideas"
]
},
{
"login": "lavafroth",
"name": "Himadri Bhattacharjee",
"avatar_url": "https://avatars.githubusercontent.com/u/107522312?v=4",
"profile": "http://lavafroth.is-a.dev",
"contributions": [
"code",
"ideas"
]
},
{
"login": "AkechiShiro",
"name": "Samy Lahfa",
"avatar_url": "https://avatars.githubusercontent.com/u/14914796?v=4",
"profile": "https://github.com/AkechiShiro",
"contributions": [
"ideas"
]
},
{
"login": "sectroyer",
"name": "sectroyer",
"avatar_url": "https://avatars.githubusercontent.com/u/6706818?v=4",
"profile": "https://github.com/sectroyer",
"contributions": [
"bug",
"ideas"
]
},
{
"login": "ktecv2000",
"name": "ktecv2000",
"avatar_url": "https://avatars.githubusercontent.com/u/19836003?v=4",
"profile": "https://medium.com/@b3rm1nG",
"contributions": [
"bug"
]
},
{
"login": "andreademurtas",
"name": "Andrea De Murtas",
"avatar_url": "https://avatars.githubusercontent.com/u/56048157?v=4",
"profile": "http://untrue.me",
"contributions": [
"code"
]
},
{
"login": "sawmj",
"name": "sawmj",
"avatar_url": "https://avatars.githubusercontent.com/u/30024085?v=4",
"profile": "https://github.com/sawmj",
"contributions": [
"bug"
]
},
{
"login": "devx00",
"name": "Zach Hanson",
"avatar_url": "https://avatars.githubusercontent.com/u/6897405?v=4",
"profile": "https://github.com/devx00",
"contributions": [
"bug"
]
},
{
"login": "ocervell",
"name": "Olivier Cervello",
"avatar_url": "https://avatars.githubusercontent.com/u/9629314?v=4",
"profile": "https://github.com/ocervell",
"contributions": [
"ideas"
]
},
{
"login": "RavySena",
"name": "RavySena",
"avatar_url": "https://avatars.githubusercontent.com/u/67729597?v=4",
"profile": "https://github.com/RavySena",
"contributions": [
"ideas"
]
},
{
"login": "stuhlmann",
"name": "Florian Stuhlmann",
"avatar_url": "https://avatars.githubusercontent.com/u/11061864?v=4",
"profile": "https://github.com/stuhlmann",
"contributions": [
"bug"
]
},
{
"login": "Mister7F",
"name": "Mister7F",
"avatar_url": "https://avatars.githubusercontent.com/u/35213773?v=4",
"profile": "https://github.com/Mister7F",
"contributions": [
"ideas"
]
},
{
"login": "manugramm",
"name": "manugramm",
"avatar_url": "https://avatars.githubusercontent.com/u/145961515?v=4",
"profile": "https://github.com/manugramm",
"contributions": [
"bug"
]
},
{
"login": "ArthurMuraro",
"name": "ArthurMuraro",
"avatar_url": "https://avatars.githubusercontent.com/u/73059809?v=4",
"profile": "https://github.com/ArthurMuraro",
"contributions": [
"bug"
]
},
{
"login": "amiremami",
"name": "Shadow",
"avatar_url": "https://avatars.githubusercontent.com/u/15929497?v=4",
"profile": "https://github.com/amiremami",
"contributions": [
"bug"
]
},
{
"login": "dirhamgithub",
"name": "dirhamgithub",
"avatar_url": "https://avatars.githubusercontent.com/u/115349974?v=4",
"profile": "https://github.com/dirhamgithub",
"contributions": [
"bug"
]
},
{
"login": "FieldOfRice",
"name": "FieldOfRice",
"avatar_url": "https://avatars.githubusercontent.com/u/85353?v=4",
"profile": "https://github.com/FieldOfRice",
"contributions": [
"infra"
]
},
{
"login": "NotoriousRebel",
"name": "Matt",
"avatar_url": "https://avatars.githubusercontent.com/u/36310667?v=4",
"profile": "https://github.com/NotoriousRebel",
"contributions": [
"ideas"
]
},
{
"login": "tritoke",
"name": "Sam Leonard",
"avatar_url": "https://avatars.githubusercontent.com/u/34941249?v=4",
"profile": "https://github.com/tritoke",
"contributions": [
"code"
]
},
{
"login": "rew1nter",
"name": "Rewinter",
"avatar_url": "https://avatars.githubusercontent.com/u/64508791?v=4",
"profile": "https://github.com/rew1nter",
"contributions": [
"ideas"
]
},
{
"login": "deadloot",
"name": "deadloot",
"avatar_url": "https://avatars.githubusercontent.com/u/92878901?v=4",
"profile": "https://github.com/deadloot",
"contributions": [
"ideas"
]
},
{
"login": "Spidle",
"name": "Spidle",
"avatar_url": "https://avatars.githubusercontent.com/u/90011249?v=4",
"profile": "https://github.com/Spidle",
"contributions": [
"ideas"
]
},
{
"login": "JulianGR",
"name": "Julián Gómez",
"avatar_url": "https://avatars.githubusercontent.com/u/53094530?v=4",
"profile": "https://github.com/JulianGR",
"contributions": [
"ideas",
"infra",
"doc"
]
},
{
"login": "soutzis",
"name": "Petros",
"avatar_url": "https://avatars.githubusercontent.com/u/25797286?v=4",
"profile": "https://github.com/soutzis",
"contributions": [
"bug"
]
},
{
"login": "sitiom",
"name": "Ryan",
"avatar_url": "https://avatars.githubusercontent.com/u/56180050?v=4",
"profile": "https://github.com/sitiom",
"contributions": [
"infra",
"doc"
]
},
{
"login": "wikamp-collaborator",
"name": "wikamp-collaborator",
"avatar_url": "https://avatars.githubusercontent.com/u/147445097?v=4",
"profile": "https://github.com/wikamp-collaborator",
"contributions": [
"ideas",
"infra"
]
},
{
"login": "L1-0",
"name": "Lino",
"avatar_url": "https://avatars.githubusercontent.com/u/123986259?v=4",
"profile": "http://lino.codes",
"contributions": [
"bug",
"ideas"
]
},
{
"login": "sa7mon",
"name": "Dan Salmon",
"avatar_url": "https://avatars.githubusercontent.com/u/3712226?v=4",
"profile": "https://danthesalmon.com",
"contributions": [
"ideas"
]
},
{
"login": "swordfish0x0",
"name": "swordfish0x0",
"avatar_url": "https://avatars.githubusercontent.com/u/21209130?v=4",
"profile": "https://github.com/swordfish0x0",
"contributions": [
"ideas"
]
},
{
"login": "libklein",
"name": "Patrick Klein",
"avatar_url": "https://avatars.githubusercontent.com/u/42714034?v=4",
"profile": "https://github.com/libklein",
"contributions": [
"ideas"
]
},
{
"login": "Raymond-JV",
"name": "Raymond",
"avatar_url": "https://avatars.githubusercontent.com/u/23642921?v=4",
"profile": "https://github.com/Raymond-JV",
"contributions": [
"ideas"
]
},
{
"login": "zer0x64",
"name": "zer0x64",
"avatar_url": "https://avatars.githubusercontent.com/u/17575242?v=4",
"profile": "https://github.com/zer0x64",
"contributions": [
"code"
]
},
{
"login": "zar3bski",
"name": "zar3bski",
"avatar_url": "https://avatars.githubusercontent.com/u/22128014?v=4",
"profile": "https://zar3bski.com",
"contributions": [
"code",
"ideas"
]
},
{
"login": "karanabe",
"name": "karanabe",
"avatar_url": "https://avatars.githubusercontent.com/u/152078880?v=4",
"profile": "https://github.com/karanabe",
"contributions": [
"doc",
"code"
]
},
{
"login": "h121h",
"name": "h121h",
"avatar_url": "https://avatars.githubusercontent.com/u/616758?v=4",
"profile": "https://github.com/h121h",
"contributions": [
"ideas"
]
},
{
"login": "s0i37",
"name": "s0i37",
"avatar_url": "https://avatars.githubusercontent.com/u/22872513?v=4",
"profile": "https://github.com/s0i37",
"contributions": [
"ideas"
]
},
{
"login": "wilco375",
"name": "Wilco",
"avatar_url": "https://avatars.githubusercontent.com/u/7385023?v=4",
"profile": "https://github.com/wilco375",
"contributions": [
"bug"
]
},
{
"login": "HenriBom",
"name": "HenriBom",
"avatar_url": "https://avatars.githubusercontent.com/u/46447744?v=4",
"profile": "https://github.com/HenriBom",
"contributions": [
"bug"
]
},
{
"login": "0x7274",
"name": "R̝͖̱͖͕̤̰̯͙ͫ͒̀ͮȁ̤͔̝̘̪̻͕̝̖ͧͪͤu̗̠̜̩̗͇͑̀ͣ̃͂̔͂c̫͔͚̲̬̓̂̿͌̿͊̐͗h͚̲̤̟͓̟̥̊ͬͪ̏̍̍ T̟̜̞͉͙̙ͣ́ͪ͗̓̇ͭo͍̰͎̼͓̟̽ͧ̓̉ͬ̐͐b͇̖̳̫̰̗̭͍ͧ̄̄̌̈i̙̪̤̝̟͓̹̋̽͋̀ͧ̒a͕̭̱͎̪̦̤ͤ͊̊̑ͣ̄s̪̯͖̰̯͍ͫ̋͑̄ͭͅͅ",
"avatar_url": "https://avatars.githubusercontent.com/u/85586890?v=4",
"profile": "https://github.com/0x7274",
"contributions": [
"bug",
"ideas",
"doc"
]
},
{
"login": "4FunAndProfit",
"name": "4FunAndProfit",
"avatar_url": "https://avatars.githubusercontent.com/u/174417079?v=4",
"profile": "https://github.com/4FunAndProfit",
"contributions": [
"ideas"
]
},
{
"login": "lidorelias3",
"name": "lidorelias3",
"avatar_url": "https://avatars.githubusercontent.com/u/41958137?v=4",
"profile": "https://github.com/lidorelias3",
"contributions": [
"ideas"
]
},
{
"login": "auk0x01",
"name": "Adnan Ullah Khan (auk0x01)",
"avatar_url": "https://avatars.githubusercontent.com/u/75381620?v=4",
"profile": "http://adnanullahkhan.com",
"contributions": [
"code"
]
},
{
"login": "mzember",
"name": "Martin Žember",
"avatar_url": "https://avatars.githubusercontent.com/u/61412285?v=4",
"profile": "https://github.com/mzember",
"contributions": [
"bug"
]
},
{
"login": "pg9051",
"name": "pg9051",
"avatar_url": "https://avatars.githubusercontent.com/u/202219877?v=4",
"profile": "https://github.com/pg9051",
"contributions": [
"doc"
]
},
{
"login": "sebastiaanspeck",
"name": "Sebastiaan Speck",
"avatar_url": "https://avatars.githubusercontent.com/u/12570668?v=4",
"profile": "https://github.com/sebastiaanspeck",
"contributions": [
"bug",
"doc"
]
},
{
"login": "OpenSourceKyle",
"name": "OpenSourceKyle",
"avatar_url": "https://avatars.githubusercontent.com/u/173112933?v=4",
"profile": "https://github.com/OpenSourceKyle",
"contributions": [
"doc",
"bug"
]
},
{
"login": "Antonio-R1",
"name": "Antonio",
"avatar_url": "https://avatars.githubusercontent.com/u/54741970?v=4",
"profile": "https://github.com/Antonio-R1",
"contributions": [
"code",
"bug"
]
},
{
"login": "redacean",
"name": "Redacean",
"avatar_url": "https://avatars.githubusercontent.com/u/125687454?v=4",
"profile": "https://github.com/redacean",
"contributions": [
"bug"
]
},
{
"login": "ghsdpolley",
"name": "ghsdpolley",
"avatar_url": "https://avatars.githubusercontent.com/u/19826831?v=4",
"profile": "https://github.com/ghsdpolley",
"contributions": [
"bug"
]
},
{
"login": "aldamd",
"name": "Daniel Aldam",
"avatar_url": "https://avatars.githubusercontent.com/u/178115486?v=4",
"profile": "https://github.com/aldamd",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
@@ -400,5 +1054,7 @@
"projectOwner": "epi052",
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": true
"skipCi": true,
"commitConvention": "angular",
"commitType": "docs"
}

View File

@@ -16,11 +16,10 @@ Long form explanations of most of the items below can be found in the [CONTRIBUT
## Documentation
- [ ] New code is documented using [doc comments](https://doc.rust-lang.org/stable/rust-by-example/meta/doc.html)
- [ ] Documentation about your PR is included in the `docs`, as needed. The docs live in a [separate repository](https://epi052.github.io/feroxbuster-docs/docs/). Update the appropriate pages at the links below.
- [ ] update [example config file section](https://epi052.github.io/feroxbuster-docs/docs/configuration/ferox-config-toml/)
- [ ] update [help output section](https://epi052.github.io/feroxbuster-docs/docs/configuration/command-line/)
- [ ] add an [example](https://epi052.github.io/feroxbuster-docs/docs/examples/)
- [ ] update [comparison table](https://epi052.github.io/feroxbuster-docs/docs/compare/)
- [ ] Documentation about your PR is included in the `docs`, as needed. The docs live in a [separate repository](https://epi052.github.io/feroxbuster-docs/). Update the appropriate pages at the links below.
- [ ] update [example config file section](https://epi052.github.io/feroxbuster-docs/configuration/ferox-config-toml/)
- [ ] update [help output section](https://epi052.github.io/feroxbuster-docs/configuration/command-line/)
- [ ] add an [example](https://epi052.github.io/feroxbuster-docs/examples/auto-tune/)
## Additional Tests
- [ ] New code is unit tested

View File

@@ -27,61 +27,108 @@ jobs:
- type: armv7
os: ubuntu-latest
target: armv7-unknown-linux-gnueabihf
name: armv7-feroxbuster
name: armv7-linux-feroxbuster
path: target/armv7-unknown-linux-gnueabihf/release/feroxbuster
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
- type: aarch64
os: ubuntu-latest
target: aarch64-unknown-linux-gnu
name: aarch64-feroxbuster
name: aarch64-linux-feroxbuster
path: target/aarch64-unknown-linux-gnu/release/feroxbuster
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
steps:
- uses: actions/checkout@v2
- name: Install System Dependencies
run: |
env
sudo apt-get update
sudo apt-get install -y --no-install-recommends libssl-dev pkg-config gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu
- uses: actions-rs/toolchain@v1
- uses: actions/checkout@v4
- name: Cache cargo & target directories
uses: Swatinem/rust-cache@v2
- name: Build binary
uses: houseabsolute/actions-rust-cross@v0
with:
toolchain: stable
target: ${{ matrix.target }}
override: true
- uses: actions-rs/cargo@v1
env:
PKG_CONFIG_PATH: ${{ matrix.pkg_config_path }}
OPENSSL_DIR: /usr/lib/ssl
with:
use-cross: true
command: build
args: --release --target=${{ matrix.target }}
- name: Strip symbols from binary
run: |
strip -s ${{ matrix.path }} || arm-linux-gnueabihf-strip -s ${{ matrix.path }} || aarch64-linux-gnu-strip -s ${{ matrix.path }}
target: ${{ matrix.target }}
args: "--locked --release"
strip: true
toolchain: stable
- name: Build tar.gz for homebrew installs
if: matrix.type == 'ubuntu-x64'
run: |
tar czf ${{ matrix.name }}.tar.gz -C target/x86_64-unknown-linux-musl/release feroxbuster
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.name }}
path: ${{ matrix.path }}
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v4
if: matrix.type == 'ubuntu-x64'
with:
name: ${{ matrix.name }}.tar.gz
path: ${{ matrix.name }}.tar.gz
build-debug:
env:
IN_PIPELINE: true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install System Dependencies
run: |
env
sudo apt-get update
sudo apt-get install -y --no-install-recommends libssl-dev pkg-config musl-tools
- name: Set up Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
target: x86_64-unknown-linux-musl
- name: Build the project
env:
PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig
OPENSSL_DIR: /usr/lib/ssl
run: cargo build --target=x86_64-unknown-linux-musl
- uses: actions/upload-artifact@v4
with:
name: x86_64-linux-debug-feroxbuster
path: target/x86_64-unknown-linux-musl/debug/feroxbuster
build-debug-windows:
env:
IN_PIPELINE: true
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Set up Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
target: x86_64-pc-windows-msvc
- name: Build the project
run: cargo build --target=x86_64-pc-windows-msvc
- uses: actions/upload-artifact@v4
with:
name: x86_64-windows-debug-feroxbuster.exe
path: target\x86_64-pc-windows-msvc\debug\feroxbuster.exe
build-deb:
needs: [build-nix]
runs-on: ubuntu-latest
env:
IN_PIPELINE: true
steps:
- uses: actions/checkout@master
- uses: actions/checkout@v4
- name: Install cargo-deb
run: cargo install -f cargo-deb
- uses: awalsh128/cache-apt-pkgs-action@v1
with:
packages: musl-tools # provides musl-gcc
version: 1.0
- name: Install musl toolchain
run: rustup target add x86_64-unknown-linux-musl
- name: Deb Build
uses: ebbflow-io/cargo-deb-amd64-ubuntu@1.0
run: cargo deb --target=x86_64-unknown-linux-musl
- name: Upload Deb Artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: feroxbuster_amd64.deb
path: ./target/x86_64-unknown-linux-musl/debian/*
@@ -92,31 +139,57 @@ jobs:
runs-on: macos-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
- uses: actions/checkout@v4
- name: Cache cargo & target directories
uses: Swatinem/rust-cache@v2
- name: Build binary
uses: houseabsolute/actions-rust-cross@v0
with:
toolchain: stable
target: x86_64-apple-darwin
override: true
- uses: actions-rs/cargo@v1
with:
use-cross: true
command: build
args: --release --target=x86_64-apple-darwin
- name: Strip symbols from binary
run: |
strip -u -r target/x86_64-apple-darwin/release/feroxbuster
target: x86_64-apple-darwin
args: "--locked --release"
strip: true
toolchain: stable
- name: Build tar.gz for homebrew installs
run: |
tar czf x86_64-macos-feroxbuster.tar.gz -C target/x86_64-apple-darwin/release feroxbuster
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v4
with:
name: x86_64-macos-feroxbuster
path: target/x86_64-apple-darwin/release/feroxbuster
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v4
with:
name: x86_64-macos-feroxbuster.tar.gz
path: x86_64-macos-feroxbuster.tar.gz
build-macos-aarch64:
env:
IN_PIPELINE: true
runs-on: macos-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Cache cargo & target directories
uses: Swatinem/rust-cache@v2
- name: Build binary
uses: houseabsolute/actions-rust-cross@v0
with:
command: build
target: aarch64-apple-darwin
args: "--locked --release"
strip: true
toolchain: stable
- name: Build tar.gz for homebrew installs
run: |
tar czf aarch64-macos-feroxbuster.tar.gz -C target/aarch64-apple-darwin/release feroxbuster
- uses: actions/upload-artifact@v4
with:
name: aarch64-macos-feroxbuster
path: target/aarch64-apple-darwin/release/feroxbuster
- uses: actions/upload-artifact@v4
with:
name: aarch64-macos-feroxbuster.tar.gz
path: aarch64-macos-feroxbuster.tar.gz
build-windows:
env:
@@ -138,18 +211,18 @@ jobs:
name: x86-windows-feroxbuster.exe
path: target\i686-pc-windows-msvc\release\feroxbuster.exe
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
- uses: actions/checkout@v4
- name: Cache cargo & target directories
uses: Swatinem/rust-cache@v2
- name: Build binary
uses: houseabsolute/actions-rust-cross@v0
with:
toolchain: stable
target: ${{ matrix.target }}
override: true
- uses: actions-rs/cargo@v1
with:
use-cross: true
command: build
args: --release --target=${{ matrix.target }}
- uses: actions/upload-artifact@v2
target: ${{ matrix.target }}
args: "--locked --release"
strip: true
toolchain: stable
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.name }}
path: ${{ matrix.path }}

View File

@@ -7,58 +7,45 @@ jobs:
name: Check
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
- uses: actions/checkout@v4
- name: Cache cargo & target directories
uses: Swatinem/rust-cache@v2
- uses: dtolnay/rust-toolchain@stable
- run: cargo check
test:
name: Test Suite
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: test
- uses: actions/checkout@v4
- name: Cache cargo & target directories
uses: Swatinem/rust-cache@v2
- name: Install latest nextest release
uses: taiki-e/install-action@nextest
- uses: dtolnay/rust-toolchain@stable
- name: Test with latest nextest release
run: cargo nextest run --all-features --all-targets --retries 4 --no-fail-fast
fmt:
name: Rust fmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
- uses: actions/checkout@v4
- name: Cache cargo & target directories
uses: Swatinem/rust-cache@v2
- uses: dtolnay/rust-toolchain@stable
with:
profile: minimal
toolchain: stable
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
components: rustfmt
- run: cargo fmt --all -- --check
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
- uses: actions/checkout@v4
- name: Cache cargo & target directories
uses: Swatinem/rust-cache@v2
- uses: dtolnay/rust-toolchain@stable
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
components: clippy
- run: cargo clippy --all-targets --all-features -- -D warnings

View File

@@ -9,21 +9,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v3
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
uses: docker/build-push-action@v6
with:
context: ./
file: ./Dockerfile
@@ -32,3 +32,29 @@ jobs:
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
- name: Verify pushed image
run: |
# Wait a moment for the image to be available
sleep 5
# Pull the image we just pushed
docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/feroxbuster:latest
# Get the digest of the pulled image
PULLED_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' ${{ secrets.DOCKER_HUB_USERNAME }}/feroxbuster:latest | cut -d'@' -f2)
PUSHED_DIGEST="${{ steps.docker_build.outputs.digest }}"
echo "Pushed digest: $PUSHED_DIGEST"
echo "Pulled digest: $PULLED_DIGEST"
# Verify they match
if [ "$PULLED_DIGEST" = "$PUSHED_DIGEST" ]; then
echo "✓ Verification successful: Pulled image matches pushed image"
# Test that the binary works
docker run --rm ${{ secrets.DOCKER_HUB_USERNAME }}/feroxbuster:latest --version
else
echo "✗ Verification failed: Digests do not match"
exit 1
fi

View File

@@ -3,42 +3,23 @@ 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@v4
- 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 --retries 4 --output-path lcov.info
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
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
token: ${{ secrets.CODECOV_TOKEN }}

21
.github/workflows/winget.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Publish to Winget
on:
release:
types: [released]
workflow_dispatch:
inputs:
tag_name:
description: 'Tag name of release'
required: true
type: string
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: vedantmgoyal2009/winget-releaser@main
with:
identifier: epi052.feroxbuster
installers-regex: '-windows-feroxbuster\.exe\.zip$'
token: ${{ secrets.WINGET_TOKEN }}
release-tag: ${{ inputs.tag_name || github.event.release.tag_name || github.ref_name }}

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
@@ -182,14 +182,17 @@ Test coverage can be checked using [grcov](https://github.com/mozilla/grcov). I
```sh
cargo install grcov
rustup component add llvm-tools
rustup install nightly
rustup default nightly
export CARGO_INCREMENTAL=0
export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
export RUSTFLAGS="-Cinstrument-coverage -Clink-dead-code -Ccodegen-units=1 -Coverflow-checks=off"
export LLVM_PROFILE_FILE="target/debug/coverage/profraw/feroxbuster-%p-%m.profraw"
export RUSTDOCFLAGS="-Cpanic=abort"
rm -r target/debug/coverage/profraw
cargo build
cargo test
grcov ./target/debug/ -s . -t html --llvm --branch --ignore-not-existing -o ./target/debug/coverage/
grcov . --source-dir . --keep-only "src/*" --binary-path ./target/debug/ -t html --branch --ignore-not-existing -o ./target/debug/coverage/
firefox target/debug/coverage/index.html
```
@@ -214,20 +217,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 +242,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`

3284
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.3"
version = "2.13.1"
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,55 @@ build = "build.rs"
maintenance = { status = "actively-developed" }
[build-dependencies]
clap = { version = "3.1.8", features = ["wrap_help", "cargo"] }
clap_complete = "3.1.1"
regex = "1.5.5"
lazy_static = "1.4.0"
dirs = "4.0.0"
clap = { version = "4.5", features = ["wrap_help", "cargo"] }
clap_complete = "4.5"
regex = "1.11"
lazy_static = "1.5"
dirs = "5.0"
[dependencies]
scraper = "0.12.0"
futures = "0.3.21"
tokio = { version = "1.17.0", features = ["full"] }
tokio-util = { version = "0.7.1", features = ["codec"] }
log = "0.4.16"
env_logger = "0.9.0"
reqwest = { version = "0.11.10", features = ["socks"] }
scraper = "0.19"
futures = "0.3"
tokio = { version = "1.47", features = ["full"] }
tokio-util = { version = "0.7", features = ["codec"] }
log = "0.4"
env_logger = "0.11"
reqwest = { version = "0.12", features = ["socks", "native-tls-alpn"] }
# 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.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"] }
indicatif = "0.15"
console = "0.15.0"
openssl = { version = "0.10.38", features = ["vendored"] }
dirs = "4.0.0"
regex = "1.5.5"
crossterm = "0.23.2"
rlimit = "0.8.3"
ctrlc = "3.2.1"
fuzzyhash = "0.2.1"
anyhow = "1.0.56"
leaky-bucket = "0.10.0" # todo: upgrade, will take a little work/thought since api changed
url = { version = "2.5", features = ["serde"] }
serde_regex = "1.1"
clap = { version = "4.5", features = ["wrap_help", "cargo"] }
lazy_static = "1.5"
toml = "0.8"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
uuid = { version = "1.17", features = ["v4"] }
indicatif = { version = "0.17.11" }
console = "0.15"
openssl = { version = "0.10", features = ["vendored"] }
dirs = "5.0"
regex = "1.11"
crossterm = "0.27"
rlimit = "0.10"
ctrlc = "3.4"
anyhow = "1.0"
leaky-bucket = "1.1"
gaoya = "0.2"
# 0.37+ relies on the broken version of indicatif and forces
# the broken version to be used regardless of the version
# specified above
self_update = { version = "0.40", 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"
tempfile = "3.20"
httpmock = "0.7"
assert_cmd = "2.1"
predicates = "3.1"
[profile.release]
lto = true
@@ -67,9 +82,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-2026 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

@@ -2,6 +2,9 @@
[tasks.upgrade]
dependencies = ["upgrade-deps", "update"]
[tasks.check]
dependencies = ["fmt", "clippy", "test"]
# cleaning
[tasks.clean-state]
script = """
@@ -11,7 +14,7 @@ rm ferox-*.state
# dependency management
[tasks.upgrade-deps]
command = "cargo"
args = ["upgrade", "--exclude", "indicatif", "leaky-bucket"]
args = ["upgrade", "--exclude", "self_update"]
[tasks.update]
command = "cargo"
@@ -23,3 +26,28 @@ clear = true
script = """
cargo clippy --all-targets --all-features -- -D warnings
"""
[tasks.fmt]
clear = true
script = """
cargo fmt --all
"""
# tests
[tasks.test]
clear = true
dependencies = ["test-local", "test-remote"]
[tasks.test-remote]
condition = { env_set = ["CI"] }
clear = true
script = """
cargo nextest run --all-features --all-targets --retries 4 --no-fail-fast
"""
[tasks.test-local]
condition = { env_not_set = ["CI"] }
clear = true
script = """
cargo nextest run --all-features --all-targets --no-fail-fast --run-ignored all --retries 4
"""

309
README.md
View File

@@ -1,3 +1,18 @@
> [!WARNING]
> **Security Notice Domain Impersonation**
>
> The domain **feroxbuster.com** is **NOT affiliated** with this project, its maintainers, or any official feroxbuster releases.
>
> Official feroxbuster downloads are distributed **ONLY** through:
>
> - [https://github.com/epi052/feroxbuster](https://github.com/epi052/feroxbuster/releases) (open source)
> - [https://www.feroxbuster.pro](https://www.feroxbuster.pro) (commercial)
> - package repositories listed in this README
> - package repositories listed in the [installation docs](https://epi052.github.io/feroxbuster-docs/installation/android/)
>
> We do **not** distribute software from feroxbuster.com, and we cannot vouch for the authenticity or safety of files hosted there.
> If you downloaded feroxbuster from any other domain, we strongly recommend deleting it and reinstalling from an official source.
<h1 align="center">
<br>
<a href="https://github.com/epi052/feroxbuster"><img src="img/logo/default-cropped.png" alt="feroxbuster"></a>
@@ -8,7 +23,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">
@@ -43,29 +58,26 @@
![demo](img/demo.gif)
<p align="center">
🦀
<a href="https://github.com/epi052/feroxbuster/releases">Releases</a> ✨
<a href="https://epi052.github.io/feroxbuster-docs/docs/examples/">Example Usage</a> ✨
<a href="https://github.com/epi052/feroxbuster/blob/main/CONTRIBUTING.md">Contributing</a> ✨
<a href="https://epi052.github.io/feroxbuster-docs/docs/">Documentation</a>
🦀
<a href="https://github.com/epi052/feroxbuster/releases"><img src="https://img.shields.io/badge/Releases-CF4F4B?style=flat-square&logo=github&logoColor=white" alt="Releases"></a>&nbsp;
<a href="https://epi052.github.io/feroxbuster-docs/examples/auto-tune/"><img src="https://img.shields.io/badge/Examples-CF4F4B?style=flat-square" alt="Example Usage"></a>&nbsp;
<a href="https://github.com/epi052/feroxbuster/blob/main/CONTRIBUTING.md"><img src="https://img.shields.io/badge/Contributing-CF4F4B?style=flat-square" alt="Contributing"></a>&nbsp;
<a href="https://epi052.github.io/feroxbuster-docs/overview"><img src="https://img.shields.io/badge/Documentation-CF4F4B?style=flat-square&logo=bookstack&logoColor=white" alt="Documentation"></a>&nbsp;
<a href="https://www.feroxbuster.pro"><img src="https://img.shields.io/badge/Pro-CF4F4B?style=flat-square" alt="Pro"></a>
</p>
---
<h1><p align="center">✨🎉👉 <a href="https://epi052.github.io/feroxbuster-docs/docs/">NEW DOCUMENTATION SITE</a> 👈🎉✨</p></h1>
> [!TIP]
> **Documentation has moved!** &mdash; Instead of having a 1300 line `README.md` (sorry...), feroxbuster's documentation has moved to GitHub Pages. The move to hosting documentation on Pages should make it a LOT easier to find the information you're looking for, whatever that may be. Please check it out for anything you need beyond a quick-start.
>
> **[View the full documentation &#8594;](https://epi052.github.io/feroxbuster-docs/overview)**
## 🚀 Documentation has **moved** 🚀
Instead of having a 1300 line `README.md` (sorry...), feroxbuster's documentation has moved to GitHub Pages. The move to hosting documentation on Pages should make it a LOT easier to find the information you're looking for, whatever that may be. Please check it out for anything you need beyond a quick-start. The new documentation can be found [here](https://epi052.github.io/feroxbuster-docs/docs/).
## 😕 What the heck is a ferox anyway?
## 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. 🤷
variation.
## 🤔 What's it do tho?
## What's it do tho?
`feroxbuster` is a tool designed to perform [Forced Browsing](https://owasp.org/www-community/attacks/Forced_browsing).
@@ -79,17 +91,17 @@ credentials, internal network addressing, etc...
This attack is also known as Predictable Resource Location, File Enumeration, Directory Enumeration, and Resource
Enumeration.
## Quick Start
## 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.
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/overview/), as it's much more comprehensive.
### 💿 Installation
### 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.
There are quite a few other [installation methods](https://epi052.github.io/feroxbuster-docs/installation/android/), but these snippets should cover the majority of users.
#### Kali
If you're using kali, this is the preferred install method. Installing from the repos adds a [**ferox-config.toml**](https://epi052.github.io/feroxbuster-docs/docs/configuration/ferox-config-toml/) in `/etc/feroxbuster/`, adds command completion for bash, fish, and zsh, includes a man page entry, and installs `feroxbuster` itself.
If you're using kali, this is the preferred install method. Installing from the repos adds a [**ferox-config.toml**](https://epi052.github.io/feroxbuster-docs/configuration/ferox-config-toml/) in `/etc/feroxbuster/`, adds command completion for bash, fish, and zsh, includes a man page entry, and installs `feroxbuster` itself.
```
sudo apt update && sudo apt install -y feroxbuster
@@ -97,10 +109,21 @@ sudo apt update && sudo apt install -y feroxbuster
#### Linux (32 and 64-bit) & MacOS
Install to a particular directory
```
curl -sL https://raw.githubusercontent.com/epi052/feroxbuster/master/install-nix.sh | bash
curl -sL https://raw.githubusercontent.com/epi052/feroxbuster/main/install-nix.sh | bash -s $HOME/.local/bin
```
Install to current working directory
```
curl -sL https://raw.githubusercontent.com/epi052/feroxbuster/main/install-nix.sh | bash
```
#### MacOS via Homebrew
```
brew install feroxbuster
```
#### Windows x86_64
@@ -110,13 +133,31 @@ Expand-Archive .\feroxbuster.zip
.\feroxbuster\feroxbuster.exe -V
```
#### Windows via Winget
```
winget install epi052.feroxbuster
```
#### Windows via Chocolatey
```
choco install feroxbuster
```
#### All others
Please refer the the [documentation](https://epi052.github.io/feroxbuster-docs/docs/).
Please refer the the [documentation](https://epi052.github.io/feroxbuster-docs/overview).
## 🧰 Example Usage
### Updating feroxbuster (new in v2.9.1)
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/).
```
./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/overview).
### Multiple Values
@@ -167,13 +208,21 @@ cat targets | ./feroxbuster --stdin --silent -s 200 301 302 --redirects -x js |
./feroxbuster -u http://127.1 --query token=0123456789ABCDEF
```
## 🚀 Documentation has **moved** 🚀
### Set the Content-Type of the body automatically with --data-json --data-urlencoded
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!
```
./feroxbuster -u http://127.1 --data-json '{"some": "payload"}'
./feroxbuster -u http://127.1 --data-json @payload.json
./feroxbuster -u http://127.1 --data-urlencoded 'some=payload'
./feroxbuster -u http://127.1 --data-urlencoded @file.payload
```
<h1><p align="center">✨🎉👉 <a href="https://epi052.github.io/feroxbuster-docs/docs/">DOCUMENTATION</a> 👈🎉✨</p></h1>
> [!TIP]
> 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/overview). Go check it out!
>
> **[View the full documentation &#8594;](https://epi052.github.io/feroxbuster-docs/overview)**
## Contributors
## Contributors
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
@@ -181,60 +230,152 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://io.fi"><img src="https://avatars.githubusercontent.com/u/5235109?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Joona Hoikkala</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=joohoi" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/jsav0"><img src="https://avatars.githubusercontent.com/u/20546041?v=4?s=100" width="100px;" alt=""/><br /><sub><b>J Savage</b></sub></a><br /><a href="#infra-jsav0" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=jsav0" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.tgotwig.dev"><img src="https://avatars.githubusercontent.com/u/30773779?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Thomas Gotwig</b></sub></a><br /><a href="#infra-TGotwig" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=TGotwig" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/spikecodes"><img src="https://avatars.githubusercontent.com/u/19519553?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Spike</b></sub></a><br /><a href="#infra-spikecodes" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=spikecodes" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/evanrichter"><img src="https://avatars.githubusercontent.com/u/330292?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Evan Richter</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=evanrichter" title="Code">💻</a> <a href="https://github.com/epi052/feroxbuster/commits?author=evanrichter" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/mzpqnxow"><img src="https://avatars.githubusercontent.com/u/8016228?v=4?s=100" width="100px;" alt=""/><br /><sub><b>AG</b></sub></a><br /><a href="#ideas-mzpqnxow" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=mzpqnxow" title="Documentation">📖</a></td>
<td align="center"><a href="https://n-thumann.de/"><img src="https://avatars.githubusercontent.com/u/46975855?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nicolas Thumann</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=n-thumann" title="Code">💻</a> <a href="https://github.com/epi052/feroxbuster/commits?author=n-thumann" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/tomtastic"><img src="https://avatars.githubusercontent.com/u/302127?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tom Matthews</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=tomtastic" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/bsysop"><img src="https://avatars.githubusercontent.com/u/9998303?v=4?s=100" width="100px;" alt=""/><br /><sub><b>bsysop</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=bsysop" title="Documentation">📖</a></td>
<td align="center"><a href="http://bpsizemore.me"><img src="https://avatars.githubusercontent.com/u/11645898?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Brian Sizemore</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=bpsizemore" title="Code">💻</a></td>
<td align="center"><a href="https://pwn.by/noraj"><img src="https://avatars.githubusercontent.com/u/16578570?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alexandre ZANNI</b></sub></a><br /><a href="#infra-noraj" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=noraj" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/craig"><img src="https://avatars.githubusercontent.com/u/99729?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Craig</b></sub></a><br /><a href="#infra-craig" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://www.reddit.com/u/EONRaider"><img src="https://avatars.githubusercontent.com/u/15611424?v=4?s=100" width="100px;" alt=""/><br /><sub><b>EONRaider</b></sub></a><br /><a href="#infra-EONRaider" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/wtwver"><img src="https://avatars.githubusercontent.com/u/53866088?v=4?s=100" width="100px;" alt=""/><br /><sub><b>wtwver</b></sub></a><br /><a href="#infra-wtwver" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</tr>
<tr>
<td align="center"><a href="https://tib3rius.com"><img src="https://avatars.githubusercontent.com/u/48113936?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tib3rius</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ATib3rius" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/0xdf"><img src="https://avatars.githubusercontent.com/u/1489045?v=4?s=100" width="100px;" alt=""/><br /><sub><b>0xdf</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3A0xdf" title="Bug reports">🐛</a></td>
<td align="center"><a href="http://secure77.de"><img src="https://avatars.githubusercontent.com/u/31564517?v=4?s=100" width="100px;" alt=""/><br /><sub><b>secure-77</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asecure-77" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/sbrun"><img src="https://avatars.githubusercontent.com/u/7712154?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sophie Brun</b></sub></a><br /><a href="#infra-sbrun" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/black-A"><img src="https://avatars.githubusercontent.com/u/30686803?v=4?s=100" width="100px;" alt=""/><br /><sub><b>black-A</b></sub></a><br /><a href="#ideas-black-A" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/dinosn"><img src="https://avatars.githubusercontent.com/u/3851678?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nicolas Krassas</b></sub></a><br /><a href="#ideas-dinosn" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/N0ur5"><img src="https://avatars.githubusercontent.com/u/24260009?v=4?s=100" width="100px;" alt=""/><br /><sub><b>N0ur5</b></sub></a><br /><a href="#ideas-N0ur5" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/moscowchill"><img src="https://avatars.githubusercontent.com/u/72578879?v=4?s=100" width="100px;" alt=""/><br /><sub><b>mchill</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Amoscowchill" title="Bug reports">🐛</a></td>
<td align="center"><a href="http://BitThr3at.github.io"><img src="https://avatars.githubusercontent.com/u/45028933?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Naman</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ABitThr3at" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/Sicks3c"><img src="https://avatars.githubusercontent.com/u/32225186?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ayoub Elaich</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asicks3c" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/HenryHoggard"><img src="https://avatars.githubusercontent.com/u/1208121?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Henry</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AHenryHoggard" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/SleepiPanda"><img src="https://avatars.githubusercontent.com/u/6428561?v=4?s=100" width="100px;" alt=""/><br /><sub><b>SleepiPanda</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ASleepiPanda" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/uBadRequest"><img src="https://avatars.githubusercontent.com/u/47282747?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Bad Requests</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AuBadRequest" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://home.dnaka91.rocks"><img src="https://avatars.githubusercontent.com/u/36804488?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dominik Nakamura</b></sub></a><br /><a href="#infra-dnaka91" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/hunter0x8"><img src="https://avatars.githubusercontent.com/u/46222314?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Muhammad Ahsan</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Ahunter0x8" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/cortantief"><img src="https://avatars.githubusercontent.com/u/34527333?v=4?s=100" width="100px;" alt=""/><br /><sub><b>cortantief</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Acortantief" title="Bug reports">🐛</a> <a href="https://github.com/epi052/feroxbuster/commits?author=cortantief" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/dsaxton"><img src="https://avatars.githubusercontent.com/u/2658661?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Daniel Saxton</b></sub></a><br /><a href="#ideas-dsaxton" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=dsaxton" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/narkopolo"><img src="https://avatars.githubusercontent.com/u/16690056?v=4?s=100" width="100px;" alt=""/><br /><sub><b>narkopolo</b></sub></a><br /><a href="#ideas-narkopolo" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://ring0.lol"><img src="https://avatars.githubusercontent.com/u/1893909?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Justin Steven</b></sub></a><br /><a href="#ideas-justinsteven" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/7047payloads"><img src="https://avatars.githubusercontent.com/u/95562424?v=4?s=100" width="100px;" alt=""/><br /><sub><b>7047payloads</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=7047payloads" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/unkn0wnsyst3m"><img src="https://avatars.githubusercontent.com/u/21272239?v=4?s=100" width="100px;" alt=""/><br /><sub><b>unkn0wnsyst3m</b></sub></a><br /><a href="#ideas-unkn0wnsyst3m" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center"><a href="https://ironwort.me/"><img src="https://avatars.githubusercontent.com/u/15280042?v=4?s=100" width="100px;" alt=""/><br /><sub><b>0x08</b></sub></a><br /><a href="#ideas-its0x08" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/MD-Levitan"><img src="https://avatars.githubusercontent.com/u/12116508?v=4?s=100" width="100px;" alt=""/><br /><sub><b>kusok</b></sub></a><br /><a href="#ideas-MD-Levitan" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=MD-Levitan" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/godylockz"><img src="https://avatars.githubusercontent.com/u/81207744?v=4?s=100" width="100px;" alt=""/><br /><sub><b>godylockz</b></sub></a><br /><a href="#ideas-godylockz" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=godylockz" title="Code">💻</a></td>
<td align="center"><a href="http://ryanmontgomery.me"><img src="https://avatars.githubusercontent.com/u/44453666?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ryan Montgomery</b></sub></a><br /><a href="#ideas-0dayCTF" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/IppSec"><img src="https://avatars.githubusercontent.com/u/24677271?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ippsec</b></sub></a><br /><a href="#ideas-ippsec" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/gtjamesa"><img src="https://avatars.githubusercontent.com/u/2078364?v=4?s=100" width="100px;" alt=""/><br /><sub><b>James</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Agtjamesa" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://twitter.com/Jhaddix"><img src="https://avatars.githubusercontent.com/u/3488554?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jason Haddix</b></sub></a><br /><a href="#ideas-jhaddix" title="Ideas, Planning, & Feedback">🤔</a></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> <a href="#ideas-Tib3rius" title="Ideas, Planning, & Feedback">🤔</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> <a href="https://github.com/epi052/feroxbuster/issues?q=author%3AN0ur5" title="Bug reports">🐛</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> <a href="https://github.com/epi052/feroxbuster/issues?q=author%3An0kovo" title="Bug reports">🐛</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>
<td align="center" valign="top" width="14.28%"><a href="https://acut3.github.io/"><img src="https://avatars.githubusercontent.com/u/17295243?v=4?s=100" width="100px;" alt="Nicolas Christin"/><br /><sub><b>Nicolas Christin</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Aacut3" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DrorDvash"><img src="https://avatars.githubusercontent.com/u/8413651?v=4?s=100" width="100px;" alt="DrDv"/><br /><sub><b>DrDv</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ADrorDvash" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aroly"><img src="https://avatars.githubusercontent.com/u/1257705?v=4?s=100" width="100px;" alt="Antoine Roly"/><br /><sub><b>Antoine Roly</b></sub></a><br /><a href="#ideas-aroly" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://lavafroth.is-a.dev"><img src="https://avatars.githubusercontent.com/u/107522312?v=4?s=100" width="100px;" alt="Himadri Bhattacharjee"/><br /><sub><b>Himadri Bhattacharjee</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=lavafroth" title="Code">💻</a> <a href="#ideas-lavafroth" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AkechiShiro"><img src="https://avatars.githubusercontent.com/u/14914796?v=4?s=100" width="100px;" alt="Samy Lahfa"/><br /><sub><b>Samy Lahfa</b></sub></a><br /><a href="#ideas-AkechiShiro" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sectroyer"><img src="https://avatars.githubusercontent.com/u/6706818?v=4?s=100" width="100px;" alt="sectroyer"/><br /><sub><b>sectroyer</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asectroyer" title="Bug reports">🐛</a> <a href="#ideas-sectroyer" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://medium.com/@b3rm1nG"><img src="https://avatars.githubusercontent.com/u/19836003?v=4?s=100" width="100px;" alt="ktecv2000"/><br /><sub><b>ktecv2000</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Aktecv2000" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://untrue.me"><img src="https://avatars.githubusercontent.com/u/56048157?v=4?s=100" width="100px;" alt="Andrea De Murtas"/><br /><sub><b>Andrea De Murtas</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=andreademurtas" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sawmj"><img src="https://avatars.githubusercontent.com/u/30024085?v=4?s=100" width="100px;" alt="sawmj"/><br /><sub><b>sawmj</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asawmj" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/devx00"><img src="https://avatars.githubusercontent.com/u/6897405?v=4?s=100" width="100px;" alt="Zach Hanson"/><br /><sub><b>Zach Hanson</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Adevx00" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ocervell"><img src="https://avatars.githubusercontent.com/u/9629314?v=4?s=100" width="100px;" alt="Olivier Cervello"/><br /><sub><b>Olivier Cervello</b></sub></a><br /><a href="#ideas-ocervell" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RavySena"><img src="https://avatars.githubusercontent.com/u/67729597?v=4?s=100" width="100px;" alt="RavySena"/><br /><sub><b>RavySena</b></sub></a><br /><a href="#ideas-RavySena" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/stuhlmann"><img src="https://avatars.githubusercontent.com/u/11061864?v=4?s=100" width="100px;" alt="Florian Stuhlmann"/><br /><sub><b>Florian Stuhlmann</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Astuhlmann" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Mister7F"><img src="https://avatars.githubusercontent.com/u/35213773?v=4?s=100" width="100px;" alt="Mister7F"/><br /><sub><b>Mister7F</b></sub></a><br /><a href="#ideas-Mister7F" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/manugramm"><img src="https://avatars.githubusercontent.com/u/145961515?v=4?s=100" width="100px;" alt="manugramm"/><br /><sub><b>manugramm</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Amanugramm" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ArthurMuraro"><img src="https://avatars.githubusercontent.com/u/73059809?v=4?s=100" width="100px;" alt="ArthurMuraro"/><br /><sub><b>ArthurMuraro</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AArthurMuraro" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/amiremami"><img src="https://avatars.githubusercontent.com/u/15929497?v=4?s=100" width="100px;" alt="Shadow"/><br /><sub><b>Shadow</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Aamiremami" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dirhamgithub"><img src="https://avatars.githubusercontent.com/u/115349974?v=4?s=100" width="100px;" alt="dirhamgithub"/><br /><sub><b>dirhamgithub</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Adirhamgithub" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/FieldOfRice"><img src="https://avatars.githubusercontent.com/u/85353?v=4?s=100" width="100px;" alt="FieldOfRice"/><br /><sub><b>FieldOfRice</b></sub></a><br /><a href="#infra-FieldOfRice" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NotoriousRebel"><img src="https://avatars.githubusercontent.com/u/36310667?v=4?s=100" width="100px;" alt="Matt"/><br /><sub><b>Matt</b></sub></a><br /><a href="#ideas-NotoriousRebel" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tritoke"><img src="https://avatars.githubusercontent.com/u/34941249?v=4?s=100" width="100px;" alt="Sam Leonard"/><br /><sub><b>Sam Leonard</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=tritoke" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rew1nter"><img src="https://avatars.githubusercontent.com/u/64508791?v=4?s=100" width="100px;" alt="Rewinter"/><br /><sub><b>Rewinter</b></sub></a><br /><a href="#ideas-rew1nter" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/deadloot"><img src="https://avatars.githubusercontent.com/u/92878901?v=4?s=100" width="100px;" alt="deadloot"/><br /><sub><b>deadloot</b></sub></a><br /><a href="#ideas-deadloot" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Spidle"><img src="https://avatars.githubusercontent.com/u/90011249?v=4?s=100" width="100px;" alt="Spidle"/><br /><sub><b>Spidle</b></sub></a><br /><a href="#ideas-Spidle" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JulianGR"><img src="https://avatars.githubusercontent.com/u/53094530?v=4?s=100" width="100px;" alt="Julián Gómez"/><br /><sub><b>Julián Gómez</b></sub></a><br /><a href="#ideas-JulianGR" title="Ideas, Planning, & Feedback">🤔</a> <a href="#infra-JulianGR" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=JulianGR" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/soutzis"><img src="https://avatars.githubusercontent.com/u/25797286?v=4?s=100" width="100px;" alt="Petros"/><br /><sub><b>Petros</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asoutzis" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sitiom"><img src="https://avatars.githubusercontent.com/u/56180050?v=4?s=100" width="100px;" alt="Ryan"/><br /><sub><b>Ryan</b></sub></a><br /><a href="#infra-sitiom" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=sitiom" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wikamp-collaborator"><img src="https://avatars.githubusercontent.com/u/147445097?v=4?s=100" width="100px;" alt="wikamp-collaborator"/><br /><sub><b>wikamp-collaborator</b></sub></a><br /><a href="#ideas-wikamp-collaborator" title="Ideas, Planning, & Feedback">🤔</a> <a href="#infra-wikamp-collaborator" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://lino.codes"><img src="https://avatars.githubusercontent.com/u/123986259?v=4?s=100" width="100px;" alt="Lino"/><br /><sub><b>Lino</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AL1-0" title="Bug reports">🐛</a> <a href="#ideas-L1-0" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://danthesalmon.com"><img src="https://avatars.githubusercontent.com/u/3712226?v=4?s=100" width="100px;" alt="Dan Salmon"/><br /><sub><b>Dan Salmon</b></sub></a><br /><a href="#ideas-sa7mon" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/swordfish0x0"><img src="https://avatars.githubusercontent.com/u/21209130?v=4?s=100" width="100px;" alt="swordfish0x0"/><br /><sub><b>swordfish0x0</b></sub></a><br /><a href="#ideas-swordfish0x0" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/libklein"><img src="https://avatars.githubusercontent.com/u/42714034?v=4?s=100" width="100px;" alt="Patrick Klein"/><br /><sub><b>Patrick Klein</b></sub></a><br /><a href="#ideas-libklein" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Raymond-JV"><img src="https://avatars.githubusercontent.com/u/23642921?v=4?s=100" width="100px;" alt="Raymond"/><br /><sub><b>Raymond</b></sub></a><br /><a href="#ideas-Raymond-JV" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zer0x64"><img src="https://avatars.githubusercontent.com/u/17575242?v=4?s=100" width="100px;" alt="zer0x64"/><br /><sub><b>zer0x64</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=zer0x64" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://zar3bski.com"><img src="https://avatars.githubusercontent.com/u/22128014?v=4?s=100" width="100px;" alt="zar3bski"/><br /><sub><b>zar3bski</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=zar3bski" title="Code">💻</a> <a href="#ideas-zar3bski" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/karanabe"><img src="https://avatars.githubusercontent.com/u/152078880?v=4?s=100" width="100px;" alt="karanabe"/><br /><sub><b>karanabe</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=karanabe" title="Documentation">📖</a> <a href="https://github.com/epi052/feroxbuster/commits?author=karanabe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/h121h"><img src="https://avatars.githubusercontent.com/u/616758?v=4?s=100" width="100px;" alt="h121h"/><br /><sub><b>h121h</b></sub></a><br /><a href="#ideas-h121h" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/s0i37"><img src="https://avatars.githubusercontent.com/u/22872513?v=4?s=100" width="100px;" alt="s0i37"/><br /><sub><b>s0i37</b></sub></a><br /><a href="#ideas-s0i37" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wilco375"><img src="https://avatars.githubusercontent.com/u/7385023?v=4?s=100" width="100px;" alt="Wilco"/><br /><sub><b>Wilco</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Awilco375" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/HenriBom"><img src="https://avatars.githubusercontent.com/u/46447744?v=4?s=100" width="100px;" alt="HenriBom"/><br /><sub><b>HenriBom</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AHenriBom" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/0x7274"><img src="https://avatars.githubusercontent.com/u/85586890?v=4?s=100" width="100px;" alt="R̝͖̱͖͕̤̰̯͙ͫ͒̀ͮȁ̤͔̝̘̪̻͕̝̖ͧͪͤu̗̠̜̩̗͇͑̀ͣ̃͂̔͂c̫͔͚̲̬̓̂̿͌̿͊̐͗h͚̲̤̟͓̟̥̊ͬͪ̏̍̍ T̟̜̞͉͙̙ͣ́ͪ͗̓̇ͭo͍̰͎̼͓̟̽ͧ̓̉ͬ̐͐b͇̖̳̫̰̗̭͍ͧ̄̄̌̈i̙̪̤̝̟͓̹̋̽͋̀ͧ̒a͕̭̱͎̪̦̤ͤ͊̊̑ͣ̄s̪̯͖̰̯͍ͫ̋͑̄ͭͅͅ"/><br /><sub><b>R̝͖̱͖͕̤̰̯͙ͫ͒̀ͮȁ̤͔̝̘̪̻͕̝̖ͧͪͤu̗̠̜̩̗͇͑̀ͣ̃͂̔͂c̫͔͚̲̬̓̂̿͌̿͊̐͗h͚̲̤̟͓̟̥̊ͬͪ̏̍̍ T̟̜̞͉͙̙ͣ́ͪ͗̓̇ͭo͍̰͎̼͓̟̽ͧ̓̉ͬ̐͐b͇̖̳̫̰̗̭͍ͧ̄̄̌̈i̙̪̤̝̟͓̹̋̽͋̀ͧ̒a͕̭̱͎̪̦̤ͤ͊̊̑ͣ̄s̪̯͖̰̯͍ͫ̋͑̄ͭͅͅ</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3A0x7274" title="Bug reports">🐛</a> <a href="#ideas-0x7274" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=0x7274" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/4FunAndProfit"><img src="https://avatars.githubusercontent.com/u/174417079?v=4?s=100" width="100px;" alt="4FunAndProfit"/><br /><sub><b>4FunAndProfit</b></sub></a><br /><a href="#ideas-4FunAndProfit" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lidorelias3"><img src="https://avatars.githubusercontent.com/u/41958137?v=4?s=100" width="100px;" alt="lidorelias3"/><br /><sub><b>lidorelias3</b></sub></a><br /><a href="#ideas-lidorelias3" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://adnanullahkhan.com"><img src="https://avatars.githubusercontent.com/u/75381620?v=4?s=100" width="100px;" alt="Adnan Ullah Khan (auk0x01)"/><br /><sub><b>Adnan Ullah Khan (auk0x01)</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=auk0x01" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mzember"><img src="https://avatars.githubusercontent.com/u/61412285?v=4?s=100" width="100px;" alt="Martin Žember"/><br /><sub><b>Martin Žember</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Amzember" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pg9051"><img src="https://avatars.githubusercontent.com/u/202219877?v=4?s=100" width="100px;" alt="pg9051"/><br /><sub><b>pg9051</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=pg9051" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sebastiaanspeck"><img src="https://avatars.githubusercontent.com/u/12570668?v=4?s=100" width="100px;" alt="Sebastiaan Speck"/><br /><sub><b>Sebastiaan Speck</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asebastiaanspeck" title="Bug reports">🐛</a> <a href="https://github.com/epi052/feroxbuster/commits?author=sebastiaanspeck" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/OpenSourceKyle"><img src="https://avatars.githubusercontent.com/u/173112933?v=4?s=100" width="100px;" alt="OpenSourceKyle"/><br /><sub><b>OpenSourceKyle</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=OpenSourceKyle" title="Documentation">📖</a> <a href="https://github.com/epi052/feroxbuster/issues?q=author%3AOpenSourceKyle" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Antonio-R1"><img src="https://avatars.githubusercontent.com/u/54741970?v=4?s=100" width="100px;" alt="Antonio"/><br /><sub><b>Antonio</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=Antonio-R1" title="Code">💻</a> <a href="https://github.com/epi052/feroxbuster/issues?q=author%3AAntonio-R1" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/redacean"><img src="https://avatars.githubusercontent.com/u/125687454?v=4?s=100" width="100px;" alt="Redacean"/><br /><sub><b>Redacean</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Aredacean" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ghsdpolley"><img src="https://avatars.githubusercontent.com/u/19826831?v=4?s=100" width="100px;" alt="ghsdpolley"/><br /><sub><b>ghsdpolley</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Aghsdpolley" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aldamd"><img src="https://avatars.githubusercontent.com/u/178115486?v=4?s=100" width="100px;" alt="Daniel Aldam"/><br /><sub><b>Daniel Aldam</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=aldamd" title="Code">💻</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
@@ -242,4 +383,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};
@@ -18,7 +18,7 @@ fn main() {
generate_to(shells::Bash, &mut app, "feroxbuster", outdir).unwrap();
generate_to(shells::Zsh, &mut app, "feroxbuster", outdir).unwrap();
generate_to(shells::Zsh, &mut app, "feroxbuster", outdir).unwrap();
generate_to(shells::Fish, &mut app, "feroxbuster", outdir).unwrap();
generate_to(shells::PowerShell, &mut app, "feroxbuster", outdir).unwrap();
generate_to(shells::Elvish, &mut app, "feroxbuster", outdir).unwrap();
@@ -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/</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/overview), as it's much more comprehensive.
### Installation
There are quite a few other [installation methods](https://epi052.github.io/feroxbuster-docs/installation/android/), but these snippets should cover the majority of users.
#### All others Docs
Please refer the the [documentation](https://epi052.github.io/feroxbuster-docs/overview).
## 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/overview).
### 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

@@ -38,6 +38,10 @@
# methods = ["GET", "POST"]
# data = [11, 12, 13, 14, 15]
# url_denylist = ["http://dont-scan.me", "https://also-not.me"]
# any subdomain of a domain provided to scope is implicitly allowed also.
# so things like "api.other.com" and "sub.third.com" would also be considered
# in-scope given the example config below.
# scope = ["example.com", "other.com", "third.com"]
# regex_denylist = ["/deny.*"]
# no_recursion = true
# add_slash = true
@@ -45,6 +49,8 @@
# dont_filter = true
# extract_links = true
# depth = 1
# limit_bars = 3
# force_recursion = true
# filter_size = [5174]
# filter_regex = ["^ignore me$"]
# filter_similar = ["https://somesite.com/soft404"]
@@ -53,6 +59,14 @@
# queries = [["name","value"], ["rick", "astley"]]
# save_state = false
# time_limit = "10m"
# server_certs = ["/some/cert.pem", "/some/other/cert.pem"]
# client_cert = "/some/client/cert.pem"
# client_key = "/some/client/key.pem"
# request_file = "/some/raw/request/file"
# protocol = "http"
# scan_dir_listings = true
# unique = true
# response_size_limit = 4194304
# headers can be specified on multiple lines or as an inline table
#
@@ -63,6 +77,7 @@
# note: if multi-line is used, all key/value pairs under it belong to the headers table until the next table
# is found or the end of the file is reached
#
# If you want to use [headers], UNCOMMENT the line below
# [headers]
# stuff = "things"
# more = "headers"

BIN
img/logo/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -13,13 +13,13 @@ LIN64_URL="$BASE_URL/$LIN64_ZIP"
EMOJI_URL=https://gist.github.com/epi052/8196b550ea51d0907ad4b93751b1b57d/raw/6112c9f32ae07922983fdc549c54fd3fb9a38e4c/NotoColorEmoji.ttf
echo "[+] Installing feroxbuster!"
INSTALL_DIR="${1:-$(pwd)}"
echo "[+] Installing feroxbuster to ${INSTALL_DIR}!"
which unzip &>/dev/null
if [ "$?" = "0" ]; then
echo "[+] unzip found"
else
echo "[ ] unzip not found, exiting. "
if [ "$?" != "0" ]; then
echo "[!] unzip not found, exiting. "
exit -1
fi
@@ -27,24 +27,24 @@ if [[ "$(uname)" == "Darwin" ]]; then
echo "[=] Found MacOS, downloading from $MAC_URL"
curl -sLO "$MAC_URL"
unzip -o "$MAC_ZIP" >/dev/null
unzip -o "$MAC_ZIP" -d "${INSTALL_DIR}" >/dev/null
rm "$MAC_ZIP"
elif [[ "$(expr substr $(uname -s) 1 5)" == "Linux" ]]; then
if [[ $(getconf LONG_BIT) == 32 ]]; then
echo "[=] Found 32-bit Linux, downloading from $LIN32_URL"
curl -sLO "$LIN32_URL"
unzip -o "$LIN32_ZIP" >/dev/null
unzip -o "$LIN32_ZIP" -d "${INSTALL_DIR}" >/dev/null
rm "$LIN32_ZIP"
else
echo "[=] Found 64-bit Linux, downloading from $LIN64_URL"
curl -sLO "$LIN64_URL"
unzip -o "$LIN64_ZIP" >/dev/null
unzip -o "$LIN64_ZIP" -d "${INSTALL_DIR}" >/dev/null
rm "$LIN64_ZIP"
fi
if [[ -e ~/.fonts/NotoColorEmoji.ttf ]]; then
if [[ "$(fc-list NotoColorEmoji | wc -l)" -gt 0 ]]; then
echo "[=] Found Noto Emoji Font, skipping install"
else
echo "[=] Installing Noto Emoji Font"
@@ -60,6 +60,8 @@ elif [[ "$(expr substr $(uname -s) 1 5)" == "Linux" ]]; then
fi
fi
chmod +x ./feroxbuster
chmod +x "${INSTALL_DIR}/feroxbuster"
echo "[+] Installed feroxbuster version $(./feroxbuster -V)"
echo "[+] Installed feroxbuster"
echo " [-] path: ${INSTALL_DIR}/feroxbuster"
echo " [-] version: $(${INSTALL_DIR}/feroxbuster -V | awk '{print $2}')"

View File

@@ -14,99 +14,115 @@ _feroxbuster() {
fi
local context curcontext="$curcontext" state line
_arguments "${_arguments_options[@]}" \
'-u+[The target URL (required, unless \[--stdin || --resume-from\] used)]:URL:_urls' \
'--url=[The target URL (required, unless \[--stdin || --resume-from\] used)]:URL:_urls' \
_arguments "${_arguments_options[@]}" : \
'-u+[The target URL (required, unless \[--stdin || --resume-from || --request-file\] used)]:URL:_urls' \
'--url=[The target URL (required, unless \[--stdin || --resume-from || --request-file\] used)]:URL:_urls' \
'(-u --url)--resume-from=[State file from which to resume a partially complete scan (ex. --resume-from ferox-1606586780.state)]:STATE_FILE:_files' \
'-p+[Proxy to use for requests (ex: http(s)://host:port, socks5(h)://host:port)]:PROXY:_urls' \
'--proxy=[Proxy to use for requests (ex: http(s)://host:port, socks5(h)://host:port)]:PROXY:_urls' \
'(-u --url)--request-file=[Raw HTTP request file to use as a template for all requests]:REQUEST_FILE:_files' \
'(--data --data-json)--data-urlencoded=[Set -H '\''Content-Type\: application/x-www-form-urlencoded'\'', --data to <data-urlencoded> (supports @file) and -m to POST]:DATA:_default' \
'(--data --data-urlencoded)--data-json=[Set -H '\''Content-Type\: application/json'\'', --data to <data-json> (supports @file) and -m to POST]:DATA:_default' \
'-p+[Proxy to use for requests (ex\: http(s)\://host\:port, socks5(h)\://host\:port)]:PROXY:_urls' \
'--proxy=[Proxy to use for requests (ex\: http(s)\://host\:port, socks5(h)\://host\:port)]:PROXY:_urls' \
'-P+[Send only unfiltered requests through a Replay Proxy, instead of all requests]:REPLAY_PROXY:_urls' \
'--replay-proxy=[Send only unfiltered requests through a Replay Proxy, instead of all requests]:REPLAY_PROXY:_urls' \
'*-R+[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \
'*--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.2)]:USER_AGENT: ' \
'--user-agent=[Sets the User-Agent (default: feroxbuster/2.6.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: ' \
'*--methods=[Which HTTP request method(s) should be sent (default: GET)]:HTTP_METHODS: ' \
'--data=[Request'\''s Body; can read data from a file if input starts with an @ (ex: @post.bin)]:DATA: ' \
'*-H+[Specify HTTP headers to be used in each request (ex: -H Header:val -H '\''stuff: things'\'')]:HEADER: ' \
'*--headers=[Specify HTTP headers to be used in each request (ex: -H Header:val -H '\''stuff: things'\'')]:HEADER: ' \
'*-b+[Specify HTTP cookies to be used in each request (ex: -b stuff=things)]:COOKIE: ' \
'*--cookies=[Specify HTTP cookies to be used in each request (ex: -b stuff=things)]:COOKIE: ' \
'*-Q+[Request'\''s URL query parameters (ex: -Q token=stuff -Q secret=key)]:QUERY: ' \
'*--query=[Request'\''s URL query parameters (ex: -Q token=stuff -Q secret=key)]:QUERY: ' \
'*--dont-scan=[URL(s) or Regex Pattern(s) to exclude from recursion/scans]:URL: ' \
'*-S+[Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)]:SIZE: ' \
'*--filter-size=[Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)]:SIZE: ' \
'*-X+[Filter out messages via regular expression matching on the response'\''s body (ex: -X '\''^ignore me$'\'')]:REGEX: ' \
'*--filter-regex=[Filter out messages via regular expression matching on the response'\''s body (ex: -X '\''^ignore me$'\'')]:REGEX: ' \
'*-W+[Filter out messages of a particular word count (ex: -W 312 -W 91,82)]:WORDS: ' \
'*--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: ' \
'*--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: ' \
'-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: ' \
'--threads=[Number of concurrent threads (default: 50)]:THREADS: ' \
'-d+[Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)]:RECURSION_DEPTH: ' \
'--depth=[Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)]:RECURSION_DEPTH: ' \
'-L+[Limit total number of concurrent scans (default: 0, i.e. no limit)]:SCAN_LIMIT: ' \
'--scan-limit=[Limit total number of concurrent scans (default: 0, i.e. no limit)]:SCAN_LIMIT: ' \
'--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' \
'*-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: ' \
'*-R+[Status Codes to send through a Replay Proxy when found (default\: --status-codes value)]:REPLAY_CODE:_default' \
'*--replay-codes=[Status Codes to send through a Replay Proxy when found (default\: --status-codes value)]:REPLAY_CODE:_default' \
'-a+[Sets the User-Agent (default\: feroxbuster/2.13.1)]:USER_AGENT:_default' \
'--user-agent=[Sets the User-Agent (default\: feroxbuster/2.13.1)]:USER_AGENT:_default' \
'*-x+[File extension(s) to search for (ex\: -x php -x pdf js); reads values (newline-separated) from file if input starts with an @ (ex\: @ext.txt)]:FILE_EXTENSION:_default' \
'*--extensions=[File extension(s) to search for (ex\: -x php -x pdf js); reads values (newline-separated) from file if input starts with an @ (ex\: @ext.txt)]:FILE_EXTENSION:_default' \
'*-m+[Which HTTP request method(s) should be sent (default\: GET)]:HTTP_METHODS:_default' \
'*--methods=[Which HTTP request method(s) should be sent (default\: GET)]:HTTP_METHODS:_default' \
'--data=[Request'\''s Body; can read data from a file if input starts with an @ (ex\: @post.bin)]:DATA:_default' \
'*-H+[Specify HTTP headers to be used in each request (ex\: -H Header\:val -H '\''stuff\: things'\'')]:HEADER:_default' \
'*--headers=[Specify HTTP headers to be used in each request (ex\: -H Header\:val -H '\''stuff\: things'\'')]:HEADER:_default' \
'*-b+[Specify HTTP cookies to be used in each request (ex\: -b stuff=things)]:COOKIE:_default' \
'*--cookies=[Specify HTTP cookies to be used in each request (ex\: -b stuff=things)]:COOKIE:_default' \
'*-Q+[Request'\''s URL query parameters (ex\: -Q token=stuff -Q secret=key)]:QUERY:_default' \
'*--query=[Request'\''s URL query parameters (ex\: -Q token=stuff -Q secret=key)]:QUERY:_default' \
'--protocol=[Specify the protocol to use when targeting via --request-file or --url with domain only (default\: https)]:PROTOCOL:_default' \
'*--dont-scan=[URL(s) or Regex Pattern(s) to exclude from recursion/scans]:URL:_default' \
'*--scope=[Additional domains/URLs to consider in-scope for scanning (in addition to current domain)]:URL:_default' \
'*-S+[Filter out messages of a particular size (ex\: -S 5120 -S 4927,1970)]:SIZE:_default' \
'*--filter-size=[Filter out messages of a particular size (ex\: -S 5120 -S 4927,1970)]:SIZE:_default' \
'*-X+[Filter out messages via regular expression matching on the response'\''s body/headers (ex\: -X '\''^ignore me\$'\'')]:REGEX:_default' \
'*--filter-regex=[Filter out messages via regular expression matching on the response'\''s body/headers (ex\: -X '\''^ignore me\$'\'')]:REGEX:_default' \
'*-W+[Filter out messages of a particular word count (ex\: -W 312 -W 91,82)]:WORDS:_default' \
'*--filter-words=[Filter out messages of a particular word count (ex\: -W 312 -W 91,82)]:WORDS:_default' \
'*-N+[Filter out messages of a particular line count (ex\: -N 20 -N 31,30)]:LINES:_default' \
'*--filter-lines=[Filter out messages of a particular line count (ex\: -N 20 -N 31,30)]:LINES:_default' \
'(-s --status-codes)*-C+[Filter out status codes (deny list) (ex\: -C 200 -C 401)]:STATUS_CODE:_default' \
'(-s --status-codes)*--filter-status=[Filter out status codes (deny list) (ex\: -C 200 -C 401)]:STATUS_CODE:_default' \
'*--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\: All Status Codes)]:STATUS_CODE:_default' \
'*--status-codes=[Status Codes to include (allow list) (default\: All Status Codes)]:STATUS_CODE:_default' \
'-T+[Number of seconds before a client'\''s request times out (default\: 7)]:SECONDS:_default' \
'--timeout=[Number of seconds before a client'\''s request times out (default\: 7)]:SECONDS:_default' \
'--server-certs=[Add custom root certificate(s) for servers with unknown certificates]:PEM|DER:_files' \
'--client-cert=[Add a PEM encoded certificate for mutual authentication (mTLS)]:PEM:_files' \
'--client-key=[Add a PEM encoded private key for mutual authentication (mTLS)]:PEM:_files' \
'-t+[Number of concurrent threads (default\: 50)]:THREADS:_default' \
'--threads=[Number of concurrent threads (default\: 50)]:THREADS:_default' \
'-d+[Maximum recursion depth, a depth of 0 is infinite recursion (default\: 4)]:RECURSION_DEPTH:_default' \
'--depth=[Maximum recursion depth, a depth of 0 is infinite recursion (default\: 4)]:RECURSION_DEPTH:_default' \
'-L+[Limit total number of concurrent scans (default\: 0, i.e. no limit)]:SCAN_LIMIT:_default' \
'--scan-limit=[Limit total number of concurrent scans (default\: 0, i.e. no limit)]:SCAN_LIMIT:_default' \
'(-v --verbosity -u --url)--parallel=[Run parallel feroxbuster instances (one child process per url passed via stdin)]:PARALLEL_SCANS:_default' \
'--rate-limit=[Limit number of requests per second (per directory) (default\: 0, i.e. no limit)]:RATE_LIMIT:_default' \
'--response-size-limit=[Limit size of response body to read in bytes (default\: 4MB)]:BYTES:_default' \
'--time-limit=[Limit total run time of all scans (ex\: --time-limit 10m)]:TIME_SPEC:_default' \
'-w+[Path or URL of the wordlist]:FILE:_files' \
'--wordlist=[Path or URL of the wordlist]:FILE:_files' \
'-B+[Automatically request likely backup extensions for "found" urls (default\: ~, .bak, .bak2, .old, .1)]' \
'--collect-backups=[Automatically request likely backup extensions for "found" urls (default\: ~, .bak, .bak2, .old, .1)]' \
'*-I+[File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)]:FILE_EXTENSION:_default' \
'*--dont-collect=[File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)]:FILE_EXTENSION:_default' \
'-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]' \
'--limit-bars=[Number of directory scan bars to show at any given time (default\: no limit)]:NUM_BARS_TO_SHOW:_default' \
'(-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]' \
'(-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]' \
'(--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 and --scan-dir-listings to true]' \
'-A[Use a random User-Agent]' \
'--random-agent[Use a random User-Agent]' \
'-f[Append / to each request'\''s URL]' \
'--add-slash[Append / to each request'\''s URL]' \
'--unique[Only show unique responses]' \
'-r[Allow client to follow redirects]' \
'--redirects[Allow client to follow redirects]' \
'-k[Disables TLS certificate validation in the client]' \
'--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]' \
'--dont-filter[Don'\''t auto-filter wildcard responses]' \
'-E[Automatically discover extensions and add them to --extensions (unless they'\''re in --dont-collect)]' \
'--collect-extensions[Automatically discover extensions and add them to --extensions (unless they'\''re in --dont-collect)]' \
'-B[Automatically request likely backup extensions for "found" urls]' \
'--collect-backups[Automatically request likely backup extensions for "found" urls]' \
'-g[Automatically discover important words from within responses and add them to the wordlist]' \
'--collect-words[Automatically discover important words from within responses and add them to the wordlist]' \
'--scan-dir-listings[Force scans to recurse into directory listings]' \
'(--silent)*-v[Increase verbosity level (use -vv or more for greater effect. \[CAUTION\] 4 -v'\''s is probably too much)]' \
'(--silent)*--verbosity[Increase verbosity level (use -vv or more for greater effect. \[CAUTION\] 4 -v'\''s is probably too much)]' \
'(-q --quiet)--silent[Only print URLs + turn off logging (good for piping a list of urls to other commands)]' \
'(-q --quiet)--silent[Only print URLs (or JSON w/ --json) + turn off logging (good for piping a list of urls to other commands)]' \
'-q[Hide progress bars and banner (good for tmux windows w/ notifications)]' \
'--quiet[Hide progress bars and banner (good for tmux windows w/ notifications)]' \
'--json[Emit JSON logs to --output and --debug-log instead of normal text]' \
'--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 +132,8 @@ _feroxbuster_commands() {
_describe -t commands 'feroxbuster commands' commands "$@"
}
_feroxbuster "$@"
if [ "$funcstack[1]" = "_feroxbuster" ]; then
_feroxbuster "$@"
else
compdef _feroxbuster feroxbuster
fi

View File

@@ -21,98 +21,114 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
$completions = @(switch ($command) {
'feroxbuster' {
[CompletionResult]::new('-u', 'u', [CompletionResultType]::ParameterName, 'The target URL (required, unless [--stdin || --resume-from] used)')
[CompletionResult]::new('--url', 'url', [CompletionResultType]::ParameterName, 'The target URL (required, unless [--stdin || --resume-from] used)')
[CompletionResult]::new('--resume-from', 'resume-from', [CompletionResultType]::ParameterName, 'State file from which to resume a partially complete scan (ex. --resume-from ferox-1606586780.state)')
[CompletionResult]::new('-p', 'p', [CompletionResultType]::ParameterName, 'Proxy to use for requests (ex: http(s)://host:port, socks5(h)://host:port)')
[CompletionResult]::new('--proxy', 'proxy', [CompletionResultType]::ParameterName, 'Proxy to use for requests (ex: http(s)://host:port, socks5(h)://host:port)')
[CompletionResult]::new('-P', 'P', [CompletionResultType]::ParameterName, 'Send only unfiltered requests through a Replay Proxy, instead of all requests')
[CompletionResult]::new('--replay-proxy', 'replay-proxy', [CompletionResultType]::ParameterName, 'Send only unfiltered requests through a Replay Proxy, instead of all requests')
[CompletionResult]::new('-R', 'R', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)')
[CompletionResult]::new('--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.2)')
[CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.6.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)')
[CompletionResult]::new('--methods', 'methods', [CompletionResultType]::ParameterName, 'Which HTTP request method(s) should be sent (default: GET)')
[CompletionResult]::new('--data', 'data', [CompletionResultType]::ParameterName, 'Request''s Body; can read data from a file if input starts with an @ (ex: @post.bin)')
[CompletionResult]::new('-H', 'H', [CompletionResultType]::ParameterName, 'Specify HTTP headers to be used in each request (ex: -H Header:val -H ''stuff: things'')')
[CompletionResult]::new('--headers', 'headers', [CompletionResultType]::ParameterName, 'Specify HTTP headers to be used in each request (ex: -H Header:val -H ''stuff: things'')')
[CompletionResult]::new('-b', 'b', [CompletionResultType]::ParameterName, 'Specify HTTP cookies to be used in each request (ex: -b stuff=things)')
[CompletionResult]::new('--cookies', 'cookies', [CompletionResultType]::ParameterName, 'Specify HTTP cookies to be used in each request (ex: -b stuff=things)')
[CompletionResult]::new('-Q', 'Q', [CompletionResultType]::ParameterName, 'Request''s URL query parameters (ex: -Q token=stuff -Q secret=key)')
[CompletionResult]::new('--query', 'query', [CompletionResultType]::ParameterName, 'Request''s URL query parameters (ex: -Q token=stuff -Q secret=key)')
[CompletionResult]::new('--dont-scan', 'dont-scan', [CompletionResultType]::ParameterName, 'URL(s) or Regex Pattern(s) to exclude from recursion/scans')
[CompletionResult]::new('-S', 'S', [CompletionResultType]::ParameterName, 'Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)')
[CompletionResult]::new('--filter-size', 'filter-size', [CompletionResultType]::ParameterName, 'Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)')
[CompletionResult]::new('-X', 'X', [CompletionResultType]::ParameterName, 'Filter out messages via regular expression matching on the response''s body (ex: -X ''^ignore me$'')')
[CompletionResult]::new('--filter-regex', 'filter-regex', [CompletionResultType]::ParameterName, 'Filter out messages via regular expression matching on the response''s body (ex: -X ''^ignore me$'')')
[CompletionResult]::new('-W', 'W', [CompletionResultType]::ParameterName, 'Filter out messages of a particular word count (ex: -W 312 -W 91,82)')
[CompletionResult]::new('--filter-words', 'filter-words', [CompletionResultType]::ParameterName, 'Filter out messages of a particular word count (ex: -W 312 -W 91,82)')
[CompletionResult]::new('-N', 'N', [CompletionResultType]::ParameterName, 'Filter out messages of a particular line count (ex: -N 20 -N 31,30)')
[CompletionResult]::new('--filter-lines', 'filter-lines', [CompletionResultType]::ParameterName, 'Filter out messages of a particular line count (ex: -N 20 -N 31,30)')
[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('-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)')
[CompletionResult]::new('--threads', 'threads', [CompletionResultType]::ParameterName, 'Number of concurrent threads (default: 50)')
[CompletionResult]::new('-d', 'd', [CompletionResultType]::ParameterName, 'Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)')
[CompletionResult]::new('--depth', 'depth', [CompletionResultType]::ParameterName, 'Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)')
[CompletionResult]::new('-L', 'L', [CompletionResultType]::ParameterName, 'Limit total number of concurrent scans (default: 0, i.e. no limit)')
[CompletionResult]::new('--scan-limit', 'scan-limit', [CompletionResultType]::ParameterName, 'Limit total number of concurrent scans (default: 0, i.e. no limit)')
[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('-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('--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')
[CompletionResult]::new('-f', 'f', [CompletionResultType]::ParameterName, 'Append / to each request''s URL')
[CompletionResult]::new('--add-slash', 'add-slash', [CompletionResultType]::ParameterName, 'Append / to each request''s URL')
[CompletionResult]::new('-r', 'r', [CompletionResultType]::ParameterName, 'Allow client to follow redirects')
[CompletionResult]::new('--redirects', 'redirects', [CompletionResultType]::ParameterName, 'Allow client to follow redirects')
[CompletionResult]::new('-k', 'k', [CompletionResultType]::ParameterName, 'Disables TLS certificate validation in the client')
[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('--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')
[CompletionResult]::new('--dont-filter', 'dont-filter', [CompletionResultType]::ParameterName, 'Don''t auto-filter wildcard responses')
[CompletionResult]::new('-E', 'E', [CompletionResultType]::ParameterName, 'Automatically discover extensions and add them to --extensions (unless they''re in --dont-collect)')
[CompletionResult]::new('--collect-extensions', 'collect-extensions', [CompletionResultType]::ParameterName, 'Automatically discover extensions and add them to --extensions (unless they''re in --dont-collect)')
[CompletionResult]::new('-B', 'B', [CompletionResultType]::ParameterName, 'Automatically request likely backup extensions for "found" urls')
[CompletionResult]::new('--collect-backups', 'collect-backups', [CompletionResultType]::ParameterName, 'Automatically request likely backup extensions for "found" urls')
[CompletionResult]::new('-g', 'g', [CompletionResultType]::ParameterName, 'Automatically discover important words from within responses and add them to the wordlist')
[CompletionResult]::new('--collect-words', 'collect-words', [CompletionResultType]::ParameterName, 'Automatically discover important words from within responses and add them to the wordlist')
[CompletionResult]::new('-v', 'v', [CompletionResultType]::ParameterName, 'Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v''s is probably too much)')
[CompletionResult]::new('--verbosity', 'verbosity', [CompletionResultType]::ParameterName, 'Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v''s is probably too much)')
[CompletionResult]::new('--silent', 'silent', [CompletionResultType]::ParameterName, 'Only print URLs + turn off logging (good for piping a list of urls to other commands)')
[CompletionResult]::new('-q', 'q', [CompletionResultType]::ParameterName, 'Hide progress bars and banner (good for tmux windows w/ notifications)')
[CompletionResult]::new('--quiet', 'quiet', [CompletionResultType]::ParameterName, 'Hide progress bars and banner (good for tmux windows w/ notifications)')
[CompletionResult]::new('--json', 'json', [CompletionResultType]::ParameterName, 'Emit JSON logs to --output and --debug-log instead of normal text')
[CompletionResult]::new('--no-state', 'no-state', [CompletionResultType]::ParameterName, 'Disable state output file (*.state)')
[CompletionResult]::new('-u', '-u', [CompletionResultType]::ParameterName, 'The target URL (required, unless [--stdin || --resume-from || --request-file] used)')
[CompletionResult]::new('--url', '--url', [CompletionResultType]::ParameterName, 'The target URL (required, unless [--stdin || --resume-from || --request-file] used)')
[CompletionResult]::new('--resume-from', '--resume-from', [CompletionResultType]::ParameterName, 'State file from which to resume a partially complete scan (ex. --resume-from ferox-1606586780.state)')
[CompletionResult]::new('--request-file', '--request-file', [CompletionResultType]::ParameterName, 'Raw HTTP request file to use as a template for all requests')
[CompletionResult]::new('--data-urlencoded', '--data-urlencoded', [CompletionResultType]::ParameterName, 'Set -H ''Content-Type: application/x-www-form-urlencoded'', --data to <data-urlencoded> (supports @file) and -m to POST')
[CompletionResult]::new('--data-json', '--data-json', [CompletionResultType]::ParameterName, 'Set -H ''Content-Type: application/json'', --data to <data-json> (supports @file) and -m to POST')
[CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Proxy to use for requests (ex: http(s)://host:port, socks5(h)://host:port)')
[CompletionResult]::new('--proxy', '--proxy', [CompletionResultType]::ParameterName, 'Proxy to use for requests (ex: http(s)://host:port, socks5(h)://host:port)')
[CompletionResult]::new('-P', '-P ', [CompletionResultType]::ParameterName, 'Send only unfiltered requests through a Replay Proxy, instead of all requests')
[CompletionResult]::new('--replay-proxy', '--replay-proxy', [CompletionResultType]::ParameterName, 'Send only unfiltered requests through a Replay Proxy, instead of all requests')
[CompletionResult]::new('-R', '-R ', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)')
[CompletionResult]::new('--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.13.1)')
[CompletionResult]::new('--user-agent', '--user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.13.1)')
[CompletionResult]::new('-x', '-x', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js); reads values (newline-separated) from file if input starts with an @ (ex: @ext.txt)')
[CompletionResult]::new('--extensions', '--extensions', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js); reads values (newline-separated) from file if input starts with an @ (ex: @ext.txt)')
[CompletionResult]::new('-m', '-m', [CompletionResultType]::ParameterName, 'Which HTTP request method(s) should be sent (default: GET)')
[CompletionResult]::new('--methods', '--methods', [CompletionResultType]::ParameterName, 'Which HTTP request method(s) should be sent (default: GET)')
[CompletionResult]::new('--data', '--data', [CompletionResultType]::ParameterName, 'Request''s Body; can read data from a file if input starts with an @ (ex: @post.bin)')
[CompletionResult]::new('-H', '-H ', [CompletionResultType]::ParameterName, 'Specify HTTP headers to be used in each request (ex: -H Header:val -H ''stuff: things'')')
[CompletionResult]::new('--headers', '--headers', [CompletionResultType]::ParameterName, 'Specify HTTP headers to be used in each request (ex: -H Header:val -H ''stuff: things'')')
[CompletionResult]::new('-b', '-b', [CompletionResultType]::ParameterName, 'Specify HTTP cookies to be used in each request (ex: -b stuff=things)')
[CompletionResult]::new('--cookies', '--cookies', [CompletionResultType]::ParameterName, 'Specify HTTP cookies to be used in each request (ex: -b stuff=things)')
[CompletionResult]::new('-Q', '-Q ', [CompletionResultType]::ParameterName, 'Request''s URL query parameters (ex: -Q token=stuff -Q secret=key)')
[CompletionResult]::new('--query', '--query', [CompletionResultType]::ParameterName, 'Request''s URL query parameters (ex: -Q token=stuff -Q secret=key)')
[CompletionResult]::new('--protocol', '--protocol', [CompletionResultType]::ParameterName, 'Specify the protocol to use when targeting via --request-file or --url with domain only (default: https)')
[CompletionResult]::new('--dont-scan', '--dont-scan', [CompletionResultType]::ParameterName, 'URL(s) or Regex Pattern(s) to exclude from recursion/scans')
[CompletionResult]::new('--scope', '--scope', [CompletionResultType]::ParameterName, 'Additional domains/URLs to consider in-scope for scanning (in addition to current domain)')
[CompletionResult]::new('-S', '-S ', [CompletionResultType]::ParameterName, 'Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)')
[CompletionResult]::new('--filter-size', '--filter-size', [CompletionResultType]::ParameterName, 'Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)')
[CompletionResult]::new('-X', '-X ', [CompletionResultType]::ParameterName, 'Filter out messages via regular expression matching on the response''s body/headers (ex: -X ''^ignore me$'')')
[CompletionResult]::new('--filter-regex', '--filter-regex', [CompletionResultType]::ParameterName, 'Filter out messages via regular expression matching on the response''s body/headers (ex: -X ''^ignore me$'')')
[CompletionResult]::new('-W', '-W ', [CompletionResultType]::ParameterName, 'Filter out messages of a particular word count (ex: -W 312 -W 91,82)')
[CompletionResult]::new('--filter-words', '--filter-words', [CompletionResultType]::ParameterName, 'Filter out messages of a particular word count (ex: -W 312 -W 91,82)')
[CompletionResult]::new('-N', '-N ', [CompletionResultType]::ParameterName, 'Filter out messages of a particular line count (ex: -N 20 -N 31,30)')
[CompletionResult]::new('--filter-lines', '--filter-lines', [CompletionResultType]::ParameterName, 'Filter out messages of a particular line count (ex: -N 20 -N 31,30)')
[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: 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('--server-certs', '--server-certs', [CompletionResultType]::ParameterName, 'Add custom root certificate(s) for servers with unknown certificates')
[CompletionResult]::new('--client-cert', '--client-cert', [CompletionResultType]::ParameterName, 'Add a PEM encoded certificate for mutual authentication (mTLS)')
[CompletionResult]::new('--client-key', '--client-key', [CompletionResultType]::ParameterName, 'Add a PEM encoded private key for mutual authentication (mTLS)')
[CompletionResult]::new('-t', '-t', [CompletionResultType]::ParameterName, 'Number of concurrent threads (default: 50)')
[CompletionResult]::new('--threads', '--threads', [CompletionResultType]::ParameterName, 'Number of concurrent threads (default: 50)')
[CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)')
[CompletionResult]::new('--depth', '--depth', [CompletionResultType]::ParameterName, 'Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)')
[CompletionResult]::new('-L', '-L ', [CompletionResultType]::ParameterName, 'Limit total number of concurrent scans (default: 0, i.e. no limit)')
[CompletionResult]::new('--scan-limit', '--scan-limit', [CompletionResultType]::ParameterName, 'Limit total number of concurrent scans (default: 0, i.e. no limit)')
[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('--response-size-limit', '--response-size-limit', [CompletionResultType]::ParameterName, 'Limit size of response body to read in bytes (default: 4MB)')
[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 or URL of the wordlist')
[CompletionResult]::new('--wordlist', '--wordlist', [CompletionResultType]::ParameterName, 'Path or URL of the wordlist')
[CompletionResult]::new('-B', '-B ', [CompletionResultType]::ParameterName, 'Automatically request likely backup extensions for "found" urls (default: ~, .bak, .bak2, .old, .1)')
[CompletionResult]::new('--collect-backups', '--collect-backups', [CompletionResultType]::ParameterName, 'Automatically request likely backup extensions for "found" urls (default: ~, .bak, .bak2, .old, .1)')
[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('--limit-bars', '--limit-bars', [CompletionResultType]::ParameterName, 'Number of directory scan bars to show at any given time (default: no limit)')
[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 --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 and --scan-dir-listings 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')
[CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'Append / to each request''s URL')
[CompletionResult]::new('--add-slash', '--add-slash', [CompletionResultType]::ParameterName, 'Append / to each request''s URL')
[CompletionResult]::new('--unique', '--unique', [CompletionResultType]::ParameterName, 'Only show unique responses')
[CompletionResult]::new('-r', '-r', [CompletionResultType]::ParameterName, 'Allow client to follow redirects')
[CompletionResult]::new('--redirects', '--redirects', [CompletionResultType]::ParameterName, 'Allow client to follow redirects')
[CompletionResult]::new('-k', '-k', [CompletionResultType]::ParameterName, 'Disables TLS certificate validation in the client')
[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('--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')
[CompletionResult]::new('--dont-filter', '--dont-filter', [CompletionResultType]::ParameterName, 'Don''t auto-filter wildcard responses')
[CompletionResult]::new('-E', '-E ', [CompletionResultType]::ParameterName, 'Automatically discover extensions and add them to --extensions (unless they''re in --dont-collect)')
[CompletionResult]::new('--collect-extensions', '--collect-extensions', [CompletionResultType]::ParameterName, 'Automatically discover extensions and add them to --extensions (unless they''re in --dont-collect)')
[CompletionResult]::new('-g', '-g', [CompletionResultType]::ParameterName, 'Automatically discover important words from within responses and add them to the wordlist')
[CompletionResult]::new('--collect-words', '--collect-words', [CompletionResultType]::ParameterName, 'Automatically discover important words from within responses and add them to the wordlist')
[CompletionResult]::new('--scan-dir-listings', '--scan-dir-listings', [CompletionResultType]::ParameterName, 'Force scans to recurse into directory listings')
[CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v''s is probably too much)')
[CompletionResult]::new('--verbosity', '--verbosity', [CompletionResultType]::ParameterName, 'Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v''s is probably too much)')
[CompletionResult]::new('--silent', '--silent', [CompletionResultType]::ParameterName, 'Only print URLs (or JSON w/ --json) + turn off logging (good for piping a list of urls to other commands)')
[CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Hide progress bars and banner (good for tmux windows w/ notifications)')
[CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Hide progress bars and banner (good for tmux windows w/ notifications)')
[CompletionResult]::new('--json', '--json', [CompletionResultType]::ParameterName, 'Emit JSON logs to --output and --debug-log instead of normal text')
[CompletionResult]::new('--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,15 +1,19 @@
_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]}"
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
cur="$2"
else
cur="${COMP_WORDS[COMP_CWORD]}"
fi
prev="$3"
cmd=""
opts=""
for i in ${COMP_WORDS[@]}
for i in "${COMP_WORDS[@]:0:COMP_CWORD}"
do
case "${i}" in
"$1")
case "${cmd},${i}" in
",$1")
cmd="feroxbuster"
;;
*)
@@ -19,7 +23,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 --request-file --burp --burp-replay --data-urlencoded --data-json --smart --thorough --proxy --replay-proxy --replay-codes --user-agent --random-agent --extensions --methods --data --headers --cookies --query --add-slash --protocol --dont-scan --scope --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --status-codes --unique --timeout --redirects --insecure --server-certs --client-cert --client-key --threads --no-recursion --depth --force-recursion --extract-links --dont-extract-links --scan-limit --parallel --rate-limit --response-size-limit --time-limit --wordlist --auto-tune --auto-bail --dont-filter --collect-extensions --collect-backups --collect-words --dont-collect --scan-dir-listings --verbosity --silent --quiet --json --output --debug-log --no-state --limit-bars --update --help --version"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
@@ -34,6 +38,40 @@ _feroxbuster() {
return 0
;;
--resume-from)
local oldifs
if [ -n "${IFS+x}" ]; then
oldifs="$IFS"
fi
IFS=$'\n'
COMPREPLY=($(compgen -f "${cur}"))
if [ -n "${oldifs+x}" ]; then
IFS="$oldifs"
fi
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o filenames
fi
return 0
;;
--request-file)
local oldifs
if [ -n "${IFS+x}" ]; then
oldifs="$IFS"
fi
IFS=$'\n'
COMPREPLY=($(compgen -f "${cur}"))
if [ -n "${oldifs+x}" ]; then
IFS="$oldifs"
fi
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o filenames
fi
return 0
;;
--data-urlencoded)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--data-json)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
@@ -113,10 +151,18 @@ _feroxbuster() {
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--protocol)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--dont-scan)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--scope)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--filter-size)
COMPREPLY=($(compgen -f "${cur}"))
return 0
@@ -177,6 +223,51 @@ _feroxbuster() {
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--server-certs)
local oldifs
if [ -n "${IFS+x}" ]; then
oldifs="$IFS"
fi
IFS=$'\n'
COMPREPLY=($(compgen -f "${cur}"))
if [ -n "${oldifs+x}" ]; then
IFS="$oldifs"
fi
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o filenames
fi
return 0
;;
--client-cert)
local oldifs
if [ -n "${IFS+x}" ]; then
oldifs="$IFS"
fi
IFS=$'\n'
COMPREPLY=($(compgen -f "${cur}"))
if [ -n "${oldifs+x}" ]; then
IFS="$oldifs"
fi
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o filenames
fi
return 0
;;
--client-key)
local oldifs
if [ -n "${IFS+x}" ]; then
oldifs="$IFS"
fi
IFS=$'\n'
COMPREPLY=($(compgen -f "${cur}"))
if [ -n "${oldifs+x}" ]; then
IFS="$oldifs"
fi
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o filenames
fi
return 0
;;
--threads)
COMPREPLY=($(compgen -f "${cur}"))
return 0
@@ -209,15 +300,49 @@ _feroxbuster() {
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--response-size-limit)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--time-limit)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--wordlist)
local oldifs
if [ -n "${IFS+x}" ]; then
oldifs="$IFS"
fi
IFS=$'\n'
COMPREPLY=($(compgen -f "${cur}"))
if [ -n "${oldifs+x}" ]; then
IFS="$oldifs"
fi
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o filenames
fi
return 0
;;
-w)
local oldifs
if [ -n "${IFS+x}" ]; then
oldifs="$IFS"
fi
IFS=$'\n'
COMPREPLY=($(compgen -f "${cur}"))
if [ -n "${oldifs+x}" ]; then
IFS="$oldifs"
fi
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o filenames
fi
return 0
;;
--collect-backups)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-B)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
@@ -230,14 +355,51 @@ _feroxbuster() {
return 0
;;
--output)
local oldifs
if [ -n "${IFS+x}" ]; then
oldifs="$IFS"
fi
IFS=$'\n'
COMPREPLY=($(compgen -f "${cur}"))
if [ -n "${oldifs+x}" ]; then
IFS="$oldifs"
fi
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o filenames
fi
return 0
;;
-o)
local oldifs
if [ -n "${IFS+x}" ]; then
oldifs="$IFS"
fi
IFS=$'\n'
COMPREPLY=($(compgen -f "${cur}"))
if [ -n "${oldifs+x}" ]; then
IFS="$oldifs"
fi
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o filenames
fi
return 0
;;
--debug-log)
local oldifs
if [ -n "${IFS+x}" ]; then
oldifs="$IFS"
fi
IFS=$'\n'
COMPREPLY=($(compgen -f "${cur}"))
if [ -n "${oldifs+x}" ]; then
IFS="$oldifs"
fi
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o filenames
fi
return 0
;;
--limit-bars)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
@@ -251,4 +413,8 @@ _feroxbuster() {
esac
}
complete -F _feroxbuster -o bashdefault -o default -o plusdirs feroxbuster
if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then
complete -F _feroxbuster -o nosort -o bashdefault -o default -o plusdirs feroxbuster
else
complete -F _feroxbuster -o bashdefault -o default -o plusdirs feroxbuster
fi

View File

@@ -18,19 +18,22 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
}
var completions = [
&'feroxbuster'= {
cand -u 'The target URL (required, unless [--stdin || --resume-from] used)'
cand --url 'The target URL (required, unless [--stdin || --resume-from] used)'
cand -u 'The target URL (required, unless [--stdin || --resume-from || --request-file] used)'
cand --url 'The target URL (required, unless [--stdin || --resume-from || --request-file] used)'
cand --resume-from 'State file from which to resume a partially complete scan (ex. --resume-from ferox-1606586780.state)'
cand --request-file 'Raw HTTP request file to use as a template for all requests'
cand --data-urlencoded 'Set -H ''Content-Type: application/x-www-form-urlencoded'', --data to <data-urlencoded> (supports @file) and -m to POST'
cand --data-json 'Set -H ''Content-Type: application/json'', --data to <data-json> (supports @file) and -m to POST'
cand -p 'Proxy to use for requests (ex: http(s)://host:port, socks5(h)://host:port)'
cand --proxy 'Proxy to use for requests (ex: http(s)://host:port, socks5(h)://host:port)'
cand -P 'Send only unfiltered requests through a Replay Proxy, instead of all requests'
cand --replay-proxy 'Send only unfiltered requests through a Replay Proxy, instead of all requests'
cand -R 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)'
cand --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.2)'
cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.6.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 -a 'Sets the User-Agent (default: feroxbuster/2.13.1)'
cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.13.1)'
cand -x 'File extension(s) to search for (ex: -x php -x pdf js); reads values (newline-separated) from file if input starts with an @ (ex: @ext.txt)'
cand --extensions 'File extension(s) to search for (ex: -x php -x pdf js); reads values (newline-separated) from file if input starts with an @ (ex: @ext.txt)'
cand -m 'Which HTTP request method(s) should be sent (default: GET)'
cand --methods 'Which HTTP request method(s) should be sent (default: GET)'
cand --data 'Request''s Body; can read data from a file if input starts with an @ (ex: @post.bin)'
@@ -40,11 +43,13 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand --cookies 'Specify HTTP cookies to be used in each request (ex: -b stuff=things)'
cand -Q 'Request''s URL query parameters (ex: -Q token=stuff -Q secret=key)'
cand --query 'Request''s URL query parameters (ex: -Q token=stuff -Q secret=key)'
cand --protocol 'Specify the protocol to use when targeting via --request-file or --url with domain only (default: https)'
cand --dont-scan 'URL(s) or Regex Pattern(s) to exclude from recursion/scans'
cand --scope 'Additional domains/URLs to consider in-scope for scanning (in addition to current domain)'
cand -S 'Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)'
cand --filter-size 'Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)'
cand -X 'Filter out messages via regular expression matching on the response''s body (ex: -X ''^ignore me$'')'
cand --filter-regex 'Filter out messages via regular expression matching on the response''s body (ex: -X ''^ignore me$'')'
cand -X 'Filter out messages via regular expression matching on the response''s body/headers (ex: -X ''^ignore me$'')'
cand --filter-regex 'Filter out messages via regular expression matching on the response''s body/headers (ex: -X ''^ignore me$'')'
cand -W 'Filter out messages of a particular word count (ex: -W 312 -W 91,82)'
cand --filter-words 'Filter out messages of a particular word count (ex: -W 312 -W 91,82)'
cand -N 'Filter out messages of a particular line count (ex: -N 20 -N 31,30)'
@@ -52,10 +57,13 @@ 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 --server-certs 'Add custom root certificate(s) for servers with unknown certificates'
cand --client-cert 'Add a PEM encoded certificate for mutual authentication (mTLS)'
cand --client-key 'Add a PEM encoded private key for mutual authentication (mTLS)'
cand -t 'Number of concurrent threads (default: 50)'
cand --threads 'Number of concurrent threads (default: 50)'
cand -d 'Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)'
@@ -64,52 +72,60 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand --scan-limit 'Limit total number of concurrent scans (default: 0, i.e. no limit)'
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 --response-size-limit 'Limit size of response body to read in bytes (default: 4MB)'
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 -B 'Automatically request likely backup extensions for "found" urls (default: ~, .bak, .bak2, .old, .1)'
cand --collect-backups 'Automatically request likely backup extensions for "found" urls (default: ~, .bak, .bak2, .old, .1)'
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 --limit-bars 'Number of directory scan bars to show at any given time (default: no limit)'
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 --thorough 'Use the same settings as --smart and set --collect-extensions 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 and --scan-dir-listings to true'
cand -A 'Use a random User-Agent'
cand --random-agent 'Use a random User-Agent'
cand -f 'Append / to each request''s URL'
cand --add-slash 'Append / to each request''s URL'
cand --unique 'Only show unique responses'
cand -r 'Allow client to follow redirects'
cand --redirects 'Allow client to follow redirects'
cand -k 'Disables TLS certificate validation in the client'
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'
cand --dont-filter 'Don''t auto-filter wildcard responses'
cand -E 'Automatically discover extensions and add them to --extensions (unless they''re in --dont-collect)'
cand --collect-extensions 'Automatically discover extensions and add them to --extensions (unless they''re in --dont-collect)'
cand -B 'Automatically request likely backup extensions for "found" urls'
cand --collect-backups 'Automatically request likely backup extensions for "found" urls'
cand -g 'Automatically discover important words from within responses and add them to the wordlist'
cand --collect-words 'Automatically discover important words from within responses and add them to the wordlist'
cand --scan-dir-listings 'Force scans to recurse into directory listings'
cand -v 'Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v''s is probably too much)'
cand --verbosity 'Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v''s is probably too much)'
cand --silent 'Only print URLs + turn off logging (good for piping a list of urls to other commands)'
cand --silent 'Only print URLs (or JSON w/ --json) + turn off logging (good for piping a list of urls to other commands)'
cand -q 'Hide progress bars and banner (good for tmux windows w/ notifications)'
cand --quiet 'Hide progress bars and banner (good for tmux windows w/ notifications)'
cand --json 'Emit JSON logs to --output and --debug-log instead of normal text'
cand --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

@@ -1,46 +1,70 @@
complete -c feroxbuster -n "__fish_use_subcommand" -s w -l wordlist -d 'Path to the wordlist'
complete -c feroxbuster -n "__fish_use_subcommand" -s u -l url -d 'The target URL(s) (required, unless --stdin used)'
complete -c feroxbuster -n "__fish_use_subcommand" -s t -l threads -d 'Number of concurrent threads (default: 50)'
complete -c feroxbuster -n "__fish_use_subcommand" -s d -l depth -d 'Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)'
complete -c feroxbuster -n "__fish_use_subcommand" -s T -l timeout -d 'Number of seconds before a request times out (default: 7)'
complete -c feroxbuster -n "__fish_use_subcommand" -s p -l proxy -d 'Proxy to use for requests (ex: http(s)://host:port, socks5(h)://host:port)'
complete -c feroxbuster -n "__fish_use_subcommand" -s P -l replay-proxy -d 'Send only unfiltered requests through a Replay Proxy, instead of all requests'
complete -c feroxbuster -n "__fish_use_subcommand" -s R -l replay-codes -d 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)'
complete -c feroxbuster -n "__fish_use_subcommand" -s s -l status-codes -d 'Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)'
complete -c feroxbuster -n "__fish_use_subcommand" -s o -l output -d 'Output file to write results to (use w/ --json for JSON entries)'
complete -c feroxbuster -n "__fish_use_subcommand" -l resume-from -d 'State file from which to resume a partially complete scan (ex. --resume-from ferox-1606586780.state)'
complete -c feroxbuster -n "__fish_use_subcommand" -l debug-log -d 'Output file to write log entries (use w/ --json for JSON entries)'
complete -c feroxbuster -n "__fish_use_subcommand" -s a -l user-agent -d 'Sets the User-Agent (default: feroxbuster/VERSION)'
complete -c feroxbuster -n "__fish_use_subcommand" -s x -l extensions -d 'File extension(s) to search for (ex: -x php -x pdf js)'
complete -c feroxbuster -n "__fish_use_subcommand" -s m -l methods -d 'HTTP request method(s) (default: GET)'
complete -c feroxbuster -n "__fish_use_subcommand" -l data -d 'HTTP Body data; can read data from a file if input starts with an @ (ex: @post.bin)'
complete -c feroxbuster -n "__fish_use_subcommand" -l dont-scan -d 'URL(s) or Regex Pattern(s) to exclude from recursion/scans'
complete -c feroxbuster -n "__fish_use_subcommand" -s H -l headers -d 'Specify HTTP headers (ex: -H Header:val \'stuff: things\')'
complete -c feroxbuster -n "__fish_use_subcommand" -s b -l cookies -d 'Specify HTTP cookies (ex: -b stuff=things)'
complete -c feroxbuster -n "__fish_use_subcommand" -s Q -l query -d 'Specify URL query parameters (ex: -Q token=stuff -Q secret=key)'
complete -c feroxbuster -n "__fish_use_subcommand" -s S -l filter-size -d 'Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)'
complete -c feroxbuster -n "__fish_use_subcommand" -s X -l filter-regex -d 'Filter out messages via regular expression matching on the response\'s body (ex: -X \'^ignore me$\')'
complete -c feroxbuster -n "__fish_use_subcommand" -s W -l filter-words -d 'Filter out messages of a particular word count (ex: -W 312 -W 91,82)'
complete -c feroxbuster -n "__fish_use_subcommand" -s N -l filter-lines -d 'Filter out messages of a particular line count (ex: -N 20 -N 31,30)'
complete -c feroxbuster -n "__fish_use_subcommand" -s C -l filter-status -d 'Filter out status codes (deny list) (ex: -C 200 -C 401)'
complete -c feroxbuster -n "__fish_use_subcommand" -l filter-similar-to -d 'Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)'
complete -c feroxbuster -n "__fish_use_subcommand" -s L -l scan-limit -d 'Limit total number of concurrent scans (default: 0, i.e. no limit)'
complete -c feroxbuster -n "__fish_use_subcommand" -l parallel -d 'Run parallel feroxbuster instances (one child process per url passed via stdin)'
complete -c feroxbuster -n "__fish_use_subcommand" -l rate-limit -d 'Limit number of requests per second (per directory) (default: 0, i.e. no limit)'
complete -c feroxbuster -n "__fish_use_subcommand" -l time-limit -d 'Limit total run time of all scans (ex: --time-limit 10m)'
complete -c feroxbuster -n "__fish_use_subcommand" -s v -l verbosity -d 'Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v\'s is probably too much)'
complete -c feroxbuster -n "__fish_use_subcommand" -l silent -d 'Only print URLs + turn off logging (good for piping a list of urls to other commands)'
complete -c feroxbuster -n "__fish_use_subcommand" -s q -l quiet -d 'Hide progress bars and banner (good for tmux windows w/ notifications)'
complete -c feroxbuster -n "__fish_use_subcommand" -l auto-tune -d 'Automatically lower scan rate when an excessive amount of errors are encountered'
complete -c feroxbuster -n "__fish_use_subcommand" -l auto-bail -d 'Automatically stop scanning when an excessive amount of errors are encountered'
complete -c feroxbuster -n "__fish_use_subcommand" -l json -d 'Emit JSON logs to --output and --debug-log instead of normal text'
complete -c feroxbuster -n "__fish_use_subcommand" -s D -l dont-filter -d 'Don\'t auto-filter wildcard responses'
complete -c feroxbuster -n "__fish_use_subcommand" -s A -l random-agent -d 'Use a random User-Agent'
complete -c feroxbuster -n "__fish_use_subcommand" -s r -l redirects -d 'Follow redirects'
complete -c feroxbuster -n "__fish_use_subcommand" -s k -l insecure -d 'Disables TLS certificate validation'
complete -c feroxbuster -n "__fish_use_subcommand" -s n -l no-recursion -d 'Do not scan recursively'
complete -c feroxbuster -n "__fish_use_subcommand" -s f -l add-slash -d 'Append / to each request'
complete -c feroxbuster -n "__fish_use_subcommand" -l stdin -d 'Read url(s) from STDIN'
complete -c feroxbuster -n "__fish_use_subcommand" -s e -l extract-links -d 'Extract links from response body (html, javascript, etc...); make new requests based on findings (default: false)'
complete -c feroxbuster -n "__fish_use_subcommand" -s h -l help -d 'Prints help information'
complete -c feroxbuster -n "__fish_use_subcommand" -s V -l version -d 'Prints version information'
complete -c feroxbuster -s u -l url -d 'The target URL (required, unless [--stdin || --resume-from || --request-file] used)' -r -f
complete -c feroxbuster -l resume-from -d 'State file from which to resume a partially complete scan (ex. --resume-from ferox-1606586780.state)' -r -F
complete -c feroxbuster -l request-file -d 'Raw HTTP request file to use as a template for all requests' -r -F
complete -c feroxbuster -l data-urlencoded -d 'Set -H \'Content-Type: application/x-www-form-urlencoded\', --data to <data-urlencoded> (supports @file) and -m to POST' -r
complete -c feroxbuster -l data-json -d 'Set -H \'Content-Type: application/json\', --data to <data-json> (supports @file) and -m to POST' -r
complete -c feroxbuster -s p -l proxy -d 'Proxy to use for requests (ex: http(s)://host:port, socks5(h)://host:port)' -r -f
complete -c feroxbuster -s P -l replay-proxy -d 'Send only unfiltered requests through a Replay Proxy, instead of all requests' -r -f
complete -c feroxbuster -s R -l replay-codes -d 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)' -r
complete -c feroxbuster -s a -l user-agent -d 'Sets the User-Agent (default: feroxbuster/2.13.1)' -r
complete -c feroxbuster -s x -l extensions -d 'File extension(s) to search for (ex: -x php -x pdf js); reads values (newline-separated) from file if input starts with an @ (ex: @ext.txt)' -r
complete -c feroxbuster -s m -l methods -d 'Which HTTP request method(s) should be sent (default: GET)' -r
complete -c feroxbuster -l data -d 'Request\'s Body; can read data from a file if input starts with an @ (ex: @post.bin)' -r
complete -c feroxbuster -s H -l headers -d 'Specify HTTP headers to be used in each request (ex: -H Header:val -H \'stuff: things\')' -r
complete -c feroxbuster -s b -l cookies -d 'Specify HTTP cookies to be used in each request (ex: -b stuff=things)' -r
complete -c feroxbuster -s Q -l query -d 'Request\'s URL query parameters (ex: -Q token=stuff -Q secret=key)' -r
complete -c feroxbuster -l protocol -d 'Specify the protocol to use when targeting via --request-file or --url with domain only (default: https)' -r
complete -c feroxbuster -l dont-scan -d 'URL(s) or Regex Pattern(s) to exclude from recursion/scans' -r
complete -c feroxbuster -l scope -d 'Additional domains/URLs to consider in-scope for scanning (in addition to current domain)' -r
complete -c feroxbuster -s S -l filter-size -d 'Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)' -r
complete -c feroxbuster -s X -l filter-regex -d 'Filter out messages via regular expression matching on the response\'s body/headers (ex: -X \'^ignore me$\')' -r
complete -c feroxbuster -s W -l filter-words -d 'Filter out messages of a particular word count (ex: -W 312 -W 91,82)' -r
complete -c feroxbuster -s N -l filter-lines -d 'Filter out messages of a particular line count (ex: -N 20 -N 31,30)' -r
complete -c feroxbuster -s C -l filter-status -d 'Filter out status codes (deny list) (ex: -C 200 -C 401)' -r
complete -c feroxbuster -l filter-similar-to -d 'Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)' -r -f
complete -c feroxbuster -s s -l status-codes -d 'Status Codes to include (allow list) (default: All Status Codes)' -r
complete -c feroxbuster -s T -l timeout -d 'Number of seconds before a client\'s request times out (default: 7)' -r
complete -c feroxbuster -l server-certs -d 'Add custom root certificate(s) for servers with unknown certificates' -r -F
complete -c feroxbuster -l client-cert -d 'Add a PEM encoded certificate for mutual authentication (mTLS)' -r -F
complete -c feroxbuster -l client-key -d 'Add a PEM encoded private key for mutual authentication (mTLS)' -r -F
complete -c feroxbuster -s t -l threads -d 'Number of concurrent threads (default: 50)' -r
complete -c feroxbuster -s d -l depth -d 'Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)' -r
complete -c feroxbuster -s L -l scan-limit -d 'Limit total number of concurrent scans (default: 0, i.e. no limit)' -r
complete -c feroxbuster -l parallel -d 'Run parallel feroxbuster instances (one child process per url passed via stdin)' -r
complete -c feroxbuster -l rate-limit -d 'Limit number of requests per second (per directory) (default: 0, i.e. no limit)' -r
complete -c feroxbuster -l response-size-limit -d 'Limit size of response body to read in bytes (default: 4MB)' -r
complete -c feroxbuster -l time-limit -d 'Limit total run time of all scans (ex: --time-limit 10m)' -r
complete -c feroxbuster -s w -l wordlist -d 'Path or URL of the wordlist' -r -F
complete -c feroxbuster -s B -l collect-backups -d 'Automatically request likely backup extensions for "found" urls (default: ~, .bak, .bak2, .old, .1)' -r
complete -c feroxbuster -s I -l dont-collect -d 'File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)' -r
complete -c feroxbuster -s o -l output -d 'Output file to write results to (use w/ --json for JSON entries)' -r -F
complete -c feroxbuster -l debug-log -d 'Output file to write log entries (use w/ --json for JSON entries)' -r -F
complete -c feroxbuster -l limit-bars -d 'Number of directory scan bars to show at any given time (default: no limit)' -r
complete -c feroxbuster -l stdin -d 'Read url(s) from STDIN'
complete -c feroxbuster -l burp -d 'Set --proxy to http://127.0.0.1:8080 and set --insecure to true'
complete -c feroxbuster -l burp-replay -d 'Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true'
complete -c feroxbuster -l smart -d 'Set --auto-tune, --collect-words, and --collect-backups to true'
complete -c feroxbuster -l thorough -d 'Use the same settings as --smart and set --collect-extensions and --scan-dir-listings to true'
complete -c feroxbuster -s A -l random-agent -d 'Use a random User-Agent'
complete -c feroxbuster -s f -l add-slash -d 'Append / to each request\'s URL'
complete -c feroxbuster -l unique -d 'Only show unique responses'
complete -c feroxbuster -s r -l redirects -d 'Allow client to follow redirects'
complete -c feroxbuster -s k -l insecure -d 'Disables TLS certificate validation in the client'
complete -c feroxbuster -s n -l no-recursion -d 'Do not scan recursively'
complete -c feroxbuster -l force-recursion -d 'Force recursion attempts on all \'found\' endpoints (still respects recursion depth)'
complete -c feroxbuster -s e -l extract-links -d 'Extract links from response body (html, javascript, etc...); make new requests based on findings (default: true)'
complete -c feroxbuster -l dont-extract-links -d 'Don\'t extract links from response body (html, javascript, etc...)'
complete -c feroxbuster -l auto-tune -d 'Automatically lower scan rate when an excessive amount of errors are encountered'
complete -c feroxbuster -l auto-bail -d 'Automatically stop scanning when an excessive amount of errors are encountered'
complete -c feroxbuster -s D -l dont-filter -d 'Don\'t auto-filter wildcard responses'
complete -c feroxbuster -s E -l collect-extensions -d 'Automatically discover extensions and add them to --extensions (unless they\'re in --dont-collect)'
complete -c feroxbuster -s g -l collect-words -d 'Automatically discover important words from within responses and add them to the wordlist'
complete -c feroxbuster -l scan-dir-listings -d 'Force scans to recurse into directory listings'
complete -c feroxbuster -s v -l verbosity -d 'Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v\'s is probably too much)'
complete -c feroxbuster -l silent -d 'Only print URLs (or JSON w/ --json) + turn off logging (good for piping a list of urls to other commands)'
complete -c feroxbuster -s q -l quiet -d 'Hide progress bars and banner (good for tmux windows w/ notifications)'
complete -c feroxbuster -l json -d 'Emit JSON logs to --output and --debug-log instead of normal text'
complete -c feroxbuster -l no-state -d 'Disable state output file (*.state)'
complete -c feroxbuster -s U -l update -d 'Update feroxbuster to the latest version'
complete -c feroxbuster -s h -l help -d 'Print help (see more with \'--help\')'
complete -c feroxbuster -s V -l version -d 'Print version'

View File

@@ -1,14 +1,15 @@
use super::entry::BannerEntry;
use crate::{
client,
config::Configuration,
event_handlers::Handles,
utils::{logged_request, status_colorizer},
DEFAULT_IGNORED_EXTENSIONS, DEFAULT_METHOD, VERSION,
utils::{make_request, parse_url_with_raw_path, status_colorizer},
DEFAULT_IGNORED_EXTENSIONS, DEFAULT_METHOD, DEFAULT_STATUS_CODES, VERSION,
};
use anyhow::{bail, Result};
use console::{style, Emoji};
use reqwest::Url;
use serde_json::Value;
use std::collections::HashMap;
use std::{io::Write, sync::Arc};
/// Url used to query github's api; specifically used to look for the latest tagged release name
@@ -59,6 +60,15 @@ pub struct Banner {
/// represents Configuration.proxy
proxy: BannerEntry,
/// represents Configuration.client_key
client_key: BannerEntry,
/// represents Configuration.client_cert
client_cert: BannerEntry,
/// represents Configuration.server_certs
server_certs: BannerEntry,
/// represents Configuration.replay_proxy
replay_proxy: BannerEntry,
@@ -146,6 +156,9 @@ pub struct Banner {
/// represents Configuration.url_denylist
url_denylist: Vec<BannerEntry>,
/// represents Configuration.scope
scope: Vec<BannerEntry>,
/// current version of feroxbuster
pub(super) version: String,
@@ -163,6 +176,24 @@ pub struct Banner {
/// represents Configuration.collect_words
collect_words: BannerEntry,
/// represents Configuration.collect_words
force_recursion: BannerEntry,
/// represents Configuration.protocol
protocol: BannerEntry,
/// represents Configuration.scan_dir_listings
scan_dir_listings: BannerEntry,
/// represents Configuration.limit_bars
limit_bars: BannerEntry,
/// represents Configuration.unique
unique: BannerEntry,
/// represents Configuration.response_size_limit
response_size_limit: BannerEntry,
}
/// implementation of Banner
@@ -171,6 +202,7 @@ impl Banner {
pub fn new(tgts: &[String], config: &Configuration) -> Self {
let mut targets = Vec::new();
let mut url_denylist = Vec::new();
let mut scope = Vec::new();
let mut code_filters = Vec::new();
let mut replay_codes = Vec::new();
let mut headers = Vec::new();
@@ -201,12 +233,34 @@ impl Banner {
));
}
let mut codes = vec![];
for code in &config.status_codes {
codes.push(status_colorizer(&code.to_string()))
for scope_url in &config.scope {
let value = match scope_url.host() {
Some(host) => host.to_string(),
None => scope_url.as_str().to_string(),
};
scope.push(BannerEntry::new("🚩", "In-Scope Url", &value));
}
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 +284,7 @@ impl Banner {
headers.push(BannerEntry::new(
"🤯",
"Header",
&format!("{}: {}", name, value),
&format!("{name}: {value}"),
));
}
@@ -294,18 +348,40 @@ impl Banner {
BannerEntry::new("🚫", "Do Not Recurse", &config.no_recursion.to_string())
};
let protocol = if config.protocol.to_lowercase() == "http" {
BannerEntry::new("🔓", "Default Protocol", &config.protocol)
} else {
BannerEntry::new("🔒", "Default Protocol", &config.protocol)
};
let scan_limit = BannerEntry::new(
"🦥",
"Concurrent Scan Limit",
&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 scan_dir_listings = BannerEntry::new(
"📂",
"Scan Dir Listings",
&config.scan_dir_listings.to_string(),
);
let cfg = BannerEntry::new("💉", "Config File", &config.config);
let proxy = BannerEntry::new("💎", "Proxy", &config.proxy);
let server_certs = BannerEntry::new(
"🏅",
"Server Certificates",
&format!("[{}]", config.server_certs.join(", ")),
);
let client_cert = BannerEntry::new("🏅", "Client Certificate", &config.client_cert);
let client_key = BannerEntry::new("🔑", "Client Key", &config.client_key);
let threads = BannerEntry::new("🚀", "Threads", &config.threads.to_string());
let limit_bars =
BannerEntry::new("📊", "Limit Dir Scan Bars", &config.limit_bars.to_string());
let wordlist = BannerEntry::new("📖", "Wordlist", &config.wordlist);
let timeout = BannerEntry::new("💥", "Timeout (secs)", &config.timeout.to_string());
let user_agent = BannerEntry::new("🦡", "User-Agent", &config.user_agent);
@@ -372,6 +448,14 @@ impl Banner {
let collect_words =
BannerEntry::new("🤑", "Collect Words", &config.collect_words.to_string());
let unique = BannerEntry::new("🎲", "Unique Responses", &config.unique.to_string());
let response_size_limit = BannerEntry::new(
"📏",
"Response Size Limit",
&format!("{} bytes", config.response_size_limit),
);
Self {
targets,
status_codes,
@@ -384,6 +468,9 @@ impl Banner {
auto_bail,
auto_tune,
proxy,
client_cert,
client_key,
server_certs,
replay_codes,
replay_proxy,
headers,
@@ -409,13 +496,20 @@ impl Banner {
no_recursion,
rate_limit,
scan_limit,
force_recursion,
time_limit,
url_denylist,
scope,
collect_extensions,
collect_backups,
collect_words,
dont_collect,
config: cfg,
scan_dir_listings,
protocol,
limit_bars,
unique,
response_size_limit,
version: VERSION.to_string(),
update_status: UpdateStatus::Unknown,
}
@@ -435,7 +529,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 +543,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
@@ -457,11 +551,56 @@ by Ben "epi" Risher {} ver: {}"#,
///
/// ex: v1.1.0
pub async fn check_for_updates(&mut self, url: &str, handles: Arc<Handles>) -> Result<()> {
log::trace!("enter: needs_update({}, {:?})", url, handles);
log::trace!("enter: needs_update({url}, {handles:?})");
let api_url = Url::parse(url)?;
let api_url = parse_url_with_raw_path(url)?;
// we don't want to leak sensitive header info / include auth headers
// with the github api request, so we'll build a client specifically
// for this task. thanks to @stuhlmann for the suggestion!
let headers = HashMap::new();
let client_cert = if handles.config.client_cert.is_empty() {
None
} else {
Some(handles.config.client_cert.as_str())
};
let client_key = if handles.config.client_key.is_empty() {
None
} else {
Some(handles.config.client_key.as_str())
};
let proxy = if handles.config.proxy.is_empty() {
None
} else {
Some(handles.config.proxy.as_str())
};
let client_config = client::ClientConfig {
timeout: handles.config.timeout,
user_agent: "feroxbuster-update-check",
redirects: handles.config.redirects,
insecure: handles.config.insecure,
headers: &headers,
proxy,
server_certs: Some(&handles.config.server_certs),
client_cert,
client_key,
scope: &handles.config.scope,
};
let client = client::initialize(client_config)?;
let level = handles.config.output_level;
let tx_stats = handles.stats.tx.clone();
let result = make_request(
&client,
&api_url,
DEFAULT_METHOD,
None,
level,
&handles.config,
tx_stats,
)
.await?;
let result = logged_request(&api_url, DEFAULT_METHOD, None, handles.clone()).await?;
let body = result.text().await?;
let json_response: Value = serde_json::from_str(&body)?;
@@ -469,7 +608,7 @@ by Ben "epi" Risher {} ver: {}"#,
let latest_version = match json_response["tag_name"].as_str() {
Some(tag) => tag.trim_start_matches('v'),
None => {
bail!("JSON has no tag_name: {}", json_response);
bail!("JSON has no tag_name: {json_response}");
}
};
@@ -502,20 +641,25 @@ 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}")?;
}
for scoped_url in &self.scope {
writeln!(&mut writer, "{scoped_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)?;
}
@@ -528,6 +672,14 @@ by Ben "epi" Risher {} ver: {}"#,
}
// followed by the maybe printed or variably displayed values
if !config.request_file.is_empty() {
writeln!(&mut writer, "{}", self.protocol)?;
}
if config.limit_bars > 0 {
writeln!(&mut writer, "{}", self.limit_bars)?;
}
if !config.config.is_empty() {
writeln!(&mut writer, "{}", self.config)?;
}
@@ -536,6 +688,18 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(&mut writer, "{}", self.proxy)?;
}
if !config.client_cert.is_empty() {
writeln!(&mut writer, "{}", self.client_cert)?;
}
if !config.client_key.is_empty() {
writeln!(&mut writer, "{}", self.client_key)?;
}
if !config.server_certs.is_empty() {
writeln!(&mut writer, "{}", self.server_certs)?;
}
if !config.replay_proxy.is_empty() {
// i include replay codes logic here because in config.rs, replay codes are set to the
// value in status codes, meaning it's never empty
@@ -544,27 +708,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,13 +740,17 @@ by Ben "epi" Risher {} ver: {}"#,
}
for query in &self.queries {
writeln!(&mut writer, "{}", query)?;
writeln!(&mut writer, "{query}")?;
}
if !config.output.is_empty() {
writeln!(&mut writer, "{}", self.output)?;
}
if config.scan_dir_listings {
writeln!(&mut writer, "{}", self.scan_dir_listings)?;
}
if !config.debug_log.is_empty() {
writeln!(&mut writer, "{}", self.debug_log)?;
}
@@ -642,6 +810,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)?;
}
@@ -658,13 +830,21 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(&mut writer, "{}", self.time_limit)?;
}
if config.unique {
writeln!(&mut writer, "{}", self.unique)?;
}
if config.response_size_limit != 4194304 {
writeln!(&mut writer, "{}", self.response_size_limit)?;
}
if matches!(self.update_status, UpdateStatus::OutOfDate) {
let update = BannerEntry::new(
"🎉",
"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,40 +1,128 @@
use anyhow::Result;
use crate::url::UrlExt;
use anyhow::{Context, Result};
use reqwest::header::HeaderMap;
use reqwest::{redirect::Policy, Client, Proxy};
use std::collections::HashMap;
use std::convert::TryInto;
use std::path::Path;
use std::time::Duration;
use url::Url;
/// Create and return an instance of [reqwest::Client](https://docs.rs/reqwest/latest/reqwest/struct.Client.html)
pub fn initialize(
timeout: u64,
user_agent: &str,
redirects: bool,
insecure: bool,
headers: &HashMap<String, String>,
proxy: Option<&str>,
) -> Result<Client> {
let policy = if redirects {
/// Configuration struct for initializing a reqwest client
pub struct ClientConfig<'a, I>
where
I: IntoIterator,
I::Item: AsRef<Path> + std::fmt::Debug,
{
/// The timeout for requests in seconds
pub timeout: u64,
/// The User-Agent string to use for requests
pub user_agent: &'a str,
/// Whether to follow redirects
pub redirects: bool,
/// Whether to allow insecure connections
pub insecure: bool,
/// Headers to include in requests
pub headers: &'a HashMap<String, String>,
/// Proxy server to use for requests
pub proxy: Option<&'a str>,
/// Server certificates to use for requests
pub server_certs: Option<I>,
/// Client certificate to use for requests
pub client_cert: Option<&'a str>,
/// Client key to use for requests
pub client_key: Option<&'a str>,
/// scope for redirect handling
pub scope: &'a [Url],
}
/// Create a redirect policy based on the provided config
fn create_redirect_policy<I>(config: &ClientConfig<'_, I>) -> Policy
where
I: IntoIterator,
I::Item: AsRef<Path> + std::fmt::Debug,
{
// old behavior set Policy::limited(10) if redirects were enabled
// and Policy::none() if they were not. New policy behavior is
// scope-aware when redirects are enabled and scope is provided.
if config.redirects && config.scope.is_empty() {
// scope should never be empty, so this should never be hit, just a fallback
Policy::limited(10)
} else if config.redirects {
// create a custom policy that checks scope for each redirect
let scoped_urls = config.scope.to_vec();
Policy::custom(move |attempt| {
let redirect_url = attempt.url();
if redirect_url.is_in_scope(&scoped_urls) {
attempt.follow()
} else {
attempt.stop()
}
})
} else {
Policy::none()
};
}
}
let header_map: HeaderMap = headers.try_into()?;
/// Create and return an instance of [reqwest::Client](https://docs.rs/reqwest/latest/reqwest/struct.Client.html)
/// with optional scope-aware redirect handling
pub fn initialize<I>(config: ClientConfig<'_, I>) -> Result<Client>
where
I: IntoIterator,
I::Item: AsRef<Path> + std::fmt::Debug,
{
let policy = create_redirect_policy(&config);
let client = Client::builder()
.timeout(Duration::new(timeout, 0))
.user_agent(user_agent)
.danger_accept_invalid_certs(insecure)
let header_map: HeaderMap = config.headers.try_into()?;
let mut client = Client::builder()
.timeout(Duration::new(config.timeout, 0))
.user_agent(config.user_agent)
.danger_accept_invalid_certs(config.insecure)
.default_headers(header_map)
.redirect(policy)
.http1_title_case_headers();
if let Some(some_proxy) = proxy {
if let Some(some_proxy) = config.proxy {
if !some_proxy.is_empty() {
// it's not an empty string; set the proxy
let proxy_obj = Proxy::all(some_proxy)?;
return Ok(client.proxy(proxy_obj).build()?);
// just add the proxy to the client
// don't build and return it just yet
client = client.proxy(proxy_obj);
}
}
for cert_path in config.server_certs.into_iter().flatten() {
let buf = std::fs::read(&cert_path)?;
let cert = match reqwest::Certificate::from_pem(&buf) {
Ok(cert) => cert,
Err(err) => reqwest::Certificate::from_der(&buf).with_context(|| {
format!(
"{:?} does not contain a valid PEM or DER certificate\n{}",
&cert_path, err
)
})?,
};
client = client.add_root_certificate(cert);
}
if let (Some(cert_path), Some(key_path)) = (config.client_cert, config.client_key) {
if !cert_path.is_empty() && !key_path.is_empty() {
let cert = std::fs::read(cert_path)?;
let key = std::fs::read(key_path)?;
let identity = reqwest::Identity::from_pkcs8_pem(&cert, &key).with_context(|| {
format!(
"either {cert_path} or {key_path} are invalid; expecting PEM encoded certificate and key")
})?;
client = client.identity(identity);
}
}
@@ -50,7 +138,19 @@ mod tests {
/// create client with a bad proxy, expect panic
fn client_with_bad_proxy() {
let headers = HashMap::new();
initialize(0, "stuff", true, false, &headers, Some("not a valid proxy")).unwrap();
let client_config = ClientConfig {
timeout: 0,
user_agent: "stuff",
redirects: true,
insecure: false,
headers: &headers,
proxy: Some("not a valid proxy"),
server_certs: Option::<Vec<String>>::None,
client_cert: None,
client_key: None,
scope: &Vec::new(),
};
initialize(client_config).unwrap();
}
#[test]
@@ -58,6 +158,154 @@ mod tests {
fn client_with_good_proxy() {
let headers = HashMap::new();
let proxy = "http://127.0.0.1:8080";
initialize(0, "stuff", true, true, &headers, Some(proxy)).unwrap();
let client_config = ClientConfig {
timeout: 0,
user_agent: "stuff",
redirects: true,
insecure: true,
headers: &headers,
proxy: Some(proxy),
server_certs: Option::<Vec<String>>::None,
client_cert: None,
client_key: None,
scope: &Vec::new(),
};
initialize(client_config).unwrap();
}
#[test]
/// create client with a server cert in pem format, expect no error
fn client_with_valid_server_pem() {
let headers = HashMap::new();
let server_certs = vec!["tests/mutual-auth/certs/server/server.crt.1".to_string()];
let client_config = ClientConfig {
timeout: 0,
user_agent: "stuff",
redirects: true,
insecure: true,
headers: &headers,
proxy: None,
server_certs: Some(server_certs),
client_cert: None,
client_key: None,
scope: &Vec::new(),
};
initialize(client_config).unwrap();
}
#[test]
/// create client with a server cert in der format, expect no error
fn client_with_valid_server_der() {
let headers = HashMap::new();
let server_certs = vec!["tests/mutual-auth/certs/server/server.der".to_string()];
let client_config = ClientConfig {
timeout: 0,
user_agent: "stuff",
redirects: true,
insecure: true,
headers: &headers,
proxy: None,
server_certs: Some(server_certs),
client_cert: None,
client_key: None,
scope: &Vec::new(),
};
initialize(client_config).unwrap();
}
#[test]
/// create client with two server certs (pem and der), expect no error
fn client_with_valid_server_pem_and_der() {
let headers = HashMap::new();
let server_certs = vec![
"tests/mutual-auth/certs/server/server.crt.1".to_string(),
"tests/mutual-auth/certs/server/server.der".to_string(),
];
println!("{}", std::env::current_dir().unwrap().display());
let client_config = ClientConfig {
timeout: 0,
user_agent: "stuff",
redirects: true,
insecure: true,
headers: &headers,
proxy: None,
server_certs: Some(server_certs),
client_cert: None,
client_key: None,
scope: &Vec::new(),
};
initialize(client_config).unwrap();
}
/// create client with invalid certificate, expect panic
#[test]
#[should_panic]
fn client_with_invalid_server_cert() {
let headers = HashMap::new();
let server_certs = vec!["tests/mutual-auth/certs/client/client.key".to_string()];
let client_config = ClientConfig {
timeout: 0,
user_agent: "stuff",
redirects: true,
insecure: true,
headers: &headers,
proxy: None,
server_certs: Some(server_certs),
client_cert: None,
client_key: None,
scope: &Vec::new(),
};
initialize(client_config).unwrap();
}
#[test]
/// test that scope-aware client can be created with valid parameters
fn initialize_with_scope_creates_client() {
let headers = HashMap::new();
let scope = vec![
Url::parse("https://api.example.com").unwrap(),
Url::parse("https://cdn.example.com").unwrap(),
];
let client_config = ClientConfig {
timeout: 5,
user_agent: "test-agent",
redirects: true,
insecure: false,
headers: &headers,
proxy: None,
server_certs: Option::<Vec<String>>::None,
client_cert: None,
client_key: None,
scope: &scope,
};
let client = initialize(client_config);
assert!(client.is_ok());
}
#[test]
/// test that scope-aware client works without scope (should use default behavior)
fn initialize_with_scope_empty_scope() {
let headers = HashMap::new();
let scope = vec![];
let client_config = ClientConfig {
timeout: 5,
user_agent: "test-agent",
redirects: true,
insecure: false,
headers: &headers,
proxy: None,
server_certs: Option::<Vec<String>>::None,
client_cert: None,
client_key: None,
scope: &scope,
};
let client = initialize(client_config);
assert!(client.is_ok());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -38,6 +38,7 @@ fn setup_config_test() -> Configuration {
methods = ["GET", "PUT", "DELETE"]
data = [31, 32, 33, 34]
url_denylist = ["http://dont-scan.me", "https://also-not.me"]
scope = ["http://example.com", "https://other.com"]
regex_denylist = ["/deny.*"]
headers = {stuff = "things", mostuff = "mothings"}
queries = [["name","value"], ["rick", "astley"]]
@@ -45,16 +46,27 @@ 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
limit_bars = 3
protocol = "http"
request_file = "/some/request/file"
scan_dir_listings = true
force_recursion = true
filter_size = [4120]
filter_regex = ["^ignore me$"]
filter_similar = ["https://somesite.com/soft404"]
filter_word_count = [994, 992]
filter_line_count = [34]
filter_status = [201]
server_certs = ["/some/cert.pem", "/some/other/cert.pem"]
client_cert = "/some/client/cert.pem"
client_key = "/some/client/key.pem"
backup_extensions = [".save"]
unique = true
response_size_limit = 8388608
"#;
let tmp_dir = TempDir::new().unwrap();
let file = tmp_dir.path().join(DEFAULT_CONFIG_NAME);
@@ -82,6 +94,7 @@ fn default_configuration() {
assert_eq!(config.timeout, timeout());
assert_eq!(config.verbosity, 0);
assert_eq!(config.scan_limit, 0);
assert_eq!(config.limit_bars, 0);
assert!(!config.silent);
assert!(!config.quiet);
assert_eq!(config.output_level, OutputLevel::Default);
@@ -95,12 +108,14 @@ 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);
assert!(!config.collect_words);
assert!(!config.scan_dir_listings);
assert!(config.regex_denylist.is_empty());
assert_eq!(config.queries, Vec::new());
assert_eq!(config.filter_size, Vec::<u64>::new());
@@ -108,6 +123,7 @@ fn default_configuration() {
assert_eq!(config.methods, vec!["GET"]);
assert_eq!(config.data, Vec::<u8>::new());
assert_eq!(config.url_denylist, Vec::<Url>::new());
assert_eq!(config.scope, Vec::<Url>::new());
assert_eq!(config.dont_collect, ignored_extensions());
assert_eq!(config.filter_regex, Vec::<String>::new());
assert_eq!(config.filter_similar, Vec::<String>::new());
@@ -115,6 +131,14 @@ fn default_configuration() {
assert_eq!(config.filter_line_count, Vec::<usize>::new());
assert_eq!(config.filter_status, Vec::<u16>::new());
assert_eq!(config.headers, HashMap::new());
assert_eq!(config.server_certs, Vec::<String>::new());
assert_eq!(config.client_cert, String::new());
assert_eq!(config.client_key, String::new());
assert_eq!(config.backup_extensions, backup_extensions());
assert_eq!(config.protocol, request_protocol());
assert_eq!(config.request_file, String::new());
assert!(!config.unique);
assert_eq!(config.response_size_limit, 4194304); // 4MB
}
#[test]
@@ -208,6 +232,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() {
@@ -243,6 +274,13 @@ fn config_reads_verbosity() {
assert_eq!(config.verbosity, 1);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_limit_bars() {
let config = setup_config_test();
assert_eq!(config.limit_bars, 3);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_output() {
@@ -296,7 +334,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]
@@ -371,6 +409,19 @@ fn config_reads_url_denylist() {
);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_scope() {
let config = setup_config_test();
assert_eq!(
config.scope,
vec![
Url::parse("http://example.com").unwrap(),
Url::parse("https://other.com").unwrap(),
]
);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_filter_regex() {
@@ -427,6 +478,27 @@ fn config_reads_time_limit() {
assert_eq!(config.time_limit, "10m");
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_scan_dir_listings() {
let config = setup_config_test();
assert!(config.scan_dir_listings);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_protocol() {
let config = setup_config_test();
assert_eq!(config.protocol, "http");
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_request_file() {
let config = setup_config_test();
assert_eq!(config.request_file, String::new());
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_resume_from() {
@@ -434,6 +506,37 @@ fn config_reads_resume_from() {
assert_eq!(config.resume_from, "/some/state/file");
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_server_certs() {
let config = setup_config_test();
assert_eq!(
config.server_certs,
["/some/cert.pem", "/some/other/cert.pem"]
);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_backup_extensions() {
let config = setup_config_test();
assert_eq!(config.backup_extensions, [".save"]);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_client_cert() {
let config = setup_config_test();
assert_eq!(config.client_cert, "/some/client/cert.pem");
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_client_key() {
let config = setup_config_test();
assert_eq!(config.client_key, "/some/client/key.pem");
}
#[test]
/// parse the test config and see that the values parsed are correct
fn config_reads_headers() {
@@ -473,7 +576,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:"));
@@ -495,3 +598,17 @@ fn as_json_returns_json_representation_of_configuration_with_newline() {
assert_eq!(json.timeout, config.timeout);
assert_eq!(json.depth, config.depth);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_unique() {
let config = setup_config_test();
assert!(config.unique);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_response_size_limit() {
let config = setup_config_test();
assert_eq!(config.response_size_limit, 8388608); // 8MB as set in setup_config_test
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,12 @@
use std::sync::Arc;
use std::time::Duration;
use reqwest::StatusCode;
use tokio::sync::oneshot::Sender;
use crate::response::FeroxResponse;
use crate::{
event_handlers::Handles,
message::FeroxMessage,
statistics::{StatError, StatField},
traits::FeroxFilter,
@@ -23,7 +25,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),
@@ -78,4 +82,20 @@ 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>),
/// inform the Stats object about which targets are being scanned
UpdateTargets(Vec<String>),
/// query the Stats handler about the position of the overall progress bar
QueryOverallBarEta(Sender<Duration>),
/// Add permits to the scan limiter (semaphore)
AddScanPermits(usize),
/// Subtract permits from the scan limiter (semaphore)
SubtractScanPermits(usize),
}

View File

@@ -112,7 +112,7 @@ impl Handles {
pub fn set_scan_handle(&self, handle: ScanHandle) {
if let Ok(mut guard) = self.scans.write() {
if guard.is_none() {
let _ = std::mem::replace(&mut *guard, Some(handle));
guard.replace(handle);
}
}
}
@@ -157,16 +157,17 @@ impl Handles {
multiplier * num_words
}
/// number of extensions plus the number of request method types plus any dynamically collected
/// extensions
/// estimate of HTTP requests per word = (base + static extensions + collected extensions)
/// multiplied by the number of request methods
pub fn expected_num_requests_multiplier(&self) -> usize {
let multiplier = self.config.extensions.len()
+ self.config.methods.len()
+ self.num_collected_extensions();
let methods = self.config.methods.len().max(1);
let base_requests = 1; // the bare word (with optional slash)
let static_extensions = self.config.extensions.len();
let dynamic_extensions = self.num_collected_extensions();
// 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)
let total_paths = base_requests + static_extensions + dynamic_extensions;
total_paths * methods
}
/// Helper to easily get the (locked) underlying FeroxScans object

View File

@@ -71,7 +71,7 @@ impl FiltersHandler {
let event_handle = FiltersHandle::new(data, tx);
log::trace!("exit: initialize -> ({:?}, {:?})", task, event_handle);
log::trace!("exit: initialize -> ({task:?}, {event_handle:?})");
(task, event_handle)
}
@@ -80,7 +80,7 @@ impl FiltersHandler {
///
/// The consumer simply receives `Command` and acts accordingly
pub async fn start(&mut self) -> Result<()> {
log::trace!("enter: start({:?})", self);
log::trace!("enter: start({self:?})");
while let Some(command) = self.receiver.recv().await {
match command {
@@ -92,7 +92,7 @@ impl FiltersHandler {
}
Command::RemoveFilters(mut indices) => self.data.remove(&mut indices),
Command::Sync(sender) => {
log::debug!("filters: {:?}", self);
log::debug!("filters: {self:?}");
sender.send(true).unwrap_or_default();
}
Command::Exit => break,

View File

@@ -12,6 +12,7 @@ use anyhow::Result;
use console::style;
use crossterm::event::{self, Event, KeyCode};
use std::{
env::temp_dir,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
@@ -43,7 +44,7 @@ impl TermInputHandler {
/// Initialize the sigint and enter handlers that are responsible for handling initial user
/// interaction during scans
pub fn initialize(handles: Arc<Handles>) {
log::trace!("enter: initialize({:?})", handles);
log::trace!("enter: initialize({handles:?})");
let handler = Self::new(handles);
handler.start();
@@ -75,9 +76,12 @@ impl TermInputHandler {
/// Writes the current state of the program to disk (if save_state is true) and then exits
pub fn sigint_handler(handles: Arc<Handles>) -> Result<()> {
log::trace!("enter: sigint_handler({:?})", handles);
log::trace!("enter: sigint_handler({handles:?})");
let filename = if !handles.config.target_url.is_empty() {
// check for STATE_FILENAME env var first; credit to Tobias Rauch for the idea
let filename = if let Ok(path) = std::env::var("STATE_FILENAME") {
path
} else if !handles.config.target_url.is_empty() {
// target url populated
slugify_filename(&handles.config.target_url, "ferox", "state")
} else {
@@ -101,10 +105,39 @@ impl TermInputHandler {
handles.filters.data.clone(),
);
let state_file = open_file(&filename);
// User didn't set the --no-state flag (so saved_state is still the default true)
if handles.config.save_state {
let Ok(mut state_file) = open_file(&filename) else {
// couldn't open the file, let the user know we're going to try again
let error = format!(
"❌ Could not save {}, falling back to {}",
filename,
temp_dir().to_string_lossy()
);
PROGRESS_PRINTER.println(error);
let mut buffered_file = state_file?;
write_to(&state, &mut buffered_file, true)?;
let temp_filename = temp_dir().join(&filename);
let Ok(mut state_file) = open_file(&temp_filename.to_string_lossy()) else {
// couldn't open the fallback file, let the user know
let error = format!("❌❌ Could not save {temp_filename:?}, giving up...");
PROGRESS_PRINTER.println(error);
log::trace!("exit: sigint_handler (failed to write)");
std::process::exit(1);
};
write_to(&state, &mut state_file, true)?;
let msg = format!("✅ Saved scan state to {temp_filename:?}");
PROGRESS_PRINTER.println(msg);
log::trace!("exit: sigint_handler (saved to temp folder)");
std::process::exit(1);
};
write_to(&state, &mut state_file, true)?;
}
log::trace!("exit: sigint_handler (end of program)");
std::process::exit(1);

View File

@@ -5,18 +5,19 @@ use anyhow::{Context, Result};
use futures::future::{BoxFuture, FutureExt};
use tokio::sync::{mpsc, oneshot};
use crate::statistics::StatField::TotalExpected;
use crate::{
config::Configuration,
filters::SimilarityFilter,
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,
CommandReceiver, CommandSender, Joiner, UNIQUE_DISTANCE,
};
use std::sync::Arc;
use url::Url;
@@ -93,12 +94,14 @@ impl FileOutHandler {
///
/// The consumer simply receives responses from the terminal handler and writes them to disk
async fn start(&mut self, tx_stats: CommandSender) -> Result<()> {
log::trace!("enter: start_file_handler({:?})", tx_stats);
log::trace!("enter: start_file_handler({tx_stats:?})");
let mut file = open_file(&self.config.output)?;
log::info!("Writing scan results to {}", self.config.output);
write_to(&*self.config, &mut file, self.config.json)?;
while let Some(command) = self.receiver.recv().await {
match command {
Command::Report(response) => {
@@ -144,6 +147,9 @@ pub struct TermOutHandler {
/// pointer to "global" configuration struct
config: Arc<Configuration>,
/// handles instance
handles: Option<Arc<Handles>>,
}
/// implementation of TermOutHandler
@@ -161,6 +167,7 @@ impl TermOutHandler {
tx_file,
file_task,
config,
handles: None,
}
}
@@ -169,7 +176,7 @@ impl TermOutHandler {
config: Arc<Configuration>,
tx_stats: CommandSender,
) -> (Joiner, TermOutHandle) {
log::trace!("enter: initialize({:?}, {:?})", config, tx_stats);
log::trace!("enter: initialize({config:?}, {tx_stats:?})");
let (tx_term, rx_term) = mpsc::unbounded_channel::<Command>();
let (tx_file, rx_file) = mpsc::unbounded_channel::<Command>();
@@ -192,7 +199,7 @@ impl TermOutHandler {
let event_handle = TermOutHandle::new(tx_term, tx_file);
log::trace!("exit: initialize -> ({:?}, {:?})", term_task, event_handle);
log::trace!("exit: initialize -> ({term_task:?}, {event_handle:?})");
(term_task, event_handle)
}
@@ -201,20 +208,29 @@ impl TermOutHandler {
///
/// The consumer simply receives `Command` and acts accordingly
async fn start(&mut self, tx_stats: CommandSender) -> Result<()> {
log::trace!("enter: start({:?})", tx_stats);
log::trace!("enter: start({tx_stats:?})");
while let Some(command) = self.receiver.recv().await {
match command {
Command::Report(resp) => {
self.process_response(tx_stats.clone(), resp, ProcessResponseCall::Recursive)
.await?;
if let Err(err) = self
.process_response(tx_stats.clone(), resp, ProcessResponseCall::Recursive)
.await
{
log::warn!("{err}");
}
}
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
if self.tx_file.send(Command::Exit).is_ok() {
if let Some(task) = self.file_task.as_mut() {
task.await??; // wait for death
}
}
break;
}
@@ -233,10 +249,19 @@ impl TermOutHandler {
mut resp: Box<FeroxResponse>,
call_type: ProcessResponseCall,
) -> BoxFuture<'_, Result<()>> {
log::trace!("enter: process_response({:?}, {:?})", resp, call_type);
log::trace!("enter: process_response({resp:?}, {call_type:?})");
async move {
let contains_sentry = self.config.status_codes.contains(&resp.status().as_u16());
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;
@@ -251,32 +276,37 @@ 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"))
})?;
}
}
log::trace!("report complete: {}", resp.url());
if self.config.replay_client.is_some() && should_process_response {
// replay proxy specified/client created and this response's status code is one that
// should be replayed; not using logged_request due to replay proxy client
let data = if self.config.data.is_empty() {
None
} else {
Some(self.config.data.as_slice())
};
if should_process_response {
if let Some(client) = self.config.replay_client.as_ref() {
// replay proxy specified/client created and this response's status code is one that
// should be replayed; not using logged_request due to replay proxy client
let data = if self.config.data.is_empty() {
None
} else {
Some(self.config.data.as_slice())
};
make_request(
self.config.replay_client.as_ref().unwrap(),
resp.url(),
resp.method().as_str(),
data,
self.config.output_level,
&self.config,
tx_stats.clone(),
)
.await
.with_context(|| "Could not replay request through replay proxy")?;
make_request(
client,
resp.url(),
resp.method().as_str(),
data,
self.config.output_level,
&self.config,
tx_stats.clone(),
)
.await
.with_context(|| "Could not replay request through replay proxy")?;
} else {
// replay proxy not configured, skip replay without exiting response processing
log::trace!("replay proxy not configured, skipping replay");
}
}
if self.config.collect_backups
@@ -284,7 +314,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
@@ -310,9 +340,31 @@ impl TermOutHandler {
resp.url().as_str(),
resp.method().as_str(),
resp.output_level,
self.config.response_size_limit,
)
.await;
let Some(handles) = self.handles.as_ref() else {
// shouldn't ever happen, but we'll log and return early if it does
log::error!("handles were unexpectedly None, this shouldn't happen");
return Ok(());
};
if handles
.filters
.data
.should_filter_response(&ferox_response, tx_stats.clone())
{
// response was filtered for one reason or another, don't process it
continue;
}
if handles.config.unique {
let mut unique_filter = SimilarityFilter::from(&ferox_response);
unique_filter.cutoff = UNIQUE_DISTANCE;
handles.filters.data.push(Box::new(unique_filter))?;
}
self.process_response(
tx_stats.clone(),
Box::new(ferox_response),
@@ -360,22 +412,22 @@ impl TermOutHandler {
/// - LICENSE.bak
/// - .LICENSE.txt.swp
async fn generate_backup_urls(&self, response: &FeroxResponse) -> Vec<Url> {
log::trace!("enter: generate_backup_urls({:?})", response);
log::trace!("enter: generate_backup_urls({response:?})");
let mut urls = vec![];
let url = response.url();
// confirmed safe: see src/response.rs for comments
let filename = url.path_segments().unwrap().last().unwrap();
let filename = url.path_segments().unwrap().next_back().unwrap();
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);
for suffix in &self.config.backup_extensions {
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
@@ -390,7 +442,7 @@ impl TermOutHandler {
}
}
log::trace!("exit: generate_backup_urls -> {:?}", urls);
log::trace!("exit: generate_backup_urls -> {urls:?}");
urls
}
}
@@ -398,6 +450,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 +461,7 @@ mod tests {
config,
receiver: rx,
};
println!("{:?}", foh);
println!("{foh:?}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
@@ -417,15 +470,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 +490,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![
@@ -460,7 +517,7 @@ mod tests {
let paths: Vec<_> = urls
.iter()
.map(|url| url.path_segments().unwrap().last().unwrap())
.map(|url| url.path_segments().unwrap().next_back().unwrap())
.collect();
assert_eq!(urls.len(), 7);
@@ -478,12 +535,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![
@@ -502,7 +561,7 @@ mod tests {
let paths: Vec<_> = urls
.iter()
.map(|url| url.path_segments().unwrap().last().unwrap())
.map(|url| url.path_segments().unwrap().next_back().unwrap())
.collect();
assert_eq!(urls.len(), 6);
@@ -521,12 +580,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![

View File

@@ -1,13 +1,14 @@
use std::sync::Arc;
use anyhow::{bail, Result};
use tokio::sync::{mpsc, Semaphore};
use tokio::sync::mpsc;
use crate::{
response::FeroxResponse,
scan_manager::{FeroxScan, FeroxScans, ScanOrder},
scanner::FeroxScanner,
scanner::{FeroxScanner, RESPONSES},
statistics::StatField::TotalScans,
sync::DynamicSemaphore,
url::FeroxUrl,
utils::should_deny_url,
CommandReceiver, CommandSender, FeroxChannel, Joiner, SLEEP_DURATION,
@@ -16,7 +17,7 @@ use crate::{
use super::command::Command::AddToUsizeField;
use super::*;
use crate::statistics::StatField;
use reqwest::Url;
use crate::utils::parse_url_with_raw_path;
use tokio::time::Duration;
#[derive(Debug)]
@@ -68,7 +69,7 @@ pub struct ScanHandler {
depths: Vec<(String, usize)>,
/// Bounded semaphore used as a barrier to limit concurrent scans
limiter: Arc<Semaphore>,
limiter: Arc<DynamicSemaphore>,
}
/// implementation of event handler for filters
@@ -81,7 +82,7 @@ impl ScanHandler {
receiver: CommandReceiver,
) -> Self {
let limit = handles.config.scan_limit;
let limiter = Semaphore::new(limit);
let limiter = DynamicSemaphore::new(limit);
if limit == 0 {
// scan_limit == 0 means no limit should be imposed... however, scoping the Semaphore
@@ -91,7 +92,7 @@ impl ScanHandler {
// note to self: the docs say max is usize::MAX >> 3, however, threads will panic if
// that value is used (says adding (1) will overflow the semaphore, even though none
// are being added...)
limiter.add_permits(usize::MAX >> 4);
limiter.increase_capacity(usize::MAX >> 4);
}
Self {
@@ -110,7 +111,7 @@ impl ScanHandler {
fn wordlist(&self, wordlist: Arc<Vec<String>>) {
if let Ok(mut guard) = self.wordlist.lock() {
if guard.is_none() {
let _ = std::mem::replace(&mut *guard, Some(wordlist));
guard.replace(wordlist);
}
}
}
@@ -120,7 +121,10 @@ impl ScanHandler {
pub fn initialize(handles: Arc<Handles>) -> (Joiner, ScanHandle) {
log::trace!("enter: initialize");
let data = Arc::new(FeroxScans::new(handles.config.output_level));
let data = Arc::new(FeroxScans::new(
handles.config.output_level,
handles.config.limit_bars,
));
let (tx, rx): FeroxChannel<Command> = mpsc::unbounded_channel();
let max_depth = handles.config.depth;
@@ -131,7 +135,7 @@ impl ScanHandler {
let event_handle = ScanHandle::new(data, tx);
log::trace!("exit: initialize -> ({:?}, {:?})", task, event_handle);
log::trace!("exit: initialize -> ({task:?}, {event_handle:?})");
(task, event_handle)
}
@@ -140,7 +144,7 @@ impl ScanHandler {
///
/// The consumer simply receives `Command` and acts accordingly
pub async fn start(&mut self) -> Result<()> {
log::trace!("enter: start({:?})", self);
log::trace!("enter: start({self:?})");
while let Some(command) = self.receiver.recv().await {
match command {
@@ -194,6 +198,24 @@ impl ScanHandler {
.unwrap_or_default();
}
}
Command::AddScanPermits(value) => {
let current = self.limiter.current_capacity();
self.limiter.increase_capacity(current + value);
log::debug!(
"increased scan permits to {} (was {current})",
current + value
);
}
Command::SubtractScanPermits(value) => {
let current = self.limiter.current_capacity();
let new_capacity = current.saturating_sub(value);
self.limiter.reduce_capacity(new_capacity);
log::debug!("decreased scan permits to {new_capacity} (was {current})");
}
_ => {} // no other commands needed for RecursionHandler
}
}
@@ -206,23 +228,23 @@ impl ScanHandler {
///
/// updating all bar lengths correctly requires a few different actions on our part.
/// - get the current number of requests expected per scan (dynamic when --collect-extensions
/// is used)
/// is used)
/// - update the overall progress bar via the statistics handler (total expected)
/// - update the expected per scan value tracked in the statistics handler
/// - update progress bars on each FeroxScan (type::directory) that are running/not-started
/// - update progress bar length on FeroxScans (this is used when creating new a FeroxScan and
/// determines the new scan's progress bar length)
/// determines the new scan's progress bar length)
fn update_all_bar_lengths(&self) -> Result<()> {
log::trace!("enter: update_all_bar_lengths");
// 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
@@ -266,7 +288,7 @@ impl ScanHandler {
let bar = scan.progress_bar();
// (4000 - 3000) / 2 => 500 words left to send
let length = bar.length();
let length = bar.length().unwrap_or(1);
let num_words_left = (length - bar.position()) / divisor;
// accumulate each bar's increment value for incrementing the total bar
@@ -290,10 +312,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())
};
}
}
@@ -302,7 +328,7 @@ impl ScanHandler {
/// wrapper around scanning a url to stay DRY
async fn ordered_scan_url(&mut self, targets: Vec<String>, order: ScanOrder) -> Result<()> {
log::trace!("enter: ordered_scan_url({:?}, {:?})", targets, order);
log::trace!("enter: ordered_scan_url({targets:?}, {order:?})");
let should_test_deny = !self.handles.config.url_denylist.is_empty()
|| !self.handles.config.regex_denylist.is_empty();
@@ -318,19 +344,34 @@ impl ScanHandler {
let scan = if let Some(ferox_scan) = self.data.get_scan_by_url(&target) {
ferox_scan // scan already known
} else {
self.data.add_directory_scan(&target, order).1 // add the new target; return FeroxScan
self.data
.add_directory_scan(&target, order, self.handles.clone())
.1 // add the new target; return FeroxScan
};
if should_test_deny && should_deny_url(&Url::parse(&target)?, self.handles.clone())? {
if should_test_deny
&& should_deny_url(&parse_url_with_raw_path(&target)?, self.handles.clone())?
{
// response was caught by a user-provided deny list
// checking this last, since it's most susceptible to longer runtimes due to what
// input is received
continue;
}
let list = self.get_wordlist()?;
let divisor = self.handles.expected_num_requests_multiplier();
log::info!("scan handler received {} - beginning scan", target);
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 {target} - beginning scan");
if matches!(order, ScanOrder::Initial) {
// keeps track of the initial targets' scan depths in order to enforce the
@@ -350,7 +391,7 @@ impl ScanHandler {
let task = tokio::spawn(async move {
if let Err(e) = scanner.scan_url().await {
log::warn!("{}", e);
log::warn!("{e}");
}
});
@@ -366,10 +407,10 @@ impl ScanHandler {
}
async fn try_recursion(&mut self, response: Box<FeroxResponse>) -> Result<()> {
log::trace!("enter: try_recursion({:?})", response,);
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 +427,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

@@ -77,7 +77,7 @@ impl StatsHandler {
///
/// The consumer simply receives `StatCommands` and updates the given `Stats` object as appropriate
async fn start(&mut self, output_file: &str) -> Result<()> {
log::trace!("enter: start({:?})", self);
log::trace!("enter: start({self:?})");
let start = Instant::now();
@@ -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)?;
@@ -124,6 +125,12 @@ impl StatsHandler {
Command::Sync(sender) => {
sender.send(true).unwrap_or_default();
}
Command::QueryOverallBarEta(sender) => {
sender.send(self.bar.eta()).unwrap_or_default();
}
Command::UpdateTargets(targets) => {
self.stats.update_targets(targets);
}
Command::Exit => break,
_ => {} // no more commands needed
}
@@ -131,7 +138,7 @@ impl StatsHandler {
self.bar.finish();
log::debug!("{:#?}", *self.stats);
log::info!("{:#?}", *self.stats);
log::trace!("exit: start");
Ok(())
}
@@ -146,8 +153,13 @@ impl StatsHandler {
self.stats.errors(),
);
self.bar.set_message(&msg);
self.bar.inc(1);
self.bar.set_message(msg);
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
@@ -164,7 +176,7 @@ impl StatsHandler {
let event_handle = StatsHandle::new(data, tx);
log::trace!("exit: initialize -> ({:?}, {:?})", task, event_handle);
log::trace!("exit: initialize -> ({task:?}, {event_handle:?})");
(task, event_handle)
}

View File

@@ -4,14 +4,59 @@ use anyhow::{bail, Result};
/// Regular expression used in [LinkFinder](https://github.com/GerbenJavado/LinkFinder)
///
/// Incorporates change from this [Pull Request](https://github.com/GerbenJavado/LinkFinder/pull/66/files)
pub(super) const LINKFINDER_REGEX: &str = r#"(?:"|')(((?:[a-zA-Z]{1,10}://|//)[^"'/]{1,}\.[a-zA-Z]{2,}[^"']{0,})|((?:/|\.\./|\./)[^"'><,;| *()(%%$^/\\\[\]][^"'><,;|()]{1,})|([a-zA-Z0-9_\-/]{1,}/[a-zA-Z0-9_\-/]{1,}\.(?:[a-zA-Z]{1,4}|action)(?:[\?|#][^"|']{0,}|))|([a-zA-Z0-9_\-/]{1,}/[a-zA-Z0-9_\-/]{3,}(?:[\?|#][^"|']{0,}|))|([a-zA-Z0-9_\-.]{1,}\.(?:php|asp|aspx|jsp|json|action|html|js|txt|xml)(?:[\?|#][^"|']{0,}|)))(?:"|')"#;
/// updated on 8 August 2025 to commit 1debac5dace4724fd6187c06f133578dae51c86f
///
/// NOTE: the ` ? or # mark with parameters` lines need to have the # character escaped as `\#`
/// to avoid being interpreted as a comment by the Rust compiler
pub(super) const LINKFINDER_REGEX: &str = r#"(?x)
(?:"|') # Start newline delimiter
(
((?:[a-zA-Z]{1,10}://|//) # Match a scheme [a-Z]*1-10 or //
[^"'/]{1,}\. # Match a domainname (any character + dot)
[a-zA-Z]{2,}[^"']{0,}) # The domainextension and/or path
|
((?:/|\.\./|\./) # Start with /,../,./
[^"'><,;| *()(%%$^/\\\[\]] # Next character can't be...
[^"'><,;|()]{1,}) # Rest of the characters can't be
|
([a-zA-Z0-9_\-/]{1,}/ # Relative endpoint with /
[a-zA-Z0-9_\-/.]{1,} # Resource name
\.(?:[a-zA-Z]{1,4}|action) # Rest + extension (length 1-4 or action)
(?:[\?|\#][^"|']{0,}|)) # ? or # mark with parameters
|
([a-zA-Z0-9_\-/]{1,}/ # REST API (no extension) with /
[a-zA-Z0-9_\-/]{3,} # Proper REST endpoints usually have 3+ chars
(?:[\?|\#][^"|']{0,}|)) # ? or # mark with parameters
|
([a-zA-Z0-9_\-]{1,} # filename
\.(?:php|asp|aspx|jsp|json|
action|html|js|txt|xml) # . + extension
(?:[\?|\#][^"|']{0,}|)) # ? or # mark with parameters
)
(?:"|') # End newline delimiter
"#;
/// Regular expression to pull url paths from robots.txt
///
/// ref: https://developers.google.com/search/reference/robots_txt
pub(super) const ROBOTS_TXT_REGEX: &str =
r#"(?m)^ *(Allow|Disallow): *(?P<url_path>[a-zA-Z0-9._/?#@!&'()+,;%=-]+?)$"#; // multi-line (?m)
r#"(?m)^[ \t]*(?i)(allow|disallow)[ \t]*:[ \t]*(?P<url_path>[^ \t\r\n#$]*)?[ \t]*\$?(?:#.*)?$"#; // multi-line (?m), case-insensitive (?i)
/// 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)]
@@ -90,11 +135,8 @@ impl<'a> ExtractorBuilder<'a> {
Ok(Extractor {
links_regex: Regex::new(LINKFINDER_REGEX).unwrap(),
robots_regex: Regex::new(ROBOTS_TXT_REGEX).unwrap(),
response: if self.response.is_some() {
Some(self.response.unwrap())
} else {
None
},
url_regex: Regex::new(URL_CHARS_REGEX).unwrap(),
response: self.response,
url: self.url.to_owned(),
handles: self.handles.as_ref().unwrap().clone(),
target: self.target,

View File

@@ -2,27 +2,70 @@ use super::*;
use crate::{
client,
event_handlers::{
Command,
Command::{AddError, AddToUsizeField},
Handles,
},
filters::SimilarityFilter,
scan_manager::ScanOrder,
statistics::{
StatError::Other,
StatField::{LinksExtracted, TotalExpected},
},
url::FeroxUrl,
utils::{logged_request, make_request, should_deny_url},
ExtractionResult, DEFAULT_METHOD,
url::{FeroxUrl, UrlExt},
utils::{
logged_request, make_request, parse_url_with_raw_path, send_try_recursion_command,
should_deny_url,
},
ExtractionResult, DEFAULT_METHOD, UNIQUE_DISTANCE,
};
use anyhow::{bail, Context, Result};
use reqwest::{Client, StatusCode, Url};
use futures::StreamExt;
use reqwest::{Client, Response, StatusCode, Url};
use scraper::{Html, Selector};
use std::collections::HashSet;
use tokio::sync::oneshot;
use std::{borrow::Cow, collections::HashSet};
/// Wrapper around link extraction logic
/// - create a new Url object based on cli options/args
/// - check if the new Url has already been seen/scanned -> None
/// - make a request to the new Url ? -> Some(response) : None
pub(super) async fn request_link(url: &str, handles: Arc<Handles>) -> Result<Response> {
log::trace!("enter: request_link({url})");
let ferox_url = FeroxUrl::from_string(url, handles.clone());
// create a url based on the given command line options
let new_url = ferox_url.format("", None)?;
let scanned_urls = handles.ferox_scans()?;
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");
}
if (!handles.config.url_denylist.is_empty() || !handles.config.regex_denylist.is_empty())
&& should_deny_url(&new_url, handles.clone())?
{
// can't allow a denied url to be requested
bail!(
"prevented request to {} due to {:?} || {:?}",
url,
handles.config.url_denylist,
handles.config.regex_denylist,
);
}
// make the request and store the response
let new_response = logged_request(&new_url, DEFAULT_METHOD, None, handles.clone()).await?;
log::trace!("exit: request_link -> {new_response:?}");
Ok(new_response)
}
/// Whether an active scan is recursive or not
#[derive(Debug)]
#[derive(Debug, Copy, Clone)]
enum RecursionStatus {
/// Scan is recursive
Recursive,
@@ -40,6 +83,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>,
@@ -70,28 +116,24 @@ impl<'a> Extractor<'a> {
/// wrapper around logic that performs the following:
/// - parses `url_to_parse`
/// - bails if the parsed url doesn't belong to the original host/domain
/// - bails if the parsed url doesn't belong to the list of in-scope urls
/// - otherwise, calls `add_all_sub_paths` with the parsed result
fn parse_url_and_add_subpaths(
&self,
url_to_parse: &str,
original_url: &Url,
links: &mut HashSet<String>,
) -> Result<()> {
log::trace!("enter: parse_url_and_add_subpaths({:?})", links);
log::trace!("enter: parse_url_and_add_subpaths({links:?})");
match Url::parse(url_to_parse) {
match parse_url_with_raw_path(url_to_parse) {
Ok(absolute) => {
if absolute.domain() != original_url.domain()
|| absolute.host() != original_url.host()
{
// domains/ips are not the same, don't scan things that aren't part of the original
// target url
bail!("parsed url does not belong to original domain/host");
if !absolute.is_in_scope(&self.handles.config.scope) {
// URL is not in scope based on domain/scope configuration
bail!("parsed url is not in scope");
}
if self.add_all_sub_paths(absolute.path(), links).is_err() {
log::warn!("could not add sub-paths from {} to {:?}", absolute, links);
log::warn!("could not add sub-paths from {absolute} to {links:?}");
}
}
Err(e) => {
@@ -99,16 +141,15 @@ impl<'a> Extractor<'a> {
// ex: Url::parse("/login") -> Err("relative URL without a base")
// while this is technically an error, these are good results for us
if e.to_string().contains("relative URL without a base") {
// scope for these should be enforced in add_all_sub_paths since
// we join the fragment with the base url there and can check
// the full Url against scope
if self.add_all_sub_paths(url_to_parse, links).is_err() {
log::warn!(
"could not add sub-paths from {} to {:?}",
url_to_parse,
links
);
log::warn!("could not add sub-paths from {url_to_parse} to {links:?}");
}
} else {
// unexpected error has occurred
log::warn!("Could not parse given url: {}", e);
log::warn!("Could not parse given url: {e}");
self.handles.stats.send(AddError(Other)).unwrap_or_default();
}
}
@@ -120,81 +161,154 @@ impl<'a> Extractor<'a> {
/// given a set of links from a normal http body response, task the request handler to make
/// the requests
pub async fn request_links(&mut self, links: HashSet<String>) -> Result<()> {
log::trace!("enter: request_links({:?})", links);
pub async fn request_links(
&mut self,
links: HashSet<String>,
) -> Result<Option<tokio::task::JoinHandle<()>>> {
log::trace!("enter: request_links({links:?})");
if links.is_empty() {
return Ok(());
return Ok(None);
}
self.update_stats(links.len())?;
// create clones/remove use of self of/from everything the async move block will need to function
let cloned_scanned_urls = self.handles.ferox_scans()?;
let cloned_handles = self.handles.clone();
let cloned_url = self.url.clone();
let threads = self.handles.config.threads;
let recursive = if self.handles.config.no_recursion {
RecursionStatus::NotRecursive
} else {
RecursionStatus::Recursive
};
let scanned_urls = self.handles.ferox_scans()?;
self.update_stats(links.len())?;
let link_request_task = tokio::spawn(async move {
let producers = futures::stream::iter(links.into_iter())
.map(|link| {
// another clone to satisfy the async move block
let inner_clone = cloned_handles.clone();
for link in links {
let mut resp = match self.request_link(&link).await {
Ok(resp) => resp,
Err(_) => continue,
};
(
tokio::spawn(async move { request_link(&link, inner_clone).await }),
cloned_handles.clone(),
cloned_scanned_urls.clone(),
recursive,
cloned_url.clone(),
)
})
.for_each_concurrent(
threads,
|(join_handle, c_handles, c_scanned_urls, c_recursive, og_url)| async move {
match join_handle.await {
Ok(Ok(reqwest_response)) => {
let mut resp = FeroxResponse::from(
reqwest_response,
&og_url,
DEFAULT_METHOD,
c_handles.config.output_level,
c_handles.config.response_size_limit,
)
.await;
// filter if necessary
if self
.handles
.filters
.data
.should_filter_response(&resp, self.handles.stats.tx.clone())
{
continue;
}
// filter if necessary
if c_handles
.filters
.data
.should_filter_response(&resp, c_handles.stats.tx.clone())
{
return;
}
// request and report assumed file
if resp.is_file() || !resp.is_directory() {
log::debug!("Extracted File: {}", resp);
if c_handles.config.unique {
// if the filter above didn't filter it out, add it as a unique filter
let mut unique_filter = SimilarityFilter::from(&resp);
unique_filter.cutoff = UNIQUE_DISTANCE;
c_handles
.filters
.data
.push(Box::new(unique_filter))
.unwrap_or_default();
}
scanned_urls.add_file_scan(resp.url().as_str(), ScanOrder::Latest);
// request and report assumed file
if !resp.is_directory() && !c_handles.config.force_recursion {
log::debug!("Extracted File: {resp}");
if self.handles.config.collect_extensions {
resp.parse_extension(self.handles.clone())?;
}
c_scanned_urls.add_file_scan(
resp.url().as_str(),
ScanOrder::Latest,
c_handles.clone(),
);
if let Err(e) = resp.send_report(self.handles.output.tx.clone()) {
log::warn!("Could not send FeroxResponse to output handler: {}", e);
}
if c_handles.config.collect_extensions {
// no real reason this should fail
resp.parse_extension(c_handles.clone()).unwrap();
}
continue;
}
if let Err(e) = resp.send_report(c_handles.output.tx.clone()) {
log::warn!(
"Could not send FeroxResponse to output handler: {e}"
);
}
if matches!(recursive, RecursionStatus::Recursive) {
log::debug!("Extracted Directory: {}", resp);
return;
}
if !resp.url().as_str().ends_with('/')
&& (resp.status().is_success()
|| matches!(resp.status(), &StatusCode::FORBIDDEN))
{
// if the url doesn't end with a /
// and the response code is either a 2xx or 403
if matches!(c_recursive, RecursionStatus::Recursive) {
log::debug!("Extracted Directory: {resp}");
// since all of these are 2xx or 403, recursion is only attempted if the
// url ends in a /. I am actually ok with adding the slash and not
// adding it, as both have merit. Leaving it in for now to see how
// things turn out (current as of: v1.1.0)
resp.set_url(&format!("{}/", resp.url()));
}
if !resp.url().as_str().ends_with('/')
&& (resp.status().is_success()
|| matches!(resp.status(), &StatusCode::FORBIDDEN))
{
// if the url doesn't end with a /
// and the response code is either a 2xx or 403
// since all of these are 2xx or 403, recursion is only attempted if the
// url ends in a /. I am actually ok with adding the slash and not
// adding it, as both have merit. Leaving it in for now to see how
// things turn out (current as of: v1.1.0)
resp.set_url(&format!("{}/", resp.url()));
}
if c_handles.config.filter_status.is_empty() {
// -C wasn't used, so -s is the only 'filter' left to account for
if c_handles
.config
.status_codes
.contains(&resp.status().as_u16())
{
send_try_recursion_command(c_handles.clone(), resp)
.await
.unwrap_or_default();
}
} 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(c_handles.clone(), resp)
.await
.unwrap_or_default();
}
}
}
Ok(Err(err)) => {
log::warn!("Error during link extraction: {err}");
}
Err(err) => {
log::warn!("JoinError during link extraction: {err}");
}
}
},
);
// wait for the requests to finish
producers.await;
});
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?;
}
}
log::trace!("exit: request_links");
Ok(())
Ok(Some(link_request_task))
}
/// wrapper around link extraction via html attributes
@@ -212,6 +326,7 @@ impl<'a> Extractor<'a> {
self.extract_links_by_attr(resp_url, links, html, "div", "src");
self.extract_links_by_attr(resp_url, links, html, "frame", "src");
self.extract_links_by_attr(resp_url, links, html, "embed", "src");
self.extract_links_by_attr(resp_url, links, html, "link", "href");
}
/// Given the body of a `reqwest::Response`, perform the following actions
@@ -241,10 +356,7 @@ impl<'a> Extractor<'a> {
// capture[0] is the entire match, additional capture groups start at [1]
let link = capture[0].trim_matches(|c| c == '\'' || c == '"');
if self
.parse_url_and_add_subpaths(link, response_url, links)
.is_err()
{
if self.parse_url_and_add_subpaths(link, links).is_err() {
// purposely not logging the error here, due to the frequency with which it gets hit
}
}
@@ -259,7 +371,7 @@ impl<'a> Extractor<'a> {
/// - homepage/assets/
/// - homepage/
fn add_all_sub_paths(&self, url_path: &str, links: &mut HashSet<String>) -> Result<()> {
log::trace!("enter: add_all_sub_paths({}, {:?})", url_path, links);
log::trace!("enter: add_all_sub_paths({url_path}, {links:?})");
for sub_path in self.get_sub_paths_from_path(url_path) {
self.add_link_to_set_of_links(&sub_path, links)?;
@@ -272,7 +384,7 @@ impl<'a> Extractor<'a> {
/// given a url path, trim whitespace, remove slashes, and queries/fragments; return the
/// normalized string
pub(super) fn normalize_url_path(&self, path: &str) -> String {
log::trace!("enter: normalize_url_path({})", path);
log::trace!("enter: normalize_url_path({path})");
// remove whitespace and leading '/'
let path_str: String = path
@@ -304,7 +416,7 @@ impl<'a> Extractor<'a> {
path_str.split_once('#').unwrap_or((&path_str, ""))
});
log::trace!("exit: normalize_url_path -> {}", path_str);
log::trace!("exit: normalize_url_path -> {path_str}");
path_str.into()
}
@@ -318,14 +430,15 @@ impl<'a> Extractor<'a> {
/// - homepage/assets/
/// - homepage/
pub(super) fn get_sub_paths_from_path(&self, path: &str) -> Vec<String> {
log::trace!("enter: get_sub_paths_from_path({})", path);
log::trace!("enter: get_sub_paths_from_path({path})");
let mut paths = vec![];
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();
@@ -349,14 +462,14 @@ 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
parts.pop(); // use .pop() to remove the last part of the path and continue iteration
}
log::trace!("exit: get_sub_paths_from_path -> {:?}", paths);
log::trace!("exit: get_sub_paths_from_path -> {paths:?}");
paths
}
@@ -366,13 +479,13 @@ impl<'a> Extractor<'a> {
link: &str,
links: &mut HashSet<String>,
) -> Result<()> {
log::trace!("enter: add_link_to_set_of_links({}, {:?})", link, links);
log::trace!("enter: add_link_to_set_of_links({link}, {links:?})");
let old_url = match self.target {
ExtractionTarget::ResponseBody | ExtractionTarget::DirectoryListing => {
self.response.unwrap().url().clone()
}
ExtractionTarget::RobotsTxt => match Url::parse(&self.url) {
ExtractionTarget::RobotsTxt => match parse_url_with_raw_path(&self.url) {
Ok(u) => u,
Err(e) => {
bail!("Could not parse {}: {}", self.url, e);
@@ -382,7 +495,14 @@ 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 !new_url.is_in_scope(&self.handles.config.scope) {
// URL is not in scope based on domain/scope configuration
log::debug!("Skipping {new_url} because it's not in scope");
log::trace!("exit: add_link_to_set_of_links");
return Ok(());
}
links.insert(new_url.to_string());
@@ -391,56 +511,6 @@ impl<'a> Extractor<'a> {
Ok(())
}
/// Wrapper around link extraction logic
/// - create a new Url object based on cli options/args
/// - check if the new Url has already been seen/scanned -> None
/// - make a request to the new Url ? -> Some(response) : None
pub(super) async fn request_link(&self, url: &str) -> Result<FeroxResponse> {
log::trace!("enter: request_link({})", url);
let ferox_url = FeroxUrl::from_string(url, self.handles.clone());
// create a url based on the given command line options
let new_url = ferox_url.format("", None)?;
let scanned_urls = self.handles.ferox_scans()?;
if scanned_urls.get_scan_by_url(&new_url.to_string()).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");
}
if (!self.handles.config.url_denylist.is_empty()
|| !self.handles.config.regex_denylist.is_empty())
&& should_deny_url(&new_url, self.handles.clone())?
{
// can't allow a denied url to be requested
bail!(
"prevented request to {} due to {:?} || {:?}",
url,
self.handles.config.url_denylist,
self.handles.config.regex_denylist,
);
}
// make the request and store the response
let new_response =
logged_request(&new_url, DEFAULT_METHOD, None, self.handles.clone()).await?;
let new_ferox_response = FeroxResponse::from(
new_response,
url,
DEFAULT_METHOD,
self.handles.config.output_level,
)
.await;
log::trace!("exit: request_link -> {:?}", new_ferox_response);
Ok(new_ferox_response)
}
/// Entry point to perform link extraction from robots.txt
///
/// `base_url` can have paths and subpaths, however robots.txt will be requested from the
@@ -460,17 +530,17 @@ impl<'a> Extractor<'a> {
for capture in self.robots_regex.captures_iter(body) {
if let Some(new_path) = capture.name("url_path") {
let mut new_url = Url::parse(&self.url)?;
let mut new_url = parse_url_with_raw_path(&self.url)?;
new_url.set_path(new_path.as_str());
if self.add_all_sub_paths(new_url.path(), &mut result).is_err() {
log::warn!("could not add sub-paths from {} to {:?}", new_url, result);
log::warn!("could not add sub-paths from {new_url} to {result:?}");
}
}
}
log::trace!("exit: extract_robots_txt -> {:?}", result);
log::trace!("exit: extract_robots_txt -> {result:?}");
Ok(result)
}
@@ -494,7 +564,7 @@ impl<'a> Extractor<'a> {
self.extract_all_links_from_html_tags(resp_url, &mut result, &html);
self.extract_all_links_from_javascript(body, resp_url, &mut result);
log::trace!("exit: extract_from_body -> {:?}", result);
log::trace!("exit: extract_from_body -> {result:?}");
Ok(result)
}
@@ -513,7 +583,7 @@ impl<'a> Extractor<'a> {
self.extract_links_by_attr(response.url(), &mut result, &html, "a", "href");
log::trace!("exit: extract_from_dir_listing -> {:?}", result);
log::trace!("exit: extract_from_dir_listing -> {result:?}");
Ok(result)
}
@@ -528,7 +598,10 @@ impl<'a> Extractor<'a> {
) {
log::trace!("enter: extract_links_by_attr");
let selector = Selector::parse(html_tag).unwrap();
let Some(selector) = Selector::parse(html_tag).ok() else {
log::warn!("Failed to parse selector for tag: {html_tag}");
return;
};
let tags = html
.select(&selector)
@@ -538,11 +611,8 @@ impl<'a> Extractor<'a> {
if let Some(link) = tag.value().attr(html_attr) {
log::debug!("Parsed link \"{}\" from {}", link, resp_url.as_str());
if self
.parse_url_and_add_subpaths(link, resp_url, links)
.is_err()
{
log::debug!("link didn't belong to the target domain/host: {}", link);
if self.parse_url_and_add_subpaths(link, links).is_err() {
log::debug!("link didn't belong to the target domain/host: {link}");
}
}
}
@@ -574,14 +644,33 @@ impl<'a> Extractor<'a> {
Some(self.handles.config.proxy.as_str())
};
client = client::initialize(
self.handles.config.timeout,
&self.handles.config.user_agent,
follow_redirects,
self.handles.config.insecure,
&self.handles.config.headers,
let server_certs = &self.handles.config.server_certs;
let client_cert = if self.handles.config.client_cert.is_empty() {
None
} else {
Some(self.handles.config.client_cert.as_str())
};
let client_key = if self.handles.config.client_key.is_empty() {
None
} else {
Some(self.handles.config.client_key.as_str())
};
let client_config = client::ClientConfig {
timeout: self.handles.config.timeout,
user_agent: &self.handles.config.user_agent,
redirects: follow_redirects,
insecure: self.handles.config.insecure,
headers: &self.handles.config.headers,
proxy,
)?;
server_certs: Some(server_certs),
client_cert,
client_key,
scope: &self.handles.config.scope,
};
client = client::initialize(client_config)?;
}
let client = if location != "/robots.txt" {
@@ -590,7 +679,7 @@ impl<'a> Extractor<'a> {
&client
};
let mut url = Url::parse(&self.url)?;
let mut url = parse_url_with_raw_path(&self.url)?;
url.set_path(location); // overwrite existing path
// purposefully not using logged_request here due to using the special client
@@ -610,11 +699,12 @@ impl<'a> Extractor<'a> {
&self.url,
DEFAULT_METHOD,
self.handles.config.output_level,
self.handles.config.response_size_limit,
)
.await;
// note: don't call parse_extension here. If we call it here, it gets called on robots.txt
log::trace!("exit: make_extract_request -> {}", ferox_response);
log::trace!("exit: make_extract_request -> {ferox_response}");
Ok(ferox_response)
}

View File

@@ -1,4 +1,5 @@
use super::builder::{LINKFINDER_REGEX, ROBOTS_TXT_REGEX};
use super::builder::{LINKFINDER_REGEX, ROBOTS_TXT_REGEX, URL_CHARS_REGEX};
use super::container::request_link;
use super::*;
use crate::config::{Configuration, OutputLevel};
use crate::scan_manager::ScanOrder;
@@ -50,7 +51,12 @@ fn setup_extractor(target: ExtractionTarget, scanned_urls: Arc<FeroxScans>) -> E
.target(ExtractionTarget::DirectoryListing),
};
let config = Arc::new(Configuration::new().unwrap());
// need to add scope to the config to allow extracted links to make it through the
// full pipeline
let mut config = Configuration::new().unwrap();
config.scope.push(Url::parse("http://localhost").unwrap());
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scanned_urls), Some(config)).0);
builder.handles(handles).build().unwrap()
@@ -267,12 +273,19 @@ async fn extractor_get_links_with_absolute_url_that_differs_from_target_domain()
let (handles, _rx) = Handles::for_testing(None, None);
let handles = Arc::new(handles);
let ferox_response =
FeroxResponse::from(response, &srv.url(""), DEFAULT_METHOD, OutputLevel::Default).await;
let ferox_response = FeroxResponse::from(
response,
&srv.url(""),
DEFAULT_METHOD,
OutputLevel::Default,
4194304,
)
.await;
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 +314,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 +324,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(())
@@ -358,13 +372,13 @@ async fn request_link_happy_path() -> Result<()> {
then.status(200).body("this is a test");
});
let r_resp = ROBOTS_EXT.request_link(&srv.url("/login.php")).await?;
let b_resp = BODY_EXT.request_link(&srv.url("/login.php")).await?;
let r_resp = request_link(&srv.url("/login.php"), ROBOTS_EXT.handles.clone()).await?;
let b_resp = request_link(&srv.url("/login.php"), BODY_EXT.handles.clone()).await?;
assert!(matches!(r_resp.status(), &StatusCode::OK));
assert!(matches!(b_resp.status(), &StatusCode::OK));
assert_eq!(r_resp.content_length(), 14);
assert_eq!(b_resp.content_length(), 14);
assert!(matches!(r_resp.status(), StatusCode::OK));
assert!(matches!(b_resp.status(), StatusCode::OK));
assert_eq!(r_resp.content_length().unwrap(), 14);
assert_eq!(b_resp.content_length().unwrap(), 14);
assert_eq!(mock.hits(), 2);
Ok(())
}
@@ -383,13 +397,17 @@ async fn request_link_bails_on_seen_url() -> Result<()> {
});
let scans = Arc::new(FeroxScans::default());
scans.add_file_scan(&served, ScanOrder::Latest);
scans.add_file_scan(
&served,
ScanOrder::Latest,
Arc::new(Handles::for_testing(None, None).0),
);
let robots = setup_extractor(ExtractionTarget::RobotsTxt, scans.clone());
let body = setup_extractor(ExtractionTarget::ResponseBody, scans);
let r_resp = robots.request_link(&served).await;
let b_resp = body.request_link(&served).await;
let r_resp = request_link(&served, robots.handles.clone()).await;
let b_resp = request_link(&served, body.handles.clone()).await;
assert!(r_resp.is_err());
assert!(b_resp.is_err());

View File

@@ -3,16 +3,16 @@ use std::sync::RwLock;
use anyhow::Result;
use serde::{ser::SerializeSeq, Serialize, Serializer};
use crate::{
event_handlers::Command::AddToUsizeField, response::FeroxResponse,
statistics::StatField::WildcardsFiltered, CommandSender,
};
use crate::response::FeroxResponse;
use super::{
FeroxFilter, LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter,
WildcardFilter, WordsFilter,
};
use crate::{
event_handlers::Command::AddToUsizeField, statistics::StatField::WildcardsFiltered,
CommandSender,
};
/// Container around a collection of `FeroxFilters`s
#[derive(Debug, Default)]
pub struct FeroxFilters {
@@ -76,6 +76,7 @@ impl FeroxFilters {
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))
@@ -104,6 +105,10 @@ impl Serialize for FeroxFilters {
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>()
{
@@ -114,10 +119,6 @@ impl Serialize for FeroxFilters {
filter.as_any().downcast_ref::<SimilarityFilter>()
{
seq.serialize_element(similarity_filter).unwrap_or_default();
} else if let Some(wildcard_filter) =
filter.as_any().downcast_ref::<WildcardFilter>()
{
seq.serialize_element(wildcard_filter).unwrap_or_default();
}
}
seq.end()

View File

@@ -1,7 +1,7 @@
use super::*;
/// Dummy filter for internal shenanigans
#[derive(Default, Debug, PartialEq)]
#[derive(Default, Debug, PartialEq, Eq)]
pub struct EmptyFilter {}
impl FeroxFilter for EmptyFilter {
@@ -12,7 +12,7 @@ impl FeroxFilter for EmptyFilter {
/// Compare one EmptyFilter to another
fn box_eq(&self, other: &dyn Any) -> bool {
other.downcast_ref::<Self>().map_or(false, |a| self == a)
other.downcast_ref::<Self>() == Some(self)
}
/// 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 number of lines
/// in a Response body; specified using -N|--filter-lines
#[derive(Default, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct LinesFilter {
/// Number of lines in a Response's body that should be filtered
pub line_count: usize,
@@ -12,18 +12,18 @@ pub struct LinesFilter {
impl FeroxFilter for LinesFilter {
/// Check `line_count` against what was passed in via -N|--filter-lines
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
log::trace!("enter: should_filter_response({:?} {})", self, response);
log::trace!("enter: should_filter_response({self:?} {response})");
let result = response.line_count() == self.line_count;
log::trace!("exit: should_filter_response -> {}", result);
log::trace!("exit: should_filter_response -> {result}");
result
}
/// Compare one LinesFilter to another
fn box_eq(&self, other: &dyn Any) -> bool {
other.downcast_ref::<Self>().map_or(false, |a| self == a)
other.downcast_ref::<Self>() == Some(self)
}
/// Return self as Any for dynamic dispatch purposes

View File

@@ -4,21 +4,20 @@ 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;
@@ -30,4 +29,5 @@ mod container;
mod tests;
mod init;
mod utils;
mod wildcard;
mod empty;

View File

@@ -27,18 +27,22 @@ impl FeroxFilter for RegexFilter {
/// Check `expression` against the response body, if the expression matches, the response
/// should be filtered out
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
log::trace!("enter: should_filter_response({:?} {})", self, response);
log::trace!("enter: should_filter_response({self:?} {response})");
let result = self.compiled.is_match(response.text());
let other = response.headers().iter().any(|(k, v)| {
self.compiled.is_match(k.as_str()) || self.compiled.is_match(v.to_str().unwrap_or(""))
});
log::trace!("exit: should_filter_response -> {}", result);
let final_result = result || other;
log::trace!("exit: should_filter_response -> {final_result}");
result
final_result
}
/// Compare one SizeFilter to another
fn box_eq(&self, other: &dyn Any) -> bool {
other.downcast_ref::<Self>().map_or(false, |a| self == a)
other.downcast_ref::<Self>() == Some(self)
}
/// Return self as Any for dynamic dispatch purposes

View File

@@ -1,18 +1,48 @@
use super::*;
use fuzzyhash::FuzzyHash;
use crate::nlp::preprocess;
use crate::NEAR_DUPLICATE_DISTANCE;
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));
}
/// Simple implementor of FeroxFilter; used to filter out responses based on the similarity of a
/// Response body with a known response; specified using --filter-similar-to
#[derive(Default, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SimilarityFilter {
/// Hash of Response's body to be used during similarity comparison
pub hash: String,
/// Percentage of similarity at which a page is determined to be a near-duplicate of another
pub threshold: u32,
pub hash: u64,
/// Url originally requested for the similarity filter
pub original_url: String,
/// Maximum hamming distance allowed between two signatures
pub cutoff: usize,
}
impl SimilarityFilter {
/// Create a new SimilarityFilter
pub fn new(hash: u64, original_url: String, cutoff: usize) -> Self {
Self {
hash,
original_url,
cutoff,
}
}
}
impl From<&FeroxResponse> for SimilarityFilter {
fn from(response: &FeroxResponse) -> Self {
Self::new(
SIM_HASHER.create_signature(preprocess(response.text()).iter()),
response.url().to_string(),
NEAR_DUPLICATE_DISTANCE,
)
}
}
/// implementation of FeroxFilter for SimilarityFilter
@@ -20,20 +50,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.hash, &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) <= self.cutoff
}
/// 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>()
.is_some_and(|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, Serialize, Deserialize)]
#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SizeFilter {
/// Overall length of a Response's body that should be filtered
pub content_length: u64,
@@ -12,18 +12,18 @@ pub struct SizeFilter {
impl FeroxFilter for SizeFilter {
/// Check `content_length` against what was passed in via -S|--filter-size
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
log::trace!("enter: should_filter_response({:?} {})", self, response);
log::trace!("enter: should_filter_response({self:?} {response})");
let result = response.content_length() == self.content_length;
log::trace!("exit: should_filter_response -> {}", result);
log::trace!("exit: should_filter_response -> {result}");
result
}
/// Compare one SizeFilter to another
fn box_eq(&self, other: &dyn Any) -> bool {
other.downcast_ref::<Self>().map_or(false, |a| self == a)
other.downcast_ref::<Self>() == Some(self)
}
/// Return self as Any for dynamic dispatch purposes

View File

@@ -2,7 +2,7 @@ use super::*;
/// Simple implementor of FeroxFilter; used to filter out status codes specified using
/// -C|--filter-status
#[derive(Default, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct StatusCodeFilter {
/// Status code that should not be displayed to the user
pub filter_code: u16,
@@ -12,7 +12,7 @@ pub struct StatusCodeFilter {
impl FeroxFilter for StatusCodeFilter {
/// Check `filter_code` against what was passed in via -C|--filter-status
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
log::trace!("enter: should_filter_response({:?} {})", self, response);
log::trace!("enter: should_filter_response({self:?} {response})");
if response.status().as_u16() == self.filter_code {
log::debug!(
@@ -30,7 +30,7 @@ impl FeroxFilter for StatusCodeFilter {
/// Compare one StatusCodeFilter to another
fn box_eq(&self, other: &dyn Any) -> bool {
other.downcast_ref::<Self>().map_or(false, |a| self == a)
other.downcast_ref::<Self>() == Some(self)
}
/// Return self as Any for dynamic dispatch purposes

View File

@@ -1,26 +1,39 @@
use super::*;
use ::fuzzyhash::FuzzyHash;
use crate::nlp::preprocess;
use crate::DEFAULT_METHOD;
use crate::NEAR_DUPLICATE_DISTANCE;
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 +124,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 +152,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 +173,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,24 +208,24 @@ fn similarity_filter_is_accurate() {
resp.set_text("sitting");
let mut filter = SimilarityFilter {
hash: FuzzyHash::new("kitten").to_string(),
threshold: 95,
hash: SIM_HASHER.create_signature(["kitten"].iter()),
original_url: "".to_string(),
cutoff: NEAR_DUPLICATE_DISTANCE,
};
// kitten/sitting is 57% similar, so a threshold of 95 should not be filtered
assert!(!filter.should_filter_response(&resp));
resp.set_text("");
filter.hash = 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.hash = 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));
}
@@ -212,20 +234,19 @@ 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 {
hash: String::from("stuff"),
threshold: 95,
hash: 1,
original_url: "".to_string(),
cutoff: NEAR_DUPLICATE_DISTANCE,
};
let filter2 = SimilarityFilter {
hash: String::from("stuff"),
threshold: 95,
hash: 1,
original_url: "".to_string(),
cutoff: NEAR_DUPLICATE_DISTANCE,
};
assert!(filter.box_eq(filter2.as_any()));
assert_eq!(filter.hash, "stuff");
assert_eq!(
*filter.as_any().downcast_ref::<SimilarityFilter>().unwrap(),
filter
@@ -254,7 +275,7 @@ fn remove_function_works_as_expected() {
assert_eq!(data.filters.read().unwrap().len(), 5);
let expected = vec![
let expected = [
WordsFilter { word_count: 1 },
WordsFilter { word_count: 3 },
WordsFilter { word_count: 5 },

View File

@@ -2,12 +2,11 @@ use super::FeroxFilter;
use super::SimilarityFilter;
use crate::event_handlers::Handles;
use crate::response::FeroxResponse;
use crate::utils::logged_request;
use crate::{DEFAULT_METHOD, SIMILARITY_THRESHOLD};
use crate::utils::{logged_request, parse_url_with_raw_path};
use crate::DEFAULT_METHOD;
use crate::NEAR_DUPLICATE_DISTANCE;
use anyhow::Result;
use fuzzyhash::FuzzyHash;
use regex::Regex;
use reqwest::Url;
use std::sync::Arc;
/// wrapper around logic necessary to create a SimilarityFilter
@@ -22,7 +21,7 @@ pub(crate) async fn create_similarity_filter(
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)?;
let url = parse_url_with_raw_path(similarity_filter)?;
// attempt to request the given url
let resp = logged_request(&url, DEFAULT_METHOD, None, handles.clone()).await?;
@@ -33,6 +32,7 @@ pub(crate) async fn create_similarity_filter(
similarity_filter,
DEFAULT_METHOD,
handles.config.output_level,
handles.config.response_size_limit,
)
.await;
@@ -40,14 +40,9 @@ pub(crate) async fn create_similarity_filter(
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::from(&fr);
Ok(SimilarityFilter {
hash,
threshold: SIMILARITY_THRESHOLD,
original_url: similarity_filter.to_string(),
})
Ok(filter)
}
/// used in conjunction with the Scan Management Menu
@@ -94,11 +89,11 @@ pub(crate) fn filter_lookup(filter_type: &str, filter_value: &str) -> Option<Box
}
}
"similarity" => {
return Some(Box::new(SimilarityFilter {
hash: String::new(),
threshold: SIMILARITY_THRESHOLD,
original_url: filter_value.to_string(),
}));
return Some(Box::new(SimilarityFilter::new(
0,
filter_value.to_string(),
NEAR_DUPLICATE_DISTANCE,
)));
}
_ => (),
}
@@ -157,9 +152,9 @@ mod tests {
assert_eq!(
filter.as_any().downcast_ref::<SimilarityFilter>().unwrap(),
&SimilarityFilter {
hash: String::new(),
threshold: SIMILARITY_THRESHOLD,
original_url: "http://localhost".to_string()
hash: 0,
original_url: "http://localhost".to_string(),
cutoff: NEAR_DUPLICATE_DISTANCE,
}
);
@@ -195,9 +190,9 @@ mod tests {
assert_eq!(
filter,
SimilarityFilter {
hash: "3:YKEpn:Yfp".to_string(),
threshold: SIMILARITY_THRESHOLD,
original_url: srv.url("/")
hash: 14897447612059286329,
original_url: srv.url("/"),
cutoff: NEAR_DUPLICATE_DISTANCE,
}
);
}

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, Serialize, Deserialize)]
/// 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,25 +37,26 @@ 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);
log::trace!("enter: should_filter_response({self:?} {response})");
// quick return if dont_filter is set
if self.dont_filter {
@@ -64,51 +66,85 @@ 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
}
/// Compare one WildcardFilter to another
fn box_eq(&self, other: &dyn Any) -> bool {
other.downcast_ref::<Self>().map_or(false, |a| self == a)
other.downcast_ref::<Self>() == Some(self)
}
/// Return self as Any for dynamic dispatch purposes
@@ -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, Serialize, Deserialize)]
#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WordsFilter {
/// Number of words in a Response's body that should be filtered
pub word_count: usize,
@@ -12,18 +12,18 @@ pub struct WordsFilter {
impl FeroxFilter for WordsFilter {
/// Check `word_count` against what was passed in via -W|--filter-words
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
log::trace!("enter: should_filter_response({:?} {})", self, response);
log::trace!("enter: should_filter_response({self:?} {response})");
let result = response.word_count() == self.word_count;
log::trace!("exit: should_filter_response -> {}", result);
log::trace!("exit: should_filter_response -> {result}");
result
}
/// Compare one WordsFilter to another
fn box_eq(&self, other: &dyn Any) -> bool {
other.downcast_ref::<Self>().map_or(false, |a| self == a)
other.downcast_ref::<Self>() == Some(self)
}
/// Return self as Any for dynamic dispatch purposes

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,7 @@ pub mod progress;
pub mod scan_manager;
pub mod scanner;
pub mod statistics;
pub mod sync;
mod traits;
pub mod utils;
mod extractor;
@@ -52,23 +53,202 @@ 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",
"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",
pub(crate) const DEFAULT_IGNORED_EXTENSIONS: [&str; 43] = [
"woff2", "woff", "ttf", "otf", "eot", "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",
];
/// Default set of extensions to search for when auto-collecting backups during scans
pub(crate) const DEFAULT_BACKUP_EXTENSIONS: [&str; 5] = ["~", ".bak", ".bak2", ".old", ".1"];
/// list of common file extensions for link density detection in directory listings
/// based on https://www.computerhope.com/issues/ch001789.htm
pub(crate) const COMMON_FILE_EXTENSIONS: [&str; 154] = [
// Web & Documents
".html",
".htm",
".php",
".asp",
".aspx",
".jsp",
".jspx",
".cgi",
".pl",
".py",
".rb",
".lua",
".txt",
".pdf",
".doc",
".docx",
".xls",
".xlsx",
".ppt",
".pptx",
".odt",
".ods",
".odp",
".rtf",
".tex",
".md",
".csv",
// Programming & Scripts
".js",
".mjs",
".ts",
".jsx",
".tsx",
".css",
".scss",
".sass",
".less",
".java",
".class",
".jar",
".c",
".cpp",
".h",
".hpp",
".cs",
".vb",
".go",
".rs",
".swift",
".kt",
".scala",
".r",
".m",
".mm",
".f",
".f90",
".pas",
".asm",
".sh",
".bash",
".zsh",
".fish",
".bat",
".cmd",
".ps1",
".psm1",
// Data & Config
".xml",
".json",
".yaml",
".yml",
".toml",
".ini",
".conf",
".config",
".cfg",
".properties",
".env",
".sql",
".db",
".sqlite",
".mdb",
".accdb",
// Archives & Compressed
".zip",
".rar",
".7z",
".tar",
".gz",
".bz2",
".xz",
".tgz",
".tbz2",
".cab",
".dmg",
".iso",
".img",
// Executables & Libraries
".exe",
".dll",
".so",
".dylib",
".app",
".deb",
".rpm",
".apk",
".msi",
// Images
".jpg",
".jpeg",
".png",
".gif",
".bmp",
".svg",
".webp",
".ico",
".tif",
".tiff",
".psd",
".ai",
".eps",
".raw",
".cr2",
".nef",
// Audio
".mp3",
".wav",
".flac",
".aac",
".ogg",
".wma",
".m4a",
".opus",
".aiff",
// Video
".mp4",
".avi",
".mkv",
".mov",
".wmv",
".flv",
".webm",
".m4v",
".mpg",
".mpeg",
".3gp",
".ogv",
// Fonts
".ttf",
".otf",
".woff",
".woff2",
".eot",
// Backups & Logs
".log",
".bak",
".tmp",
".temp",
".swp",
".swo",
".old",
".orig",
".backup",
];
/// 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 +256,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
@@ -124,6 +348,15 @@ pub const USER_AGENTS: [&str; 12] = [
"Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)",
];
/// maximum hamming distance allowed between two simhash signatures when detecting near-duplicates
///
/// ref: https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/33026.pdf
/// section: 4.1 Choice of Parameters
pub(crate) const NEAR_DUPLICATE_DISTANCE: usize = 3;
/// maximum hamming distance allowed between two simhash signatures when unique'ifying responses
pub(crate) const UNIQUE_DISTANCE: usize = 1;
#[cfg(test)]
mod tests {
use super::*;

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,13 @@
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, Stdio},
sync::{atomic::Ordering, Arc},
};
@@ -17,36 +19,41 @@ 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, UpdateTargets,
UpdateWordlist,
},
FiltersHandler, Handles, ScanHandler, StatsHandler, Tasks, TermInputHandler,
TermOutHandler, SCAN_COMPLETE,
},
filters, heuristics, logger,
progress::{PROGRESS_BAR, PROGRESS_PRINTER},
scan_manager::{self},
progress::PROGRESS_PRINTER,
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);
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()
@@ -75,7 +91,7 @@ fn get_unique_words_from_wordlist(path: &str) -> Result<Arc<Vec<String>>> {
/// Determine whether it's a single url scan or urls are coming from stdin, then scan as needed
async fn scan(targets: Vec<String>, handles: Arc<Handles>) -> Result<()> {
log::trace!("enter: scan({:?}, {:?})", targets, handles);
log::trace!("enter: scan({targets:?}, {handles:?})");
let scanned_urls = handles.ferox_scans()?;
@@ -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?;
@@ -103,7 +131,7 @@ async fn scan(targets: Vec<String>, handles: Arc<Handles>) -> Result<()> {
scanned_urls.print_completed_bars(handles.wordlist.len())?;
}
log::debug!("sending {:?} to be scanned as initial targets", targets);
log::debug!("sending {targets:?} to be scanned as initial targets");
handles.send_scan_command(ScanInitialUrls(targets))?;
log::trace!("exit: scan");
@@ -113,11 +141,11 @@ async fn scan(targets: Vec<String>, handles: Arc<Handles>) -> Result<()> {
/// Get targets from either commandline or stdin, pass them back to the caller as a Result<Vec>
async fn get_targets(handles: Arc<Handles>) -> Result<Vec<String>> {
log::trace!("enter: get_targets({:?})", handles);
log::trace!("enter: get_targets({handles:?})");
let mut targets = vec![];
if handles.config.stdin {
if handles.config.stdin && handles.config.cached_stdin.is_empty() {
// got targets from stdin, i.e. cat sites | ./feroxbuster ...
// just need to read the targets from stdin and spawn a future for each target found
let stdin = io::stdin(); // tokio's stdin, not std
@@ -126,6 +154,10 @@ async fn get_targets(handles: Arc<Handles>) -> Result<Vec<String>> {
while let Some(line) = reader.next().await {
targets.push(line?);
}
} else if !handles.config.cached_stdin.is_empty() {
// cached_stdin populated from config::container if --stdin was used
// keeping the if block above as a failsafe, but i dont think we'll hit it anymore
targets = handles.config.cached_stdin.clone();
} else if handles.config.resumed {
// resume-from can't be used with --url, and --stdin is marked false for every resumed
// scan, making it mutually exclusive from either of the other two options
@@ -148,7 +180,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,9 +199,17 @@ async fn get_targets(handles: Arc<Handles>) -> Result<Vec<String>> {
);
}
}
if !target.starts_with("http") {
// --url hackerone.com
// as of the 2.13.0 update, config::container handles both --url hackerone.com
// and urls coming in from --stdin. I think this is dead code now, but leaving
// it in just in case
*target = format!("{}://{target}", handles.config.protocol);
}
}
log::trace!("exit: get_targets -> {:?}", targets);
log::trace!("exit: get_targets -> {targets:?}");
Ok(targets)
}
@@ -187,13 +227,78 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
// PROGRESS_PRINTER and PROGRESS_BAR have been used at least once. This call satisfies
// that constraint
PROGRESS_PRINTER.println("");
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 {version} is up to date")
}
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
.context(format!(
"Unable to download wordlist from remote url: {}",
config.wordlist
))?;
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(mut path_segments) = response.url().path_segments() else {
bail!("Unable to parse path from url: {}", response.url());
};
let Some(filename) = path_segments.next_back() 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,15 +325,22 @@ 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
// create new Tasks object, each of these handles is one that will be joined on later
let tasks = Tasks::new(out_task, stats_task, filters_task, scan_task);
if !config.time_limit.is_empty() {
if !config.time_limit.is_empty() && config.parallel == 0 {
// --time-limit value not an empty string, need to kick off the thread that enforces
// the limit
//
// if --parallel is used, this branch won't execute in the main process, but will in the
// children. This is because --parallel is stripped from the children's command line
// arguments, so, when spawned, they won't have --parallel, the parallel value will be set
// to the default of 0, and will hit this branch. This makes it so that the time limit
// is enforced on each individual child process, instead of the main process
let time_handles = handles.clone();
tokio::spawn(async move { scan_manager::start_max_time_thread(time_handles).await });
}
@@ -271,8 +383,7 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
let invocation = args();
let para_regex =
Regex::new("--stdin|-q|--quiet|--silent|--verbosity|-v|-vv|-vvv|-vvvv").unwrap();
let para_regex = Regex::new("--stdin").unwrap();
// remove stdin since only the original process will process targets
// remove quiet and silent so we can force silent later to normalize output
@@ -280,8 +391,6 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
.filter(|s| !para_regex.is_match(s))
.collect::<Vec<String>>();
original.push("--silent".to_string()); // only output modifier allowed
// we need remove --parallel from command line so we don't hit this branch over and over
// but we must remove --parallel N manually; the filter above never sees --parallel and the
// value passed to it at the same time, so can't filter them out in one pass
@@ -308,7 +417,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
@@ -356,16 +465,33 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
log::debug!("parallel exec: {} {}", bin, args.join(" "));
tokio::task::spawn_blocking(move || {
let result = Command::new(bin)
tokio::task::spawn(async move {
let mut output = Command::new(bin)
.args(&args)
.stdout(Stdio::piped())
.spawn()
.expect("failed to spawn a child process")
.wait()
.expect("child process errored during execution");
.expect("failed to spawn a child process");
let stdout = output.stdout.take().unwrap();
let mut bufread = BufReader::new(stdout);
// output for a single line is a minimum of 51 bytes, so we'll start with that
// + a little wiggle room, and grow as needed
let mut buf: String = String::with_capacity(128);
while let Ok(n) = bufread.read_line(&mut buf) {
if n > 0 {
let trimmed = buf.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
}
buf.clear();
} else {
break;
}
}
let _ = output.wait();
drop(permit);
result
});
}
@@ -389,6 +515,14 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
return Ok(());
}
// in order for the Stats object to know about which targets are being scanned, we need to
// wait until the parallel branch has been handled before sending the UpdateTargets command
// this ensures that only the targets being scanned are sent to the Stats object
//
// if sent before the parallel branch is handled, the Stats object will have duplicate
// targets
handles.stats.send(UpdateTargets(targets.clone()))?;
if matches!(config.output_level, OutputLevel::Default) {
// only print banner if output level is default (no banner on --quiet|--silent)
let std_stderr = stderr(); // std::io::stderr
@@ -422,9 +556,9 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
let live_targets = {
let test = heuristics::HeuristicTests::new(handles.clone());
let result = test.connectivity(&targets).await;
if result.is_err() {
if let Err(err) = result {
clean_up(handles, tasks).await?;
bail!(fmt_err(&result.unwrap_err().to_string()));
bail!(fmt_err(&err.to_string()));
}
result?
};
@@ -439,7 +573,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}")));
}
}
@@ -452,7 +586,7 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
/// Single cleanup function that handles all the necessary drops/finishes etc required to gracefully
/// shutdown the program
async fn clean_up(handles: Arc<Handles>, tasks: Tasks) -> Result<()> {
log::trace!("enter: clean_up({:?}, {:?})", handles, tasks);
log::trace!("enter: clean_up({handles:?}, {tasks:?})");
let (tx, rx) = oneshot::channel::<bool>();
handles.send_scan_command(JoinTasks(tx))?;
@@ -484,6 +618,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 +658,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
@@ -520,16 +672,16 @@ fn main() -> Result<()> {
.contains("/definitely/doesnt/exist/0cd7fed0-47f4-4b18-a1b0-ac39708c1676")
{
// support the handful of tests that use `--stdin`
let targets: Vec<_> = if config.stdin {
stdin().lock().lines().map(|tgt| tgt.unwrap()).collect()
} else {
let targets: Vec<_> = if config.cached_stdin.is_empty() {
vec!["http://localhost".to_string()]
} else {
config.cached_stdin.clone()
};
// print the banner to stderr
let std_stderr = stderr(); // std::io::stderr
let banner = Banner::new(&targets, &config);
if !config.quiet && !config.silent {
if (!config.quiet && !config.silent) || config.parallel != 0 {
banner.print_to(std_stderr, config).unwrap();
}
}

View File

@@ -20,11 +20,10 @@ impl Document {
let processed = preprocess(text);
document.number_of_terms += processed.len();
for normalized in processed {
if normalized.len() > 2 {
document.add_term(&normalized)
if normalized.len() >= 2 {
document.add_term(&normalized);
document.number_of_terms += 1;
}
}
document
@@ -35,20 +34,19 @@ impl Document {
fn add_term(&mut self, word: &str) {
let term = Term::new(word);
let metadata = self.terms.entry(term).or_insert_with(TermMetaData::new);
let metadata = self.terms.entry(term).or_default();
*metadata.count_mut() += 1;
}
/// create a new `Document` from the given HTML string
pub(crate) fn from_html(raw_html: &str) -> Self {
pub(crate) fn from_html(raw_html: &str) -> Option<Self> {
let selector = Selector::parse("body").unwrap();
let html = Html::parse_document(raw_html);
let text = html
.select(&selector)
.next()
.unwrap()
let element = html.select(&selector).next()?;
let text = element
.descendants()
.filter_map(|node| {
if !node.value().is_text() && !node.value().is_comment() {
@@ -85,7 +83,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
@@ -95,7 +93,7 @@ impl Document {
// call `new` to push the parsed html through the pre-processing pipeline and process all
// the words
Self::new(&text)
Some(Self::new(&text))
}
/// Log normalized weighting scheme for term frequency
@@ -146,19 +144,20 @@ mod tests {
#[test]
/// `Document::new` should preprocess html and generate a hashmap of `Term, TermMetadata`
fn nlp_document_creation_from_html() {
let empty = Document::from_html("<html></html>");
let empty = Document::from_html("<html></html>").unwrap();
assert_eq!(empty.number_of_terms, 0);
let other_empty = Document::from_html("<html><body><p></p></body></html>");
let other_empty = Document::from_html("<html><body><p></p></body></html>").unwrap();
assert_eq!(other_empty.number_of_terms, 0);
let third_empty = Document::from_html("<!DOCTYPE html><html><!DOCTYPE html><p></p></html>");
let third_empty =
Document::from_html("<!DOCTYPE html><html><!DOCTYPE html><p></p></html>").unwrap();
assert_eq!(third_empty.number_of_terms, 0);
// p tag for is_text check and comment for is_comment
let doc = Document::from_html(
"<html><body><p>The air quality in Singapore.</p><!--got worse on Wednesday--></body></html>",
);
).unwrap();
let expected_terms = ["air", "quality", "singapore", "worse", "wednesday"];
@@ -209,7 +208,7 @@ mod tests {
/// ensure words in script/style tags aren't processed
fn document_creation_skips_script_and_style_tags() {
let html = "<body><script>The air quality</script><style>in Singapore</style><p>got worse on Wednesday.</p></body>";
let doc = Document::from_html(html);
let doc = Document::from_html(html).unwrap();
let keys = doc.terms().keys().map(|key| key.raw()).collect::<Vec<_>>();
let expected = ["worse", "wednesday"];

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

@@ -73,7 +73,11 @@ impl TfIdf {
to_add.push(score);
}
let average: f32 = to_add.iter().sum::<f32>() / to_add.len() as f32;
let average = if to_add.is_empty() {
0.0
} else {
to_add.iter().sum::<f32>() / to_add.len() as f32
};
*metadata.tf_idf_score_mut() = average;
}

View File

@@ -22,6 +22,15 @@ impl Term {
}
/// metadata to be associated with a `Term`
///
/// # Design Note
///
/// The `count` field represents the number of times a term appeared in a **single document**
/// and is only meaningful in the per-document context (i.e., within a `Document`).
///
/// When `TermMetaData` is stored in the global `TfIdf` model, the `count` field becomes stale
/// and is not used. Instead, the model relies on `term_frequencies` (which tracks the term
/// frequency for each document the term appears in) and calculates TF-IDF scores from those.
#[derive(Debug, Clone, Default)]
pub(super) struct TermMetaData {
/// number of times the associated `Term` was seen in a single document
@@ -35,11 +44,6 @@ pub(super) struct TermMetaData {
}
impl TermMetaData {
/// create a new metadata container
pub(super) fn new() -> Self {
Self::default()
}
/// number of times a `Term` has appeared in any `Document` within the corpus
pub(super) fn document_frequency(&self) -> usize {
self.term_frequencies().len()
@@ -90,7 +94,7 @@ mod tests {
#[test]
/// test accessors for correctness
fn nlp_term_metadata_accessor_test() {
let mut metadata = TermMetaData::new();
let mut metadata = TermMetaData::default();
*metadata.count_mut() += 1;
assert_eq!(metadata.count(), 1);

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,18 +40,18 @@ 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", "request_file"])
.help_heading("Target selection")
.value_name("URL")
.use_value_delimiter(true)
.value_hint(ValueHint::Url)
.help("The target URL (required, unless [--stdin || --resume-from] used)"),
.help("The target URL (required, unless [--stdin || --resume-from || --request-file] used)"),
)
.arg(
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,16 @@ 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),
).arg(
Arg::new("request_file")
.long("request-file")
.help_heading("Target selection")
.value_hint(ValueHint::FilePath)
.conflicts_with("url")
.num_args(1)
.value_name("REQUEST_FILE")
.help("Raw HTTP request file to use as a template for all requests"),
);
/////////////////////////////////////////////////////////////////////
@@ -72,27 +82,52 @@ 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("data-urlencoded")
.long("data-urlencoded")
.value_name("DATA")
.num_args(1)
.help_heading("Composite settings")
.conflicts_with_all(["data", "data-json"])
.help("Set -H 'Content-Type: application/x-www-form-urlencoded', --data to <data-urlencoded> (supports @file) and -m to POST"),
)
.arg(
Arg::new("data-json")
.long("data-json")
.value_name("DATA")
.num_args(1)
.help_heading("Composite settings")
.conflicts_with_all(["data", "data-urlencoded"])
.help("Set -H 'Content-Type: application/json', --data to <data-json> (supports @file) and -m to POST"),
)
.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")
.help("Use the same settings as --smart and set --collect-extensions to true"),
.conflicts_with_all(["rate_limit", "auto_bail"])
.help("Use the same settings as --smart and set --collect-extensions and --scan-dir-listings to true"),
);
/////////////////////////////////////////////////////////////////////
@@ -103,7 +138,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 +150,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 +163,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 +182,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 +190,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,13 +199,12 @@ 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(
"File extension(s) to search for (ex: -x php -x pdf js)",
"File extension(s) to search for (ex: -x php -x pdf js); reads values (newline-separated) from file if input starts with an @ (ex: @ext.txt)",
),
)
.arg(
@@ -179,9 +212,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 +224,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,11 +235,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 +247,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 +260,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,8 +273,15 @@ 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")
).arg(
Arg::new("protocol")
.long("protocol")
.value_name("PROTOCOL")
.num_args(1)
.help_heading("Request settings")
.help("Specify the protocol to use when targeting via --request-file or --url with domain only (default: https)"),
);
/////////////////////////////////////////////////////////////////////
@@ -256,12 +291,20 @@ 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"),
).arg(
Arg::new("scope")
.long("scope")
.value_name("URL")
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Request filters")
.help("Additional domains/URLs to consider in-scope for scanning (in addition to current domain)"),
);
/////////////////////////////////////////////////////////////////////
@@ -273,9 +316,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,13 +329,12 @@ 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(
"Filter out messages via regular expression matching on the response's body (ex: -X '^ignore me$')",
"Filter out messages via regular expression matching on the response's body/headers (ex: -X '^ignore me$')",
),
)
.arg(
@@ -301,9 +342,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 +355,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 +368,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 +381,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,14 +395,20 @@ 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)",
),
)
.arg(
Arg::new("unique")
.long("unique")
.num_args(0)
.help_heading("Response filters")
.help("Only show unique responses")
);
/////////////////////////////////////////////////////////////////////
@@ -376,7 +420,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 +428,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,9 +436,38 @@ 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"),
)
.arg(
Arg::new("server_certs")
.long("server-certs")
.value_name("PEM|DER")
.value_hint(ValueHint::FilePath)
.num_args(1..)
.help_heading("Client settings")
.help("Add custom root certificate(s) for servers with unknown certificates"),
)
.arg(
Arg::new("client_cert")
.long("client-cert")
.value_name("PEM")
.value_hint(ValueHint::FilePath)
.num_args(1)
.requires("client_key")
.help_heading("Client settings")
.help("Add a PEM encoded certificate for mutual authentication (mTLS)"),
)
.arg(
Arg::new("client_key")
.long("client-key")
.value_name("PEM")
.value_hint(ValueHint::FilePath)
.num_args(1)
.requires("client_cert")
.help_heading("Client settings")
.help("Add a PEM encoded private key for mutual authentication (mTLS)"),
);
/////////////////////////////////////////////////////////////////////
@@ -406,7 +479,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 +487,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 +496,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 +535,9 @@ pub fn initialize() -> Command<'static> {
Arg::new("parallel")
.long("parallel")
.value_name("PARALLEL_SCANS")
.takes_value(true)
.conflicts_with("verbosity")
.conflicts_with("url")
.num_args(1)
.requires("stdin")
.help_heading("Scan settings")
.help("Run parallel feroxbuster instances (one child process per url passed via stdin)")
@@ -456,17 +546,24 @@ pub fn initialize() -> Command<'static> {
Arg::new("rate_limit")
.long("rate-limit")
.value_name("RATE_LIMIT")
.takes_value(true)
.conflicts_with("auto_tune")
.num_args(1)
.help_heading("Scan settings")
.help("Limit number of requests per second (per directory) (default: 0, i.e. no limit)")
)
.arg(
Arg::new("response_size_limit")
.long("response-size-limit")
.value_name("BYTES")
.num_args(1)
.help_heading("Scan settings")
.help("Limit size of response body to read in bytes (default: 4MB)"),
)
.arg(
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 +573,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 +587,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(
.help("Automatically request likely backup extensions for \"found\" urls (default: ~, .bak, .bak2, .old, .1)")
)
.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,14 +624,19 @@ 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(
"File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)",
),
).arg(
Arg::new("scan_dir_listings")
.long("scan-dir-listings")
.num_args(0)
.help_heading("Scan settings")
.help("Force scans to recurse into directory listings")
);
/////////////////////////////////////////////////////////////////////
@@ -544,24 +647,24 @@ 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)")
.help("Only print URLs (or JSON w/ --json) + turn off logging (good for piping a list of urls to other commands)")
)
.arg(
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 +672,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 +684,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,14 +693,21 @@ 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)")
).arg(
Arg::new("limit_bars")
.long("limit-bars")
.value_name("NUM_BARS_TO_SHOW")
.num_args(1)
.help_heading("Output settings")
.help("Number of directory scan bars to show at any given time (default: no limit)"),
);
/////////////////////////////////////////////////////////////////////
@@ -606,9 +716,23 @@ pub fn initialize() -> Command<'static> {
let mut app = app
.group(
ArgGroup::new("output_files")
.args(&["debug_log", "output"])
.args(["debug_log", "output", "silent"])
.multiple(true),
)
.group(
ArgGroup::new("output_limiters")
.args(["quiet", "silent"])
.multiple(false),
)
.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);
/////////////////////////////////////////////////////////////////////
@@ -619,12 +743,18 @@ pub fn initialize() -> Command<'static> {
// which is fine, but if you add -h|--help, it still errors out on the bad flag/option,
// never showing the full help message. This code addresses that behavior
if arg == "--help" {
app.print_long_help().unwrap();
if let Err(err) = app.print_long_help() {
eprintln!("couldn't print help message: {}", err);
process::exit(1);
}
println!(); // just a newline to mirror original --help output
process::exit(0);
} else if arg == "-h" {
// same for -h, just shorter
app.print_help().unwrap();
if let Err(err) = app.print_help() {
eprintln!("couldn't print help message: {}", err);
process::exit(1);
}
println!();
process::exit(0);
}
@@ -634,13 +764,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 +797,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 +805,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
@@ -693,7 +819,7 @@ EXAMPLES:
./feroxbuster -u http://127.1 --auto-tune
Examples and demonstrations of all features
https://epi052.github.io/feroxbuster-docs/docs/examples/
https://epi052.github.io/feroxbuster-docs/examples/auto-tune/
"#;
#[cfg(test)]

View File

@@ -31,30 +31,51 @@ pub enum BarType {
/// Add an [indicatif::ProgressBar](https://docs.rs/indicatif/latest/indicatif/struct.ProgressBar.html)
/// to the global [PROGRESS_BAR](../config/struct.PROGRESS_BAR.html)
pub fn add_bar(prefix: &str, length: u64, bar_type: BarType) -> ProgressBar {
let mut style = ProgressStyle::default_bar().progress_chars("#>-");
let pb = ProgressBar::new(length).with_prefix(prefix.to_string());
update_style(&pb, bar_type);
PROGRESS_BAR.add(pb)
}
/// Update the style of a progress bar based on the `BarType`
pub fn update_style(bar: &ProgressBar, bar_type: BarType) {
let mut style = ProgressStyle::default_bar().progress_chars("#>-").with_key(
"smoothed_per_sec",
|state: &indicatif::ProgressState, w: &mut dyn std::fmt::Write| match (
state.pos(),
state.elapsed().as_millis(),
) {
// https://github.com/console-rs/indicatif/issues/394#issuecomment-1309971049
//
// indicatif released a change to how they reported eta/per_sec
// and the results looked really weird based on how we use the progress
// bars. this fixes that
(pos, elapsed_ms) if elapsed_ms > 0 => {
write!(w, "{:.0}/s", pos as f64 * 1000_f64 / elapsed_ms as f64).unwrap()
}
_ => write!(w, "-").unwrap(),
},
);
style = match bar_type {
BarType::Hidden => style.template(""),
BarType::Default => style.template(
"[{bar:.cyan/blue}] - {elapsed:<4} {pos:>7}/{len:7} {per_sec:7} {prefix} {msg}",
),
BarType::Message => style.template(&format!(
BarType::Hidden => style.template("").unwrap(),
BarType::Default => style
.template("[{bar:.cyan/blue}] - {elapsed:<4} {pos:>7}/{len:7} {smoothed_per_sec:7} {prefix} {msg}")
.unwrap(),
BarType::Message => style
.template(&format!(
"[{{bar:.cyan/blue}}] - {{elapsed:<4}} {{pos:>7}}/{{len:7}} {:7} {{prefix}} {{msg}}",
"-"
)),
BarType::Total => {
style.template("[{bar:.yellow/blue}] - {elapsed:<4} {pos:>7}/{len:7} {eta:7} {msg}")
}
BarType::Quiet => style.template("Scanning: {prefix}"),
))
.unwrap(),
BarType::Total => style
.template("[{bar:.yellow/blue}] - {elapsed:<4} {pos:>7}/{len:7} {eta:7} {msg}")
.unwrap(),
BarType::Quiet => style.template("Scanning: {prefix}").unwrap(),
};
let progress_bar = PROGRESS_BAR.add(ProgressBar::new(length));
progress_bar.set_style(style);
progress_bar.set_prefix(prefix);
progress_bar
bar.set_style(style);
}
#[cfg(test)]

View File

@@ -21,7 +21,7 @@ use crate::{
event_handlers::{Command, Handles},
traits::FeroxSerialize,
url::FeroxUrl,
utils::{self, fmt_err, status_colorizer},
utils::{self, fmt_err, parse_url_with_raw_path, status_colorizer, timestamp},
CommandSender,
};
@@ -63,6 +63,12 @@ pub struct FeroxResponse {
/// Url's file extension, if one exists
pub(crate) extension: Option<String>,
/// Whether the response body was truncated due to size limits
truncated: bool,
/// Timestamp of when this response was received
timestamp: f64,
}
/// implement Default trait for FeroxResponse
@@ -82,6 +88,8 @@ impl Default for FeroxResponse {
wildcard: false,
output_level: Default::default(),
extension: None,
truncated: false,
timestamp: timestamp(),
}
}
}
@@ -138,14 +146,24 @@ impl FeroxResponse {
self.content_length
}
/// Get the timestamp of this response
pub fn timestamp(&self) -> f64 {
self.timestamp
}
/// Get whether this response was truncated due to size limits
pub fn truncated(&self) -> bool {
self.truncated
}
/// Set `FeroxResponse`'s `url` attribute, has no affect if an error occurs
pub fn set_url(&mut self, url: &str) {
match Url::parse(url) {
match parse_url_with_raw_path(url) {
Ok(url) => {
self.url = url;
}
Err(e) => {
log::warn!("Could not parse {} into a Url: {}", url, e);
log::warn!("Could not parse {url} into a Url: {e}");
}
};
}
@@ -170,28 +188,8 @@ impl FeroxResponse {
/// free the `text` data, reducing memory usage
pub fn drop_text(&mut self) {
self.text = String::new();
}
/// Make a reasonable guess at whether the response is a file or not
///
/// Examines the last part of a path to determine if it has an obvious extension
/// i.e. http://localhost/some/path/stuff.js where stuff.js indicates a file
///
/// Additionally, inspects query parameters, as they're also often indicative of a file
pub fn is_file(&self) -> bool {
let has_extension = match self.url.path_segments() {
Some(path) => {
if let Some(last) = path.last() {
last.contains('.') // last segment has some sort of extension, probably
} else {
false
}
}
None => false,
};
self.url.query_pairs().count() > 0 || has_extension
self.text.clear(); // length is set to 0
self.text.shrink_to_fit(); // allocated capacity shrinks to reflect the new size
}
/// Returns line count of the response text.
@@ -206,22 +204,83 @@ impl FeroxResponse {
/// Create a new `FeroxResponse` from the given `Response`
pub async fn from(
response: Response,
mut response: Response,
original_url: &str,
method: &str,
output_level: OutputLevel,
max_size_read: usize,
) -> Self {
let url = response.url().clone();
let status = response.status();
let headers = response.headers().clone();
let content_length = response.content_length().unwrap_or(0);
let timestamp = timestamp();
// .text() consumes the response, must be called last
let text = response
.text()
.await
.with_context(|| "Could not parse body from response")
.unwrap_or_default();
// Read the response bytes with size limit to prevent OOM issues
// Use chunk() to limit bytes during reading, not after
let mut bytes_read = Vec::new();
let mut total_bytes_read = 0;
let mut was_truncated = false;
while let Some(chunk_result) = response.chunk().await.transpose() {
match chunk_result.with_context(|| "Could not read chunk from response") {
Ok(chunk) => {
let chunk_len = chunk.len();
if total_bytes_read + chunk_len > max_size_read {
// Only read the remaining bytes up to the limit
let remaining = max_size_read - total_bytes_read;
total_bytes_read += remaining;
bytes_read.extend_from_slice(&chunk[..remaining]);
was_truncated = true;
log::debug!("Response body truncated at {max_size_read} bytes for {url}");
break;
} else {
bytes_read.extend_from_slice(&chunk);
total_bytes_read += chunk_len;
}
}
Err(_) => {
// Error reading chunk, break and use what we have
break;
}
}
}
// Convert to text, handling UTF-8 errors gracefully
let text = String::from_utf8_lossy(&bytes_read).to_string();
// Log warning if content was truncated
if was_truncated {
log::warn!(
"Response body truncated to {} bytes for {url} (original size may be larger)",
bytes_read.len()
);
}
// 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
//
// update v2.12.0: added max_size_read to limit how much of the body we read
// this means we need to account for the possibility that the content_length
// is larger than what we actually read. That means we should only use the
// actual bytes we read if we truncated the response body.
let converted = total_bytes_read as u64;
let content_length = if was_truncated && content_length > converted {
// content_length is larger than what we read, use what we read
log::debug!(
"Using actual bytes read ({total_bytes_read}) as content_length instead of reported content_length ({content_length}) for {url}");
// set content_length to what we actually read
total_bytes_read as u64
} else {
// content_length is accurate or smaller than what we read, use old logic that
// deals with content_length of 0
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();
@@ -239,6 +298,8 @@ impl FeroxResponse {
output_level,
wildcard: false,
extension: None,
truncated: was_truncated,
timestamp,
}
}
@@ -259,7 +320,7 @@ impl FeroxResponse {
// (which may be empty).
//
// meaning: the two unwraps here are fine, the worst outcome is an empty string
let filename = self.url.path_segments().unwrap().last().unwrap();
let filename = self.url.path_segments().unwrap().next_back().unwrap();
if !filename.is_empty() {
// non-empty string, try to get extension
@@ -279,7 +340,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
@@ -307,12 +370,7 @@ impl FeroxResponse {
max_depth: usize,
handles: Arc<Handles>,
) -> bool {
log::trace!(
"enter: reached_max_depth({}, {}, {:?})",
base_depth,
max_depth,
handles
);
log::trace!("enter: reached_max_depth({base_depth}, {max_depth}, {handles:?})");
if max_depth == 0 {
// early return, as 0 means recurse forever; no additional processing needed
@@ -335,7 +393,7 @@ impl FeroxResponse {
/// handles 2xx and 3xx responses by either checking if the url ends with a / (2xx)
/// or if the Location header is present and matches the base url + / (3xx)
pub fn is_directory(&self) -> bool {
log::trace!("enter: is_directory({})", self);
log::trace!("enter: is_directory({self})");
if self.status().is_redirection() {
// status code is 3xx
@@ -343,7 +401,7 @@ impl FeroxResponse {
// and has a Location header
Some(loc) => {
// get absolute redirect Url based on the already known base url
log::debug!("Location header: {:?}", loc);
log::debug!("Location header: {loc:?}");
if let Ok(loc_str) = loc.to_str() {
if let Ok(abs_url) = self.url().join(loc_str) {
@@ -361,7 +419,7 @@ impl FeroxResponse {
}
}
None => {
log::debug!("expected Location header, but none was found: {}", self);
log::debug!("expected Location header, but none was found: {self}");
log::trace!("exit: is_directory -> false");
return false;
}
@@ -382,9 +440,16 @@ impl FeroxResponse {
/// Simple helper to send a `FeroxResponse` over the tx side of an `mpsc::unbounded_channel`
pub fn send_report(self, report_sender: CommandSender) -> Result<()> {
log::trace!("enter: send_report({:?}", report_sender);
log::trace!("enter: send_report({report_sender:?}");
report_sender.send(Command::Report(Box::new(self)))?;
// there's no reason to send the response body across the mpsc
//
// the only possible reason is for filtering on the body, but both `send_report`
// calls are gated behind checks for `should_filter_response`
let mut me = self;
me.drop_text();
report_sender.send(Command::Report(Box::new(me)))?;
log::trace!("exit: send_report");
Ok(())
@@ -405,25 +470,54 @@ impl FeroxSerialize for FeroxResponse {
let mut url_with_redirect = match (
self.status().is_redirection(),
self.headers().get("Location").is_some(),
matches!(
self.output_level,
OutputLevel::Silent | OutputLevel::SilentJSON
),
) {
(true, true) => {
(true, true, false) => {
// redirect with Location header, show where it goes if possible
let loc = self
.headers()
.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();
format!("{} => {loc}", self.url())
}
_ => {
// no redirect, just use the normal url
(_, _, true) => {
// --silent was used, just show the url
self.url().to_string()
}
_ => {
// no redirect, no silent; check for truncation and report if needed
let mut url_display = self.url().to_string();
if self.truncated {
// only add truncation indicator if content was truncated and --silent not used
url_display.push_str(&format!(
" ({} to size limit)",
style("truncated").yellow().bright()
));
}
url_display
}
};
if self.wildcard && matches!(self.output_level, OutputLevel::Default | OutputLevel::Quiet) {
@@ -432,7 +526,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 +534,6 @@ impl FeroxSerialize for FeroxResponse {
chars,
status_colorizer(status),
self.url(),
FeroxUrl::path_length_of_url(&self.url)
);
if self.status().is_redirection() {
@@ -457,15 +550,19 @@ impl FeroxSerialize for FeroxResponse {
message
} else {
// not a wildcard, just create a normal entry
utils::create_report_string(
self.status.as_str(),
method,
&lines,
&words,
&chars,
&url_with_redirect,
self.output_level,
)
if matches!(self.output_level, OutputLevel::SilentJSON) {
self.as_json().unwrap_or_default()
} else {
utils::create_report_string(
self.status.as_str(),
method,
&lines,
&words,
&chars,
&url_with_redirect,
self.output_level,
)
}
}
}
@@ -514,7 +611,7 @@ impl Serialize for FeroxResponse {
S: Serializer,
{
let mut headers = HashMap::new();
let mut state = serializer.serialize_struct("FeroxResponse", 8)?;
let mut state = serializer.serialize_struct("FeroxResponse", 9)?;
// need to convert the HeaderMap to a HashMap in order to pass it to the serializer
for (key, value) in &self.headers {
@@ -538,6 +635,8 @@ impl Serialize for FeroxResponse {
"extension",
self.extension.as_ref().unwrap_or(&String::new()),
)?;
state.serialize_field("truncated", &self.truncated)?;
state.serialize_field("timestamp", &self.timestamp)?;
state.end()
}
@@ -563,6 +662,8 @@ impl<'de> Deserialize<'de> for FeroxResponse {
line_count: 0,
word_count: 0,
extension: None,
truncated: false,
timestamp: timestamp(),
};
let map: HashMap<String, Value> = HashMap::deserialize(deserializer)?;
@@ -571,7 +672,7 @@ impl<'de> Deserialize<'de> for FeroxResponse {
match key.as_str() {
"url" => {
if let Some(url) = value.as_str() {
if let Ok(parsed) = Url::parse(url) {
if let Ok(parsed) = parse_url_with_raw_path(url) {
response.url = parsed;
}
}
@@ -636,6 +737,16 @@ impl<'de> Deserialize<'de> for FeroxResponse {
response.extension = Some(result.to_string());
}
}
"truncated" => {
if let Some(result) = value.as_bool() {
response.truncated = result;
}
}
"timestamp" => {
if let Some(result) = value.as_f64() {
response.timestamp = result;
}
}
_ => {}
}
}
@@ -780,4 +891,30 @@ mod tests {
assert_eq!(response.extension, None);
}
#[test]
/// test that the truncated getter returns the correct value
fn truncated_getter_returns_correct_value() {
let mut response = FeroxResponse::default();
// Default should be false
assert!(!response.truncated());
// Manually set truncated to true to test getter
response.truncated = true;
assert!(response.truncated());
}
#[test]
/// test that truncated responses show [TRUNCATED] in URL display
fn truncated_response_shows_in_url_display() {
let response = FeroxResponse {
url: Url::parse("http://localhost/test").unwrap(),
truncated: true,
..Default::default()
};
let display = response.as_str();
assert!(display.contains("truncated"));
}
}

View File

@@ -1,8 +1,12 @@
use std::sync::Arc;
use std::time::Duration;
use crate::filters::filter_lookup;
use crate::progress::PROGRESS_BAR;
use crate::sync::DynamicSemaphore;
use crate::traits::FeroxFilter;
use console::{measure_text_width, pad_str, style, Alignment, Term};
use indicatif::ProgressDrawTarget;
use indicatif::{HumanCount, HumanDuration, ProgressDrawTarget};
use regex::Regex;
/// Data container for a command entered by the user interactively
@@ -19,6 +23,9 @@ pub enum MenuCmd {
/// user wants to remove one or more active filters
RemoveFilter(Vec<usize>),
/// user wants to set the number of scan permits
SetScanPermits(usize),
}
/// Data container for a command result to be used internally by the ferox_scanner
@@ -32,6 +39,12 @@ pub enum MenuCmdResult {
/// Filter to be added to current list of `FeroxFilters`
Filter(Box<dyn FeroxFilter>),
/// number of permits to be added to the semaphore
NumPermitsToAdd(usize),
/// number of permits to be subtracted from the semaphore
NumPermitsToSubtract(usize),
}
/// Interactive scan cancellation menu
@@ -43,6 +56,9 @@ pub(super) struct Menu {
/// footer: instructions surrounded by separators
footer: String,
/// length of longest displayed line (suitable for ascii/unicode)
longest: usize,
/// unicode line border, matched to longest displayed line
border: String,
@@ -96,33 +112,42 @@ impl Menu {
);
let rm_filter_cmd = format!(
" {}[{}] FILTER_ID[-FILTER_ID[,...]] (ex: {} 1-4,8,9-13 or {} 3)",
" {}[{}] FILTER_ID[-FILTER_ID[,...]] (ex: {} 1-4,8,9-13 or {} 3)\n",
style("r").red(),
style("m-filter").red(),
style("rm-filter").red(),
style("r").red(),
);
let set_limit_cmd = format!(
" {}[{}] VALUE (ex: {} 5)",
style("s").green(),
style("et-limit").green(),
style("set-limit").green(),
);
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);
commands.push_str(&set_limit_cmd);
let longest = measure_text_width(&canx_cmd).max(measure_text_width(&name));
let longest = measure_text_width(&canx_cmd).max(measure_text_width(&name)) + 1;
let border = separator.repeat(longest);
let padded_name = pad_str(&name, longest, Alignment::Center, None);
let header = format!("{}\n{}\n{}", border, padded_name, border);
let footer = format!("{}\n{}", commands, border);
let header = format!("{border}\n{padded_name}\n{border}");
let footer = format!("{commands}\n{border}");
Self {
header,
footer,
border,
longest,
term: Term::stderr(),
}
}
@@ -142,6 +167,24 @@ impl Menu {
self.println(&self.footer);
}
/// print time remaining in a human-readable format
pub(super) fn print_eta(&self, eta: Duration) {
let inner = format!("{} remaining ⏳", HumanDuration(eta));
let padded_eta = pad_str(&inner, self.longest, Alignment::Center, None);
self.println(&format!("{padded_eta}\n{}", self.border));
}
/// print time remaining in a human-readable format
pub(super) fn print_scan_limit(&self, limiter: Arc<DynamicSemaphore>) {
let inner = format!(
"🦥 Scan limit {}; active {} 🦥",
HumanCount(limiter.current_capacity() as u64),
HumanCount(limiter.permits_in_use() as u64)
);
let padded_eta = pad_str(&inner, self.longest, Alignment::Center, None);
self.println(&format!("{padded_eta}\n{}", self.border));
}
/// set PROGRESS_BAR bar target to hidden
pub(super) fn hide_progress_bars(&self) {
PROGRESS_BAR.set_draw_target(ProgressDrawTarget::hidden());
@@ -174,7 +217,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
})
}
@@ -198,7 +241,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;
}
@@ -210,10 +253,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);
}
}
@@ -280,6 +327,25 @@ impl Menu {
Some(MenuCmd::RemoveFilter(indices))
}
's' => {
// set scan permits
// remove s[et-limit] from the command so it can be passed to the number
// splitter
let re = Regex::new(r"^[sS][etETlimitLIMIT-]*").unwrap();
let line = re.replace(line, "").trim().to_string();
let Ok(value) = line.parse::<usize>() else {
return None;
};
if value == 0 {
// if the value is 0, we don't want to set the limit, so return None
return None;
}
Some(MenuCmd::SetScanPermits(value))
}
_ => {
// invalid input
None
@@ -290,8 +356,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

@@ -8,7 +8,7 @@ mod state;
#[cfg(test)]
mod tests;
pub(self) use menu::Menu;
use menu::Menu;
pub use menu::{MenuCmd, MenuCmdResult};
pub use order::ScanOrder;
pub use response_container::FeroxResponses;

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;
}
}
@@ -53,3 +53,37 @@ impl FeroxResponses {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::response::FeroxResponse;
fn create_response_json(
url: &str,
status: u16,
word_count: usize,
content_length: u64,
) -> FeroxResponse {
let json = format!(
r#"{{"type":"response","url":"{url}","path":"/test","wildcard":false,"status":{status},"content_length":{content_length},"line_count":10,"word_count":{word_count},"headers":{{}},"extension":""}}"#,
);
serde_json::from_str(&json).unwrap()
}
#[test]
/// test that contains method works correctly
fn contains_method_works_correctly() {
let responses = FeroxResponses::default();
let response1 = create_response_json("http://example.com/page1", 200, 100, 1024);
responses.insert(response1.clone());
// Same URL and method should be contained
assert!(responses.contains(&response1));
// Different URL should not be contained
let response2 = create_response_json("http://example.com/page2", 200, 100, 1024);
assert!(!responses.contains(&response2));
}
}

View File

@@ -1,7 +1,10 @@
use super::*;
use crate::{
config::OutputLevel,
event_handlers::Handles,
progress::update_style,
progress::{add_bar, BarType},
scan_manager::utils::determine_bar_type,
scanner::PolicyTrigger,
};
use anyhow::Result;
@@ -16,10 +19,20 @@ use std::{
time::Instant,
};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use tokio::{sync, task::JoinHandle};
use uuid::Uuid;
#[derive(Debug, Default, Copy, Clone)]
pub enum Visibility {
/// whether a FeroxScan's progress bar is currently shown
#[default]
Visible,
/// whether a FeroxScan's progress bar is currently hidden
Hidden,
}
/// Struct to hold scan-related state
///
/// The purpose of this container is to open up the pathway to aborting currently running tasks and
@@ -32,15 +45,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>,
@@ -48,7 +71,7 @@ pub struct FeroxScan {
pub(super) task: sync::Mutex<Option<JoinHandle<()>>>,
/// The progress bar associated with this scan
pub(super) progress_bar: Mutex<Option<ProgressBar>>,
pub progress_bar: Mutex<Option<ProgressBar>>,
/// whether or not the user passed --silent|--quiet on the command line
pub(super) output_level: OutputLevel,
@@ -63,50 +86,99 @@ pub struct FeroxScan {
pub(super) errors: AtomicUsize,
/// tracker for the time at which this scan was started
pub(super) start_time: Instant,
pub(super) start_time: Mutex<Instant>,
/// whether the progress bar is currently visible or hidden
pub(super) visible: AtomicBool,
/// handles object pointer
pub(super) handles: Option<Arc<Handles>>,
}
/// Default implementation for 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()),
handles: None,
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(),
errors: Default::default(),
status_429s: Default::default(),
status_403s: Default::default(),
start_time: Instant::now(),
start_time: Mutex::new(Instant::now()),
visible: AtomicBool::new(true),
}
}
}
/// Implementation of FeroxScan
impl FeroxScan {
/// return the visibility of the scan as a boolean
pub fn visible(&self) -> bool {
self.visible.load(Ordering::Relaxed)
}
pub fn swap_visibility(&self) {
// fetch_xor toggles the boolean to its opposite and returns the previous value
let visible = self.visible.fetch_xor(true, Ordering::Relaxed);
let Ok(bar) = self.progress_bar.lock() else {
log::warn!("couldn't unlock progress bar for {}", self.url);
return;
};
if bar.is_none() {
log::warn!("there is no progress bar for {}", self.url);
return;
}
let Some(handles) = self.handles.as_ref() else {
log::warn!("couldn't access handles pointer for {}", self.url);
return;
};
let bar_type = if !visible {
// visibility was false before we xor'd the value
match handles.config.output_level {
OutputLevel::Default => BarType::Default,
OutputLevel::Quiet => BarType::Quiet,
OutputLevel::Silent | OutputLevel::SilentJSON => BarType::Hidden,
}
} else {
// visibility was true before we xor'd the value
BarType::Hidden
};
update_style(bar.as_ref().unwrap(), bar_type);
}
/// Stop a currently running scan
pub async fn abort(&self) -> Result<()> {
pub async fn abort(&self, active_bars: usize) -> Result<()> {
log::trace!("enter: abort");
match self.task.try_lock() {
Ok(mut guard) => {
if let Some(task) = std::mem::replace(&mut *guard, None) {
log::trace!("aborting {:?}", self);
if let Some(task) = guard.take() {
log::trace!("aborting {self:?}");
task.abort();
self.set_status(ScanStatus::Cancelled)?;
self.stop_progress_bar();
self.stop_progress_bar(active_bars);
}
}
Err(e) => {
log::warn!("Could not acquire lock to abort scan (we're already waiting for its results): {:?} {}", self, e);
log::warn!("Could not acquire lock to abort scan (we're already waiting for its results): {self:?} {e}");
}
}
log::trace!("exit: abort");
@@ -118,10 +190,15 @@ 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;
let _ = std::mem::replace(&mut *guard, Some(task));
guard.replace(task);
Ok(())
}
@@ -133,11 +210,36 @@ impl FeroxScan {
Ok(())
}
/// small wrapper to set `start_time`
pub fn set_start_time(&self, start_time: Instant) -> Result<()> {
if let Ok(mut guard) = self.start_time.lock() {
let _ = std::mem::replace(&mut *guard, start_time);
}
Ok(())
}
/// Simple helper to call .finish on the scan's progress bar
pub(super) fn stop_progress_bar(&self) {
pub(super) fn stop_progress_bar(&self, active_bars: usize) {
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();
let bar_limit = if let Some(handles) = self.handles.as_ref() {
handles.config.limit_bars
} else {
0
};
if bar_limit > 0 && bar_limit < active_bars {
pb.finish_and_clear();
return;
}
if pb.position() > self.num_requests {
pb.finish();
} else {
pb.abandon();
}
}
}
}
@@ -149,29 +251,43 @@ impl FeroxScan {
if guard.is_some() {
(*guard).as_ref().unwrap().clone()
} else {
let bar_type = match self.output_level {
OutputLevel::Default => BarType::Default,
OutputLevel::Quiet => BarType::Quiet,
OutputLevel::Silent => BarType::Hidden,
let (active_bars, bar_limit) = if let Some(handles) = self.handles.as_ref() {
if let Ok(scans) = handles.ferox_scans() {
(scans.number_of_bars(), handles.config.limit_bars)
} else {
(0, handles.config.limit_bars)
}
} else {
(0, 0)
};
let bar_type = determine_bar_type(bar_limit, active_bars, self.output_level);
let pb = add_bar(&self.url, self.num_requests, bar_type);
pb.reset_elapsed();
let _ = std::mem::replace(&mut *guard, Some(pb.clone()));
pb.set_position(self.requests_made_so_far);
guard.replace(pb.clone());
pb
}
}
Err(_) => {
log::warn!("Could not unlock progress bar on {:?}", self);
log::warn!("Could not unlock progress bar on {self:?}");
let bar_type = match self.output_level {
OutputLevel::Default => BarType::Default,
OutputLevel::Quiet => BarType::Quiet,
OutputLevel::Silent => BarType::Hidden,
let (active_bars, bar_limit) = if let Some(handles) = self.handles.as_ref() {
if let Ok(scans) = handles.ferox_scans() {
(scans.number_of_bars(), handles.config.limit_bars)
} else {
(0, handles.config.limit_bars)
}
} else {
(0, 0)
};
let bar_type = determine_bar_type(bar_limit, active_bars, self.output_level);
let pb = add_bar(&self.url, self.num_requests, bar_type);
pb.reset_elapsed();
@@ -181,6 +297,7 @@ impl FeroxScan {
}
/// Given a URL and ProgressBar, create a new FeroxScan, wrap it in an Arc and return it
#[allow(clippy::too_many_arguments)]
pub fn new(
url: &str,
scan_type: ScanType,
@@ -188,22 +305,27 @@ impl FeroxScan {
num_requests: u64,
output_level: OutputLevel,
pb: Option<ProgressBar>,
visibility: bool,
handles: Arc<Handles>,
) -> Arc<Self> {
Arc::new(Self {
url: url.to_string(),
normalized_url: format!("{}/", url.trim_end_matches('/')),
scan_type,
scan_order,
num_requests,
output_level,
progress_bar: Mutex::new(pb),
visible: AtomicBool::new(visibility),
handles: Some(handles),
..Default::default()
})
}
/// Mark the scan as complete and stop the scan's progress bar
pub fn finish(&self) -> Result<()> {
pub fn finish(&self, active_bars: usize) -> Result<()> {
self.set_status(ScanStatus::Complete)?;
self.stop_progress_bar();
self.stop_progress_bar(active_bars);
Ok(())
}
@@ -228,20 +350,44 @@ 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
}
/// small wrapper to inspect ScanStatus and see if it's Running
pub fn is_running(&self) -> bool {
if let Ok(guard) = self.status.lock() {
return matches!(*guard, ScanStatus::Running);
}
false
}
/// small wrapper to inspect ScanStatus and see if it's NotStarted
pub fn is_not_started(&self) -> bool {
if let Ok(guard) = self.status.lock() {
return matches!(*guard, ScanStatus::NotStarted);
}
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);
log::trace!("enter join({self:?})");
let mut guard = self.task.lock().await;
if guard.is_some() {
if let Some(task) = std::mem::replace(&mut *guard, None) {
if let Some(task) = guard.take() {
task.await.unwrap();
self.set_status(ScanStatus::Complete)
.unwrap_or_else(|e| log::warn!("Could not mark scan complete: {}", e))
.unwrap_or_else(|e| log::warn!("Could not mark scan complete: {e}"))
}
}
log::trace!("exit join({:?})", self);
log::trace!("exit join({self:?})");
}
/// increment the value in question by 1
pub(crate) fn add_403(&self) {
@@ -264,6 +410,7 @@ impl FeroxScan {
PolicyTrigger::Status403 => self.status_403s(),
PolicyTrigger::Status429 => self.status_429s(),
PolicyTrigger::Errors => self.errors(),
PolicyTrigger::TryAdjustUp => 0,
}
}
@@ -289,9 +436,24 @@ impl FeroxScan {
}
let reqs = self.requests();
let seconds = self.start_time.elapsed().as_secs();
let seconds = if let Ok(guard) = self.start_time.lock() {
guard.elapsed().as_secs_f64()
} else {
log::warn!("Could not acquire lock to read start_time for requests_per_second calculation on scan: {self:?}");
0.0
};
reqs.checked_div(seconds).unwrap_or(0)
if seconds == 0.0 || !seconds.is_finite() {
return 0;
}
let rate = reqs as f64 / seconds;
if rate > u64::MAX as f64 {
u64::MAX
} else {
rate as u64
}
}
/// return the number of requests performed by this scan's scanner
@@ -309,6 +471,7 @@ impl fmt::Display for FeroxScan {
ScanStatus::Complete => style("complete").green(),
ScanStatus::Cancelled => style("cancelled").red(),
ScanStatus::Running => style("running").bright().yellow(),
ScanStatus::Waiting => style("waiting").bright().cyan(),
}
} else {
style("unknown").red()
@@ -332,13 +495,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 +552,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;
}
}
_ => {}
}
}
@@ -432,6 +607,9 @@ pub enum ScanStatus {
/// Scan has started, but hasn't finished, nor been cancelled
Running,
/// Scan is waiting to be started due to max concurrent scan limit
Waiting,
}
/// Default implementation for ScanStatus
@@ -460,6 +638,8 @@ mod tests {
1000,
OutputLevel::Default,
None,
true,
Arc::new(Handles::for_testing(None, None).0),
);
scan.add_error();
@@ -480,17 +660,21 @@ 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,
visible: AtomicBool::new(true),
status: Mutex::new(ScanStatus::Running),
task: Default::default(),
progress_bar: Mutex::new(None),
output_level: Default::default(),
output_level: OutputLevel::Silent,
status_403s: Default::default(),
status_429s: Default::default(),
errors: Default::default(),
start_time: Instant::now(),
start_time: Mutex::new(Instant::now()),
handles: None,
};
let pb = scan.progress_bar();
@@ -500,9 +684,70 @@ mod tests {
let req_sec = scan.requests_per_second();
assert_eq!(req_sec, 100);
// allow for timing imprecision: sleep overhead makes elapsed time slightly > 1 second
// e.g., 100 reqs / 1.01s = 99 req/s
assert!(
(99..=101).contains(&req_sec),
"Expected ~100 req/s, got {}",
req_sec
);
scan.finish().unwrap();
scan.finish(0).unwrap();
assert_eq!(scan.requests_per_second(), 0);
}
#[test]
fn test_swap_visibility() {
let scan = FeroxScan::new(
"http://localhost",
ScanType::Directory,
ScanOrder::Latest,
1000,
OutputLevel::Default,
None,
true,
Arc::new(Handles::for_testing(None, None).0),
);
assert!(scan.visible());
scan.swap_visibility();
assert!(!scan.visible());
scan.swap_visibility();
assert!(scan.visible());
scan.swap_visibility();
assert!(!scan.visible());
scan.swap_visibility();
assert!(scan.visible());
}
#[test]
/// test for is_running method
fn test_is_running() {
let scan = FeroxScan::new(
"http://localhost",
ScanType::Directory,
ScanOrder::Latest,
1000,
OutputLevel::Default,
None,
true,
Arc::new(Handles::for_testing(None, None).0),
);
assert!(scan.is_not_started());
assert!(!scan.is_running());
assert!(!scan.is_complete());
assert!(!scan.is_cancelled());
*scan.status.lock().unwrap() = ScanStatus::Running;
assert!(!scan.is_not_started());
assert!(scan.is_running());
assert!(!scan.is_complete());
assert!(!scan.is_cancelled());
}
}

View File

@@ -5,12 +5,15 @@ use crate::filters::{
EmptyFilter, LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter,
WildcardFilter, WordsFilter,
};
use crate::sync::DynamicSemaphore;
use crate::traits::FeroxFilter;
use crate::Command::AddFilter;
use crate::{
banner::Banner,
config::OutputLevel,
progress::PROGRESS_PRINTER,
progress::{add_bar, BarType},
scan_manager::utils::determine_bar_type,
scan_manager::{MenuCmd, MenuCmdResult},
scanner::RESPONSES,
traits::FeroxSerialize,
@@ -32,6 +35,7 @@ use std::{
},
thread::sleep,
};
use tokio::sync::oneshot;
use tokio::time::{self, Duration};
/// Single atomic number that gets incremented once, used to track first thread to interact with
@@ -59,6 +63,9 @@ pub struct FeroxScans {
/// vector of extensions discovered and collected during scans
pub(crate) collected_extensions: RwLock<HashSet<String>>,
/// stored value for Configuration.limit_bars
bar_limit: usize,
}
/// Serialize implementation for FeroxScans
@@ -75,7 +82,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()
}
@@ -91,9 +98,10 @@ impl Serialize for FeroxScans {
/// Implementation of `FeroxScans`
impl FeroxScans {
/// given an OutputLevel, create a new FeroxScans object
pub fn new(output_level: OutputLevel) -> Self {
pub fn new(output_level: OutputLevel, bar_limit: usize) -> Self {
Self {
output_level,
bar_limit,
..Default::default()
}
}
@@ -115,7 +123,7 @@ impl FeroxScans {
scans.push(scan);
}
Err(e) => {
log::warn!("FeroxScans' container's mutex is poisoned: {}", e);
log::warn!("FeroxScans' container's mutex is poisoned: {e}");
return false;
}
}
@@ -126,7 +134,7 @@ impl FeroxScans {
/// load serialized FeroxScan(s) and any previously collected extensions into this FeroxScans
pub fn add_serialized_scans(&self, filename: &str, handles: Arc<Handles>) -> Result<()> {
log::trace!("enter: add_serialized_scans({})", filename);
log::trace!("enter: add_serialized_scans({filename})");
let file = File::open(filename)?;
let reader = BufReader::new(file);
@@ -138,6 +146,15 @@ impl FeroxScans {
let mut deser_scan: FeroxScan =
serde_json::from_value(scan.clone()).unwrap_or_default();
if deser_scan.is_cancelled() {
// if the scan was cancelled by the user, mark it as complete. This will
// prevent the scan from being resumed as well as prevent the wordlist
// from requesting it again
if let Ok(mut guard) = deser_scan.status.lock() {
*guard = ScanStatus::Complete;
}
}
// FeroxScans gets -q value from config as usual; the FeroxScans themselves
// rely on that value being passed in. If the user starts a scan without -q
// and resumes the scan but adds -q, FeroxScan will not have the proper value
@@ -213,8 +230,10 @@ impl FeroxScans {
/// on the given URL
pub fn contains(&self, url: &str) -> bool {
if let Ok(scans) = self.scans.read() {
let normalized = format!("{}/", url.trim_end_matches('/'));
for scan in scans.iter() {
if scan.url == url {
if scan.normalized_url == normalized {
return true;
}
}
@@ -225,8 +244,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());
}
}
@@ -235,7 +256,7 @@ impl FeroxScans {
}
pub fn get_base_scan_by_url(&self, url: &str) -> Option<Arc<FeroxScan>> {
log::trace!("enter: get_base_scan_by_url({})", url);
log::trace!("enter: get_base_scan_by_url({url})");
// rmatch_indices returns tuples in index, match form, i.e. (10, "/")
// with the furthest-right match in the first position in the vector
@@ -258,8 +279,8 @@ 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 {
log::trace!("enter: get_base_scan_by_url -> {}", scan);
if slice == scan.url || format!("{slice}/").as_str() == scan.url {
log::trace!("enter: get_base_scan_by_url -> {scan}");
return Some(scan.clone());
}
}
@@ -311,19 +332,21 @@ impl FeroxScans {
let mut printed = 0;
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;
}
if matches!(scan.scan_type, ScanType::Directory) {
if printed == 0 {
self.menu
.println(&format!("{}:", style("Scans").bright().blue()));
}
if let Ok(guard) = scan.status.lock() {
if matches!(*guard, ScanStatus::Cancelled) {
continue;
}
}
// 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;
}
@@ -347,11 +370,18 @@ 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;
}
u_scans.index(num).clone()
let selected = u_scans.index(num);
if matches!(selected.scan_type, ScanType::File) {
continue;
}
selected.clone()
}
Err(..) => continue,
};
@@ -364,14 +394,14 @@ impl FeroxScans {
if input == 'y' || input == '\n' {
self.menu.println(&format!("Stopping {}...", selected.url));
let active_bars = self.number_of_bars();
selected
.abort()
.abort(active_bars)
.await
.unwrap_or_else(|e| log::warn!("Could not cancel task: {}", e));
.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().unwrap_or(0) as usize - pb.position() as usize;
} else {
self.menu.println("Ok, doing nothing...");
}
@@ -404,10 +434,23 @@ impl FeroxScans {
}
/// CLI menu that allows for interactive cancellation of recursed-into directories
async fn interactive_menu(&self, handles: Arc<Handles>) -> Option<MenuCmdResult> {
async fn interactive_menu(
&self,
handles: Arc<Handles>,
limiter: Arc<DynamicSemaphore>,
) -> Option<MenuCmdResult> {
self.menu.hide_progress_bars();
self.menu.clear_screen();
self.menu.print_header();
let (tx, rx) = oneshot::channel::<Duration>();
if handles.stats.send(Command::QueryOverallBarEta(tx)).is_ok() {
if let Ok(y) = rx.await {
self.menu.print_eta(y);
}
}
self.menu.print_scan_limit(limiter.clone());
self.display_scans().await;
self.display_filters(handles.clone());
self.menu.print_footer();
@@ -433,12 +476,65 @@ impl FeroxScans {
.unwrap_or_default();
None
}
Some(MenuCmd::SetScanPermits(value)) => {
if limiter.current_capacity() == value {
// value is equal to current capacity, so we don't need to do anything
return None;
}
if limiter.current_capacity() < value {
// value is greater than current capacity, so we need to increase it
Some(MenuCmdResult::NumPermitsToAdd(
value - limiter.current_capacity(),
))
} else {
// value is less than current capacity, so we need to decrease it
Some(MenuCmdResult::NumPermitsToSubtract(
limiter.current_capacity() - value,
))
}
}
None => None,
};
self.menu.clear_screen();
let banner = Banner::new(
std::slice::from_ref(&handles.config.target_url),
&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
}
@@ -459,14 +555,22 @@ impl FeroxScans {
/// if a resumed scan is already complete, display a completed progress bar to the user
pub fn print_completed_bars(&self, bar_length: usize) -> Result<()> {
let bar_type = match self.output_level {
OutputLevel::Default => BarType::Message,
OutputLevel::Quiet => BarType::Quiet,
OutputLevel::Silent => return Ok(()), // fast exit when --silent was used
};
if self.output_level == OutputLevel::SilentJSON || self.output_level == OutputLevel::Silent
{
// fast exit when --silent was used
return Ok(());
}
let bar_type: BarType =
determine_bar_type(self.bar_limit, self.number_of_bars(), self.output_level);
if let Ok(scans) = self.scans.read() {
for scan in scans.iter() {
if matches!(bar_type, BarType::Hidden) {
// no need to show hidden bars
continue;
}
if scan.is_complete() {
// these scans are complete, and just need to be shown to the user
let pb = add_bar(
@@ -491,6 +595,7 @@ impl FeroxScans {
&self,
get_user_input: bool,
handles: Arc<Handles>,
limiter: Arc<DynamicSemaphore>,
) -> Option<MenuCmdResult> {
// function uses tokio::time, not std
@@ -503,7 +608,7 @@ impl FeroxScans {
INTERACTIVE_BARRIER.fetch_add(1, Ordering::Relaxed);
if get_user_input {
command_result = self.interactive_menu(handles).await;
command_result = self.interactive_menu(handles, limiter).await;
PAUSE_SCAN.store(false, Ordering::Relaxed);
self.print_known_responses();
}
@@ -520,7 +625,7 @@ impl FeroxScans {
INTERACTIVE_BARRIER.fetch_sub(1, Ordering::Relaxed);
}
log::trace!("exit: pause_scan -> {:?}", command_result);
log::trace!("exit: pause_scan -> {command_result:?}");
return command_result;
}
}
@@ -543,6 +648,7 @@ impl FeroxScans {
url: &str,
scan_type: ScanType,
scan_order: ScanOrder,
handles: Arc<Handles>,
) -> (bool, Arc<FeroxScan>) {
let bar_length = if let Ok(guard) = self.bar_length.lock() {
*guard
@@ -550,14 +656,11 @@ impl FeroxScans {
0
};
let active_bars = self.number_of_bars();
let bar_type = determine_bar_type(self.bar_limit, active_bars, self.output_level);
let bar = match scan_type {
ScanType::Directory => {
let bar_type = match self.output_level {
OutputLevel::Default => BarType::Default,
OutputLevel::Quiet => BarType::Quiet,
OutputLevel::Silent => BarType::Hidden,
};
let progress_bar = add_bar(url, bar_length, bar_type);
progress_bar.reset_elapsed();
@@ -567,6 +670,8 @@ impl FeroxScans {
ScanType::File => None,
};
let is_visible = !matches!(bar_type, BarType::Hidden);
let ferox_scan = FeroxScan::new(
url,
scan_type,
@@ -574,6 +679,8 @@ impl FeroxScans {
bar_length,
self.output_level,
bar,
is_visible,
handles,
);
// If the set did not contain the scan, true is returned.
@@ -588,8 +695,14 @@ impl FeroxScans {
/// If `FeroxScans` did not already contain the scan, return true; otherwise return false
///
/// 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)
pub fn add_directory_scan(
&self,
url: &str,
scan_order: ScanOrder,
handles: Arc<Handles>,
) -> (bool, Arc<FeroxScan>) {
let normalized = format!("{}/", url.trim_end_matches('/'));
self.add_scan(&normalized, ScanType::Directory, scan_order, handles)
}
/// Given a url, create a new `FeroxScan` and add it to `FeroxScans` as a File Scan
@@ -597,8 +710,65 @@ impl FeroxScans {
/// If `FeroxScans` did not already contain the scan, return true; otherwise return false
///
/// Also return a reference to the new `FeroxScan`
pub fn add_file_scan(&self, url: &str, scan_order: ScanOrder) -> (bool, Arc<FeroxScan>) {
self.add_scan(url, ScanType::File, scan_order)
pub fn add_file_scan(
&self,
url: &str,
scan_order: ScanOrder,
handles: Arc<Handles>,
) -> (bool, Arc<FeroxScan>) {
self.add_scan(url, ScanType::File, scan_order, handles)
}
/// returns the number of active AND visible scans; supports --limit-bars functionality
pub fn number_of_bars(&self) -> usize {
let Ok(scans) = self.scans.read() else {
return 0;
};
// starting at one ensures we don't have an extra bar
// due to counting up from 0 when there's actually 1 bar
let mut count = 1;
for scan in &*scans {
if scan.is_active() && scan.visible() {
count += 1;
}
}
count
}
/// make one hidden bar visible; supports --limit-bars functionality
pub fn make_visible(&self) {
if let Ok(guard) = self.scans.read() {
// when swapping visibility, we'll prefer an actively running scan
// if none are found, we'll
let mut queued = None;
for scan in &*guard {
if !matches!(scan.scan_type, ScanType::Directory) {
// visibility only makes sense for directory scans
continue;
}
if scan.visible() {
continue;
}
if scan.is_running() {
scan.swap_visibility();
return;
}
if queued.is_none() && scan.is_not_started() {
queued = Some(scan.clone());
}
}
if let Some(scan) = queued {
scan.swap_visibility();
}
}
}
/// small helper to determine whether any scans are active or not
@@ -631,7 +801,7 @@ impl FeroxScans {
/// given an extension, add it to `collected_extensions` if all constraints are met
/// returns `true` if an extension was added, `false` otherwise
pub fn add_discovered_extension(&self, extension: String) -> bool {
log::trace!("enter: add_discovered_extension({})", extension);
log::trace!("enter: add_discovered_extension({extension})");
let mut extension_added = false;
// note: the filter by --dont-collect happens in the event handler, since it has access
@@ -646,12 +816,12 @@ impl FeroxScans {
}
if let Ok(mut extensions) = self.collected_extensions.write() {
log::info!("discovered new extension: {}", extension);
log::info!("discovered new extension: {extension}");
extensions.insert(extension);
extension_added = true;
}
log::trace!("exit: add_discovered_extension -> {}", extension_added);
log::trace!("exit: add_discovered_extension -> {extension_added}");
extension_added
}
}
@@ -663,7 +833,7 @@ mod tests {
#[test]
/// unknown extension should be added to collected_extensions
fn unknown_extension_is_added_to_collected_extensions() {
let scans = FeroxScans::new(OutputLevel::Default);
let scans = FeroxScans::new(OutputLevel::Default, 0);
assert_eq!(0, scans.collected_extensions.read().unwrap().len());
@@ -676,7 +846,7 @@ mod tests {
#[test]
/// known extension should not be added to collected_extensions
fn known_extension_is_added_to_collected_extensions() {
let scans = FeroxScans::new(OutputLevel::Default);
let scans = FeroxScans::new(OutputLevel::Default, 0);
scans
.collected_extensions
.write()

View File

@@ -58,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

@@ -3,6 +3,7 @@ use crate::filters::{
FeroxFilters, LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter,
WordsFilter,
};
use crate::sync::DynamicSemaphore;
use crate::{
config::{Configuration, OutputLevel},
event_handlers::Handles,
@@ -10,11 +11,12 @@ use crate::{
scanner::RESPONSES,
statistics::Stats,
traits::FeroxSerialize,
SIMILARITY_THRESHOLD, SLEEP_DURATION, VERSION,
NEAR_DUPLICATE_DISTANCE, SLEEP_DURATION, VERSION,
};
use indicatif::ProgressBar;
use predicates::prelude::*;
use regex::Regex;
use std::sync::atomic::AtomicBool;
use std::sync::{atomic::Ordering, Arc};
use std::thread::sleep;
use std::time::Instant;
@@ -47,7 +49,8 @@ async fn scanner_pause_scan_with_finished_spinner() {
PAUSE_SCAN.store(false, Ordering::Relaxed);
});
urls.pause(false, handles).await;
urls.pause(false, handles, Arc::new(DynamicSemaphore::new(100)))
.await;
assert!(now.elapsed() > expected);
}
@@ -57,7 +60,12 @@ async fn scanner_pause_scan_with_finished_spinner() {
fn add_url_to_list_of_scanned_urls_with_unknown_url() {
let urls = FeroxScans::default();
let url = "http://unknown_url";
let (result, _scan) = urls.add_scan(url, ScanType::Directory, ScanOrder::Latest);
let (result, _scan) = urls.add_scan(
url,
ScanType::Directory,
ScanOrder::Latest,
Arc::new(Handles::for_testing(None, None).0),
);
assert!(result);
}
@@ -72,14 +80,21 @@ fn add_url_to_list_of_scanned_urls_with_known_url() {
url,
ScanType::Directory,
ScanOrder::Latest,
pb.length(),
pb.length().unwrap(),
OutputLevel::Default,
Some(pb),
true,
Arc::new(Handles::for_testing(None, None).0),
);
assert!(urls.insert(scan));
let (result, _scan) = urls.add_scan(url, ScanType::Directory, ScanOrder::Latest);
let (result, _scan) = urls.add_scan(
url,
ScanType::Directory,
ScanOrder::Latest,
Arc::new(Handles::for_testing(None, None).0),
);
assert!(!result);
}
@@ -94,9 +109,11 @@ fn stop_progress_bar_stops_bar() {
url,
ScanType::Directory,
ScanOrder::Latest,
pb.length(),
pb.length().unwrap(),
OutputLevel::Default,
Some(pb),
true,
Arc::new(Handles::for_testing(None, None).0),
);
assert!(!scan
@@ -107,7 +124,7 @@ fn stop_progress_bar_stops_bar() {
.unwrap()
.is_finished());
scan.stop_progress_bar();
scan.stop_progress_bar(0);
assert!(scan
.progress_bar
@@ -124,18 +141,25 @@ fn add_url_to_list_of_scanned_urls_with_known_url_without_slash() {
let urls = FeroxScans::default();
let url = "http://unknown_url";
let scan = FeroxScan::new(
let scan: Arc<FeroxScan> = FeroxScan::new(
url,
ScanType::File,
ScanOrder::Latest,
0,
OutputLevel::Default,
None,
true,
Arc::new(Handles::for_testing(None, None).0),
);
assert!(urls.insert(scan));
let (result, _scan) = urls.add_scan(url, ScanType::File, ScanOrder::Latest);
let (result, _scan) = urls.add_scan(
url,
ScanType::File,
ScanOrder::Latest,
Arc::new(Handles::for_testing(None, None).0),
);
assert!(!result);
}
@@ -152,20 +176,24 @@ async fn call_display_scans() {
url,
ScanType::Directory,
ScanOrder::Latest,
pb.length(),
pb.length().unwrap(),
OutputLevel::Default,
Some(pb),
true,
Arc::new(Handles::for_testing(None, None).0),
);
let scan_two = FeroxScan::new(
url_two,
ScanType::Directory,
ScanOrder::Latest,
pb_two.length(),
pb_two.length().unwrap(),
OutputLevel::Default,
Some(pb_two),
true,
Arc::new(Handles::for_testing(None, None).0),
);
scan_two.finish().unwrap(); // one complete, one incomplete
scan_two.finish(0).unwrap(); // one complete, one incomplete
scan_two
.set_task(tokio::spawn(async move {
sleep(Duration::from_millis(SLEEP_DURATION));
@@ -190,6 +218,8 @@ fn partial_eq_compares_the_id_field() {
0,
OutputLevel::Default,
None,
true,
Arc::new(Handles::for_testing(None, None).0),
);
let scan_two = FeroxScan::new(
url,
@@ -198,10 +228,13 @@ fn partial_eq_compares_the_id_field() {
0,
OutputLevel::Default,
None,
true,
Arc::new(Handles::for_testing(None, None).0),
);
assert!(!scan.eq(&scan_two));
#[allow(clippy::redundant_clone)]
let scan_two = scan.clone();
assert!(scan.eq(&scan_two));
@@ -224,7 +257,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}"#;
@@ -246,9 +279,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!();
}
}
@@ -275,9 +312,11 @@ fn ferox_scan_serialize() {
0,
OutputLevel::Default,
None,
true,
Arc::new(Handles::for_testing(None, None).0),
);
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());
@@ -293,10 +332,12 @@ fn ferox_scans_serialize() {
0,
OutputLevel::Default,
None,
true,
Arc::new(Handles::for_testing(None, None).0),
);
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);
@@ -309,7 +350,7 @@ fn ferox_scans_serialize() {
#[test]
/// given a FeroxResponses, test that it serializes into the proper JSON entry
fn ferox_responses_serialize() {
let json_response = r#"{"type":"response","url":"https://nerdcore.com/css","original_url":"https://nerdcore.com","path":"/css","wildcard":true,"status":301,"method":"GET","content_length":173,"line_count":10,"word_count":16,"headers":{"server":"nginx/1.16.1"},"extension":""}"#;
let json_response = r#"{"type":"response","url":"https://nerdcore.com/css","original_url":"https://nerdcore.com","path":"/css","wildcard":true,"status":301,"method":"GET","content_length":173,"line_count":10,"word_count":16,"headers":{"server":"nginx/1.16.1"},"extension":"","truncated":false,"timestamp":1711796681.3455093}"#;
let response: FeroxResponse = serde_json::from_str(json_response).unwrap();
let responses = FeroxResponses::default();
@@ -317,7 +358,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);
@@ -327,7 +368,7 @@ fn ferox_responses_serialize() {
/// given a FeroxResponse, test that it serializes into the proper JSON entry
fn ferox_response_serialize_and_deserialize() {
// deserialize
let json_response = r#"{"type":"response","url":"https://nerdcore.com/css","original_url":"https://nerdcore.com","path":"/css","wildcard":true,"status":301,"method":"GET","content_length":173,"line_count":10,"word_count":16,"headers":{"server":"nginx/1.16.1"},"extension":""}"#;
let json_response = r#"{"type":"response","url":"https://nerdcore.com/css","original_url":"https://nerdcore.com","path":"/css","wildcard":true,"status":301,"method":"GET","content_length":173,"line_count":10,"word_count":16,"headers":{"server":"nginx/1.16.1"},"extension":"","truncated":false,"timestamp":1711796681.3455093}"#;
let response: FeroxResponse = serde_json::from_str(json_response).unwrap();
assert_eq!(response.url().as_str(), "https://nerdcore.com/css");
@@ -338,6 +379,8 @@ fn ferox_response_serialize_and_deserialize() {
assert_eq!(response.line_count(), 10);
assert_eq!(response.word_count(), 16);
assert_eq!(response.headers().get("server").unwrap(), "nginx/1.16.1");
assert_eq!(response.timestamp(), 1711796681.3455093);
assert!(!response.truncated());
// serialize, however, this can fail when headers are out of order
let new_json = serde_json::to_string(&response).unwrap();
@@ -354,6 +397,8 @@ fn feroxstates_feroxserialize_implementation() {
0,
OutputLevel::Default,
None,
true,
Arc::new(Handles::for_testing(None, None).0),
);
let ferox_scans = FeroxScans::default();
let saved_id = ferox_scan.id.clone();
@@ -399,9 +444,9 @@ fn feroxstates_feroxserialize_implementation() {
.unwrap();
filters
.push(Box::new(SimilarityFilter {
hash: "3:YKEpn:Yfp".to_string(),
threshold: SIMILARITY_THRESHOLD,
hash: 1,
original_url: "http://localhost:12345/".to_string(),
cutoff: NEAR_DUPLICATE_DISTANCE,
}))
.unwrap();
@@ -426,11 +471,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""#,
@@ -442,8 +487,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"#,
@@ -452,10 +497,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"#,
@@ -465,7 +511,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"#,
@@ -483,8 +529,12 @@ fn feroxstates_feroxserialize_implementation() {
r#""time_limit":"""#,
r#""filter_similar":[]"#,
r#""url_denylist":[]"#,
r#""scope":[]"#,
r#""responses""#,
r#""type":"response""#,
r#""client_cert":"""#,
r#""client_key":"""#,
r#""server_certs":[]"#,
r#""url":"https://nerdcore.com/css""#,
r#""path":"/css""#,
r#""wildcard":true"#,
@@ -492,15 +542,20 @@ fn feroxstates_feroxserialize_implementation() {
r#""method":"GET""#,
r#""content_length":173"#,
r#""line_count":10"#,
r#""limit_bars":0"#,
r#""word_count":16"#,
r#""headers""#,
r#""server":"nginx/1.16.1"#,
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":"3:YKEpn:Yfp","threshold":95,"original_url":"http://localhost:12345/"}]"#,
r#""scan_dir_listings":false"#,
r#""protocol":"https""#,
r#""unique":false"#,
r#""response_size_limit":4194304"#,
r#""filters":[{"filter_code":100},{"word_count":200},{"content_length":300},{"line_count":400},{"compiled":".*","raw_string":".*"},{"hash":1,"original_url":"http://localhost:12345/","cutoff":3}]"#,
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"]"#,
r#""dont_collect":["woff2","woff","ttf","otf","eot","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"]"#,
]
.iter()
{
@@ -555,10 +610,14 @@ 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(),
handles: Some(Arc::new(Handles::for_testing(None, None).0)),
num_requests: 0,
start_time: Instant::now(),
requests_made_so_far: 0,
visible: AtomicBool::new(true),
start_time: std::sync::Mutex::new(Instant::now()),
output_level: OutputLevel::Default,
status_403s: Default::default(),
status_429s: Default::default(),
@@ -568,26 +627,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));
@@ -599,11 +658,14 @@ 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,
start_time: Instant::now(),
requests_made_so_far: 0,
start_time: std::sync::Mutex::new(Instant::now()),
output_level: OutputLevel::Default,
visible: AtomicBool::new(true),
status_403s: Default::default(),
status_429s: Default::default(),
status: std::sync::Mutex::new(ScanStatus::Running),
@@ -612,9 +674,10 @@ async fn ferox_scan_abort() {
}))),
progress_bar: std::sync::Mutex::new(None),
errors: Default::default(),
handles: Some(Arc::new(Handles::for_testing(None, None).0)),
};
scan.abort().await.unwrap();
scan.abort(0).await.unwrap();
assert!(matches!(
*scan.status.lock().unwrap(),
@@ -633,10 +696,7 @@ fn menu_print_header_and_footer() {
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();
@@ -650,12 +710,12 @@ fn menu_get_command_input_from_user_returns_cancel() {
let menu = Menu::new();
for (idx, cmd) in ["cancel", "Cancel", "c", "C"].iter().enumerate() {
let force = idx % 2 == 0;
let force = idx.is_multiple_of(2);
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();
@@ -663,11 +723,7 @@ 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);
}
}
@@ -680,7 +736,7 @@ fn menu_get_command_input_from_user_returns_add() {
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();
@@ -712,15 +768,26 @@ fn split_to_nums_is_correct() {
#[test]
/// given a deep url, find the correct scan
fn get_base_scan_by_url_finds_correct_scan() {
let handles = Arc::new(Handles::for_testing(None, None).0);
let urls = FeroxScans::default();
let url = "http://localhost";
let url1 = "http://localhost/stuff";
let url2 = "http://shlocalhost/stuff/things";
let url3 = "http://shlocalhost/stuff/things/mostuff";
let (_, scan) = urls.add_scan(url, ScanType::Directory, ScanOrder::Latest);
let (_, scan1) = urls.add_scan(url1, ScanType::Directory, ScanOrder::Latest);
let (_, scan2) = urls.add_scan(url2, ScanType::Directory, ScanOrder::Latest);
let (_, scan3) = urls.add_scan(url3, ScanType::Directory, ScanOrder::Latest);
let (_, scan) = urls.add_scan(url, ScanType::Directory, ScanOrder::Latest, handles.clone());
let (_, scan1) = urls.add_scan(
url1,
ScanType::Directory,
ScanOrder::Latest,
handles.clone(),
);
let (_, scan2) = urls.add_scan(
url2,
ScanType::Directory,
ScanOrder::Latest,
handles.clone(),
);
let (_, scan3) = urls.add_scan(url3, ScanType::Directory, ScanOrder::Latest, handles);
assert_eq!(
urls.get_base_scan_by_url("http://localhost/things.php")
@@ -753,7 +820,12 @@ fn get_base_scan_by_url_finds_correct_scan() {
fn get_base_scan_by_url_finds_correct_scan_without_trailing_slash() {
let urls = FeroxScans::default();
let url = "http://localhost";
let (_, scan) = urls.add_scan(url, ScanType::Directory, ScanOrder::Latest);
let (_, scan) = urls.add_scan(
url,
ScanType::Directory,
ScanOrder::Latest,
Arc::new(Handles::for_testing(None, None).0),
);
assert_eq!(
urls.get_base_scan_by_url("http://localhost/BKPMiherrortBPKcw")
.unwrap()
@@ -767,7 +839,12 @@ fn get_base_scan_by_url_finds_correct_scan_without_trailing_slash() {
fn get_base_scan_by_url_finds_correct_scan_with_trailing_slash() {
let urls = FeroxScans::default();
let url = "http://127.0.0.1:41971/";
let (_, scan) = urls.add_scan(url, ScanType::Directory, ScanOrder::Latest);
let (_, scan) = urls.add_scan(
url,
ScanType::Directory,
ScanOrder::Latest,
Arc::new(Handles::for_testing(None, None).0),
);
assert_eq!(
urls.get_base_scan_by_url("http://127.0.0.1:41971/BKPMiherrortBPKcw")
.unwrap()

View File

@@ -1,7 +1,12 @@
#[cfg(not(test))]
use crate::event_handlers::TermInputHandler;
use crate::{
config::Configuration, event_handlers::Handles, parser::TIMESPEC_REGEX, scanner::RESPONSES,
config::{Configuration, OutputLevel},
event_handlers::Handles,
parser::TIMESPEC_REGEX,
progress::BarType,
scan_manager::scan::Visibility,
scanner::RESPONSES,
};
use std::{fs::File, io::BufReader, sync::Arc};
@@ -12,7 +17,7 @@ use tokio::time;
/// of time has elapsed, kill all currently running scans and dump a state file to disk that can
/// be used to resume any unfinished scan.
pub async fn start_max_time_thread(handles: Arc<Handles>) {
log::trace!("enter: start_max_time_thread({:?})", handles);
log::trace!("enter: start_max_time_thread({handles:?})");
// as this function has already made it through the parser, which calls is_match on
// the value passed to --time-limit using TIMESPEC_REGEX; we can safely assume that
@@ -41,7 +46,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());
}
@@ -55,10 +60,10 @@ pub async fn start_max_time_thread(handles: Arc<Handles>) {
/// Primary logic used to load a Configuration from disk and populate the appropriate data
/// structures
pub fn resume_scan(filename: &str) -> Configuration {
log::trace!("enter: resume_scan({})", filename);
log::trace!("enter: resume_scan({filename})");
let file = File::open(filename).unwrap_or_else(|e| {
log::error!("{}", e);
log::error!("{e}");
log::error!("Could not open state file, exiting");
std::process::exit(1);
});
@@ -72,7 +77,7 @@ pub fn resume_scan(filename: &str) -> Configuration {
});
let config = serde_json::from_value(conf.clone()).unwrap_or_else(|e| {
log::error!("{}", e);
log::error!("{e}");
log::error!("Could not deserialize configuration found in state file, exiting");
std::process::exit(1);
});
@@ -87,6 +92,82 @@ pub fn resume_scan(filename: &str) -> Configuration {
}
}
log::trace!("exit: resume_scan -> {:?}", config);
log::trace!("exit: resume_scan -> {config:?}");
config
}
/// determine the type of progress bar to display
/// takes both --limit-bars and output-level (--quiet|--silent|etc)
/// into account to arrive at a `BarType`
pub fn determine_bar_type(
bar_limit: usize,
number_of_bars: usize,
output_level: OutputLevel,
) -> BarType {
let visibility = if bar_limit == 0 {
// no limit from cli, just set the value to visible
// this protects us from a mutex unlock in number_of_bars
// in the normal case
Visibility::Visible
} else if bar_limit < number_of_bars {
// active bars exceed limit; hidden
Visibility::Hidden
} else {
Visibility::Visible
};
match (output_level, visibility) {
(OutputLevel::Default, Visibility::Visible) => BarType::Default,
(OutputLevel::Quiet, Visibility::Visible) => BarType::Quiet,
(OutputLevel::Default, Visibility::Hidden) => BarType::Hidden,
(OutputLevel::Quiet, Visibility::Hidden) => BarType::Hidden,
(OutputLevel::Silent | OutputLevel::SilentJSON, _) => BarType::Hidden,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_limit_visible() {
let bar_type = determine_bar_type(0, 1, OutputLevel::Default);
assert!(matches!(bar_type, BarType::Default));
}
#[test]
fn test_limit_exceeded_hidden() {
let bar_type = determine_bar_type(1, 2, OutputLevel::Default);
assert!(matches!(bar_type, BarType::Hidden));
}
#[test]
fn test_limit_not_exceeded_visible() {
let bar_type = determine_bar_type(2, 1, OutputLevel::Default);
assert!(matches!(bar_type, BarType::Default));
}
#[test]
fn test_quiet_visible() {
let bar_type = determine_bar_type(0, 1, OutputLevel::Quiet);
assert!(matches!(bar_type, BarType::Quiet));
}
#[test]
fn test_quiet_hidden() {
let bar_type = determine_bar_type(1, 2, OutputLevel::Quiet);
assert!(matches!(bar_type, BarType::Hidden));
}
#[test]
fn test_silent_hidden() {
let bar_type = determine_bar_type(0, 1, OutputLevel::Silent);
assert!(matches!(bar_type, BarType::Hidden));
}
#[test]
fn test_silent_json_hidden() {
let bar_type = determine_bar_type(0, 1, OutputLevel::SilentJSON);
assert!(matches!(bar_type, BarType::Hidden));
}
}

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};
@@ -6,9 +7,10 @@ use console::style;
use futures::{stream, StreamExt};
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::sync::DynamicSemaphore;
use crate::Command::AddFilter;
use crate::{
event_handlers::{
@@ -41,28 +43,26 @@ async fn check_for_user_input(
pause_flag: &AtomicBool,
scanned_urls: Arc<FeroxScans>,
handles: Arc<Handles>,
limiter: Arc<DynamicSemaphore>,
) {
log::trace!(
"enter: check_for_user_input({:?}, SCANNED_URLS, HANDLES)",
pause_flag
);
log::trace!("enter: check_for_user_input({pause_flag:?}, SCANNED_URLS, HANDLES)",);
// todo write a test or two for this function at some point...
if pause_flag.load(Ordering::Acquire) {
match scanned_urls.pause(true, handles.clone()).await {
match scanned_urls.pause(true, handles.clone(), limiter).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
handles
.send_scan_command(Command::ScanNewUrl(url))
.unwrap_or_else(|e| log::warn!("Could not add scan to scan queue: {}", e))
.unwrap_or_else(|e| log::warn!("Could not add scan to scan queue: {e}"))
}
Some(MenuCmdResult::NumCancelled(num_canx)) => {
if num_canx > 0 {
handles
.stats
.send(SubtractFromUsizeField(TotalExpected, num_canx))
.unwrap_or_else(|e| log::warn!("Could not update overall scan bar: {}", e));
.unwrap_or_else(|e| log::warn!("Could not update overall scan bar: {e}"));
}
}
Some(MenuCmdResult::Filter(mut filter)) => {
@@ -95,7 +95,17 @@ async fn check_for_user_input(
handles
.filters
.send(AddFilter(filter))
.unwrap_or_else(|e| log::warn!("Could not add new filter: {}", e));
.unwrap_or_else(|e| log::warn!("Could not add new filter: {e}"));
}
Some(MenuCmdResult::NumPermitsToAdd(num_permits)) => {
handles
.send_scan_command(Command::AddScanPermits(num_permits))
.unwrap_or_else(|e| log::warn!("Could not increase scan limit: {e}"));
}
Some(MenuCmdResult::NumPermitsToSubtract(num_permits)) => {
handles
.send_scan_command(Command::SubtractScanPermits(num_permits))
.unwrap_or_else(|e| log::warn!("Could not decrease scan limit: {e}"));
}
_ => {}
}
@@ -119,7 +129,7 @@ pub struct FeroxScanner {
wordlist: Arc<Vec<String>>,
/// limiter that restricts the number of active FeroxScanners
scan_limiter: Arc<Semaphore>,
scan_limiter: Arc<DynamicSemaphore>,
}
/// FeroxScanner implementation
@@ -129,7 +139,7 @@ impl FeroxScanner {
target_url: &str,
order: ScanOrder,
wordlist: Arc<Vec<String>>,
scan_limiter: Arc<Semaphore>,
scan_limiter: Arc<DynamicSemaphore>,
handles: Arc<Handles>,
) -> Self {
Self {
@@ -157,17 +167,25 @@ impl FeroxScanner {
let scanned_urls_clone = scanned_urls.clone();
let requester_clone = requester.clone();
let handles_clone = self.handles.clone();
let limiter_clone = self.scan_limiter.clone();
(
tokio::spawn(async move {
// for every word in the wordlist, check to see if user has pressed enter
// in order to go into the interactive menu
check_for_user_input(&PAUSE_SCAN, scanned_urls_clone, handles_clone).await;
check_for_user_input(
&PAUSE_SCAN,
scanned_urls_clone,
handles_clone,
limiter_clone,
)
.await;
// after checking for user input, send the request
requester_clone
.request(&word)
.await
.unwrap_or_else(|e| log::warn!("Requester encountered an error: {}", e))
.unwrap_or_else(|e| log::warn!("Requester encountered an error: {e}"))
}),
pb,
)
@@ -179,8 +197,9 @@ impl FeroxScanner {
bar.inc(increment_len);
}
Err(e) => {
log::warn!("error awaiting a response: {}", e);
log::warn!("error awaiting a response: {e}");
self.handles.stats.send(AddError(Other)).unwrap_or_default();
std::process::exit(1);
}
}
});
@@ -200,6 +219,9 @@ impl FeroxScanner {
log::info!("Starting scan against: {}", self.target_url);
let mut scan_timer = Instant::now();
// every time we extract links we'll need to await the task to make sure
// it completes before the scan ends
let mut extraction_tasks = Vec::new();
if self.handles.config.extract_links && matches!(self.order, ScanOrder::Initial) {
// check for robots.txt (cannot be in sub-directories, so limited to Initial)
@@ -208,17 +230,14 @@ impl FeroxScanner {
.url(&self.target_url)
.handles(self.handles.clone())
.build()?;
let result = extractor.extract().await?;
extractor.request_links(result).await?;
if let Ok(result) = extractor.extract().await {
extraction_tasks.push(extractor.request_links(result).await?)
}
}
let scanned_urls = self.handles.ferox_scans()?;
let ferox_scan = match scanned_urls.get_scan_by_url(&self.target_url) {
Some(scan) => {
scan.set_status(ScanStatus::Running)?;
scan
}
Some(scan) => scan,
None => {
let msg = format!(
"Could not find FeroxScan associated with {}; this shouldn't happen... exiting",
@@ -234,7 +253,10 @@ impl FeroxScanner {
// returns a permit. However, if no remaining permits are available, acquire (asynchronously)
// waits until an outstanding permit is dropped, at which point, the freed permit is assigned
// to the caller.
ferox_scan.set_status(ScanStatus::Waiting)?;
let _permit = self.scan_limiter.acquire().await;
ferox_scan.set_status(ScanStatus::Running)?;
ferox_scan.set_start_time(Instant::now())?;
if self.handles.config.scan_limit > 0 {
scan_timer = Instant::now();
@@ -242,60 +264,102 @@ 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(Some(dirlist_result)) = test.directory_listing(&self.target_url).await {
// at this point, we have a DirListingType, and it's not the None variant
// which means we found directory listing based on the heuristic; now we need
// to process the links that are available if --extract-links was used
if let Ok(dirlist_result) = test.directory_listing(&self.target_url).await {
if dirlist_result.is_some() {
let dirlist_result = dirlist_result.unwrap();
// at this point, we have a DirListingType, and it's not the None variant
// which means we found directory listing based on the heuristic; now we need
// to process the links that are available if --extract-links was used
if self.handles.config.extract_links {
let mut extractor = ExtractorBuilder::default()
.response(&dirlist_result.response)
.target(ExtractionTarget::DirectoryListing)
.url(&self.target_url)
.handles(self.handles.clone())
.build()?;
if self.handles.config.extract_links {
let mut extractor = ExtractorBuilder::default()
.response(&dirlist_result.response)
.target(ExtractionTarget::DirectoryListing)
.url(&self.target_url)
.handles(self.handles.clone())
.build()?;
let result = extractor.extract_from_dir_listing().await?;
let result = extractor.extract_from_dir_listing().await?;
extraction_tasks.push(extractor.request_links(result).await?);
extractor.request_links(result).await?;
log::trace!("exit: scan_url -> Directory listing heuristic");
log::trace!("exit: scan_url -> Directory listing heuristic");
self.handles.stats.send(AddToF64Field(
DirScanTimes,
scan_timer.elapsed().as_secs_f64(),
))?;
self.handles.stats.send(AddToF64Field(
DirScanTimes,
scan_timer.elapsed().as_secs_f64(),
))?;
self.handles.stats.send(SubtractFromUsizeField(
TotalExpected,
progress_bar.length().unwrap_or(0) as usize,
))?;
}
self.handles.stats.send(SubtractFromUsizeField(
TotalExpected,
progress_bar.length() as usize,
))?;
}
let mut message = format!("=> {}", style("Directory listing").blue().bright());
let mut message = format!("=> {}", style("Directory listing").blue().bright());
if !self.handles.config.scan_dir_listings {
write!(
message,
" (add {} to scan)",
style("--scan-dir-listings").bright().yellow()
)?;
}
if !self.handles.config.extract_links {
message
.push_str(&format!(" (add {} to scan)", style("-e").bright().yellow()))
if !self.handles.config.extract_links {
write!(
message,
" (remove {} to scan)",
style("--dont-extract-links").bright().yellow()
)?;
}
if !self.handles.config.force_recursion && !self.handles.config.scan_dir_listings {
for handle in extraction_tasks.into_iter().flatten() {
_ = handle.await;
}
progress_bar.reset_eta();
progress_bar.finish_with_message(&message);
progress_bar.finish_with_message(message);
ferox_scan.finish()?;
if self.handles.config.limit_bars > 0 {
let scans = self.handles.ferox_scans()?;
let num_bars = scans.number_of_bars();
ferox_scan.finish(num_bars)?;
scans.make_visible();
} else {
ferox_scan.finish(0)?;
}
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
@@ -315,7 +379,7 @@ impl FeroxScanner {
let new_words = TF_IDF.read().unwrap().all_words();
let new_words_len = new_words.len();
let cur_length = progress_bar.length();
let cur_length = progress_bar.length().unwrap_or(0);
let new_length = cur_length + new_words_len as u64;
progress_bar.set_length(new_length);
@@ -328,7 +392,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(
@@ -345,7 +409,18 @@ impl FeroxScanner {
scan_timer.elapsed().as_secs_f64(),
))?;
ferox_scan.finish()?;
for handle in extraction_tasks.into_iter().flatten() {
_ = handle.await;
}
if self.handles.config.limit_bars > 0 {
let scans = self.handles.ferox_scans()?;
let num_bars = scans.number_of_bars();
ferox_scan.finish(num_bars)?;
scans.make_visible();
} else {
ferox_scan.finish(0)?;
}
log::trace!("exit: scan_url");

View File

@@ -8,7 +8,7 @@ use std::{convert::TryInto, sync::Arc};
/// Perform steps necessary to run scans that only need to be performed once (warming up the
/// engine, as it were)
pub async fn initialize(num_words: usize, handles: Arc<Handles>) -> Result<()> {
log::trace!("enter: initialize({}, {:?})", num_words, handles);
log::trace!("enter: initialize({num_words}, {handles:?})");
// number of requests only needs to be calculated once, and then can be reused
let num_reqs_expected: u64 = handles.expected_num_requests_per_dir().try_into()?;

View File

@@ -1,3 +1,4 @@
use std::cmp::max;
use std::fmt::{Debug, Formatter, Result};
/// bespoke variation on an array-backed max-heap
@@ -41,7 +42,7 @@ impl Debug for LimitHeap {
"LimitHeap {{ original: {}, current: {}, inner: [{}...] }}",
self.original, self.current, self.inner[0]
);
write!(f, "{}", msg)
write!(f, "{msg}")
}
}
@@ -51,7 +52,18 @@ impl LimitHeap {
pub(super) fn move_right(&mut self) -> usize {
if self.has_children() {
let tmp = self.current;
self.current = self.current * 2 + 2;
let new_index = self.current * 2 + 2;
// bounds check to prevent overflow
if new_index < self.inner.len() {
self.current = new_index;
} else {
log::warn!(
"Heap navigation out of bounds: move_right from {} would go to {}",
tmp,
new_index
);
}
return tmp;
}
self.current
@@ -61,7 +73,18 @@ impl LimitHeap {
pub(super) fn move_left(&mut self) -> usize {
if self.has_children() {
let tmp = self.current;
self.current = self.current * 2 + 1;
let new_index = self.current * 2 + 1;
// Bounds check to prevent overflow
if new_index < self.inner.len() {
self.current = new_index;
} else {
log::warn!(
"Heap navigation out of bounds: move_left from {} would go to {}",
tmp,
new_index
);
}
return tmp;
}
self.current
@@ -79,17 +102,42 @@ impl LimitHeap {
/// move directly to the given index
pub(super) fn move_to(&mut self, index: usize) {
self.current = index;
if index < self.inner.len() {
self.current = index;
} else {
log::warn!(
"Heap navigation out of bounds: move_to({}) exceeds array length {}",
index,
self.inner.len()
);
}
}
/// get the current node's value
pub(super) fn value(&self) -> i32 {
self.inner[self.current]
if self.current < self.inner.len() {
self.inner[self.current]
} else {
log::error!(
"Heap index out of bounds in value(): current={}, len={}",
self.current,
self.inner.len()
);
0 // Return safe default
}
}
/// set the current node's value
pub(super) fn set_value(&mut self, value: i32) {
self.inner[self.current] = value;
if self.current < self.inner.len() {
self.inner[self.current] = value;
} else {
log::error!(
"Heap index out of bounds in set_value(): current={}, len={}",
self.current,
self.inner.len()
);
}
}
/// check that this node has a parent (true for all except root)
@@ -144,17 +192,35 @@ impl LimitHeap {
self.move_up();
}
/// clamp all heap values to a maximum limit
///
/// this is used when --rate-limit is set alongside --auto-tune to ensure
/// that no auto-tuning adjustment can exceed the user's specified rate limit.
/// only clamps non-zero values to preserve the "unset" marker (0) used during
/// heap construction.
pub(super) fn clamp_to_max(&mut self, max: i32) {
for i in 0..self.inner.len() {
if self.inner[i] > 0 && self.inner[i] > max {
self.inner[i] = max;
}
}
}
/// iterate over the backing array, filling in each child's value based on the original value
pub(super) fn build(&mut self) {
// ex: original is 400
// arr[0] == 200
// arr[1] (left child) == 300
// arr[2] (right child) == 100
let root = self.original / 2;
// safety: ensure original is at least 2 so root = original/2 >= 1
// this prevents heap from producing limit=0 which would panic in rate limiter
let original = max(self.original, 2);
let root = original / 2;
self.inner[0] = root; // set root node to half of the original value
self.inner[1] = ((self.original - root).abs() / 2) + root;
self.inner[2] = root - ((self.original - root).abs() / 2);
self.inner[1] = ((original - root).abs() / 2) + root;
self.inner[2] = root - ((original - root).abs() / 2);
// start with index 1 and fill in each child below that node
for i in 1..self.inner.len() {

View File

@@ -2,7 +2,7 @@ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use crate::{atomic_load, atomic_store, config::RequesterPolicy};
use super::limit_heap::LimitHeap;
use super::{limit_heap::LimitHeap, PolicyTrigger};
/// data regarding policy and metadata about last enforced trigger etc...
#[derive(Default, Debug)]
@@ -19,8 +19,11 @@ pub struct PolicyData {
/// rate limit (at last interval)
limit: AtomicUsize,
/// whether the heap has been initialized
pub(super) heap_initialized: AtomicBool,
/// number of errors (at last interval)
pub(super) errors: AtomicUsize,
pub(super) errors: [AtomicUsize; 3],
/// whether or not the owning Requester should remove the rate_limiter, happens when a scan
/// has been limited and moves back up to the point of its original scan speed
@@ -28,6 +31,11 @@ pub struct PolicyData {
/// heap of values used for adjusting # of requests/second
pub(super) heap: std::sync::RwLock<LimitHeap>,
/// maximum limit for requests per second; optionally set by --rate-limit
/// if not set, the maximum limit during auto-tuning is unbounded and determined
/// dynamically based on the observed request rate
pub(super) rate_limit: Option<usize>,
}
/// implementation of PolicyData
@@ -35,7 +43,10 @@ impl PolicyData {
/// given a RequesterPolicy, create a new PolicyData
pub fn new(policy: RequesterPolicy, timeout: u64) -> Self {
// can use this as a tweak for how aggressively adjustments should be made when tuning
// cap at 30 seconds to prevent unbounded waits (e.g., with timeout=100000)
const MAX_WAIT_TIME_MS: u64 = 30_000;
let wait_time = ((timeout as f64 / 2.0) * 1000.0) as u64;
let wait_time = wait_time.min(MAX_WAIT_TIME_MS);
Self {
policy,
@@ -44,18 +55,62 @@ impl PolicyData {
}
}
/// builder for rate limit
///
/// builder method chosen to not conflict with existing `new` api
pub fn with_rate_limit(mut self, rate_limit: usize) -> Self {
self.rate_limit = Some(rate_limit);
self
}
/// setter for requests / second; populates the underlying heap with values from req/sec seed
pub(super) fn set_reqs_sec(&self, reqs_sec: usize) {
if let Ok(mut guard) = self.heap.write() {
guard.original = reqs_sec as i32;
guard.build();
if let Some(cap) = self.rate_limit {
// if a rate limit was set, clamp the heap to that maximum
// this method is only called from tune, which implies that auto-tune is enabled
guard.clamp_to_max(cap as i32);
}
self.set_limit(guard.inner[0] as usize); // set limit to 1/2 of current request rate
self.heap_initialized.store(true, Ordering::Release);
} else {
log::warn!("Could not acquire heap write lock in set_reqs_sec; heap not initialized");
}
}
/// setter for errors
pub(super) fn set_errors(&self, errors: usize) {
atomic_store!(self.errors, errors);
/// setter for errors (trigger-specific)
pub(super) fn set_errors(&self, trigger: PolicyTrigger, errors: usize) {
if trigger == PolicyTrigger::TryAdjustUp {
return;
}
atomic_store!(self.errors[trigger.as_index()], errors);
}
/// getter for errors (trigger-specific)
pub(super) fn get_errors(&self, trigger: PolicyTrigger) -> usize {
if trigger == PolicyTrigger::TryAdjustUp {
return 0;
}
atomic_load!(self.errors[trigger.as_index()])
}
/// status of heap initialization
pub(super) fn heap_initialized(&self) -> bool {
atomic_load!(self.heap_initialized, Ordering::Acquire)
}
/// reset the heap and initialization flag, called when auto-tune is being disabled
pub(super) fn reset_heap(&self) {
if let Ok(mut guard) = self.heap.write() {
*guard = LimitHeap::default();
self.heap_initialized.store(false, Ordering::Release);
} else {
log::warn!("Could not acquire heap write lock in reset_heap");
}
}
/// setter for limit
@@ -84,10 +139,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,7 +154,15 @@ 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);
} else {
log::debug!("Could not acquire heap write lock in adjust_up; rate limit unchanged");
}
}
@@ -114,6 +173,8 @@ impl PolicyData {
heap.move_right();
self.set_limit(heap.value() as usize);
}
} else {
log::debug!("Could not acquire heap write lock in adjust_down; rate limit unchanged");
}
}
}
@@ -140,8 +201,12 @@ mod tests {
/// PolicyData setters/getters tests for code coverage / sanity
fn policy_data_getters_and_setters() {
let pd = PolicyData::new(RequesterPolicy::AutoBail, 7);
pd.set_errors(20);
assert_eq!(pd.errors.load(Ordering::Relaxed), 20);
pd.set_errors(PolicyTrigger::Errors, 20);
assert_eq!(pd.get_errors(PolicyTrigger::Errors), 20);
pd.set_errors(PolicyTrigger::Status403, 15);
assert_eq!(pd.get_errors(PolicyTrigger::Status403), 15);
pd.set_errors(PolicyTrigger::Status429, 10);
assert_eq!(pd.get_errors(PolicyTrigger::Status429), 10);
pd.set_limit(200);
assert_eq!(pd.get_limit(), 200);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use tokio::sync::Semaphore;
use crate::sync::DynamicSemaphore;
use crate::{
config::OutputLevel,
@@ -14,8 +14,8 @@ use super::*;
#[should_panic]
/// try to hit struct field coverage of FileOutHandler
async fn get_scan_by_url_bails_on_unfound_url() {
let sem = Semaphore::new(10);
let urls = FeroxScans::new(OutputLevel::Default);
let sem = DynamicSemaphore::new(10);
let urls = FeroxScans::new(OutputLevel::Default, 0);
let scanner = FeroxScanner::new(
"http://localhost",

View File

@@ -1,4 +1,4 @@
#[derive(Copy, Clone, PartialEq, Debug)]
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
/// represents different situations where different criteria can trigger auto-tune/bail behavior
pub enum PolicyTrigger {
/// excessive 403 trigger
@@ -9,4 +9,21 @@ pub enum PolicyTrigger {
/// excessive general errors
Errors,
/// dummy error for upward rate adjustment
TryAdjustUp,
}
impl PolicyTrigger {
/// get the index into the `PolicyData.errors` array for this trigger
pub fn as_index(&self) -> usize {
match self {
PolicyTrigger::Status403 => 0,
PolicyTrigger::Status429 => 1,
PolicyTrigger::Errors => 2,
PolicyTrigger::TryAdjustUp => {
unreachable!("TryAdjustUp should never be used to access the errors array");
}
}
}
}

View File

@@ -124,6 +124,9 @@ pub struct Stats {
/// tracker for number of errors related to the request used
request_errors: AtomicUsize,
/// tracker for number of certificate/TLS/SSL errors
certificate_errors: AtomicUsize,
/// tracker for each directory's total scan time in seconds as a float
directory_scan_times: Mutex<Vec<f64>>,
@@ -132,6 +135,9 @@ pub struct Stats {
/// tracker for whether to use json during serialization or not
json: bool,
/// tracker for the initial targets that were passed in to the scan
targets: Mutex<Vec<String>>,
}
/// FeroxSerialize implementation for Stats
@@ -194,8 +200,10 @@ impl Serialize for Stats {
state.serialize_field("redirection_errors", &atomic_load!(self.redirection_errors))?;
state.serialize_field("connection_errors", &atomic_load!(self.connection_errors))?;
state.serialize_field("request_errors", &atomic_load!(self.request_errors))?;
state.serialize_field("certificate_errors", &atomic_load!(self.certificate_errors))?;
state.serialize_field("directory_scan_times", &self.directory_scan_times)?;
state.serialize_field("total_runtime", &self.total_runtime)?;
state.serialize_field("targets", &self.targets)?;
state.end()
}
@@ -446,6 +454,17 @@ impl<'a> Deserialize<'a> for Stats {
}
}
}
"targets" => {
if let Some(arr) = value.as_array() {
for val in arr {
if let Some(parsed) = val.as_str() {
if let Ok(mut guard) = stats.targets.lock() {
guard.push(parsed.to_string())
}
}
}
}
}
_ => {}
}
}
@@ -514,6 +533,13 @@ impl Stats {
}
}
/// update targets with the given vector of strings
pub fn update_targets(&self, targets: Vec<String>) {
if let Ok(mut locked_targets) = self.targets.lock() {
*locked_targets = targets;
}
}
/// save an instance of `Stats` to disk after updating the total runtime for the scan
pub fn save(&self, seconds: f64, location: &str) -> Result<()> {
let mut file = open_file(location)?;
@@ -550,6 +576,9 @@ impl Stats {
StatError::Request => {
atomic_increment!(self.request_errors);
}
StatError::Certificate => {
atomic_increment!(self.certificate_errors);
}
_ => {} // no need to hit Other as we always increment self.errors anyway
}
}
@@ -667,8 +696,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

@@ -16,6 +16,9 @@ pub enum StatError {
/// Represents an error resulting from the client's request
Request,
/// Represents certificate-related errors (TLS/SSL)
Certificate,
/// Represents any other error not explicitly defined above
Other,
}

View File

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

View File

@@ -0,0 +1,749 @@
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use tokio::sync::{Semaphore, SemaphorePermit};
/// A wrapper around Tokio's [`Semaphore`] that supports dynamic capacity reduction.
///
/// Unlike the standard Tokio semaphore, this implementation allows for reduction of the
/// effective capacity even when permits are already acquired and other tasks are waiting.
/// This is particularly useful for rate limiting scenarios where we need to dynamically
/// adjust the concurrency level based on runtime conditions.
///
/// # Key Features
///
/// - **Dynamic Capacity Reduction**: Can reduce capacity even when permits are in use
/// - **Queued Waiter Preservation**: Existing waiters remain in queue during capacity changes
/// - **Thread-Safe**: All operations are atomic and safe for concurrent use
/// - **Drop Safety**: Automatically manages capacity when permits are released
///
/// # Example
///
/// ```rust,no_run
/// use feroxbuster::sync::DynamicSemaphore;
///
/// #[tokio::main]
/// async fn main() {
/// let semaphore = DynamicSemaphore::new(2);
///
/// // Acquire permits
/// let _permit1 = semaphore.acquire().await.unwrap();
/// let _permit2 = semaphore.acquire().await.unwrap();
///
/// // Reduce capacity from 2 to 1 (takes effect when permits are released)
/// semaphore.reduce_capacity(1);
///
/// // When permits are dropped, only 1 permit will be available instead of 2
/// }
/// ```
#[derive(Debug)]
pub struct DynamicSemaphore {
/// The underlying Tokio semaphore that handles the actual permit management
inner: Arc<Semaphore>,
/// The current maximum capacity for this semaphore
///
/// This value represents the desired maximum number of permits that should be
/// available. When permits are released, the semaphore ensures that the total
/// available permits never exceed this capacity.
max_capacity: AtomicUsize,
/// Counter for permits currently in use
///
/// This is incremented when permits are acquired and decremented when released.
/// We use this to track how many permits are actually in use vs the virtual capacity.
permits_in_use: AtomicUsize,
}
/// A permit acquired from a [`DynamicSemaphore`].
///
/// This permit automatically manages the dynamic capacity when dropped. If releasing
/// the permit would cause the semaphore to exceed its current capacity limit, the
/// permit is "forgotten" instead of being returned to the available pool.
///
/// The permit provides the same guarantees as Tokio's [`SemaphorePermit`] but with
/// additional capacity management logic.
#[derive(Debug)]
pub struct DynamicSemaphorePermit<'a> {
/// The underlying Tokio semaphore permit
///
/// This is wrapped in an Option to allow for controlled dropping during
/// capacity management in the Drop implementation.
permit: Option<SemaphorePermit<'a>>,
/// Reference to the parent semaphore for capacity checking
semaphore: &'a DynamicSemaphore,
}
impl DynamicSemaphore {
/// Creates a new [`DynamicSemaphore`] with the specified number of permits.
///
/// # Arguments
///
/// * `permits` - The initial number of permits available in the semaphore
///
/// # Panics
///
/// Panics if `permits` exceeds the maximum number of permits supported by
/// the underlying Tokio semaphore implementation.
///
/// # Examples
///
/// ```rust,no_run
/// use feroxbuster::sync::DynamicSemaphore;
///
/// let semaphore = DynamicSemaphore::new(10);
/// assert_eq!(semaphore.current_capacity(), 10);
/// ```
pub fn new(permits: usize) -> Self {
Self {
inner: Arc::new(Semaphore::new(permits)),
max_capacity: AtomicUsize::new(permits),
permits_in_use: AtomicUsize::new(0),
}
}
/// Acquires a permit from the semaphore.
///
/// This method will wait until a permit becomes available. The returned permit
/// will automatically manage capacity constraints when dropped.
///
/// # Returns
///
/// A [`Result`] containing a [`DynamicSemaphorePermit`] on success, or an
/// [`tokio::sync::AcquireError`] if the semaphore has been closed.
///
/// # Examples
///
/// ```rust,no_run
/// use feroxbuster::sync::DynamicSemaphore;
///
/// #[tokio::main]
/// async fn main() {
/// let semaphore = DynamicSemaphore::new(1);
/// let permit = semaphore.acquire().await.unwrap();
/// // permit is automatically released when dropped
/// }
/// ```
pub async fn acquire(&self) -> Result<DynamicSemaphorePermit<'_>, tokio::sync::AcquireError> {
loop {
// Check if we're already at or over capacity before acquiring
let current_in_use = self.permits_in_use.load(Ordering::Acquire);
let current_capacity = self.current_capacity();
if current_in_use >= current_capacity {
// We're at or over capacity, wait for a permit to be released
let _temp_permit = self.inner.acquire().await?;
// Drop the permit immediately and try again - this ensures we wait
// for permits to become available but don't actually consume them
// if we're over capacity
drop(_temp_permit);
continue;
}
// Try to acquire a permit
let permit = self.inner.acquire().await?;
// Atomically increment in_use and check if we're still within capacity
let new_in_use = self.permits_in_use.fetch_add(1, Ordering::AcqRel) + 1;
if new_in_use <= current_capacity {
// We're within capacity, return the permit
return Ok(DynamicSemaphorePermit {
permit: Some(permit),
semaphore: self,
});
} else {
// We exceeded capacity between the check and increment, backtrack
self.permits_in_use.fetch_sub(1, Ordering::AcqRel);
drop(permit);
// implicit try again
}
}
}
/// Attempts to acquire a permit without waiting.
///
/// If a permit is immediately available, it is returned. Otherwise, this method
/// returns an error indicating why the permit could not be acquired.
///
/// # Returns
///
/// A [`Result`] containing a [`DynamicSemaphorePermit`] if successful, or a
/// [`tokio::sync::TryAcquireError`] if no permit is available or the semaphore is closed.
///
/// # Examples
///
/// ```rust,no_run
/// use feroxbuster::sync::DynamicSemaphore;
/// use tokio::sync::TryAcquireError;
///
/// let semaphore = DynamicSemaphore::new(1);
/// match semaphore.try_acquire() {
/// Ok(permit) => println!("Got permit"),
/// Err(TryAcquireError::NoPermits) => println!("No permits available"),
/// Err(TryAcquireError::Closed) => println!("Semaphore closed"),
/// };
/// ```
pub fn try_acquire(&self) -> Result<DynamicSemaphorePermit<'_>, tokio::sync::TryAcquireError> {
// Check if we're already at or over capacity
let current_in_use = self.permits_in_use.load(Ordering::Acquire);
let current_capacity = self.current_capacity();
if current_in_use >= current_capacity {
// We're at or over capacity, cannot acquire
return Err(tokio::sync::TryAcquireError::NoPermits);
}
// Try to acquire a permit from the underlying semaphore
let permit = self.inner.try_acquire()?;
// Atomically increment in_use and check if we're still within capacity
let new_in_use = self.permits_in_use.fetch_add(1, Ordering::AcqRel) + 1;
if new_in_use <= current_capacity {
// We're within capacity, return the permit
Ok(DynamicSemaphorePermit {
permit: Some(permit),
semaphore: self,
})
} else {
// We exceeded capacity between the check and increment, backtrack
self.permits_in_use.fetch_sub(1, Ordering::AcqRel);
drop(permit);
Err(tokio::sync::TryAcquireError::NoPermits)
}
}
/// Reduces the maximum capacity of the semaphore.
///
/// This method sets a new maximum capacity for the semaphore. The change takes
/// effect immediately for new permit acquisitions. If there are currently more
/// permits in use than the new capacity allows, the reduction will take effect
/// gradually as permits are released.
///
/// # Arguments
///
/// * `new_capacity` - The new maximum number of permits that should be available
///
/// # Returns
///
/// The previous capacity value before the change.
///
/// # Notes
///
/// - This operation is atomic and thread-safe
/// - Existing permit holders are not affected until they release their permits
/// - Queued waiters remain in the queue and will eventually be served
/// - If available permits exceed the new capacity, excess permits are immediately forgotten
///
/// # Examples
///
/// ```rust,no_run
/// use feroxbuster::sync::DynamicSemaphore;
///
/// #[tokio::main]
/// async fn main() {
/// let semaphore = DynamicSemaphore::new(5);
///
/// // Reduce capacity from 5 to 2
/// let old_capacity = semaphore.reduce_capacity(2);
/// assert_eq!(old_capacity, 5);
/// assert_eq!(semaphore.current_capacity(), 2);
/// }
/// ```
pub fn reduce_capacity(&self, new_capacity: usize) -> usize {
let old_capacity = self.max_capacity.swap(new_capacity, Ordering::AcqRel);
// If we're reducing capacity and there are available permits that exceed
// the new capacity, we should forget the excess permits immediately
if new_capacity < old_capacity {
let available = self.inner.available_permits();
let to_forget = available.saturating_sub(new_capacity);
if to_forget > 0 {
self.inner.forget_permits(to_forget);
}
}
old_capacity
}
/// Increases the maximum capacity of the semaphore.
///
/// This method sets a new maximum capacity that is higher than the current one.
/// Additional permits are immediately added to the semaphore up to the new capacity.
///
/// # Arguments
///
/// * `new_capacity` - The new maximum number of permits that should be available
///
/// # Returns
///
/// The previous capacity value before the change.
///
/// # Panics
///
/// Panics if the new capacity would cause the semaphore to exceed its maximum
/// supported permit count.
///
/// # Examples
///
/// ```rust,no_run
/// use feroxbuster::sync::DynamicSemaphore;
///
/// #[tokio::main]
/// async fn main() {
/// let semaphore = DynamicSemaphore::new(2);
///
/// // Increase capacity from 2 to 5
/// let old_capacity = semaphore.increase_capacity(5);
/// assert_eq!(old_capacity, 2);
/// assert_eq!(semaphore.current_capacity(), 5);
/// }
/// ```
pub fn increase_capacity(&self, new_capacity: usize) -> usize {
let old_capacity = self.max_capacity.swap(new_capacity, Ordering::AcqRel);
// If we're increasing capacity, add the additional permits
if new_capacity > old_capacity {
let to_add = new_capacity - old_capacity;
self.inner.add_permits(to_add);
}
old_capacity
}
/// Returns the current maximum capacity of the semaphore.
///
/// This represents the maximum number of permits that can be available at any
/// given time, which may be different from the number of currently available permits.
///
/// # Examples
///
/// ```rust,no_run
/// use feroxbuster::sync::DynamicSemaphore;
///
/// let semaphore = DynamicSemaphore::new(10);
/// assert_eq!(semaphore.current_capacity(), 10);
/// ```
pub fn current_capacity(&self) -> usize {
self.max_capacity.load(Ordering::Acquire)
}
/// Returns the number of permits currently available for immediate acquisition.
///
/// This value represents permits that can be acquired without waiting. Note that
/// this number may be less than the capacity if permits are currently in use.
///
/// # Examples
///
/// ```rust,no_run
/// use feroxbuster::sync::DynamicSemaphore;
///
/// #[tokio::main]
/// async fn main() {
/// let semaphore = DynamicSemaphore::new(3);
/// assert_eq!(semaphore.available_permits(), 3);
///
/// let _permit = semaphore.acquire().await.unwrap();
/// assert_eq!(semaphore.available_permits(), 2);
/// }
/// ```
pub fn available_permits(&self) -> usize {
self.inner.available_permits()
}
/// Closes the semaphore, preventing new permits from being acquired.
///
/// This will wake up all tasks currently waiting to acquire a permit, causing
/// them to receive an [`tokio::sync::AcquireError`]. Existing permits remain
/// valid until dropped.
///
/// # Examples
///
/// ```rust,no_run
/// use feroxbuster::sync::DynamicSemaphore;
///
/// #[tokio::main]
/// async fn main() {
/// let semaphore = DynamicSemaphore::new(1);
/// semaphore.close();
///
/// // This will return an error
/// assert!(semaphore.acquire().await.is_err());
/// }
/// ```
pub fn close(&self) {
self.inner.close();
}
/// Returns whether the semaphore has been closed.
///
/// # Examples
///
/// ```rust,no_run
/// use feroxbuster::sync::DynamicSemaphore;
///
/// let semaphore = DynamicSemaphore::new(1);
/// assert!(!semaphore.is_closed());
///
/// semaphore.close();
/// assert!(semaphore.is_closed());
/// ```
pub fn is_closed(&self) -> bool {
self.inner.is_closed()
}
/// Returns the current number of permits in use (for debugging).
///
/// This is primarily useful for debugging and testing to understand
/// the internal state of the semaphore.
///
/// # Examples
///
/// ```rust,no_run
/// use feroxbuster::sync::DynamicSemaphore;
///
/// #[tokio::main]
/// async fn main() {
/// let semaphore = DynamicSemaphore::new(3);
/// assert_eq!(semaphore.permits_in_use(), 0);
///
/// let _permit = semaphore.acquire().await.unwrap();
/// assert_eq!(semaphore.permits_in_use(), 1);
/// }
/// ```
pub fn permits_in_use(&self) -> usize {
self.permits_in_use.load(Ordering::Acquire)
}
}
impl<'a> Drop for DynamicSemaphorePermit<'a> {
/// Handles the automatic release of the permit with capacity management.
///
/// This implementation uses an approach designed to avoid race conditions:
///
/// We make the decision atomically BEFORE releasing the permit by checking if we're
/// currently over capacity. If we are, we "forget" the permit instead of releasing it.
/// If we're not over capacity, we release it normally.
///
/// This works because:
/// 1. We decrement permits_in_use first (atomically)
/// 2. We check if permits_in_use + available_permits > capacity
/// 3. If so, we're over capacity and should forget this permit
/// 4. If not, we can safely release it
///
/// The key insight is that permits_in_use represents permits about to be released,
/// so permits_in_use + available_permits tells us what the total would be after release.
fn drop(&mut self) {
if let Some(permit) = self.permit.take() {
// First, atomically decrement our usage counter
self.semaphore.permits_in_use.fetch_sub(1, Ordering::AcqRel);
// Check current state
let current_capacity = self.semaphore.current_capacity();
let current_available = self.semaphore.available_permits();
// Calculate what the total would be if we released this permit
let total_after_release = current_available + 1;
// If releasing would exceed capacity, forget the permit instead
if total_after_release > current_capacity {
// Forget the permit - it never gets added to available permits
permit.forget();
} else {
// Safe to release normally
drop(permit);
}
}
}
}
// Ensure the permit can be safely sent between threads
unsafe impl<'a> Send for DynamicSemaphorePermit<'a> {}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::time::Duration;
use tokio::time::sleep;
#[tokio::test]
async fn test_basic_acquire_release() {
let semaphore = DynamicSemaphore::new(2);
assert_eq!(semaphore.available_permits(), 2);
assert_eq!(semaphore.current_capacity(), 2);
assert_eq!(semaphore.permits_in_use(), 0);
let permit1 = semaphore.acquire().await.unwrap();
assert_eq!(semaphore.available_permits(), 1);
assert_eq!(semaphore.permits_in_use(), 1);
let permit2 = semaphore.acquire().await.unwrap();
assert_eq!(semaphore.available_permits(), 0);
assert_eq!(semaphore.permits_in_use(), 2);
drop(permit1);
assert_eq!(semaphore.available_permits(), 1);
assert_eq!(semaphore.permits_in_use(), 1);
drop(permit2);
assert_eq!(semaphore.available_permits(), 2);
assert_eq!(semaphore.permits_in_use(), 0);
}
#[tokio::test]
async fn test_capacity_reduction() {
let semaphore = DynamicSemaphore::new(3);
// Acquire all permits
let permit1 = semaphore.acquire().await.unwrap();
let permit2 = semaphore.acquire().await.unwrap();
let permit3 = semaphore.acquire().await.unwrap();
assert_eq!(semaphore.available_permits(), 0);
assert_eq!(semaphore.permits_in_use(), 3);
// Reduce capacity to 2
let old_capacity = semaphore.reduce_capacity(2);
assert_eq!(old_capacity, 3);
assert_eq!(semaphore.current_capacity(), 2);
// Drop one permit - should be returned since we're within the new capacity (0 + 1 <= 2)
drop(permit1);
assert_eq!(semaphore.available_permits(), 1);
assert_eq!(semaphore.permits_in_use(), 2);
// Drop another permit - should be returned since we're still within capacity (1 + 1 <= 2)
drop(permit2);
assert_eq!(semaphore.available_permits(), 2);
assert_eq!(semaphore.permits_in_use(), 1);
// Drop the last permit - this would exceed capacity (2 + 1 > 2), so should be forgotten
drop(permit3);
assert_eq!(semaphore.available_permits(), 2); // Still 2, excess was forgotten
assert_eq!(semaphore.permits_in_use(), 0);
}
#[tokio::test]
async fn test_capacity_increase() {
let semaphore = DynamicSemaphore::new(2);
assert_eq!(semaphore.available_permits(), 2);
// Increase capacity
let old_capacity = semaphore.increase_capacity(5);
assert_eq!(old_capacity, 2);
assert_eq!(semaphore.current_capacity(), 5);
assert_eq!(semaphore.available_permits(), 5);
}
#[tokio::test]
async fn test_try_acquire() {
let semaphore = DynamicSemaphore::new(1);
let permit1 = semaphore.try_acquire().unwrap();
assert!(semaphore.try_acquire().is_err());
drop(permit1);
assert!(semaphore.try_acquire().is_ok());
}
#[tokio::test]
async fn test_close() {
let semaphore = DynamicSemaphore::new(1);
assert!(!semaphore.is_closed());
semaphore.close();
assert!(semaphore.is_closed());
assert!(semaphore.acquire().await.is_err());
}
/// Test that reproduces the exact live site issue that was discovered
#[tokio::test]
async fn test_over_capacity_acquisition_prevention() {
let semaphore = Arc::new(DynamicSemaphore::new(5));
// Step 1: Acquire permits like a live site would
let permit1 = semaphore.acquire().await.unwrap();
let permit2 = semaphore.acquire().await.unwrap();
assert_eq!(semaphore.available_permits(), 3);
assert_eq!(semaphore.permits_in_use(), 2);
// Step 2: Reduce capacity while permits are in use (the critical scenario)
semaphore.reduce_capacity(1);
assert_eq!(semaphore.current_capacity(), 1);
assert_eq!(semaphore.available_permits(), 1); // Should be 1 (5-2=3, but capped at 1)
assert_eq!(semaphore.permits_in_use(), 2); // Still 2 in use (over capacity)
// Step 3: Try to acquire a new permit while over capacity - should FAIL
assert!(
semaphore.try_acquire().is_err(),
"Should not be able to acquire when over capacity"
);
// Step 4: Release permits and verify capacity is enforced
drop(permit1);
assert_eq!(semaphore.available_permits(), 1);
assert_eq!(semaphore.permits_in_use(), 1);
drop(permit2);
assert_eq!(semaphore.available_permits(), 1);
assert_eq!(semaphore.permits_in_use(), 0);
// Step 5: Now acquisition should work since we're at capacity
let permit_new = semaphore.try_acquire().unwrap();
assert_eq!(semaphore.available_permits(), 0);
assert_eq!(semaphore.permits_in_use(), 1);
drop(permit_new);
assert_eq!(semaphore.available_permits(), 1);
assert_eq!(semaphore.permits_in_use(), 0);
}
/// Test concurrent operations under load to verify race condition fixes
#[tokio::test]
async fn test_concurrent_capacity_reduction() {
let semaphore = Arc::new(DynamicSemaphore::new(10));
let mut handles = vec![];
// Start many tasks that acquire permits and hold them briefly
for _ in 0..20 {
let sem = semaphore.clone();
handles.push(tokio::spawn(async move {
if let Ok(permit) = sem.try_acquire() {
sleep(Duration::from_millis(50)).await;
drop(permit);
}
// Some tasks won't get permits due to capacity limits - this is expected
}));
}
// While tasks are running, reduce capacity
sleep(Duration::from_millis(10)).await;
semaphore.reduce_capacity(5);
// Wait for all tasks to complete
for handle in handles {
handle.await.unwrap();
}
// Verify final state - available permits should never exceed capacity
assert!(semaphore.available_permits() <= semaphore.current_capacity());
assert_eq!(semaphore.current_capacity(), 5);
}
/// Stress test with continuous capacity changes and concurrent acquisitions
#[tokio::test]
async fn test_stress_concurrent_operations() {
let semaphore = Arc::new(DynamicSemaphore::new(50));
let mut handles = vec![];
// Start tasks that continuously try to acquire and release permits
for _ in 0..100 {
let sem = semaphore.clone();
handles.push(tokio::spawn(async move {
for _ in 0..5 {
if let Ok(permit) = sem.try_acquire() {
tokio::task::yield_now().await;
drop(permit);
}
tokio::task::yield_now().await;
}
}));
}
// Continuously reduce capacity while tasks are running
let sem_reducer = semaphore.clone();
let reducer_handle = tokio::spawn(async move {
for new_capacity in (1..=50).rev() {
sem_reducer.reduce_capacity(new_capacity);
tokio::task::yield_now().await;
}
});
// Wait for all tasks
for handle in handles {
handle.await.unwrap();
}
reducer_handle.await.unwrap();
// Final verification - the semaphore should be in a valid state
assert!(semaphore.available_permits() <= semaphore.current_capacity());
assert_eq!(semaphore.current_capacity(), 1);
assert_eq!(semaphore.permits_in_use(), 0);
}
/// Test that demonstrates integration scenarios similar to feroxbuster usage
#[tokio::test]
async fn test_feroxbuster_integration_scenario() {
let limiter = Arc::new(DynamicSemaphore::new(3));
// Simulate 3 active scans by acquiring all permits
let permit1 = limiter.acquire().await.unwrap();
let permit2 = limiter.acquire().await.unwrap();
let permit3 = limiter.acquire().await.unwrap();
assert_eq!(limiter.available_permits(), 0);
assert_eq!(limiter.current_capacity(), 3);
// Simulate user reducing scan limit from 3 to 1 via scan management menu
limiter.reduce_capacity(1);
assert_eq!(limiter.current_capacity(), 1);
// Verify no new scans can start when over capacity
assert!(limiter.try_acquire().is_err());
// As scans complete, capacity reduction takes effect
drop(permit1);
assert_eq!(limiter.available_permits(), 1);
drop(permit2);
assert_eq!(limiter.available_permits(), 1); // Excess forgotten
drop(permit3);
assert_eq!(limiter.available_permits(), 1); // Excess forgotten
// Now only 1 scan can run concurrently
let _new_permit = limiter.acquire().await.unwrap();
assert_eq!(limiter.available_permits(), 0);
assert!(limiter.try_acquire().is_err());
}
/// Test edge cases and boundary conditions
#[tokio::test]
async fn test_edge_cases() {
// Test zero capacity
let semaphore = DynamicSemaphore::new(0);
assert_eq!(semaphore.current_capacity(), 0);
assert_eq!(semaphore.available_permits(), 0);
assert!(semaphore.try_acquire().is_err());
// Test capacity reduction to zero
let semaphore = DynamicSemaphore::new(2);
let permit = semaphore.acquire().await.unwrap();
semaphore.reduce_capacity(0);
assert_eq!(semaphore.current_capacity(), 0);
assert!(semaphore.try_acquire().is_err());
drop(permit);
assert_eq!(semaphore.available_permits(), 0);
assert!(semaphore.try_acquire().is_err());
// Test large capacity values
let semaphore = DynamicSemaphore::new(1000);
assert_eq!(semaphore.current_capacity(), 1000);
assert_eq!(semaphore.available_permits(), 1000);
let permit = semaphore.try_acquire().unwrap();
assert_eq!(semaphore.available_permits(), 999);
drop(permit);
assert_eq!(semaphore.available_permits(), 1000);
}
}

9
src/sync/mod.rs Normal file
View File

@@ -0,0 +1,9 @@
//! Synchronization primitives for feroxbuster
//!
//! This module provides enhanced synchronization primitives that extend
//! the functionality of standard async synchronization tools to meet
//! feroxbuster's specific needs.
mod dynamic_semaphore;
pub use dynamic_semaphore::{DynamicSemaphore, DynamicSemaphorePermit};

View File

@@ -4,6 +4,7 @@ use crate::filters::{
WordsFilter,
};
use crate::response::FeroxResponse;
use crate::utils::status_colorizer;
use anyhow::Result;
use crossterm::style::{style, Stylize};
use serde::Serialize;
@@ -38,11 +39,42 @@ impl Display for dyn FeroxFilter {
} 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>() {
if filter.dynamic != u64::MAX {
write!(f, "Dynamic wildcard: {}", style(filter.dynamic).cyan())
} else {
write!(f, "Static wildcard: {}", style(filter.size).cyan())
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 {lc} lines"));
}
(None, Some(wc), None) => {
msg.push_str(&format!("containing {wc} words"));
}
(None, Some(wc), Some(lc)) => {
msg.push_str(&format!("containing {wc} words and {lc} lines"));
}
(Some(cl), None, None) => {
msg.push_str(&format!("containing {cl} bytes"));
}
(Some(cl), None, Some(lc)) => {
msg.push_str(&format!("containing {cl} bytes and {lc} lines"));
}
(Some(cl), Some(wc), None) => {
msg.push_str(&format!("containing {cl} bytes and {wc} words"));
}
(Some(cl), Some(wc), Some(lc)) => {
msg.push_str(&format!(
"containing {cl} bytes, {wc} words, and {lc} lines"
));
}
}
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>() {
@@ -52,7 +84,7 @@ impl Display for dyn FeroxFilter {
style(&filter.original_url).cyan()
)
} else {
write!(f, "Filter: {:?}", self)
write!(f, "Filter: {self:?}")
}
}
}

View File

@@ -1,8 +1,85 @@
use crate::utils::parse_url_with_raw_path;
use crate::{event_handlers::Handles, statistics::StatError::UrlFormat, Command::AddError};
use anyhow::{anyhow, bail, Result};
use reqwest::Url;
use std::collections::HashSet;
use std::{convert::TryInto, fmt, sync::Arc};
use std::{fmt, sync::Arc};
/// Trait extension for reqwest::Url to add scope checking functionality
pub trait UrlExt {
/// Check if this URL is allowed based on scope configuration
///
/// A URL is considered in-scope if:
/// 1. It belongs to the same domain as an in-scope url, OR
/// 2. It belongs to a subdomain of an in-scope url
///
/// note: the scope list passed in is populated from either --url or --stdin
/// as well as --scope. This means we don't have to worry about checking
/// against the original target url, as that is already in the scope list
fn is_in_scope(&self, scope: &[Url]) -> bool;
/// Check if this URL is a subdomain of the given parent domain
fn is_subdomain_of(&self, parent_url: &Url) -> bool;
}
impl UrlExt for Url {
fn is_in_scope(&self, scope: &[Url]) -> bool {
log::trace!("enter: is_in_scope({}, scope: {:?})", self.as_str(), scope);
if scope.is_empty() {
log::error!("is_in_scope check failed (scope is empty, this should not happen)");
log::trace!("exit: is_in_scope -> false");
return false;
}
for url in scope {
if self.host() == url.host() {
log::trace!("exit: is_in_scope -> true (same domain/host)");
return true;
}
if self.is_subdomain_of(url) {
log::trace!("exit: is_in_scope -> true (subdomain)");
return true;
}
}
log::trace!("exit: is_in_scope -> false");
false
}
fn is_subdomain_of(&self, parent_url: &Url) -> bool {
if let (Some(url_domain), Some(parent_domain)) = (self.domain(), parent_url.domain()) {
let candidate = url_domain.to_lowercase();
let candidate = candidate.trim_end_matches('.');
let parent = parent_domain.to_lowercase();
let parent = parent.trim_end_matches('.');
if candidate == parent {
// same domain is not a subdomain
return false;
}
let candidate_parts: Vec<&str> = candidate.split('.').collect();
let parent_parts: Vec<&str> = parent.split('.').collect();
if candidate_parts.len() <= parent_parts.len() {
// candidate has fewer or equal parts than parent, so it can't be a subdomain
return false;
}
// check if parent parts match the rightmost parts of candidate
candidate_parts
.iter()
.rev()
.zip(parent_parts.iter().rev())
.all(|(c, p)| c == p)
} else {
false
}
}
}
/// abstraction around target urls; collects all Url related shenanigans in one place
#[derive(Debug)]
@@ -43,7 +120,7 @@ impl FeroxUrl {
word: &str,
collected_extensions: HashSet<String>,
) -> Result<Vec<Url>> {
log::trace!("enter: formatted_urls({})", word);
log::trace!("enter: formatted_urls({word})");
let mut urls = vec![];
@@ -72,7 +149,7 @@ impl FeroxUrl {
Err(_) => self.handles.stats.send(AddError(UrlFormat))?,
}
}
log::trace!("exit: formatted_urls -> {:?}", urls);
log::trace!("exit: formatted_urls -> {urls:?}");
Ok(urls)
}
@@ -80,7 +157,7 @@ impl FeroxUrl {
///
/// Errors during parsing `url` or joining `word` are propagated up the call stack
pub fn format(&self, word: &str, extension: Option<&str>) -> Result<Url> {
log::trace!("enter: format({}, {:?})", word, extension);
log::trace!("enter: format({word}, {extension:?})");
if Url::parse(word).is_ok() {
// when a full url is passed in as a word to be joined to a base url using
@@ -90,9 +167,9 @@ 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);
log::warn!("{}", message);
log::trace!("exit: format -> Err({})", message);
let message = format!("word ({word}) from wordlist is a URL, skipping...");
log::warn!("{message}");
log::trace!("exit: format -> Err({message})");
bail!(message);
}
@@ -122,9 +199,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)
@@ -142,60 +219,19 @@ impl FeroxUrl {
word = word.trim_start_matches('/').to_string();
};
let base_url = Url::parse(&url)?;
let joined = base_url.join(&word)?;
let base_url = parse_url_with_raw_path(&url)?;
let mut joined = base_url.join(&word)?;
if self.handles.config.queries.is_empty() {
// no query params to process
log::trace!("exit: format -> {}", joined);
Ok(joined)
} else {
let with_params =
Url::parse_with_params(joined.as_str(), &self.handles.config.queries)?;
log::trace!("exit: format_url -> {}", with_params);
Ok(with_params) // request with params attached
}
}
/// 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;
if !self.handles.config.queries.is_empty() {
// if called, this adds a '?' to the url, whether or not there are queries to be added
// so we need to check if there are queries to be added before blindly adding the '?'
joined
.query_pairs_mut()
.extend_pairs(self.handles.config.queries.iter());
}
log::trace!("exit: get_path_length -> 0");
0
log::trace!("exit: format_url -> {joined}");
Ok(joined)
}
/// Simple helper to abstract away adding a forward-slash to a url if not present
@@ -210,7 +246,7 @@ impl FeroxUrl {
format!("{}/", self.target)
};
log::trace!("exit: normalize -> {}", normalized);
log::trace!("exit: normalize -> {normalized}");
normalized
}
@@ -230,7 +266,7 @@ impl FeroxUrl {
let target = self.normalize();
let parsed = Url::parse(&target)?;
let parsed = parse_url_with_raw_path(&target)?;
let parts = parsed
.path_segments()
.ok_or_else(|| anyhow!("No path segments found"))?;
@@ -242,7 +278,7 @@ impl FeroxUrl {
depth += 1;
}
log::trace!("exit: get_depth -> {}", depth);
log::trace!("exit: get_depth -> {depth}");
Ok(depth)
}
}
@@ -310,7 +346,7 @@ mod tests {
let pdf = Url::parse("http://localhost/turbo.pdf").unwrap();
let tar = Url::parse("http://localhost/turbo.tar.gz").unwrap();
let expected = vec![
let expected = [
vec![base.clone(), js.clone()],
vec![base.clone(), js.clone(), php.clone()],
vec![base.clone(), js.clone(), php.clone(), pdf.clone()],
@@ -483,7 +519,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()
@@ -529,4 +565,186 @@ mod tests {
Err(err) => panic!("{}", err.to_string()),
}
}
#[test]
/// test is_in_scope function to ensure that it checks for presence within scope list
fn test_is_in_scope() {
let url = Url::parse("http://localhost").unwrap();
let scope = vec![
Url::parse("http://localhost").unwrap(),
Url::parse("http://example.com").unwrap(),
];
assert!(url.is_in_scope(&scope));
}
#[test]
/// test is_in_scope function to ensure that it checks that a subdomain of a domain within
/// the scope list returns true
fn test_is_in_scope_subdomain() {
let url = Url::parse("http://sub.localhost").unwrap();
let scope = vec![
Url::parse("http://localhost").unwrap(),
Url::parse("http://example.com").unwrap(),
];
assert!(url.is_in_scope(&scope));
}
#[test]
/// test is_in_scope returns false when url is not in scope
fn test_is_in_scope_not_in_scope() {
let url = Url::parse("http://notinscope.com").unwrap();
let scope = vec![
Url::parse("http://localhost").unwrap(),
Url::parse("http://example.com").unwrap(),
];
assert!(!url.is_in_scope(&scope));
}
#[test]
/// test is_in_scope with empty scope returns false
fn test_is_in_scope_empty_scope() {
let url = Url::parse("http://localhost").unwrap();
let scope: Vec<Url> = vec![];
assert!(!url.is_in_scope(&scope));
}
#[test]
/// test is_in_scope with domain-only scope entry (not a URL)
fn test_is_in_scope_domain_only_scope() {
let url = Url::parse("http://example.com").unwrap();
let scope = vec![Url::parse("http://example.com").unwrap()];
assert!(url.is_in_scope(&scope));
}
#[test]
/// test is_in_scope with subdomain and domain-only scope entry
fn test_is_in_scope_subdomain_domain_only_scope() {
let url = Url::parse("http://sub.example.com").unwrap();
let scope = vec![Url::parse("http://example.com").unwrap()];
assert!(url.is_in_scope(&scope));
}
#[test]
/// test is_in_scope with URL that has no domain
fn test_is_in_scope_no_domain() {
// This creates a URL that may not have a domain (like a file:// URL)
let url = Url::parse("file:///path/to/file").unwrap();
let scope = vec![Url::parse("http://example.com").unwrap()];
assert!(!url.is_in_scope(&scope));
}
#[test]
/// test is_subdomain_of basic functionality
fn test_is_subdomain_of_true() {
let subdomain_url = Url::parse("http://sub.example.com").unwrap();
let parent_url = Url::parse("http://example.com").unwrap();
assert!(subdomain_url.is_subdomain_of(&parent_url));
}
#[test]
/// test is_subdomain_of returns false for same domain
fn test_is_subdomain_of_same_domain() {
let url = Url::parse("http://example.com").unwrap();
let parent_url = Url::parse("http://example.com").unwrap();
assert!(!url.is_subdomain_of(&parent_url));
}
#[test]
/// test is_subdomain_of returns false for different domain
fn test_is_subdomain_of_different_domain() {
let url = Url::parse("http://other.com").unwrap();
let parent_url = Url::parse("http://example.com").unwrap();
assert!(!url.is_subdomain_of(&parent_url));
}
#[test]
/// test is_subdomain_of with multi-level subdomain
fn test_is_subdomain_of_multi_level() {
let subdomain_url = Url::parse("http://deep.sub.example.com").unwrap();
let parent_url = Url::parse("http://example.com").unwrap();
assert!(subdomain_url.is_subdomain_of(&parent_url));
}
#[test]
/// test is_subdomain_of with URLs that have no domain
fn test_is_subdomain_of_no_domain() {
let url = Url::parse("file:///path/to/file").unwrap();
let parent_url = Url::parse("http://example.com").unwrap();
assert!(!url.is_subdomain_of(&parent_url));
}
#[test]
/// test is_subdomain_of where parent has no domain
fn test_is_subdomain_of_parent_no_domain() {
let url = Url::parse("http://example.com").unwrap();
let parent_url = Url::parse("file:///path/to/file").unwrap();
assert!(!url.is_subdomain_of(&parent_url));
}
#[test]
/// test is_in_scope with same domain/host
fn test_is_not_in_empty_scope() {
let url = Url::parse("http://example.com/path").unwrap();
let scope: Vec<Url> = Vec::new();
assert!(!url.is_in_scope(&scope));
}
#[test]
/// test is_in_scope with subdomain
fn test_is_in_scope_subdomain_with_empty_scope() {
let url = Url::parse("http://sub.example.com").unwrap();
let scope: Vec<Url> = vec![];
assert!(!url.is_in_scope(&scope));
}
#[test]
/// test is_in_scope with scope match
fn test_is_in_scope_scope_match() {
let url = Url::parse("http://other.com").unwrap();
let scope = vec![Url::parse("http://other.com").unwrap()];
assert!(url.is_in_scope(&scope));
}
#[test]
/// test is_in_scope returns false when not in scope
fn test_is_in_scope_not_allowed() {
let url = Url::parse("http://notallowed.com").unwrap();
let scope = vec![Url::parse("http://other.com").unwrap()];
assert!(!url.is_in_scope(&scope));
}
#[test]
/// test is_in_scope with empty scope and different domain
fn test_is_in_scope_empty_scope_different_domain() {
let url = Url::parse("http://other.com").unwrap();
let scope: Vec<Url> = vec![];
assert!(!url.is_in_scope(&scope));
}
#[test]
/// test is_in_scope with subdomain in scope
fn test_is_in_scope_subdomain_in_scope() {
let url = Url::parse("http://sub.allowed.com").unwrap();
let scope = vec![Url::parse("http://allowed.com").unwrap()];
assert!(url.is_in_scope(&scope));
}
}

View File

@@ -6,24 +6,26 @@ use reqwest::{Client, Method, Response, StatusCode, Url};
#[cfg(not(target_os = "windows"))]
use rlimit::{getrlimit, setrlimit, Resource};
use std::{
error::Error,
fs,
io::{self, BufWriter, Write},
sync::Arc,
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},
statistics::StatError::{Certificate, Connection, Other, Redirection, Request, Timeout},
traits::FeroxSerialize,
USER_AGENTS,
};
@@ -31,20 +33,77 @@ use crate::{
/// simple counter for grabbing 'random' user agents
static mut USER_AGENT_CTR: usize = 0;
/// detects certificate-related errors by analyzing the error chain
fn is_certificate_error(error: &reqwest::Error) -> bool {
let full_error = format!("{error:?}").to_lowercase();
let error_msg = error.to_string().to_lowercase();
// check the main error message first
if error_msg.contains("certificate verify failed")
|| error_msg.contains("self-signed certificate")
|| error_msg.contains("certificate has expired")
|| error_msg.contains("hostname mismatch")
|| error_msg.contains("certificate")
{
return true;
}
// check the full debug representation for OpenSSL patterns
if full_error.contains("ssl routines")
|| full_error.contains("certificate verify failed")
|| full_error.contains("self-signed certificate")
|| full_error.contains("certificate has expired")
|| full_error.contains("hostname mismatch")
|| full_error.contains("tls_post_process_server_certificate")
|| full_error.contains("certificate")
|| full_error.contains("cert")
{
return true;
}
// walk the error source chain to find underlying TLS/certificate errors
let mut source = error.source();
while let Some(err) = source {
let source_msg = err.to_string().to_lowercase();
// check for specific OpenSSL certificate error patterns
if source_msg.contains("ssl routines")
|| source_msg.contains("certificate verify failed")
|| source_msg.contains("self-signed certificate")
|| source_msg.contains("certificate has expired")
|| source_msg.contains("hostname mismatch")
|| source_msg.contains("unable to get local issuer certificate")
|| source_msg.contains("certificate is not yet valid")
|| source_msg.contains("invalid certificate")
|| source_msg.contains("unknown ca")
|| source_msg.contains("certificate")
|| source_msg.contains("cert")
|| source_msg.contains("tls")
|| source_msg.contains("ssl")
{
return true;
}
source = err.source();
}
false
}
/// Given the path to a file, open the file in append mode (create it if it doesn't exist) and
/// return a reference to the buffered file
pub fn open_file(filename: &str) -> Result<BufWriter<fs::File>> {
log::trace!("enter: open_file({})", filename);
log::trace!("enter: open_file({filename})");
let file = fs::OpenOptions::new() // std fs
.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
log::trace!("exit: open_file -> {:?}", writer);
log::trace!("exit: open_file -> {writer:?}");
Ok(writer)
}
@@ -67,6 +126,39 @@ pub fn fmt_err(msg: &str) -> String {
format!("{}: {}", status_colorizer("ERROR"), msg)
}
/// simple wrapper to get the current system time as
/// time elapsed from unix epoch
pub fn timestamp() -> f64 {
let since_the_epoch = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| Duration::from_secs(0));
let secs = since_the_epoch.as_secs() as f64;
let nanos = since_the_epoch.subsec_nanos() as f64;
// Convert nanoseconds to fractional seconds and add to secs
secs + (nanos / 1_000_000_000.0)
}
/// 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<()> {
// make the response mutable so we can drop the body before
// sending it over the mpsc
let mut response = response;
response.drop_text();
handles.send_scan_command(Command::TryRecursion(Box::new(response)))?;
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 +181,7 @@ pub fn ferox_print(msg: &str, bar: &ProgressBar) {
bar.println(msg);
} else {
let stripped = strip_ansi_codes(msg);
println!("{}", stripped);
println!("{stripped}");
}
}
@@ -119,7 +211,7 @@ pub async fn logged_request(
Ok(resp)
}
Err(e) => {
log::warn!("err: {:?}", e);
log::warn!("err: {e:?}");
scans.increment_error(url.as_str());
bail!(e)
}
@@ -137,10 +229,7 @@ pub async fn make_request(
tx_stats: UnboundedSender<Command>,
) -> Result<Response> {
log::trace!(
"enter: make_request(Configuration::Client, {}, {:?}, {:?})",
url,
output_level,
tx_stats
"enter: make_request(Configuration::Client, {url}, {output_level:?}, {tx_stats:?})"
);
let tmp_workaround: Option<&[u8]> = Some(&[0xd_u8, 0xa]); // \r\n
@@ -183,7 +272,7 @@ pub async fn make_request(
match request.send().await {
Err(e) => {
log::trace!("exit: make_request -> {}", e);
log::trace!("exit: make_request -> {e}");
if e.is_timeout() {
send_command!(tx_stats, AddError(Timeout));
@@ -216,6 +305,10 @@ pub async fn make_request(
ferox_print(&report, &PROGRESS_PRINTER)
};
} else if is_certificate_error(&e) {
log::warn!("Certificate error detected: {e}");
send_command!(tx_stats, AddError(Certificate));
bail!(":SSL: {e}");
} else if e.is_connect() {
send_command!(tx_stats, AddError(Connection));
} else if e.is_request() {
@@ -224,11 +317,11 @@ pub async fn make_request(
send_command!(tx_stats, AddError(Other));
}
log::warn!("Error while making request: {}", e);
log::warn!("Error while making request: {e}");
bail!("{}", e)
}
Ok(resp) => {
log::trace!("exit: make_request -> {:?}", resp);
log::trace!("exit: make_request -> {resp:?}");
send_command!(tx_stats, AddStatus(resp.status()));
Ok(resp)
}
@@ -250,19 +343,17 @@ 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);
if status.contains("MSG") {
format!(
"{} {:>8} {:>9} {:>9} {:>9} {}\n",
color_status, method, line_count, word_count, content_length, url
"{color_status} {method:>8} {line_count:>9} {word_count:>9} {content_length:>9} {url}\n"
)
} else {
format!(
"{} {:>8} {:>8}l {:>8}w {:>8}c {}\n",
color_status, method, line_count, word_count, content_length, url
"{color_status} {method:>8} {line_count:>8}l {word_count:>8}w {content_length:>8}c {url}\n"
)
}
}
@@ -293,7 +384,7 @@ pub fn set_open_file_limit(limit: u64) -> bool {
// set the soft limit to our default
if setrlimit(Resource::NOFILE, limit, hard).is_ok() {
log::debug!("set open file descriptor limit to {}", limit);
log::debug!("set open file descriptor limit to {limit}");
log::trace!("exit: set_open_file_limit -> {}", true);
return true;
@@ -302,7 +393,7 @@ pub fn set_open_file_limit(limit: u64) -> bool {
// hard limit is lower than our default, the next best option is to set the soft limit as
// high as the hard limit will allow
if setrlimit(Resource::NOFILE, hard, hard).is_ok() {
log::debug!("set open file descriptor limit to {}", limit);
log::debug!("set open file descriptor limit to {limit}");
log::trace!("exit: set_open_file_limit -> {}", true);
return true;
@@ -312,7 +403,7 @@ pub fn set_open_file_limit(limit: u64) -> bool {
// failed to set a new limit, as limit adjustments are a 'nice to have', we'll just log
// and move along
log::warn!("could not set open file descriptor limit to {}", limit);
log::warn!("could not set open file descriptor limit to {limit}");
log::trace!("exit: set_open_file_limit -> {}", false);
false
@@ -407,8 +498,13 @@ fn should_deny_absolute(url_to_test: &Url, denier: &Url, handles: Arc<Handles>)
// current deny-url, now we just need to check to see if this deny-url is a parent
// to a scanned url that is also a parent of the given url
for ferox_scan in handles.ferox_scans()?.get_active_scans() {
let scanner = Url::parse(ferox_scan.url().trim_end_matches('/'))
.with_context(|| format!("Could not parse {} as a url", ferox_scan))?;
let scanner = parse_url_with_raw_path(ferox_scan.url().trim_end_matches('/'))
.with_context(|| format!("Could not parse {ferox_scan} as a url"))?;
// by calling the new parse_url_with_raw_path, and reaching this point without an
// error, we know we have an authority and therefore a host. leaving the code
// below, but we should never hit the else condition. leaving it in so if we find
// a case where i'm mistaken, we'll know about it and can address it
if let Some(scan_host) = scanner.host() {
// same domain/ip check we perform on the denier above
@@ -418,7 +514,7 @@ fn should_deny_absolute(url_to_test: &Url, denier: &Url, handles: Arc<Handles>)
}
} else {
// couldn't process .host from scanner
continue;
unreachable!("should_deny_absolute: scanner.host() returned None, which shouldn't be possible");
};
let scan_path = scanner.path();
@@ -453,7 +549,7 @@ fn should_deny_regex(url_to_test: &Url, denier: &Regex) -> bool {
let result = denier.is_match(url_to_test.as_str());
log::trace!("exit: should_deny_regex -> {}", result);
log::trace!("exit: should_deny_regex -> {result}");
result
}
@@ -469,7 +565,7 @@ pub fn should_deny_url(url: &Url, handles: Arc<Handles>) -> Result<bool> {
// normalization for comparison is to remove the trailing / if one exists, this is done for
// the given url and any url to which it's compared
let normed_url = Url::parse(url.to_string().trim_end_matches('/'))?;
let normed_url = parse_url_with_raw_path(url.to_string().trim_end_matches('/'))?;
for denier in &handles.config.url_denylist {
// note to self: it may seem as though we can use regex only for --dont-scan, however, in
@@ -498,7 +594,7 @@ pub fn should_deny_url(url: &Url, handles: Arc<Handles>) -> Result<bool> {
///
/// ex: ferox-http_telsa_com-1606947491.state
pub fn slugify_filename(url: &str, prefix: &str, suffix: &str) -> String {
log::trace!("enter: slugify({:?}, {:?}, {:?})", url, prefix, suffix);
log::trace!("enter: slugify({url:?}, {prefix:?}, {suffix:?})");
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
@@ -506,19 +602,207 @@ 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);
log::trace!("exit: slugify -> {filename}");
filename
}
/// This function takes a url string and returns a `url::Url`
///
/// It is primarily used to detect url paths that `url::Url::parse` will
/// silently transform, such as /path/../file.html -> /file.html
///
/// # Warning
///
/// In the instance of a url with encoded path traversal strings, such as
/// /path/%2e%2e/file.html, the underlying `url::Url::parse` will
/// further encode the %-signs and return /path/%252e%252e/file.html
pub fn parse_url_with_raw_path(url: &str) -> Result<Url> {
log::trace!("enter: parse_url_with_raw_path({url})");
let parsed = Url::parse(url)?;
if !parsed.has_authority() {
// parsed correctly, but no authority, meaning mailto: or tel: or
// some other url that we don't care about
bail!("url to parse has no authority and is therefore invalid");
}
// thanks to @devx00: the possibility exists for Url to return true for
// has_authority, but not have a host/port, so we'll check for that
// and bail if it's the case
if parsed.host().is_none() {
bail!("url to parse doesn't have a host");
}
// we have a valid url, the next step is to check the path and see if it's
// something that url::Url::parse would silently transform
//
// i.e. if the path is /path/../file.html, url::Url::parse will transform it
// to /file.html, which is not what we want
let farthest_right_authority_part;
// we want to find the farthest right authority component, which is the
// component that is the furthest right in the url that is part of the
// authority
//
// per RFC 3986, the authority is defined as:
// - authority = [ userinfo "@" ] host [ ":" port ]
//
// so the farthest right authority component is either the port or the host
//
// i.e. in http://example.com:80/path/file.html, the farthest right authority
// component is :80
//
// in http://example.com/path/file.html, the farthest right authority component
// is example.com
//
// the farthest right authority component is used to split the url into two
// parts: the part before the authority and the part after the authority
if let Some(port) = parsed.port() {
// if the url has a port, then the farthest right authority component is
// the port
farthest_right_authority_part = format!(":{port}");
} else if parsed.has_host() {
// if the url has a host, then the farthest right authority component is
// the host
farthest_right_authority_part = parsed.host_str().unwrap().to_owned();
} else {
// if the url has neither a port nor a host, then the url is invalid
// and we can't do anything with it, but i don't think this is possible
unreachable!("url has an authority, but has neither a port nor a host");
}
// split the original url string into two parts: the part before the authority and the part
// after the authority (i.e. the path + query + fragment)
let Some((_, after_authority)) = url.split_once(&farthest_right_authority_part) else {
// if we can't split the url string into two parts, then the url doesn't conform to our
// expectations, and we can't continue processing it, so we'll return the parsed url
return Ok(parsed);
};
// when there is a port, but it matches the default port for the scheme,
// url::Url::parse will mark the port as None, giving us a
// `after_authority` that looks something like this:
// - :80/path/file.html
let after_authority = after_authority
.replacen(":80", "", 1)
.replacen(":443", "", 1);
// snippets from rfc-3986:
//
// foo://example.com:8042/over/there?name=ferret#nose
// \_/ \______________/\_________/ \_________/ \__/
// | | | | |
// scheme authority path query fragment
//
// The path component is terminated
// by the first question mark ("?") or number sign ("#") character, or
// by the end of the URI.
//
// The query component is indicated by the first question
// mark ("?") character and terminated by a number sign ("#") character
// or by the end of the URI.
let (path, _discarded) = after_authority
.split_once('?')
// if there isn't a '?', try to remove a fragment
.unwrap_or_else(|| {
// if there isn't a '#', return (original, empty)
after_authority
.split_once('#')
.unwrap_or((&after_authority, ""))
});
// at this point, we have the path, all by itself
// each of the following is a string that we can expect url::Url::parse to
// transform. The variety is to ensure we cover most common path traversal
// encodings
let transformation_detectors = [
// ascii
"..",
// single url encoded
"%2e%2e",
// double url encoded
"%25%32%65%25%32%65",
// utf-8 encoded
"%c0%ae%c0%ae",
"%e0%40%ae%e0%40%ae",
"%c0ae%c0ae",
// 16 bit shenanigans
"%uff0e%uff0e",
"%u002e%u002e",
];
let parsing_will_transform_path = transformation_detectors
.iter()
.any(|detector| path.to_lowercase().contains(detector));
if !parsing_will_transform_path {
// there's no string in the path of the url that will trigger a transformation
// so, we can return it as-is
return Ok(parsed);
}
// if we reach this point, the path contains a string that will trigger a transformation
// so we need to manually create a Url that doesn't have the transformation
// and return that
//
// special thanks to github user @lavafroth for this workaround
let mut hacked_url = if path.ends_with('/') {
// from_file_path silently strips trailing slashes, and
// from_directory_path adds them, so we'll choose the appropriate
// constructor based on the presence of a path's trailing slash
// according to from_file_path docs:
// from_file_path returns `Err` if the given path is not absolute or,
// on Windows, if the prefix is not a disk prefix (e.g. `C:`) or a UNC prefix (`\\`).
//
// since we parsed out a valid url path, we know it is absolute, so on non-windows
// platforms, we can safely unwrap. On windows, we need to fix up the path
#[cfg(target_os = "windows")]
{
let path = format!("\\/IGNOREME{path}");
Url::from_directory_path(path).unwrap()
}
#[cfg(not(target_os = "windows"))]
Url::from_directory_path(path).unwrap()
} else {
#[cfg(target_os = "windows")]
{
let path = format!("\\/IGNOREME{path}");
Url::from_file_path(path).unwrap()
}
#[cfg(not(target_os = "windows"))]
Url::from_file_path(path).unwrap()
};
// host must be set first, otherwise multiple components may return Err
hacked_url.set_host(parsed.host_str())?;
// scheme/port/username/password can fail, but in this instance, we know they won't
hacked_url.set_scheme(parsed.scheme()).unwrap();
hacked_url.set_port(parsed.port()).unwrap();
hacked_url.set_username(parsed.username()).unwrap();
hacked_url.set_password(parsed.password()).unwrap();
// query/fragment can't fail
hacked_url.set_query(parsed.query());
hacked_url.set_fragment(parsed.fragment());
log::trace!("exit: parse_url_with_raw_path -> {hacked_url}");
Ok(hacked_url)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -526,31 +810,171 @@ mod tests {
use crate::scan_manager::{FeroxScans, ScanOrder};
#[test]
/// set_open_file_limit with a low requested limit succeeds
fn utils_set_open_file_limit_with_low_requested_limit() {
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
let lower_limit = hard - 1;
assert!(set_open_file_limit(lower_limit));
/// parse_url_with_raw_path with javascript:// should not throw an unimplemented! error
fn utils_parse_url_with_raw_path_javascript() {
let url = "javascript://";
let parsed = parse_url_with_raw_path(url);
assert!(parsed.is_err());
assert!(parsed
.unwrap_err()
.to_string()
.contains("url to parse doesn't have a host"));
}
#[test]
/// set_open_file_limit with a high requested limit succeeds
fn utils_set_open_file_limit_with_high_requested_limit() {
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
let higher_limit = hard + 1;
// calculate a new soft to ensure soft != hard and hit that logic branch
let new_soft = hard - 1;
setrlimit(Resource::NOFILE, new_soft, hard).unwrap();
assert!(set_open_file_limit(higher_limit));
/// multiple tests for parse_url_with_raw_path
fn utils_parse_url_with_raw_path() {
// ../.. is preserved
let url = "https://www.google.com/../../stuff";
let parsed = parse_url_with_raw_path(url).unwrap();
assert_eq!(parsed.as_str(), url);
// ../.. is preserved as well as the trailing slash
let url = "https://www.google.com/../../stuff/";
let parsed = parse_url_with_raw_path(url).unwrap();
assert_eq!(parsed.as_str(), url);
// no trailing slash is preserved
let url = "https://www.google.com/stuff";
let parsed = parse_url_with_raw_path(url).unwrap();
assert_eq!(parsed.as_str(), url);
// trailing slash is preserved
let url = "https://www.google.com/stuff/";
let parsed: Url = parse_url_with_raw_path(url).unwrap();
assert_eq!(parsed.as_str(), url);
// mailto is an error
let url = "mailto:user@example.com";
let parsed = parse_url_with_raw_path(url);
assert!(parsed.is_err());
// relative url is an error
let url = "../../stuff";
let parsed = parse_url_with_raw_path(url);
assert!(parsed.is_err());
// absolute without host is an error
let url = "/../../stuff";
let parsed = parse_url_with_raw_path(url);
assert!(parsed.is_err());
// default ports are parsed correctly
for url in [
"http://example.com:80/path/file.html",
"https://example.com:443/path/file.html",
] {
let parsed = parse_url_with_raw_path(url).unwrap();
assert!(parsed.port().is_none());
assert_eq!(parsed.host().unwrap().to_string().as_str(), "example.com");
}
// non-default ports are parsed correctly
for url in [
"http://example.com:8080/path/file.html",
"https://example.com:4433/path/file.html",
] {
let parsed = parse_url_with_raw_path(url).unwrap();
assert!(parsed.port().is_some());
assert_eq!(parsed.as_str(), url);
}
// different encodings are respected if found in doubles
//
// note that the % sign is encoded as %25...
let url = "http://user:pass@example.com/%2e%2e/stuff.php";
let parsed = parse_url_with_raw_path(url).unwrap();
assert_eq!(
parsed.as_str(),
"http://user:pass@example.com/%252e%252e/stuff.php"
);
let url = "http://user:pass@example.com/%25%32%65%25%32%65/stuff.php";
let parsed = parse_url_with_raw_path(url).unwrap();
assert_eq!(parsed.username(), "user");
assert_eq!(parsed.password().unwrap(), "pass");
assert_eq!(
parsed.as_str(),
"http://user:pass@example.com/%2525%2532%2565%2525%2532%2565/stuff.php"
);
let url = "http://user:pass@example.com/%c0%ae%c0%ae/stuff.php";
let parsed = parse_url_with_raw_path(url).unwrap();
assert_eq!(parsed.username(), "user");
assert_eq!(parsed.password().unwrap(), "pass");
assert_eq!(
parsed.as_str(),
"http://user:pass@example.com/%25c0%25ae%25c0%25ae/stuff.php"
);
let url = "http://user:pass@example.com/%e0%40%ae%e0%40%ae/stuff.php";
let parsed = parse_url_with_raw_path(url).unwrap();
assert_eq!(parsed.username(), "user");
assert_eq!(parsed.password().unwrap(), "pass");
assert_eq!(
parsed.as_str(),
"http://user:pass@example.com/%25e0%2540%25ae%25e0%2540%25ae/stuff.php"
);
let url = "http://user:pass@example.com/%c0ae%c0ae/stuff.php";
let parsed = parse_url_with_raw_path(url).unwrap();
assert_eq!(parsed.username(), "user");
assert_eq!(parsed.password().unwrap(), "pass");
assert_eq!(
parsed.as_str(),
"http://user:pass@example.com/%25c0ae%25c0ae/stuff.php"
);
let url = "http://user:pass@example.com/%uff0e%uff0e/stuff.php";
let parsed = parse_url_with_raw_path(url).unwrap();
assert_eq!(parsed.username(), "user");
assert_eq!(parsed.password().unwrap(), "pass");
assert_eq!(
parsed.as_str(),
"http://user:pass@example.com/%25uff0e%25uff0e/stuff.php"
);
let url = "http://user:pass@example.com/%u002e%u002e/stuff.php";
let parsed = parse_url_with_raw_path(url).unwrap();
assert_eq!(parsed.username(), "user");
assert_eq!(parsed.password().unwrap(), "pass");
assert_eq!(
parsed.as_str(),
"http://user:pass@example.com/%25u002e%25u002e/stuff.php"
);
}
#[test]
/// set_open_file_limit should fail when hard == soft
fn utils_set_open_file_limit_with_fails_when_both_limits_are_equal() {
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
// calculate a new soft to ensure soft == hard and hit the failure logic branch
setrlimit(Resource::NOFILE, hard, hard).unwrap();
assert!(!set_open_file_limit(hard)); // returns false
#[cfg(not(target_os = "windows"))]
mod nix_only_tests {
use super::*;
#[test]
/// set_open_file_limit with a low requested limit succeeds
fn utils_set_open_file_limit_with_low_requested_limit() {
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
let lower_limit = hard - 1;
assert!(set_open_file_limit(lower_limit));
}
#[test]
/// set_open_file_limit with a high requested limit succeeds
fn utils_set_open_file_limit_with_high_requested_limit() {
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
let higher_limit = hard + 1;
// calculate a new soft to ensure soft != hard and hit that logic branch
let new_soft = hard - 1;
setrlimit(Resource::NOFILE, new_soft, hard).unwrap();
assert!(set_open_file_limit(higher_limit));
}
#[test]
/// set_open_file_limit should fail when hard == soft
fn utils_set_open_file_limit_with_fails_when_both_limits_are_equal() {
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
// calculate a new soft to ensure soft == hard and hit the failure logic branch
setrlimit(Resource::NOFILE, hard, hard).unwrap();
assert!(!set_open_file_limit(hard)); // returns false
}
}
#[test]
@@ -610,7 +1034,11 @@ mod tests {
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
scans.add_directory_scan(
scan_url,
ScanOrder::Initial,
Arc::new(Handles::for_testing(None, None).0),
);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
@@ -629,7 +1057,11 @@ mod tests {
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
scans.add_directory_scan(
scan_url,
ScanOrder::Initial,
Arc::new(Handles::for_testing(None, None).0),
);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
@@ -648,7 +1080,11 @@ mod tests {
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
scans.add_directory_scan(
scan_url,
ScanOrder::Initial,
Arc::new(Handles::for_testing(None, None).0),
);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
@@ -669,7 +1105,11 @@ mod tests {
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
scans.add_directory_scan(
scan_url,
ScanOrder::Initial,
Arc::new(Handles::for_testing(None, None).0),
);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
@@ -684,21 +1124,31 @@ mod tests {
/// provide a denier from which we can't check a host, which results in no comparison, expect false
/// because the denier is a parent to the tested, even tho the scanned doesn't compare, it
/// still returns true
///
/// note: adding parse_url_with_raw_path changed the behavior of this test, it used to return
/// true, now it returns false. see my note in should_deny_absolute and the unreachable!
/// call block to see why
///
/// leaving this test here to document the behavior change and to catch regressions in the
/// new expected behavior
fn should_deny_url_doesnt_compare_non_domains_in_scanned() {
let deny_url = "https://testdomain.com/";
let scan_url = "unix:/run/foo.socket";
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
scans.add_directory_scan(
scan_url,
ScanOrder::Initial,
Arc::new(Handles::for_testing(None, None).0),
);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(should_deny_url(&tested_url, handles).unwrap());
assert!(!should_deny_url(&tested_url, handles).unwrap());
}
#[test]
@@ -709,7 +1159,11 @@ mod tests {
let tested_url = Url::parse("https://testdomain.com/api/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
scans.add_directory_scan(
scan_url,
ScanOrder::Initial,
Arc::new(Handles::for_testing(None, None).0),
);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
@@ -728,7 +1182,11 @@ mod tests {
let tested_url = Url::parse("https://testdomain.com/not-denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
scans.add_directory_scan(
scan_url,
ScanOrder::Initial,
Arc::new(Handles::for_testing(None, None).0),
);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
@@ -747,7 +1205,11 @@ mod tests {
let tested_url = Url::parse("https://testdomain.com/stuff/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
scans.add_directory_scan(
scan_url,
ScanOrder::Initial,
Arc::new(Handles::for_testing(None, None).0),
);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
@@ -766,7 +1228,11 @@ mod tests {
let tested_url = Url::parse("https://testdomain.com/api/not-denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
scans.add_directory_scan(
scan_url,
ScanOrder::Initial,
Arc::new(Handles::for_testing(None, None).0),
);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
@@ -786,7 +1252,11 @@ mod tests {
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
scans.add_directory_scan(
scan_url,
ScanOrder::Initial,
Arc::new(Handles::for_testing(None, None).0),
);
let mut config = Configuration::new().unwrap();
config.regex_denylist = vec![Regex::new(deny_pattern).unwrap()];
@@ -807,7 +1277,11 @@ mod tests {
let tested_https_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
scans.add_directory_scan(
scan_url,
ScanOrder::Initial,
Arc::new(Handles::for_testing(None, None).0),
);
let mut config = Configuration::new().unwrap();
config.regex_denylist = vec![Regex::new(deny_pattern).unwrap()];

View File

@@ -0,0 +1,17 @@
(mTLS) {
tls {
client_auth {
mode require_and_verify
trusted_ca_cert_file certs/server/ca.crt
}
}
}
https://localhost:8001 {
import mTLS
log
handle / {
file_server browse
}
}

View File

@@ -0,0 +1,6 @@
# Testing mTLS
- run `gen-certs.sh`
- run `sudo /path/to/caddy run`
- expect listener on port 8001
- run `feroxbuster -u https://localhost:8001 --client-key certs/client/client.key --client-cert certs/client/client.crt`

View File

@@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE-----
MIICqzCCAZMCFE22XDzrLwkJIkb3EdP333d4HoXQMA0GCSqGSIb3DQEBCwUAMBMx
ETAPBgNVBAMMCFNlcnZlckNBMB4XDTIzMDUwNjExMDYyM1oXDTI0MDUwNTExMDYy
M1owETEPMA0GA1UEAwwGQ2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAz3EPWMsh+dfPdbHtpNhizZZs+r0djzdHHgkbnNQ1PodWDnv0Rf1YgNEa
umQuUvIgjMtorRqbz9HLG4+H2aR5KHgPwBNHyKS4PEiQvWDV88aJxdMbgL/IfzAt
di85UcBUkyqUe1r6vIS0smJo1wVwxLEmD6kdt1BEI3LaK1j99JeG8TAS8f+/xf4s
ouE4lA+y3bJQP18wUGuyudntFQBKgjY2Tx+RWbBcx0zW68M7IMQ5bDz0oK9MYw8G
q2vwcRyMLuoyNpbDT5mI2wsQu/r2O0CCNbtkg5JxasdYR7Llw9YTl74st3dshM9e
4V5uuVotcWXW6U518nWHOQy9qiBSOQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB4
xOVvWrRZ4SBqzaen32COXpjddX28Q7YmNB/UKl3ZT7R1dIjUMfJz2le0mj2UpSAr
rDT7PCsXnDP0KswGiJC3IVTa/hnkUk798jwUvp221jvebyy8/NMWfWPoIKfhELdb
3uJfrGyQuB8Zf9Q1hc9jYDX27EbGaDSpOrpE9Ej2riVnbgBKZsS5jcfY8JDrkv+F
4cP2pTu6mVRuU1Bzx3SB0Vg2uGi1QTJuuA905Y3zpoRfTtybKlRRkMQk+46xrdyV
x64wq9zcL6Kq4D/UE3EjLnjbRw6H6g8jbnBjT5KRfP2tmbF9RTZs44Dl0hYvXber
HrvWtxHG8OJ8BLQg1rQd
-----END CERTIFICATE-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDPcQ9YyyH51891
se2k2GLNlmz6vR2PN0ceCRuc1DU+h1YOe/RF/ViA0Rq6ZC5S8iCMy2itGpvP0csb
j4fZpHkoeA/AE0fIpLg8SJC9YNXzxonF0xuAv8h/MC12LzlRwFSTKpR7Wvq8hLSy
YmjXBXDEsSYPqR23UEQjctorWP30l4bxMBLx/7/F/iyi4TiUD7LdslA/XzBQa7K5
2e0VAEqCNjZPH5FZsFzHTNbrwzsgxDlsPPSgr0xjDwara/BxHIwu6jI2lsNPmYjb
CxC7+vY7QII1u2SDknFqx1hHsuXD1hOXviy3d2yEz17hXm65Wi1xZdbpTnXydYc5
DL2qIFI5AgMBAAECggEAC8XVeoM1w4uITDxLucMnkVYgC3dj5/K5zCY1bVg8SNcO
rt4BSh8TkKT9ZLZmjCHOb9sj7s4PqXLVOXRTAAq17xJoR2z4shYKGC7AmyTVo6MB
AuuFGDCaMQCzlc1ejgmRqzP7jwgl6oDIDgcofsqB4MHSgIlHJNYO9emQ4OypJgJA
xd8KT5S/hThJG1VqJ6P0oiB/WBlzcJ5wX4GSVE25RlpRX8ogqCyI9V+SRq2CrG7U
Jqv3Kbag7derTfqmsKyjv/kckOgfKH/rm61HMrYshcPfgxL2fZe2Q8wCTexvhZwZ
8vD8bvR++SxOxbigCIB7ReYgmoj4bocjqDX4vUhe8QKBgQD35oDdOa2uiOs7NWVf
IV1ZwPWxxwnYFIEA8paQwsYGIxHrYNdGSsGBzwvLDPpTeOO0VdoC+sP5zytTv547
djeOzGf9Hj6swa5tPdzkYjZV/85mnmGKaEmmCN4AvpYol5l2BTetFtX0v6QEaqvU
uZbV5X2UcuClExA0frNUJDVHkQKBgQDWOCZq1r9X3iEkcFSBhironVNj80jFqIum
rMbGUUcOI05U2hkmMDluSW1NNL2k+SNJXq7fmkjIQEXffqcbsXUSIQB0MU6yddt5
7+c19ioZChx91Kl049rKQ21kPTh7D0TCUvDQLapt2xbUNg6rGCLSrkkVlWwxLnDU
pNk/c4QcKQKBgEreedLWhabtwSV7pecKO5hM16dedpGk96UinuiPeqEF3HabI8kd
8L1Um7oybDPjkdm4CATYWXHL6Mj9WTuaI4NkJo/in4krYZOqmFj9dG2auWpysQDN
KFkV2n6dENqnlnh3cO48tFebvVx8HvM7Ldvh2ICKBWC1ljJUhbKG0PSRAoGBAJVy
fNLCWKEbVbHPMBVgnaTExT2Qp29F4493MAGBCHpDhU1LDoqG0DoxvbBEIB3stYJl
LMjQIQCbXmPKPxjh15O7NE7ba1SzRleuV3Zc8wee9zuN1l6265d6LOHml/W6NDUB
mgESKrkTRLztrZQNdZXXgyMsqFszVAH1s55Bn6PpAoGBAN13Ev7Ynysdvkc3aHO6
qM0hH6mAlEOAyCTk5r/0cyz9rGyYWXiVXen0ftSaBcISdzhrVkRDs3rLrHwEXdu1
Y2Z1HhZkILw/C4t+Eaa6FOWfwwPAdOpaxYpxKxCEeCBKmkd1z0Dx0vDEDrt+AaHa
UYIQ9wAbZpuKGfFQceyr1lBO
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDBzCCAe+gAwIBAgIUG3vb4pIbvaI/+LzOpu6Z4b6s4iIwDQYJKoZIhvcNAQEL
BQAwEzERMA8GA1UEAwwIU2VydmVyQ0EwHhcNMjMwNTA2MTEwNjIzWhcNMzMwNTAz
MTEwNjIzWjATMREwDwYDVQQDDAhTZXJ2ZXJDQTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAKazNKPaH8LDzcaZRvBLrDNJkL1pukmB36mbczj07hZVbPmS
/hyBvAdBFom0ZTw5dIpsUtRSZbDPrsCVpdY9O1jxwhrDi6mfvyJtKLEbTW4PvARq
WwDhpa2SYwBMI+0ilXWTAzwJuWT1NhuUsAcB6SGwkNm3iKqZUDxn3V2L2AHRcKEJ
9Zn9ePP4BsvtAS8ZBLxTnoo7R2SHiWwjDwuTtS4fQ5bWzGkmdmeuJ7JJt4ZzQV+m
MBqrK3XVi+MXayvt5affGvHj/KuhlVBXHnUgSvEgFpuhK9elsds2iRho8mp1d0iH
EIMp9LHVftsIpUxbKt/Pa/JL7oG9LBIvPj/SIjsCAwEAAaNTMFEwHQYDVR0OBBYE
FLlRKsDb/ducVIBirME0VJZ3TwfkMB8GA1UdIwQYMBaAFLlRKsDb/ducVIBirME0
VJZ3TwfkMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAD7lqNzU
wxuyO60Gn2q6DUBb1Kseq6bSndNHeagdfMfKManKl1YObnB0ciTO3bnmNXiXktSu
BsQzlmr3O+H6X39Vpdyqq4SoOcOt0I+bvBykk1UZqEoc7jGXdZVmnk9Q0uoKtWxJ
rV9CHEhyPNnEh4W07y05UUn9S6EiKy5232yi4USdmk44GXhFblS5inhTTxca2vEq
9h+FH+QZ7ehaAaWR+EaQjXNwm2mN7gWxM3Q6RfK9N67MHD9ggmfdyZmnyt5gCidC
ys4W4stEh6d6fXZT77dcGaHKdXW3GwP3ZcAlRFYPqpAvWzndC9kDCgIULeSP1ALy
cILcb0HQvNS0t60=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE-----
MIICrzCCAZcCFGMKRtmMLuut+sxC+TbWQfum7oXZMA0GCSqGSIb3DQEBCwUAMBQx
EjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMzA1MDYxMTA2MjNaFw0zMzA1MDMxMTA2
MjNaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAKazNKPaH8LDzcaZRvBLrDNJkL1pukmB36mbczj07hZVbPmS/hyB
vAdBFom0ZTw5dIpsUtRSZbDPrsCVpdY9O1jxwhrDi6mfvyJtKLEbTW4PvARqWwDh
pa2SYwBMI+0ilXWTAzwJuWT1NhuUsAcB6SGwkNm3iKqZUDxn3V2L2AHRcKEJ9Zn9
ePP4BsvtAS8ZBLxTnoo7R2SHiWwjDwuTtS4fQ5bWzGkmdmeuJ7JJt4ZzQV+mMBqr
K3XVi+MXayvt5affGvHj/KuhlVBXHnUgSvEgFpuhK9elsds2iRho8mp1d0iHEIMp
9LHVftsIpUxbKt/Pa/JL7oG9LBIvPj/SIjsCAwEAATANBgkqhkiG9w0BAQsFAAOC
AQEAjPAtZs1by2h/1fr/ypojw16llzbReT8J+T8YHSTf6YwjoE83I0QDOLEo1ax+
e/8qyQLs0EnlfdomNyA4Z/ECbY5c1nY0Dp//u6WH7AwLUx5HiwUw4Fmxu9Q/oB1o
3vhIPl5Vd/VpdxDzuO8q8WvagwjVaxsZP3PVaBDRzZZPldPgTakfk+w5XnjNfgJi
RDRutTRe6KBOxt7PAzAVV71FtOIq0b4xCNJGNurYBhRgZ5iQ7yMw+I5Vte1TakWr
9gfE/yoKbU1W+y0QxSDTsnTCO4i3mXmBTuceTVWELwqZcr34W7n3vD8UtZQfanML
cHCZaLPSMDuDtS74FSamP3i+oQ==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE-----
MIICrzCCAZcCFGMKRtmMLuut+sxC+TbWQfum7oXZMA0GCSqGSIb3DQEBCwUAMBQx
EjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMzA1MDYxMTA2MjNaFw0zMzA1MDMxMTA2
MjNaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAKazNKPaH8LDzcaZRvBLrDNJkL1pukmB36mbczj07hZVbPmS/hyB
vAdBFom0ZTw5dIpsUtRSZbDPrsCVpdY9O1jxwhrDi6mfvyJtKLEbTW4PvARqWwDh
pa2SYwBMI+0ilXWTAzwJuWT1NhuUsAcB6SGwkNm3iKqZUDxn3V2L2AHRcKEJ9Zn9
ePP4BsvtAS8ZBLxTnoo7R2SHiWwjDwuTtS4fQ5bWzGkmdmeuJ7JJt4ZzQV+mMBqr
K3XVi+MXayvt5affGvHj/KuhlVBXHnUgSvEgFpuhK9elsds2iRho8mp1d0iHEIMp
9LHVftsIpUxbKt/Pa/JL7oG9LBIvPj/SIjsCAwEAATANBgkqhkiG9w0BAQsFAAOC
AQEAjPAtZs1by2h/1fr/ypojw16llzbReT8J+T8YHSTf6YwjoE83I0QDOLEo1ax+
e/8qyQLs0EnlfdomNyA4Z/ECbY5c1nY0Dp//u6WH7AwLUx5HiwUw4Fmxu9Q/oB1o
3vhIPl5Vd/VpdxDzuO8q8WvagwjVaxsZP3PVaBDRzZZPldPgTakfk+w5XnjNfgJi
RDRutTRe6KBOxt7PAzAVV71FtOIq0b4xCNJGNurYBhRgZ5iQ7yMw+I5Vte1TakWr
9gfE/yoKbU1W+y0QxSDTsnTCO4i3mXmBTuceTVWELwqZcr34W7n3vD8UtZQfanML
cHCZaLPSMDuDtS74FSamP3i+oQ==
-----END CERTIFICATE-----

Some files were not shown because too many files have changed in this diff Show More