Compare commits

..

29 Commits

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
41 changed files with 1668 additions and 471 deletions

View File

@@ -963,6 +963,90 @@
"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,

View File

@@ -16,10 +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/)
- [ ] 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

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

13
Cargo.lock generated
View File

@@ -116,13 +116,12 @@ dependencies = [
[[package]]
name = "assert_cmd"
version = "2.0.17"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66"
checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85"
dependencies = [
"anstyle",
"bstr",
"doc-comment",
"libc",
"predicates",
"predicates-core",
@@ -790,12 +789,6 @@ dependencies = [
"syn 2.0.104",
]
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "dtoa"
version = "1.0.10"
@@ -946,7 +939,7 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "feroxbuster"
version = "2.13.0"
version = "2.13.1"
dependencies = [
"anyhow",
"assert_cmd",

View File

@@ -1,6 +1,6 @@
[package]
name = "feroxbuster"
version = "2.13.0"
version = "2.13.1"
authors = ["Ben 'epi' Risher (@epi052)"]
license = "MIT"
edition = "2021"
@@ -69,7 +69,7 @@ self_update = { version = "0.40", features = [
[dev-dependencies]
tempfile = "3.20"
httpmock = "0.7"
assert_cmd = "2.0"
assert_cmd = "2.1"
predicates = "3.1"
[profile.release]

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020-2023 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

@@ -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>
@@ -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
@@ -135,7 +147,7 @@ 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).
### Updating feroxbuster (new in v2.9.1)
@@ -143,9 +155,9 @@ Please refer the the [documentation](https://epi052.github.io/feroxbuster-docs/d
./feroxbuster --update
```
## 🧰 Example Usage
## Example Usage
Here are a few brief examples to get you started. Please note, feroxbuster can do a **lot more** than what's listed below. As a result, there are **many more** examples, with **demonstration gifs** that highlight specific features, in the [documentation](https://epi052.github.io/feroxbuster-docs/docs/).
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
@@ -205,13 +217,12 @@ cat targets | ./feroxbuster --stdin --silent -s 200 301 302 --redirects -x js |
./feroxbuster -u http://127.1 --data-urlencoded @file.payload
```
## 🚀 Documentation has **moved** 🚀
> [!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)**
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!
<h1><p align="center">✨🎉👉 <a href="https://epi052.github.io/feroxbuster-docs/docs/">DOCUMENTATION</a> 👈🎉✨</p></h1>
## Contributors ✨
## Contributors
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
@@ -352,6 +363,17 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<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>

View File

@@ -13,7 +13,7 @@
<licenseUrl>https://github.com/epi052/feroxbuster/blob/main/LICENSE</licenseUrl>
<requireLicenseAcceptance>true</requireLicenseAcceptance>
<projectSourceUrl>https://github.com/epi052/feroxbuster</projectSourceUrl>
<docsUrl>https://epi052.github.io/feroxbuster-docs/docs/</docsUrl>
<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>
@@ -43,19 +43,19 @@ Enumeration.
## Quick Start
This section will cover the minimum amount of information to get up and running with feroxbuster. Please refer the the [documentation](https://epi052.github.io/feroxbuster-docs/docs/), as it's much more comprehensive.
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/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.
#### All others Docs
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
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/).
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

View File

@@ -27,8 +27,8 @@ _feroxbuster() {
'--replay-proxy=[Send only unfiltered requests through a Replay Proxy, instead of all requests]:REPLAY_PROXY:_urls' \
'*-R+[Status Codes to send through a Replay Proxy when found (default\: --status-codes value)]:REPLAY_CODE:_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.0)]:USER_AGENT:_default' \
'--user-agent=[Sets the User-Agent (default\: feroxbuster/2.13.0)]:USER_AGENT:_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' \
@@ -68,7 +68,7 @@ _feroxbuster() {
'-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' \
'(--auto-tune)--rate-limit=[Limit number of requests per second (per directory) (default\: 0, i.e. no limit)]:RATE_LIMIT:_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' \

View File

@@ -33,8 +33,8 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('--replay-proxy', '--replay-proxy', [CompletionResultType]::ParameterName, 'Send only unfiltered requests through a Replay Proxy, instead of all requests')
[CompletionResult]::new('-R', '-R ', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)')
[CompletionResult]::new('--replay-codes', '--replay-codes', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)')
[CompletionResult]::new('-a', '-a', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.13.0)')
[CompletionResult]::new('--user-agent', '--user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.13.0)')
[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)')

View File

@@ -30,8 +30,8 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand --replay-proxy 'Send only unfiltered requests through a Replay Proxy, instead of all requests'
cand -R 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)'
cand --replay-codes 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)'
cand -a 'Sets the User-Agent (default: feroxbuster/2.13.0)'
cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.13.0)'
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)'

View File

@@ -6,7 +6,7 @@ complete -c feroxbuster -l data-json -d 'Set -H \'Content-Type: application/json
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.0)' -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

View File

@@ -381,32 +381,54 @@ impl ContentType {
/// unless overridden by CLI options.
///
pub fn parse_request_file(config: &mut Configuration) -> Result<()> {
// read in the file located at config.request_file
// read in the file (raw bytes) located at config.request_file
// parse the file into a Request struct
let contents = std::fs::read_to_string(&config.request_file)?;
let contents = std::fs::read(&config.request_file)?;
if contents.is_empty() {
bail!("Empty --request-file file provided");
}
// this should split the body from the request line and headers
let lines = contents.split("\r\n\r\n").collect::<Vec<&str>>();
// find the first header/body separator
// locate both \r\n\r\n and \n\n and pick whichever appears earliest,
// so that a \r\n\r\n inside the body doesn't shadow a \n\n separator
// that terminates the headers
let crlf = contents.windows(4).position(|w| w == b"\r\n\r\n");
let lf = contents.windows(2).position(|w| w == b"\n\n");
if lines.len() < 2 {
bail!("Invalid request: Missing head/body CRLF separator");
}
let (sep_idx, sep_len) = match (crlf, lf) {
(Some(c), Some(l)) => {
if c <= l {
(c, 4)
} else {
(l, 2)
}
}
(Some(c), None) => (c, 4),
(None, Some(l)) => (l, 2),
(None, None) => bail!("Invalid request: Missing head/body separator"),
};
let head = lines[0];
let body = lines[1].as_bytes().to_vec();
// split the request head and body
let head_bytes = &contents[..sep_idx];
let body_bytes = &contents[sep_idx + sep_len..];
// we only want to use the request's body if the user hasn't
// decode only the head; HTTP framing is generally ascii/utf-8
// compatible
let head = std::str::from_utf8(head_bytes)
.map_err(|_| anyhow::anyhow!("Request headers contain invalid UTF-8"))?;
// normalize line endings in the decoded head
let normalized = head.replace("\r\n", "\n");
// we only want to use the request's body bytes if the user hasn't
// overridden it on the cli
if config.data.is_empty() {
config.data = body;
config.data = body_bytes.to_vec();
}
// begin parsing the request line and headers
let mut head_parts = head.split("\r\n");
// begin parsing the request line and normalized headers
let mut head_parts = normalized.split("\n");
let Some(request_line) = head_parts.next() else {
bail!("Invalid request: Missing request line");
@@ -441,7 +463,7 @@ pub fn parse_request_file(config: &mut Configuration) -> Result<()> {
}
for mut line in head_parts {
line = line.trim();
line = line.trim_matches('\r').trim();
if line.is_empty() {
break; // Empty line signals the end of headers
@@ -904,7 +926,7 @@ mod tests {
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"Invalid request: Missing head/body CRLF separator"
"Invalid request: Missing head/body separator"
);
tmp.cleanup();
@@ -1345,4 +1367,145 @@ mod tests {
let result = split_query("");
assert!(result.is_err());
}
#[test]
fn test_parse_raw_lf_only_request() -> io::Result<()> {
let mut tmp = TempSetup::new();
tmp.file
.write_all(b"GET / HTTP/1.1\nHost: example.com\n\nbody")?;
let result = parse_request_file(&mut tmp.config);
assert!(result.is_ok());
assert_eq!(tmp.config.data, b"body".to_vec());
tmp.cleanup();
Ok(())
}
#[test]
fn test_parse_raw_crlf_request() -> io::Result<()> {
let mut tmp = TempSetup::new();
tmp.file
.write_all(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\nbody")?;
let result = parse_request_file(&mut tmp.config);
assert!(result.is_ok());
assert_eq!(tmp.config.data, b"body".to_vec());
tmp.cleanup();
Ok(())
}
#[test]
fn test_parse_raw_crlf_body_preserved() -> io::Result<()> {
let mut tmp = TempSetup::new();
let body = b"line1\r\nline2\r\nbinary\x00data";
let mut request = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n".to_vec();
request.extend_from_slice(body);
tmp.file.write_all(&request)?;
parse_request_file(&mut tmp.config).unwrap();
assert_eq!(tmp.config.data, body.to_vec());
tmp.cleanup();
Ok(())
}
#[test]
fn test_parse_raw_lf_headers_crlf_body() -> io::Result<()> {
let mut tmp = TempSetup::new();
let body = b"line1\r\nline2\r\n";
let mut request = b"GET / HTTP/1.1\nHost: example.com\n\n".to_vec();
request.extend_from_slice(body);
tmp.file.write_all(&request)?;
parse_request_file(&mut tmp.config).unwrap();
assert_eq!(tmp.config.data, body.to_vec());
tmp.cleanup();
Ok(())
}
#[test]
fn test_parse_raw_mixed_newlines_headers() -> io::Result<()> {
let mut tmp = TempSetup::new();
tmp.file
.write_all(b"GET / HTTP/1.1\r\nHost: example.com\nUser-Agent: test\r\n\nbody")?;
let result = parse_request_file(&mut tmp.config);
assert!(result.is_ok());
assert_eq!(tmp.config.data, b"body".to_vec());
assert!(tmp.config.headers.contains_key("Host"));
assert_eq!(tmp.config.headers.get("Host").unwrap(), "example.com");
assert_eq!(tmp.config.user_agent, "test");
tmp.cleanup();
Ok(())
}
#[test]
fn test_parse_raw_binary_body_preserved() -> io::Result<()> {
let mut tmp = TempSetup::new();
let body = b"\x00\xde\xad\xbe\xef\x80binary";
let mut request = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n".to_vec();
request.extend_from_slice(body);
tmp.file.write_all(&request)?;
parse_request_file(&mut tmp.config).unwrap();
assert_eq!(tmp.config.data, body.to_vec());
tmp.cleanup();
Ok(())
}
#[test]
fn test_parse_raw_lf_headers_with_crlf_crlf_in_body() -> io::Result<()> {
// headers are LF-separated; body contains \r\n\r\n
let mut tmp = TempSetup::new();
tmp.file.write_all(
b"POST /upload HTTP/1.1\nHost: example.com\nContent-Type: application/octet-stream\n\nabc\r\n\r\ndef",
)?;
let result = parse_request_file(&mut tmp.config);
assert!(result.is_ok());
assert_eq!(tmp.config.data, b"abc\r\n\r\ndef".to_vec());
assert_eq!(tmp.config.target_url, "https://example.com/upload");
tmp.cleanup();
Ok(())
}
#[test]
fn test_parse_raw_crlf_headers_with_lf_lf_in_body() -> io::Result<()> {
let mut tmp = TempSetup::new();
tmp.file
.write_all(b"POST /upload HTTP/1.1\r\nHost: example.com\r\n\r\nabc\n\ndef")?;
parse_request_file(&mut tmp.config).unwrap();
assert_eq!(tmp.config.data, b"abc\n\ndef".to_vec());
tmp.cleanup();
Ok(())
}
}

View File

@@ -227,8 +227,10 @@ impl TermOutHandler {
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;
}
@@ -280,26 +282,31 @@ impl TermOutHandler {
}
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

View File

@@ -51,7 +51,7 @@ pub(super) const LINKFINDER_REGEX: &str = r#"(?x)
///
/// 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
///
@@ -136,11 +136,7 @@ impl<'a> ExtractorBuilder<'a> {
links_regex: Regex::new(LINKFINDER_REGEX).unwrap(),
robots_regex: Regex::new(ROBOTS_TXT_REGEX).unwrap(),
url_regex: Regex::new(URL_CHARS_REGEX).unwrap(),
response: if self.response.is_some() {
Some(self.response.unwrap())
} else {
None
},
response: self.response,
url: self.url.to_owned(),
handles: self.handles.as_ref().unwrap().clone(),
target: self.target,

View File

@@ -233,9 +233,7 @@ impl<'a> Extractor<'a> {
}
// request and report assumed file
if (resp.is_file() || !resp.is_directory())
&& !c_handles.config.force_recursion
{
if !resp.is_directory() && !c_handles.config.force_recursion {
log::debug!("Extracted File: {resp}");
c_scanned_urls.add_file_scan(
@@ -600,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)

View File

@@ -1,9 +1,10 @@
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use anyhow::{bail, Result};
use console::style;
use futures::future;
use lazy_static::lazy_static;
use scraper::{Html, Selector};
use uuid::Uuid;
@@ -18,9 +19,26 @@ use crate::{
skip_fail,
url::FeroxUrl,
utils::{ferox_print, fmt_err, logged_request},
DEFAULT_METHOD,
COMMON_FILE_EXTENSIONS, DEFAULT_BACKUP_EXTENSIONS, DEFAULT_METHOD,
};
lazy_static! {
/// Pre-built HashSet of file extensions for O(1) lookup in directory listing detection
/// Combines COMMON_FILE_EXTENSIONS and DEFAULT_BACKUP_EXTENSIONS
static ref FILE_EXTENSION_SET: HashSet<&'static str> = {
let mut set = HashSet::with_capacity(
COMMON_FILE_EXTENSIONS.len() + DEFAULT_BACKUP_EXTENSIONS.len()
);
for ext in COMMON_FILE_EXTENSIONS.iter() {
set.insert(*ext);
}
for ext in DEFAULT_BACKUP_EXTENSIONS.iter() {
set.insert(*ext);
}
set
};
}
/// enum representing the different servers that `parse_html` can detect when directory listing is
/// enabled
#[derive(Copy, Debug, Clone)]
@@ -34,6 +52,9 @@ pub enum DirListingType {
/// ASP.NET server, detected by `Directory Listing -- /`
AspDotNet,
/// custom/non-standard directory listing, detected by high-signal heuristics
Custom,
// /// IIS/Azure server, detected by `HOST_NAME - /` (not currently used)
// IIS_AZURE,
/// variant that represents the absence of directory listing
@@ -176,16 +197,14 @@ impl HeuristicTests {
let body = ferox_response.text();
let html = Html::parse_document(body);
let dirlist_type = self.detect_directory_listing(&html);
if dirlist_type.is_some() {
if let Some(dir_type) = self.detect_directory_listing(&html) {
// folks that run things and step away/rely on logs need to be notified of directory
// listing, since they won't see the message on the bar; bastardizing FeroxMessage
// for ease of implementation. This could use a bit of polish at some point.
let msg = format!(
"detected directory listing: {} ({:?})",
target_url,
dirlist_type.unwrap()
target_url, dir_type
);
let ferox_msg = FeroxMessage {
kind: "log".to_string(),
@@ -203,7 +222,7 @@ impl HeuristicTests {
log::info!("{msg}");
let result = DirListingResult {
dir_list_type: dirlist_type,
dir_list_type: Some(dir_type),
response: ferox_response,
};
@@ -221,10 +240,11 @@ impl HeuristicTests {
/// - tomcat/python: `Directory Listing for /`
/// - ASP.NET: `Directory Listing -- /`
/// - <host> - /: iis, azure, skipping due to loose heuristic
/// - custom: detected by combining multiple high-signal heuristics
fn detect_directory_listing(&self, html: &Html) -> Option<DirListingType> {
log::trace!("enter: detect_directory_listing(html body...)");
let title_selector = Selector::parse("title").expect("couldn't parse title selector");
let title_selector = Selector::parse("title").ok()?;
for t in html.select(&title_selector) {
let title = t.inner_html().to_lowercase();
@@ -246,10 +266,228 @@ impl HeuristicTests {
}
}
// If no standard title-based detection, try high-signal custom heuristics
let has_parent_link = self.has_parent_directory_link(html);
let has_table_headers = self.has_directory_table_headers(html);
let has_sorting_params = self.has_sorting_query_params(html);
let has_link_density = self.has_high_link_density(html);
let signal_count = [
has_parent_link,
has_table_headers,
has_sorting_params,
has_link_density,
]
.iter()
.filter(|&&x| x)
.count();
if signal_count >= 2 {
let mut signals = Vec::new();
if has_parent_link {
signals.push("parent-link");
}
if has_table_headers {
signals.push("table-headers");
}
if has_sorting_params {
signals.push("sorting-params");
}
if has_link_density {
signals.push("link-density");
}
log::debug!("custom directory listing signals: [{}]", signals.join(", "));
log::trace!("exit: detect_directory_listing -> Some(Custom)");
return Some(DirListingType::Custom);
}
log::trace!("exit: detect_directory_listing -> None");
None
}
/// check if the HTML contains a link to the parent directory
///
/// returns true if any anchor element has:
/// - href equals "../" or ".."
/// - visible text contains "parent directory", "to parent", or "up to parent"
fn has_parent_directory_link(&self, html: &Html) -> bool {
log::trace!("enter: has_parent_directory_link");
let Some(anchor_selector) = Selector::parse("a").ok() else {
log::warn!("failed to parse anchor selector in has_parent_directory_link");
return false;
};
for anchor in html.select(&anchor_selector) {
if let Some(href) = anchor.value().attr("href") {
let href_lower = href.trim().to_lowercase();
if href_lower == "../" || href_lower == ".." {
log::trace!("exit: has_parent_directory_link -> true (href match)");
return true;
}
}
let text = anchor.text().collect::<String>().to_lowercase();
let text_trimmed = text.trim();
if text_trimmed.contains("parent directory")
|| text_trimmed.contains("to parent")
|| text_trimmed.contains("up to parent")
{
log::trace!("exit: has_parent_directory_link -> true (text match)");
return true;
}
}
log::trace!("exit: has_parent_directory_link -> false");
false
}
/// check if the HTML contains table headers typical of directory listings
///
/// returns true if at least two of the following header categories are present:
/// - name headers: "file name", "filename", "name"
/// - size headers: "size", "file size"
/// - time headers: "date", "last modified", "modified", "last mod"
fn has_directory_table_headers(&self, html: &Html) -> bool {
log::trace!("enter: has_directory_table_headers");
let Some(th_selector) = Selector::parse("th").ok() else {
log::warn!("failed to parse th selector in has_directory_table_headers");
return false;
};
let Some(td_selector) = Selector::parse("td").ok() else {
log::warn!("failed to parse td selector in has_directory_table_headers");
return false;
};
let mut headers = Vec::new();
// try <th> elements first
for th in html.select(&th_selector) {
let text = th.text().collect::<String>().to_lowercase();
headers.push(text.trim().to_string());
}
// fallback: if no <th> elements, try first row of <td> elements
if headers.is_empty() {
if let Ok(tr_selector) = Selector::parse("tr") {
if let Some(first_row) = html.select(&tr_selector).next() {
for td in first_row.select(&td_selector) {
let text = td.text().collect::<String>().to_lowercase();
headers.push(text.trim().to_string());
}
}
}
}
let mut has_name = false;
let mut has_size = false;
let mut has_time = false;
for header in headers {
if header == "name" || header.contains("file name") || header.contains("filename") {
has_name = true;
}
if header.contains("size") || header.contains("file size") {
has_size = true;
}
if header.contains("date")
|| header.contains("last modified")
|| header.contains("modified")
|| header.contains("last mod")
{
has_time = true;
}
}
let category_count = [has_name, has_size, has_time]
.iter()
.filter(|&&x| x)
.count();
let result = category_count >= 2;
log::trace!("exit: has_directory_table_headers -> {result}");
result
}
/// check if the HTML contains sorting query parameters typical of auto-index pages
///
/// returns true if any anchor href contains sorting parameters like:
/// - ?C=N (name), ?C=S (size), ?C=M (modified), ?C=D (date)
/// - optionally combined with &O=A or &O=D (ascending/descending)
fn has_sorting_query_params(&self, html: &Html) -> bool {
log::trace!("enter: has_sorting_query_params");
let Some(anchor_selector) = Selector::parse("a").ok() else {
log::warn!("failed to parse anchor selector in has_sorting_query_params");
return false;
};
for anchor in html.select(&anchor_selector) {
if let Some(href) = anchor.value().attr("href") {
let href_lower = href.to_lowercase();
if href_lower.contains("?c=n")
|| href_lower.contains("?c=s")
|| href_lower.contains("?c=m")
|| href_lower.contains("?c=d")
{
log::trace!("exit: has_sorting_query_params -> true");
return true;
}
}
}
log::trace!("exit: has_sorting_query_params -> false");
false
}
/// check if the HTML has a high density of file/directory links
///
/// returns true if there are at least 3 links that look like files or directories:
/// - href ends with '/' (likely subdirectory)
/// - href looks like a file (common extensions)
fn has_high_link_density(&self, html: &Html) -> bool {
log::trace!("enter: has_high_link_density");
const MIN_LINKS: usize = 3;
let Some(anchor_selector) = Selector::parse("a").ok() else {
log::warn!("failed to parse anchor selector in has_high_link_density");
return false;
};
let mut count = 0;
for anchor in html.select(&anchor_selector) {
if let Some(href) = anchor.value().attr("href") {
let href_trimmed = href.trim();
// skip parent directory links and fragments
if href_trimmed == "../" || href_trimmed == ".." || href_trimmed.starts_with('#') {
continue;
}
// check if it's a directory (ends with /)
if href_trimmed.ends_with('/') {
count += 1;
continue;
}
// check if it looks like a file - extract extension and O(1) lookup
let href_lower = href_trimmed.to_lowercase();
if let Some(dot_pos) = href_lower.rfind('.') {
let extension = &href_lower[dot_pos..];
if FILE_EXTENSION_SET.contains(extension) {
count += 1;
}
}
}
}
let result = count >= MIN_LINKS;
log::trace!("exit: has_high_link_density -> {result} (count: {count})");
result
}
/// given a target's base url, attempt to automatically detect its 404 response
/// pattern(s), and then set filters that will exclude those patterns from future
/// responses
@@ -660,4 +898,210 @@ mod tests {
let dirlist_type = heuristics.detect_directory_listing(&parsed);
assert!(dirlist_type.is_none());
}
#[test]
/// `has_parent_directory_link` detects parent directory links by href
fn has_parent_directory_link_detects_by_href() {
let html = r#"<a href="../">Go up</a>"#;
let parsed = Html::parse_document(html);
let handles = Handles::for_testing(None, None);
let heuristics = HeuristicTests::new(Arc::new(handles.0));
assert!(heuristics.has_parent_directory_link(&parsed));
let html2 = r#"<a href="..">Go up</a>"#;
let parsed2 = Html::parse_document(html2);
assert!(heuristics.has_parent_directory_link(&parsed2));
}
#[test]
/// `has_parent_directory_link` detects parent directory links by text
fn has_parent_directory_link_detects_by_text() {
let html = r#"<a href="/parent">Parent Directory</a>"#;
let parsed = Html::parse_document(html);
let handles = Handles::for_testing(None, None);
let heuristics = HeuristicTests::new(Arc::new(handles.0));
assert!(heuristics.has_parent_directory_link(&parsed));
let html2 = r#"<a href="/up">To Parent</a>"#;
let parsed2 = Html::parse_document(html2);
assert!(heuristics.has_parent_directory_link(&parsed2));
}
#[test]
/// `has_parent_directory_link` returns false when no parent link
fn has_parent_directory_link_returns_false_when_absent() {
let html = r#"<a href="/about">About</a>"#;
let parsed = Html::parse_document(html);
let handles = Handles::for_testing(None, None);
let heuristics = HeuristicTests::new(Arc::new(handles.0));
assert!(!heuristics.has_parent_directory_link(&parsed));
}
#[test]
/// `has_directory_table_headers` detects table headers with name and size
fn has_directory_table_headers_detects_name_and_size() {
let html = r#"<table><thead><tr><th>File Name</th><th>Size</th></tr></thead></table>"#;
let parsed = Html::parse_document(html);
let handles = Handles::for_testing(None, None);
let heuristics = HeuristicTests::new(Arc::new(handles.0));
assert!(heuristics.has_directory_table_headers(&parsed));
}
#[test]
/// `has_directory_table_headers` detects table headers with name and date
fn has_directory_table_headers_detects_name_and_date() {
let html = r#"<table><thead><tr><th>Name</th><th>Last Modified</th></tr></thead></table>"#;
let parsed = Html::parse_document(html);
let handles = Handles::for_testing(None, None);
let heuristics = HeuristicTests::new(Arc::new(handles.0));
assert!(heuristics.has_directory_table_headers(&parsed));
}
#[test]
/// `has_directory_table_headers` returns false with only one category
fn has_directory_table_headers_requires_two_categories() {
let html = r#"<table><thead><tr><th>Name</th><th>Description</th></tr></thead></table>"#;
let parsed = Html::parse_document(html);
let handles = Handles::for_testing(None, None);
let heuristics = HeuristicTests::new(Arc::new(handles.0));
assert!(!heuristics.has_directory_table_headers(&parsed));
}
#[test]
/// `has_sorting_query_params` detects Apache-style sorting parameters
fn has_sorting_query_params_detects_apache_style() {
let html = r#"<a href="?C=N&O=A">Name</a><a href="?C=S&O=D">Size</a>"#;
let parsed = Html::parse_document(html);
let handles = Handles::for_testing(None, None);
let heuristics = HeuristicTests::new(Arc::new(handles.0));
assert!(heuristics.has_sorting_query_params(&parsed));
}
#[test]
/// `has_sorting_query_params` returns false when no sorting params
fn has_sorting_query_params_returns_false_when_absent() {
let html = r#"<a href="/page?q=search">Search</a>"#;
let parsed = Html::parse_document(html);
let handles = Handles::for_testing(None, None);
let heuristics = HeuristicTests::new(Arc::new(handles.0));
assert!(!heuristics.has_sorting_query_params(&parsed));
}
#[test]
/// `has_high_link_density` detects high density of file/directory links
fn has_high_link_density_detects_files_and_dirs() {
let html = r#"
<a href="backup/">backup/</a>
<a href="file1.html">file1.html</a>
<a href="file2.txt">file2.txt</a>
"#;
let parsed = Html::parse_document(html);
let handles = Handles::for_testing(None, None);
let heuristics = HeuristicTests::new(Arc::new(handles.0));
assert!(heuristics.has_high_link_density(&parsed));
}
#[test]
/// `has_high_link_density` requires at least 3 links
fn has_high_link_density_requires_minimum_links() {
let html = r#"
<a href="backup/">backup/</a>
<a href="file.html">file.html</a>
"#;
let parsed = Html::parse_document(html);
let handles = Handles::for_testing(None, None);
let heuristics = HeuristicTests::new(Arc::new(handles.0));
assert!(!heuristics.has_high_link_density(&parsed));
}
#[test]
/// `has_high_link_density` ignores parent directory links
fn has_high_link_density_ignores_parent_links() {
let html = r#"
<a href="../">Parent</a>
<a href="backup/">backup/</a>
<a href="file.html">file.html</a>
"#;
let parsed = Html::parse_document(html);
let handles = Handles::for_testing(None, None);
let heuristics = HeuristicTests::new(Arc::new(handles.0));
assert!(!heuristics.has_high_link_density(&parsed));
}
#[test]
/// `detect_directory_listing` detects custom listing with 2+ signals
fn detect_directory_listing_detects_custom_with_multiple_signals() {
// This HTML has parent link, table headers, sorting params, and link density
let html = r#"
<table><thead><tr>
<th><a href="?C=N&O=A">File Name</a></th>
<th><a href="?C=S&O=A">Size</a></th>
</tr></thead>
<tbody>
<tr><td><a href="../">Parent directory/</a></td></tr>
<tr><td><a href="backup/">backup/</a></td></tr>
<tr><td><a href="pass.html">pass.html</a></td></tr>
</tbody></table>
"#;
let parsed = Html::parse_document(html);
let handles = Handles::for_testing(None, None);
let heuristics = HeuristicTests::new(Arc::new(handles.0));
let dirlist_type = heuristics.detect_directory_listing(&parsed);
assert!(matches!(dirlist_type, Some(DirListingType::Custom)));
}
#[test]
/// `detect_directory_listing` requires at least 2 signals for custom detection
fn detect_directory_listing_requires_two_signals() {
// This HTML has only parent link (1 signal)
let html = r#"<a href="../">Parent directory/</a>"#;
let parsed = Html::parse_document(html);
let handles = Handles::for_testing(None, None);
let heuristics = HeuristicTests::new(Arc::new(handles.0));
let dirlist_type = heuristics.detect_directory_listing(&parsed);
assert!(dirlist_type.is_none());
}
#[test]
/// `detect_directory_listing` detects Root-Me sample page as custom
fn detect_directory_listing_detects_rootme_sample() {
// Simplified version of response.html from Root-Me
let html = r#"
<table id="list">
<thead><tr>
<th><a href="?C=N&O=A">File Name</a></th>
<th><a href="?C=S&O=A">File Size</a></th>
<th><a href="?C=M&O=A">Date</a></th>
</tr></thead>
<tbody>
<tr><td><a href="../">Parent directory/</a></td><td>-</td><td>-</td></tr>
<tr><td><a href="backup/">backup/</a></td><td>-</td><td>2021-Dec-10</td></tr>
<tr><td><a href="pass.html">pass.html</a></td><td>346 B</td><td>2021-Dec-10</td></tr>
</tbody>
</table>
"#;
let parsed = Html::parse_document(html);
let handles = Handles::for_testing(None, None);
let heuristics = HeuristicTests::new(Arc::new(handles.0));
let dirlist_type = heuristics.detect_directory_listing(&parsed);
assert!(matches!(dirlist_type, Some(DirListingType::Custom)));
}
#[test]
/// `detect_directory_listing` does not trigger on pages with many random links
fn detect_directory_listing_ignores_generic_pages() {
let html = r#"
<nav>
<a href="/about">About</a>
<a href="/contact">Contact</a>
<a href="/services">Services</a>
<a href="/products">Products</a>
</nav>
"#;
let parsed = Html::parse_document(html);
let handles = Handles::for_testing(None, None);
let heuristics = HeuristicTests::new(Arc::new(handles.0));
let dirlist_type = heuristics.detect_directory_listing(&parsed);
assert!(dirlist_type.is_none());
}
}

View File

@@ -54,15 +54,185 @@ pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const DEFAULT_OPEN_FILE_LIMIT: u64 = 8192;
/// 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.
///

View File

@@ -556,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?
};

View File

@@ -547,7 +547,6 @@ pub fn initialize() -> Command {
.long("rate-limit")
.value_name("RATE_LIMIT")
.num_args(1)
.conflicts_with("auto_tune")
.help_heading("Scan settings")
.help("Limit number of requests per second (per directory) (default: 0, i.e. no limit)")
)
@@ -744,12 +743,18 @@ pub fn initialize() -> Command {
// 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);
}
@@ -814,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

@@ -192,26 +192,6 @@ impl FeroxResponse {
self.text.shrink_to_fit(); // allocated capacity shrinks to reflect the new size
}
/// 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 = if let Some(mut path) = self.url.path_segments() {
if let Some(last) = path.next_back() {
last.contains('.') // last segment has some sort of extension, probably
} else {
false
}
} else {
false
};
self.url.query_pairs().count() > 0 || has_extension
}
/// Returns line count of the response text.
pub fn line_count(&self) -> usize {
self.line_count

View File

@@ -555,7 +555,7 @@ fn feroxstates_feroxserialize_implementation() {
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()
{

View File

@@ -192,6 +192,20 @@ 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

View File

@@ -31,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
@@ -50,11 +55,26 @@ 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 {

View File

@@ -80,17 +80,18 @@ impl Requester {
pub fn from(scanner: &FeroxScanner, ferox_scan: Arc<FeroxScan>) -> Result<Self> {
let limit = scanner.handles.config.rate_limit;
let mut policy_data = PolicyData::new(
scanner.handles.config.requester_policy,
scanner.handles.config.timeout,
);
let rate_limiter = if limit > 0 {
policy_data = policy_data.with_rate_limit(limit);
Some(Self::build_a_bucket(limit)?)
} else {
None
};
let policy_data = PolicyData::new(
scanner.handles.config.requester_policy,
scanner.handles.config.timeout,
);
Ok(Self {
ferox_scan,
policy_data,
@@ -285,7 +286,12 @@ impl Requester {
} // guard is dropped here automatically
if atomic_load!(self.policy_data.remove_limit) {
self.set_rate_limiter(None).await?;
if let Some(rate_limit) = self.policy_data.rate_limit {
self.set_rate_limiter(Some(rate_limit)).await?;
} else {
self.set_rate_limiter(None).await?;
}
atomic_store!(self.policy_data.remove_limit, false);
// reset the auto-tune state machine so it can be re-triggered if needed
@@ -350,8 +356,15 @@ impl Requester {
return Ok(());
}
// only initialize if we have a valid req/sec value
self.policy_data.set_reqs_sec(reqs_sec);
// cap the initial reqs/sec to the user-specified rate limit if it exists
// this ensures that the heap is built in such a way that clamping occurs correctly
let seed = if let Some(cap) = self.policy_data.rate_limit {
reqs_sec.min(cap)
} else {
reqs_sec
};
self.policy_data.set_reqs_sec(seed);
// set the flag to indicate that we have triggered the rate limiter
// at least once
@@ -451,7 +464,7 @@ impl Requester {
continue;
}
// auto_tune is true, or rate_limit was set (mutually exclusive to user)
// check if rate limiting should be applied (either via --rate-limit or auto-tune)
// and a rate_limiter has been created
// short-circuiting the lock access behind the first boolean check
let should_tune =
@@ -1444,4 +1457,301 @@ mod tests {
"should_enforce_policy should use per-scan requests, not global"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
/// verify heap values are clamped when rate_limit cap is set
async fn heap_values_clamped_to_rate_limit_cap() {
let policy_data = PolicyData::new(RequesterPolicy::AutoTune, 7).with_rate_limit(100);
// Set a high RPS that exceeds the cap
policy_data.set_reqs_sec(500);
// All heap values should be clamped to 100
let heap = policy_data.heap.read().unwrap();
for i in 0..heap.inner.len() {
if heap.inner[i] > 0 {
assert!(
heap.inner[i] <= 100,
"Heap value at index {} is {}, expected <= 100",
i,
heap.inner[i]
);
}
}
// Root should be 100 (clamped from 250)
assert_eq!(heap.inner[0], 100, "Root should be clamped to cap");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
/// verify auto-tune with cap adjusts down correctly on errors
async fn auto_tune_with_cap_adjusts_down_on_errors() {
let policy_data = PolicyData::new(RequesterPolicy::AutoTune, 7).with_rate_limit(100);
// Build heap with cap of 100
policy_data.set_reqs_sec(100);
// Initial limit should be 50 (half of 100)
assert_eq!(policy_data.get_limit(), 50);
// Adjust down (simulating errors)
policy_data.adjust_down();
// Should move to right child, which is 25
assert_eq!(policy_data.get_limit(), 25);
// Adjust down again
policy_data.adjust_down();
// Should continue moving down the tree
let new_limit = policy_data.get_limit();
assert!(new_limit < 25, "Limit should decrease further");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
/// verify auto-tune with cap never exceeds cap on upward adjustment
async fn auto_tune_with_cap_never_exceeds_cap_on_upward_adjustment() {
let policy_data = PolicyData::new(RequesterPolicy::AutoTune, 7).with_rate_limit(100);
// Build heap with cap of 100
policy_data.set_reqs_sec(100);
// Move to a low value in the tree
{
let mut heap = policy_data.heap.write().unwrap();
heap.move_to(15); // Deep in the tree
}
// Continuously adjust up with streak counter to reach root
for _ in 0..10 {
policy_data.adjust_up(&3); // Use high streak to move up faster
let current_limit = policy_data.get_limit();
assert!(
current_limit <= 100,
"Limit {} exceeded cap of 100",
current_limit
);
}
// Should be at or near the cap, but heap navigation may not reach exact root
let final_limit = policy_data.get_limit();
assert!(
(50..=100).contains(&final_limit),
"Final limit {} should be between 50 and 100",
final_limit
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
/// verify remove_limit with cap sets to cap instead of removing
async fn remove_limit_with_cap_sets_to_cap_instead_of_removing() {
let mut config = Configuration::new().unwrap_or_default();
config.rate_limit = 100;
config.auto_tune = true;
config.requester_policy = RequesterPolicy::AutoTune;
let (handles, _) = setup_requester_test(Some(Arc::new(config))).await;
let ferox_scan = Arc::new(FeroxScan::default());
let policy_data = PolicyData::new(RequesterPolicy::AutoTune, 7).with_rate_limit(100);
let requester = Requester {
handles: handles.clone(),
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: ferox_scan.clone(),
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(Some(Requester::build_a_bucket(50).unwrap())),
policy_data,
policy_triggered: AtomicBool::new(true),
};
// Set remove_limit flag
atomic_store!(requester.policy_data.remove_limit, true);
// Call adjust_limit
requester
.adjust_limit(PolicyTrigger::Errors, true)
.await
.unwrap();
// Verify limiter was set to cap, not removed
let limiter = requester.rate_limiter.read().await;
assert!(
limiter.is_some(),
"Limiter should not be removed when cap exists"
);
assert_eq!(
limiter.as_ref().unwrap().max(),
100,
"Limiter should be set to cap value"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
/// verify initial limiter set to cap when both rate_limit and auto_tune are present
async fn initial_limiter_set_to_cap_when_both_flags_present() {
let mut config = Configuration::new().unwrap_or_default();
config.rate_limit = 100;
config.auto_tune = true;
let (handles, _) = setup_requester_test(Some(Arc::new(config))).await;
let ferox_scan = Arc::new(FeroxScan::default());
let policy_data = PolicyData::new(RequesterPolicy::AutoTune, 7).with_rate_limit(100);
// Manually construct requester to verify initialization
let requester = Requester {
handles: handles.clone(),
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: ferox_scan.clone(),
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(Some(Requester::build_a_bucket(100).unwrap())),
policy_data,
policy_triggered: AtomicBool::new(false),
};
// Verify initial limiter is set
let limiter = requester.rate_limiter.read().await;
assert!(limiter.is_some(), "Limiter should be initialized");
assert_eq!(
limiter.as_ref().unwrap().max(),
100,
"Initial limiter should be set to rate_limit value"
);
// Verify policy_data has the cap
assert_eq!(
requester.policy_data.rate_limit,
Some(100),
"PolicyData should have rate_limit set"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
/// Full lifecycle test: --rate-limit 100 --auto-tune
/// Simulates errors triggering reduction, then success allowing increase, never exceeding cap
async fn capped_auto_tune_full_lifecycle() {
let mut config = Configuration::new().unwrap_or_default();
config.rate_limit = 100;
config.auto_tune = true;
config.requester_policy = RequesterPolicy::AutoTune;
config.threads = 50;
let (handles, _) = setup_requester_test(Some(Arc::new(config))).await;
// Create a proper Directory scan that will report as active
let ferox_scan = FeroxScan::new(
"http://localhost",
ScanType::Directory,
ScanOrder::Latest,
0,
OutputLevel::Default,
None,
true,
handles.clone(),
);
// Simulate scan running - need at least 2 req/s for tune() to initialize
ferox_scan.set_status(ScanStatus::Running).unwrap();
ferox_scan.set_start_time(Instant::now()).unwrap();
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
// Add enough requests to get RPS >= 2 (100 requests in 0.1s = 1000 req/s)
ferox_scan.progress_bar().inc(100);
let policy_data = PolicyData::new(RequesterPolicy::AutoTune, 7).with_rate_limit(100);
let requester = Requester {
handles: handles.clone(),
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: ferox_scan.clone(),
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(Some(Requester::build_a_bucket(100).unwrap())),
policy_data,
policy_triggered: AtomicBool::new(false),
};
// Step 1: Trigger auto-tune due to errors
for _ in 0..50 {
ferox_scan.add_error();
}
requester.tune(PolicyTrigger::Errors).await.unwrap();
// Heap should be initialized now (RPS is high, capped to 100)
assert!(
requester.policy_data.heap_initialized(),
"Heap should be initialized after tune()"
);
let initial_limit = requester.policy_data.get_limit();
assert!(
initial_limit <= 100,
"Initial limit {} should not exceed cap",
initial_limit
);
assert_eq!(
initial_limit, 50,
"Initial limit should be 50 (half of capped seed 100)"
);
// Step 2: More errors - adjust down
// Don't reset policy errors - they're already set to 50 from tune()
// Add more scan errors so scan_errors (75) > policy_errors (50)
for _ in 0..25 {
ferox_scan.add_error();
}
requester
.adjust_limit(PolicyTrigger::Errors, true)
.await
.unwrap();
let reduced_limit = requester.policy_data.get_limit();
assert!(
reduced_limit < initial_limit,
"Limit should decrease on errors: {} < {}",
reduced_limit,
initial_limit
);
// Step 3: Success - adjust up multiple times
// Set policy errors higher than scan errors to trigger upward adjustment
requester.policy_data.set_errors(PolicyTrigger::Errors, 200);
for i in 0..5 {
requester
.adjust_limit(PolicyTrigger::Errors, true)
.await
.unwrap();
let current_limit = requester.policy_data.get_limit();
// Should never exceed cap
assert!(
current_limit <= 100,
"Iteration {}: Limit {} exceeded cap of 100",
i,
current_limit
);
}
// Step 4: Verify limiter stays at cap (not removed)
atomic_store!(requester.policy_data.remove_limit, true);
requester
.adjust_limit(PolicyTrigger::Errors, true)
.await
.unwrap();
let final_limiter = requester.rate_limiter.read().await;
assert!(
final_limiter.is_some(),
"Limiter should not be removed when cap exists"
);
assert_eq!(
final_limiter.as_ref().unwrap().max(),
100,
"Limiter should be at cap value"
);
}
}

View File

@@ -22,7 +22,7 @@ impl PolicyTrigger {
PolicyTrigger::Status429 => 1,
PolicyTrigger::Errors => 2,
PolicyTrigger::TryAdjustUp => {
unreachable!("TryAdjustUp should never be used to access the errors array")
unreachable!("TryAdjustUp should never be used to access the errors array");
}
}
}

View File

@@ -183,7 +183,7 @@ impl DynamicSemaphore {
/// 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

View File

@@ -1,6 +1,8 @@
mod utils;
use assert_cmd::Command;
use assert_cmd::cargo_bin;
use assert_cmd::prelude::*;
use predicates::prelude::*;
use std::process::Command;
use utils::{setup_tmp_directory, teardown_tmp_directory};
#[test]
@@ -13,15 +15,13 @@ fn banner_prints_proxy() -> Result<(), Box<dyn std::error::Error>> {
];
let (tmp_dir, file) = setup_tmp_directory(&urls, "wordlist")?;
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--stdin")
.arg("--proxy")
.arg("http://127.0.0.1:8080")
.arg("--wordlist")
.arg("/definitely/doesnt/exist/0cd7fed0-47f4-4b18-a1b0-ac39708c1676")
.pipe_stdin(file)
.unwrap()
.stdin(std::fs::File::open(file)?)
.assert()
.success()
.stderr(
@@ -53,15 +53,13 @@ fn banner_prints_replay_proxy() -> Result<(), Box<dyn std::error::Error>> {
];
let (tmp_dir, file) = setup_tmp_directory(&urls, "wordlist")?;
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--stdin")
.arg("--wordlist")
.arg("/definitely/doesnt/exist/0cd7fed0-47f4-4b18-a1b0-ac39708c1676")
.arg("--replay-proxy")
.arg("http://127.0.0.1:8081")
.pipe_stdin(file)
.unwrap()
.stdin(std::fs::File::open(file)?)
.assert()
.success()
.stderr(
@@ -87,8 +85,7 @@ fn banner_prints_replay_proxy() -> Result<(), Box<dyn std::error::Error>> {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + multiple headers
fn banner_prints_headers() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--headers")
@@ -119,8 +116,7 @@ fn banner_prints_headers() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + multiple dont scan url & regex entries
fn banner_prints_denied_urls() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--dont-scan")
@@ -155,8 +151,7 @@ fn banner_prints_denied_urls() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + multiple scope url entries
fn banner_prints_scope_urls() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--scope")
@@ -188,8 +183,7 @@ fn banner_prints_scope_urls() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + multiple headers
fn banner_prints_random_agent() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--random-agent")
@@ -215,8 +209,7 @@ fn banner_prints_random_agent() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + multiple size filters
fn banner_prints_filter_sizes() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("-S")
@@ -261,8 +254,7 @@ fn banner_prints_filter_sizes() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + queries
fn banner_prints_queries() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("-Q")
@@ -293,8 +285,7 @@ fn banner_prints_queries() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + status codes
fn banner_prints_status_codes() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("-s")
@@ -321,8 +312,7 @@ fn banner_prints_status_codes() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + replay codes
fn banner_prints_replay_codes() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--replay-codes")
@@ -353,8 +343,7 @@ fn banner_prints_replay_codes() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + output file
fn banner_prints_output_file() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--output")
@@ -385,8 +374,7 @@ fn banner_prints_output_file() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + insecure
fn banner_prints_insecure() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("-k")
@@ -413,8 +401,7 @@ fn banner_prints_insecure() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + follow redirects
fn banner_prints_redirects() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("-r")
@@ -441,8 +428,7 @@ fn banner_prints_redirects() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + extensions
fn banner_prints_extensions() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("-x")
@@ -472,8 +458,7 @@ fn banner_prints_extensions() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + dont_filter
fn banner_prints_dont_filter() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--dont-filter")
@@ -500,8 +485,7 @@ fn banner_prints_dont_filter() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + verbosity=1
fn banner_prints_verbosity_one() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("-v")
@@ -528,8 +512,7 @@ fn banner_prints_verbosity_one() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + verbosity=2
fn banner_prints_verbosity_two() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("-vv")
@@ -556,8 +539,7 @@ fn banner_prints_verbosity_two() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + verbosity=3
fn banner_prints_verbosity_three() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("-vvv")
@@ -584,8 +566,7 @@ fn banner_prints_verbosity_three() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + verbosity=4
fn banner_prints_verbosity_four() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("-vvvv")
@@ -612,8 +593,7 @@ fn banner_prints_verbosity_four() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + add slash
fn banner_prints_add_slash() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("-f")
@@ -640,8 +620,7 @@ fn banner_prints_add_slash() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + INFINITE recursion
fn banner_prints_infinite_depth() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--depth")
@@ -669,8 +648,7 @@ fn banner_prints_infinite_depth() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + recursion depth
fn banner_prints_recursion_depth() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--depth")
@@ -698,8 +676,7 @@ fn banner_prints_recursion_depth() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + server certs
fn banner_prints_server_certs() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--server-certs")
@@ -729,8 +706,7 @@ fn banner_prints_server_certs() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + server certs
fn banner_prints_client_cert_and_key() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--client-cert")
@@ -762,8 +738,7 @@ fn banner_prints_client_cert_and_key() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + no recursion
fn banner_prints_no_recursion() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("-n")
@@ -790,8 +765,7 @@ fn banner_prints_no_recursion() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see nothing
fn banner_doesnt_print() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("-q")
@@ -808,8 +782,7 @@ fn banner_doesnt_print() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + extract-links
fn banner_prints_extract_links() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("-e")
@@ -836,8 +809,7 @@ fn banner_prints_extract_links() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + scan-limit
fn banner_prints_scan_limit() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("-L")
@@ -865,8 +837,7 @@ fn banner_prints_scan_limit() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + response-size-limit
fn banner_prints_response_size_limit() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--response-size-limit")
@@ -894,8 +865,7 @@ fn banner_prints_response_size_limit() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + filter-status
fn banner_prints_filter_status() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("-C")
@@ -922,8 +892,7 @@ fn banner_prints_filter_status() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + json
fn banner_prints_json() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--json")
@@ -952,8 +921,7 @@ fn banner_prints_json() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + json
fn banner_prints_debug_log() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--debug-log")
@@ -981,8 +949,7 @@ fn banner_prints_debug_log() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + regex filters
fn banner_prints_filter_regex() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--filter-regex")
@@ -1010,8 +977,7 @@ fn banner_prints_filter_regex() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + time limit
fn banner_prints_time_limit() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--time-limit")
@@ -1039,8 +1005,7 @@ fn banner_prints_time_limit() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + similarity filter
fn banner_prints_similarity_filter() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--filter-similar-to")
@@ -1068,8 +1033,7 @@ fn banner_prints_similarity_filter() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + rate limit
fn banner_prints_rate_limit() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--rate-limit")
@@ -1097,8 +1061,7 @@ fn banner_prints_rate_limit() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + auto tune
fn banner_prints_auto_tune() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--auto-tune")
@@ -1125,8 +1088,7 @@ fn banner_prints_auto_tune() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + auto bail
fn banner_prints_auto_bail() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--auto-bail")
@@ -1153,8 +1115,7 @@ fn banner_prints_auto_bail() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see no banner output
fn banner_doesnt_print_when_silent() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--silent")
@@ -1179,8 +1140,7 @@ fn banner_doesnt_print_when_silent() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see no banner output
fn banner_doesnt_print_when_quiet() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--quiet")
@@ -1205,8 +1165,7 @@ fn banner_doesnt_print_when_quiet() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see nothing as --parallel forces --silent to be true
fn banner_prints_parallel() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--stdin")
.arg("--quiet")
.arg("--parallel")
@@ -1231,8 +1190,7 @@ fn banner_prints_parallel() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + methods
fn banner_prints_methods() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("-m")
@@ -1262,8 +1220,7 @@ fn banner_prints_methods() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + data body
fn banner_prints_data() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("-m")
@@ -1295,8 +1252,7 @@ fn banner_prints_data() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + ignored extensions
fn banner_prints_collect_extensions_and_dont_collect_default() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--collect-extensions")
@@ -1324,8 +1280,7 @@ fn banner_prints_collect_extensions_and_dont_collect_default() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + collect extensions
fn banner_prints_collect_extensions_and_dont_collect_with_input() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--collect-extensions")
@@ -1356,8 +1311,7 @@ fn banner_prints_collect_extensions_and_dont_collect_with_input() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + collect backups
fn banner_prints_collect_backups() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--collect-backups")
@@ -1383,8 +1337,7 @@ fn banner_prints_collect_backups() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + collect words
fn banner_prints_collect_words() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--collect-words")
@@ -1410,8 +1363,7 @@ fn banner_prints_collect_words() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + collect words
fn banner_prints_all_composite_settings_smart() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--smart")
@@ -1440,8 +1392,7 @@ fn banner_prints_all_composite_settings_smart() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + collect words
fn banner_prints_all_composite_settings_thorough() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--thorough")
@@ -1470,8 +1421,7 @@ fn banner_prints_all_composite_settings_thorough() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + collect words
fn banner_prints_all_composite_settings_burp() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--burp")
@@ -1498,8 +1448,7 @@ fn banner_prints_all_composite_settings_burp() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + collect words
fn banner_prints_all_composite_settings_data_json_stdin() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--data-json")
@@ -1525,8 +1474,7 @@ fn banner_prints_all_composite_settings_data_json_stdin() {
#[test]
fn banner_prints_all_composite_settings_data_json_file() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("-m")
@@ -1555,8 +1503,7 @@ fn banner_prints_all_composite_settings_data_json_file() {
#[test]
fn banner_prints_all_composite_settings_data_urlencoded_stdin() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("-m")
@@ -1590,8 +1537,7 @@ fn banner_prints_all_composite_settings_data_urlencoded_stdin() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + collect words
fn banner_prints_all_composite_settings_data_urlencoded_file() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--data-urlencoded")
@@ -1622,8 +1568,7 @@ fn banner_prints_all_composite_settings_data_urlencoded_file() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + collect words
fn banner_prints_all_composite_settings_burp_replay() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--burp-replay")
@@ -1650,8 +1595,7 @@ fn banner_prints_all_composite_settings_burp_replay() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + force recursion
fn banner_prints_force_recursion() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--force-recursion")
@@ -1677,8 +1621,7 @@ fn banner_prints_force_recursion() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + scan-dir-listings
fn banner_prints_scan_dir_listings() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--scan-dir-listings")
@@ -1704,8 +1647,7 @@ fn banner_prints_scan_dir_listings() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + protocol
fn banner_prints_limit_dirs() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("localhost")
.arg("--limit-bars")
@@ -1732,8 +1674,7 @@ fn banner_prints_limit_dirs() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + force recursion
fn banner_prints_update_app() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--update")
.assert()
.success()
@@ -1744,8 +1685,7 @@ fn banner_prints_update_app() {
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + unique
fn banner_prints_unique() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://localhost")
.arg("--unique")

View File

@@ -1,4 +1,5 @@
mod utils;
use assert_cmd::cargo_bin;
use assert_cmd::prelude::*;
use httpmock::MockServer;
use predicates::prelude::*;
@@ -12,8 +13,7 @@ fn read_in_config_file_for_settings() -> Result<(), Box<dyn std::error::Error>>
let (tmp_dir, file) = setup_tmp_directory(&["threads = 37".to_string()], "ferox-config.toml")?;
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.current_dir(&tmp_dir)
.arg("--url")
.arg(srv.url("/"))

View File

@@ -1,9 +1,10 @@
mod utils;
use assert_cmd::cargo_bin;
use assert_cmd::prelude::*;
use assert_cmd::Command;
use httpmock::Method::GET;
use httpmock::MockServer;
use predicates::prelude::*;
use std::process::Command;
use utils::{setup_tmp_directory, teardown_tmp_directory};
#[test]
@@ -17,8 +18,7 @@ fn deny_list_works_during_with_a_normal_scan() {
then.status(200).body("this is a test");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -54,8 +54,7 @@ fn deny_list_works_during_extraction() {
then.status(200);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -109,8 +108,7 @@ fn deny_list_works_during_recursion() {
.body("this is a test and is more bytes than other ones");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -179,8 +177,7 @@ fn deny_list_works_during_recursion_with_inverted_parents() {
.body("this is a test and is more bytes than other ones");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/js"))
.arg("--wordlist")
@@ -222,8 +219,7 @@ fn deny_list_prevents_regex_that_denies_base_url() {
then.status(200).body("this is a test");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -256,8 +252,7 @@ fn deny_list_prevents_url_that_denies_base_url() {
then.status(200).body("this is a test");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")

View File

@@ -1,4 +1,5 @@
mod utils;
use assert_cmd::cargo_bin;
use assert_cmd::prelude::*;
use httpmock::Method::GET;
use httpmock::MockServer;
@@ -25,8 +26,7 @@ fn extractor_finds_absolute_url() -> Result<(), Box<dyn std::error::Error>> {
then.status(200);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -61,8 +61,7 @@ fn extractor_finds_absolute_url_to_different_domain() -> Result<(), Box<dyn std:
.body("\"http://localhost/homepage/assets/img/icons/handshake.svg\"");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -102,8 +101,7 @@ fn extractor_finds_relative_url() -> Result<(), Box<dyn std::error::Error>> {
then.status(200);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -151,8 +149,7 @@ fn extractor_finds_same_relative_url_twice() {
then.status(200);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -194,8 +191,7 @@ fn extractor_finds_filtered_content() -> Result<(), Box<dyn std::error::Error>>
then.status(200).body("im a little teapot");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -266,8 +262,7 @@ fn extractor_finds_robots_txt_links_and_displays_files_or_scans_directories() {
then.status(403);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -341,8 +336,7 @@ fn extractor_finds_robots_txt_links_and_displays_files_non_recursive() {
then.status(404);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -448,8 +442,7 @@ fn extractor_finds_directory_listing_links_and_displays_files() {
then.status(200).body("im a little teapot too"); // 22
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -557,8 +550,7 @@ fn extractor_finds_directory_listing_links_and_displays_files_non_recursive() {
then.status(200).body("im a little teapot too"); // 22
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -613,8 +605,7 @@ fn extractor_recurses_into_403_directories() -> Result<(), Box<dyn std::error::E
then.status(403);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -685,8 +676,7 @@ fn robots_text_extraction_doesnt_run_with_dont_extract_links() {
then.status(404);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")

View File

@@ -1,4 +1,5 @@
mod utils;
use assert_cmd::cargo_bin;
use assert_cmd::prelude::*;
use httpmock::Method::GET;
use httpmock::MockServer;
@@ -24,8 +25,7 @@ fn filters_status_code_should_filter_response() {
then.status(200).body("this is also a test of some import");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -71,8 +71,7 @@ fn filters_lines_should_filter_response() {
.body("this is also a test of some import\nwith 2 lines, no less");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -117,8 +116,7 @@ fn filters_words_should_filter_response() {
.body("this is also a test of some import\nwith 2 lines, no less");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -163,8 +161,7 @@ fn filters_size_should_filter_response() {
.body("this is also a test of some import\nwith 2 lines, no less");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -222,8 +219,7 @@ fn filters_similar_should_filter_response() {
then.status(200).body(mutated);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -266,8 +262,7 @@ fn collect_backups_should_be_filtered() {
.body("im a backup file, but filtered out because im not 200");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -316,8 +311,7 @@ fn filters_regex_should_filter_response_based_on_headers() {
.body("this is also a test");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")

View File

@@ -1,9 +1,10 @@
mod utils;
use assert_cmd::cargo_bin;
use assert_cmd::prelude::*;
use assert_cmd::Command;
use httpmock::Method::GET;
use httpmock::{MockServer, Regex};
use predicates::prelude::*;
use std::process::Command;
use utils::{setup_tmp_directory, teardown_tmp_directory};
#[test]
@@ -12,8 +13,7 @@ use utils::{setup_tmp_directory, teardown_tmp_directory};
fn test_single_target_cannot_connect() -> Result<(), Box<dyn std::error::Error>> {
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://fjdksafjkdsajfkdsajkfdsajkfsdjkdsfdsafdsafdsajkr3l2ajfdskafdsjk")
.arg("--wordlist")
@@ -37,12 +37,11 @@ fn test_two_targets_cannot_connect() -> Result<(), Box<dyn std::error::Error>> {
let urls = vec![not_real.clone(), not_real];
let (tmp_dir, file) = setup_tmp_directory(&urls, "wordlist")?;
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--stdin")
.arg("--wordlist")
.arg(file.as_os_str())
.pipe_stdin(file)
.stdin(std::fs::File::open(file)?)
.unwrap()
.assert()
.success()
@@ -70,12 +69,11 @@ fn test_one_good_and_one_bad_target_scan_succeeds() -> Result<(), Box<dyn std::e
then.status(200).body("this is a test");
});
let mut cmd = Command::cargo_bin("feroxbuster").unwrap();
cmd.arg("--stdin")
Command::new(cargo_bin!("feroxbuster"))
.arg("--stdin")
.arg("--wordlist")
.arg(file.as_os_str())
.pipe_stdin(file)
.stdin(std::fs::File::open(file)?)
.unwrap()
.assert()
.success()
@@ -96,8 +94,7 @@ fn test_one_good_and_one_bad_target_scan_succeeds() -> Result<(), Box<dyn std::e
fn test_single_target_cannot_connect_due_to_ssl_errors() -> Result<(), Box<dyn std::error::Error>> {
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("https://expired.badssl.com")
.arg("--wordlist")
@@ -132,13 +129,11 @@ fn test_two_good_targets_scan_succeeds() -> Result<(), Box<dyn std::error::Error
then.status(403).body("this also is a test");
});
let mut cmd = Command::cargo_bin("feroxbuster").unwrap();
cmd.arg("--stdin")
Command::new(cargo_bin!("feroxbuster"))
.arg("--stdin")
.arg("--wordlist")
.arg(file.as_os_str())
.pipe_stdin(file)
.unwrap()
.stdin(std::fs::File::open(file)?)
.assert()
.success()
.stdout(
@@ -168,8 +163,7 @@ fn test_static_wildcard_request_found() -> Result<(), Box<dyn std::error::Error>
then.status(200).body("this is a test");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -205,8 +199,7 @@ fn heuristics_static_wildcard_request_with_dont_filter() -> Result<(), Box<dyn s
then.status(200).body("this is a test");
});
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -289,8 +282,7 @@ fn heuristics_wildcard_test_with_two_static_wildcards_with_silent_enabled(
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -341,8 +333,7 @@ fn heuristics_wildcard_test_that_auto_filtering_403s_still_allows_for_recursion_
then.status(200);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")

View File

@@ -1,9 +1,11 @@
mod utils;
use assert_cmd::Command;
use assert_cmd::cargo_bin;
use assert_cmd::prelude::*;
use httpmock::Method::GET;
use httpmock::{MockServer, Regex};
use predicates::prelude::*;
use std::fs::{read_dir, read_to_string};
use std::process::Command;
use utils::{setup_tmp_directory, teardown_tmp_directory};
#[test]
@@ -16,8 +18,7 @@ fn main_use_root_owned_file_as_wordlist() {
then.status(200).body("this is a test");
});
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -41,8 +42,7 @@ fn main_use_empty_wordlist() -> Result<(), Box<dyn std::error::Error>> {
then.status(200).body("this is a test");
});
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -65,14 +65,12 @@ fn main_use_empty_stdin_targets() -> Result<(), Box<dyn std::error::Error>> {
// get_targets is called before scan, so the empty wordlist shouldn't trigger
// the 'Did not find any words' error
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--stdin")
.arg("--wordlist")
.arg(file.as_os_str())
.arg("-vvv")
.pipe_stdin(file)
.unwrap()
.stdin(std::fs::File::open(file)?)
.assert()
.success()
.stderr(
@@ -106,8 +104,7 @@ fn main_parallel_spawns_children() -> Result<(), Box<dyn std::error::Error>> {
let (tgt_tmp_dir, targets) =
setup_tmp_directory(&[t1.url("/"), t2.url("/"), t3.url("/")], "targets")?;
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.env("RUST_LOG", "trace")
.arg("--stdin")
.arg("--parallel")
@@ -117,8 +114,7 @@ fn main_parallel_spawns_children() -> Result<(), Box<dyn std::error::Error>> {
.arg(outfile.as_os_str())
.arg("--wordlist")
.arg(wordlist.as_os_str())
.pipe_stdin(targets)
.unwrap()
.stdin(std::fs::File::open(targets)?)
.assert()
.success()
.stderr(
@@ -170,8 +166,7 @@ fn main_parallel_creates_output_directory() -> Result<(), Box<dyn std::error::Er
let (tgt_tmp_dir, targets) =
setup_tmp_directory(&[t1.url("/"), t2.url("/"), t3.url("/")], "targets")?;
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--stdin")
.arg("--quiet")
.arg("--parallel")
@@ -180,8 +175,7 @@ fn main_parallel_creates_output_directory() -> Result<(), Box<dyn std::error::Er
.arg(outfile.as_os_str())
.arg("--wordlist")
.arg(wordlist.as_os_str())
.pipe_stdin(targets)
.unwrap()
.stdin(std::fs::File::open(targets)?)
.assert()
.success()
.stderr(
@@ -276,8 +270,7 @@ fn main_download_wordlist_from_url() -> Result<(), Box<dyn std::error::Error>> {
then.status(200);
});
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.current_dir(&tmp_dir)
.arg("--url")
.arg(srv.url("/"))

View File

@@ -1,5 +1,7 @@
use assert_cmd::Command;
use assert_cmd::cargo_bin;
use assert_cmd::prelude::*;
use predicates::prelude::*;
use std::process::Command;
#[test]
/// specify an incorrect param (-fc) with --help after it on the command line
@@ -14,8 +16,7 @@ use predicates::prelude::*;
/// the new behavior we expect to see is to print the long form help message, of which
/// Ludicrous speed... go! is near the bottom of that output, so we can test for that
fn parser_incorrect_param_with_tack_tack_help() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("-fc")
.arg("--help")
.assert()
@@ -36,8 +37,7 @@ fn parser_incorrect_param_with_tack_tack_help() {
/// the new behavior we expect to see is to print the short form help message, of which
/// "[CAUTION] 4 -v's is probably too much" is near the bottom of that output, so we can test for that
fn parser_incorrect_param_with_tack_h() {
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("-fc")
.arg("-h")
.assert()

View File

@@ -1,4 +1,5 @@
mod utils;
use assert_cmd::cargo_bin;
use assert_cmd::prelude::*;
use httpmock::Method::GET;
use httpmock::MockServer;
@@ -48,8 +49,7 @@ fn auto_bail_cancels_scan_with_403s() {
.body("these guys need to be 403 in order to trigger 90% threshold");
});
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -130,8 +130,7 @@ fn auto_bail_cancels_scan_with_429s() {
.body("these guys need to be 403 in order to trigger 90% threshold");
});
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -213,8 +212,7 @@ fn auto_tune_slows_scan_with_429s() {
let start = Instant::now();
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -277,8 +275,7 @@ fn auto_tune_slows_scan_with_403s() {
let start = Instant::now();
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -342,8 +339,7 @@ fn auto_tune_slows_scan_with_general_errors() {
let start = Instant::now();
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")

View File

@@ -1,10 +1,12 @@
mod utils;
use assert_cmd::Command;
use assert_cmd::cargo_bin;
use assert_cmd::prelude::*;
use httpmock::prelude::*;
use httpmock::MockServer;
use regex::Regex;
use std::fs::{read_to_string, write};
use std::process::Command;
use utils::{setup_tmp_directory, teardown_tmp_directory};
/// Helper to create a test wordlist with controllable patterns
@@ -63,8 +65,7 @@ fn scenario_high_403_rate() {
then.status(403).body("Forbidden");
});
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -124,8 +125,7 @@ fn scenario_high_429_rate() {
then.status(429).body("Too Many Requests");
});
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -191,8 +191,7 @@ fn scenario_recovery_pattern() {
then.status(403).body("Forbidden");
});
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -267,8 +266,7 @@ fn scenario_mixed_steady_state() {
then.status(429).body("Too Many Requests");
});
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -304,3 +302,98 @@ fn scenario_mixed_steady_state() {
// With mixed but not extreme errors, should see some adjustments
assert!(total > 100, "Should complete significant portion of scan");
}
/// Scenario 5: Capped auto-tune - --rate-limit caps --auto-tune adjustments
#[test]
fn scenario_capped_auto_tune() {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&[], "wordlist").unwrap();
let (log_dir, logfile) = setup_tmp_directory(&[], "debug-log").unwrap();
// Pattern: errors first to trigger rate limiting, then normal responses to allow upward adjustment
// The rate limit cap should prevent exceeding the specified limit
let mut wordlist = Vec::new();
// Start with many errors to trigger auto-tune
for i in 0..200 {
wordlist.push(format!("s403_{:04}", i));
}
// Then many normal responses to allow upward adjustment
for i in 0..400 {
wordlist.push(format!("normal_{:04}", i));
}
write(&file, wordlist.join("\n")).unwrap();
let _normal_mock = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/normal_.*").unwrap());
then.status(200).body("OK");
});
let _error_mock = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/s403_.*").unwrap());
then.status(403).body("Forbidden");
});
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--auto-tune")
.arg("--rate-limit")
.arg("50") // Cap at 50 req/s
.arg("--dont-filter")
.arg("--threads")
.arg("10")
.arg("--debug-log")
.arg(logfile.as_os_str())
.arg("--json")
.arg("-vv")
.assert()
.success();
let debug_log = read_to_string(&logfile).unwrap();
let mut auto_tune_triggered = false;
let mut max_rate_seen = 0;
for line in debug_log.lines() {
if let Ok(log) = serde_json::from_str::<serde_json::Value>(line) {
if let Some(msg) = log.get("message").and_then(|m| m.as_str()) {
// Check for auto-tune activation
if msg.contains("auto-tune:") && msg.contains("enforcing limit") {
auto_tune_triggered = true;
}
// Extract rate values from messages like "set rate limit (25/s)" or "scan speed (30/s)"
if msg.contains("/s)") {
if let Some(start) = msg.rfind('(') {
if let Some(end) = msg.rfind("/s)") {
if let Ok(rate) = msg[start + 1..end].parse::<usize>() {
max_rate_seen = max_rate_seen.max(rate);
}
}
}
}
}
}
}
teardown_tmp_directory(tmp_dir);
teardown_tmp_directory(log_dir);
assert!(
auto_tune_triggered,
"Auto-tune should be triggered by errors"
);
assert!(
max_rate_seen <= 50,
"Auto-tune should never exceed rate-limit cap of 50, but saw {}",
max_rate_seen
);
}

View File

@@ -1,4 +1,5 @@
mod utils;
use assert_cmd::cargo_bin;
use assert_cmd::prelude::*;
use httpmock::Method::GET;
use httpmock::MockServer;
@@ -19,8 +20,7 @@ fn response_size_limit_small_response_not_truncated() {
then.status(200).body(small_body);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -55,8 +55,7 @@ fn response_size_limit_large_response_truncated() {
then.status(200).body(&large_body);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -109,8 +108,7 @@ fn response_size_limit_mixed_response_sizes() {
then.status(200).body(&large_body);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -173,8 +171,7 @@ fn response_size_limit_default_4mb() {
then.status(200).body(&body);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -206,8 +203,7 @@ fn response_size_limit_very_small_limit() {
then.status(200).body(body);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -242,8 +238,7 @@ fn response_size_limit_with_redirects() {
.body(&large_redirect_body);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -281,8 +276,7 @@ fn response_size_limit_with_error_responses() {
then.status(500).body(&large_error_body);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -318,8 +312,7 @@ fn response_size_limit_json_output_includes_truncated_field() {
then.status(200).body(&large_body);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -350,8 +343,7 @@ fn response_size_limit_json_output_includes_truncated_field() {
fn response_size_limit_shows_in_banner() {
let (tmp_dir, file) = setup_tmp_directory(&["test".to_string()], "wordlist").unwrap();
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg("http://127.0.0.1:1") // Non-existent server to trigger quick exit
.arg("--wordlist")
@@ -385,8 +377,7 @@ fn response_size_limit_exact_limit() {
then.status(200).body(&exact_body);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -425,8 +416,7 @@ fn response_size_limit_from_config_file() {
then.status(200).body(&large_body);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.current_dir(tmp_dir.path())
.arg("--url")
.arg(srv.url("/"))

View File

@@ -1,10 +1,12 @@
mod utils;
use assert_cmd::Command;
use assert_cmd::cargo_bin;
use assert_cmd::prelude::*;
use httpmock::Method::GET;
use httpmock::MockServer;
use predicates::prelude::*;
use std::fs::{read_to_string, write};
use std::path::Path;
use std::process::Command;
use std::time;
use utils::{setup_tmp_directory, teardown_tmp_directory};
@@ -67,8 +69,7 @@ fn resume_scan_works() {
let (tmp_dir2, state_file) = setup_tmp_directory(&[state_file_contents], "state-file").unwrap();
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("-vvv")
.arg("--resume-from")
.arg(state_file.as_os_str())
@@ -115,16 +116,14 @@ fn time_limit_enforced_when_specified() {
let lower_bound = time::Duration::new(5, 0);
let upper_bound = time::Duration::new(6, 0);
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--stdin")
.arg("-vv")
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--time-limit")
.arg("5s")
.pipe_stdin(targets)
.unwrap()
.stdin(std::fs::File::open(targets).unwrap())
.assert()
.failure();

View File

@@ -1,4 +1,5 @@
mod utils;
use assert_cmd::cargo_bin;
use assert_cmd::prelude::*;
use httpmock::Method::GET;
use httpmock::MockServer;
@@ -19,8 +20,7 @@ fn scanner_single_request_scan() -> Result<(), Box<dyn std::error::Error>> {
then.status(200).body("this is a test");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -72,8 +72,7 @@ fn scanner_recursive_request_scan() -> Result<(), Box<dyn std::error::Error>> {
.body("this is a test and is more bytes than other ones");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -135,8 +134,7 @@ fn scanner_recursive_request_scan_using_only_success_responses(
.body("this is a test and is more bytes than other ones");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -178,8 +176,7 @@ fn scanner_single_request_scan_with_file_output() -> Result<(), Box<dyn std::err
let outfile = tmp_dir.path().join("output");
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -214,8 +211,7 @@ fn scanner_single_request_scan_with_file_output_and_tack_q(
let outfile = tmp_dir.path().join("output");
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -250,8 +246,7 @@ fn scanner_single_request_scan_with_invalid_file_output() -> Result<(), Box<dyn
let outfile = tmp_dir.path(); // outfile is a directory
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -281,8 +276,7 @@ fn scanner_single_request_quiet_scan() -> Result<(), Box<dyn std::error::Error>>
then.status(200).body("this is a test");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -317,8 +311,7 @@ fn scanner_single_request_returns_301_without_location_header(
then.status(301).body("this is a test");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -358,8 +351,7 @@ fn scanner_single_request_replayed_to_proxy() -> Result<(), Box<dyn std::error::
then.status(200).body("this is a test");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -402,8 +394,7 @@ fn scanner_single_request_scan_with_filtered_result() -> Result<(), Box<dyn std:
then.status(200).body("this is a test");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -443,8 +434,7 @@ fn scanner_single_request_scan_with_debug_logging() {
let outfile = tmp_dir.path().join("debug.log");
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -481,8 +471,7 @@ fn scanner_single_request_scan_with_debug_logging_as_json() {
let outfile = tmp_dir.path().join("debug.log");
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -530,8 +519,7 @@ fn scanner_single_request_scan_with_regex_filtered_result() {
.body("this is a test\nThat rug really tied the room together");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -578,8 +566,7 @@ fn scanner_recursion_works_with_403_directories() {
.body("this is a test\nThat rug really tied the room together");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -628,8 +615,7 @@ fn rate_limit_enforced_when_specified() {
let now = time::Instant::now();
let lower_bound = time::Duration::new(5, 0);
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -665,8 +651,7 @@ fn add_discovered_extension_updates_bars_and_stats() {
assert!(!file_path.exists());
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -735,8 +720,7 @@ fn collect_backups_makes_appropriate_requests() {
})
.collect();
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--collect-backups")
@@ -831,8 +815,7 @@ fn collect_words_makes_appropriate_requests() {
})
.collect();
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("-vv")
@@ -890,8 +873,7 @@ fn scanner_forced_recursion_ignores_normal_redirect_logic() -> Result<(), Box<dy
let outfile = tmp_dir.path().join("output");
Command::cargo_bin("feroxbuster")
.unwrap()
Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")

View File

@@ -1,4 +1,5 @@
mod utils;
use assert_cmd::cargo_bin;
use assert_cmd::prelude::*;
use httpmock::Method::GET;
use httpmock::MockServer;
@@ -26,8 +27,7 @@ fn word_and_status_makes_a_response_unique_and_isnt_seen() -> Result<(), Box<dyn
.body(srv.url("this is a word count supplier"));
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
@@ -69,8 +69,7 @@ fn bytes_and_status_makes_a_redirect_response_unique_and_isnt_seen(
.body(srv.url("this is a word count supplier")); // redirect + same body
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
let cmd = Command::new(cargo_bin!("feroxbuster"))
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")