Compare commits

..

115 Commits

Author SHA1 Message Date
epi
e0f9a30ba9 allow CD workflow for testing 2023-03-09 06:06:42 -06:00
epi
867da048b4 allow CD workflow for testing 2023-03-09 06:06:03 -06:00
epi
31e66c1fa0 allow CD workflow for testing 2023-03-09 06:04:13 -06:00
Aan
703da383a7 Fix for fmt, clippy and nextest 2023-03-09 11:28:11 +07:00
Aan
aa83e40c4f Update README.md 2023-03-09 10:55:09 +07:00
Aan
a77c436e04 New feature checklist 2023-03-09 10:49:25 +07:00
Aan
c3455d123e Implement auto update feature 2023-03-09 10:06:17 +07:00
epi
2d381e7e05 added logo for chocolatey packaging 2023-03-08 06:20:37 -06:00
epi
7d26f368f5 Merge pull request #808 from epi052/fix-wildcard-directory-redirect-v2
Fix wildcard directory redirect v2
2023-03-08 06:14:27 -06:00
epi
36970896ca Merge pull request #810 from epi052/all-contributors/add-aancw
docs: add aancw as a contributor for code, and infra
2023-03-08 05:54:56 -06:00
epi
39a75f0608 Merge pull request #804 from aancw/scanmanager-banner
Showing banner again after finish scan management menu
2023-03-08 05:54:26 -06:00
allcontributors[bot]
ab8537beeb docs: update .all-contributorsrc [skip ci] 2023-03-08 11:54:12 +00:00
allcontributors[bot]
9e907d37d5 docs: update README.md [skip ci] 2023-03-08 11:54:11 +00:00
epi
19e0a7f48b Merge branch 'main' into fix-wildcard-directory-redirect-v2 2023-03-08 05:50:29 -06:00
epi
5e93da0a65 fixed #809; thorough/smart bypassed mutual exclusion 2023-03-08 05:29:30 -06:00
Aan
2704e33178 Update the code as requested in suggestion 2023-03-08 14:47:39 +07:00
epi
8392f6d26b fixed menu filter display; fixed wildcard filter comparison 2023-03-07 21:14:20 -06:00
epi
ca43a767d2 fixed failing test 2023-03-07 20:15:10 -06:00
epi
291ccedba3 clippy 2023-03-07 18:54:32 -06:00
epi
6d01bc8ec4 added a few tests taht were removed previously 2023-03-07 18:38:10 -06:00
epi
94aafccf8a bumped version 2023-03-07 06:30:53 -06:00
epi
8dd8871ae5 old tests pass 2023-03-07 06:27:24 -06:00
epi
ad0df8ccd3 updated deps 2023-03-07 06:00:00 -06:00
epi
31cdba64e4 fmt 2023-03-06 20:44:24 -06:00
epi
584fc940cd implemented fix for wildcard directories 2023-03-06 20:44:14 -06:00
Aan
43116f9aab Showing banner again after finish scan management menu 2023-03-06 19:49:50 +07:00
Aan
aec083ea58 Showing banner again after finish scan management menu 2023-03-06 19:47:33 +07:00
epi
52d08e504d Merge pull request #801 from epi052/all-contributors/add-Luoooio
docs: add Luoooio as a contributor for ideas
2023-02-28 15:55:12 -06:00
allcontributors[bot]
a254574ce7 docs: update .all-contributorsrc [skip ci] 2023-02-28 21:55:03 +00:00
allcontributors[bot]
6cb7c8e342 docs: update README.md [skip ci] 2023-02-28 21:55:02 +00:00
epi
98670f367f Merge pull request #800 from epi052/all-contributors/add-xaeroborg
docs: add xaeroborg as a contributor for ideas
2023-02-28 15:53:42 -06:00
allcontributors[bot]
68913c9950 docs: update .all-contributorsrc [skip ci] 2023-02-28 21:53:34 +00:00
allcontributors[bot]
5901c75187 docs: update README.md [skip ci] 2023-02-28 21:53:33 +00:00
epi
8499901bfe Merge pull request #799 from epi052/all-contributors/add-pich4ya
docs: add pich4ya as a contributor for ideas
2023-02-28 15:52:00 -06:00
allcontributors[bot]
69dcb38360 docs: update .all-contributorsrc [skip ci] 2023-02-28 21:51:46 +00:00
allcontributors[bot]
eb8b70668d docs: update README.md [skip ci] 2023-02-28 21:51:45 +00:00
epi
f0702794b0 Merge pull request #796 from epi052/751-resume-scan-with-offset
resume scan starts from offset in wordlist
2023-02-28 06:32:17 -06:00
epi
367dcdbd72 fixed hanging test 2023-02-28 06:06:53 -06:00
epi
4b7a25c13b fixed ordering of dir bars / overall bar; fixed overall offset when resuming 2023-02-27 19:25:17 -06:00
epi
339189ff13 resume scan starts from offset in wordlist 2023-02-27 07:26:59 -06:00
epi
ed701c13b0 Merge pull request #794 from epi052/784-content-based-auto-filtering
Content-based auto filtering
2023-02-26 19:50:41 -06:00
epi
e034734df4 Merge branch 'main' into 784-content-based-auto-filtering 2023-02-26 13:21:55 -06:00
epi
73e2558404 fmt 2023-02-26 13:05:37 -06:00
epi
eb7ad68c01 all tests passing 2023-02-26 12:48:02 -06:00
epi
c61688f984 fmt 2023-02-26 07:31:03 -06:00
epi
6c96589ca5 fixed some tests 2023-02-26 07:30:41 -06:00
epi
0437c2baac updated deps harder 2023-02-26 07:30:28 -06:00
epi
0d689942eb updated deps 2023-02-26 06:54:23 -06:00
epi
74a1a8d597 bumped version to 2.8.0 2023-02-26 06:50:31 -06:00
epi
729d88a724 clippy 2023-02-26 06:49:48 -06:00
epi
ad38b56473 finalized new detections; removed wildcard filter and supporting code 2023-02-26 06:39:03 -06:00
epi
655364d9bd removed wildcard test, integrated into 404 detection 2023-02-25 20:58:28 -06:00
epi
ac7f59cd3f updated default status codes to all; adjusted banner entry 2023-02-25 07:28:27 -06:00
epi
0d64d28fe6 removed cruft 2023-02-25 06:28:14 -06:00
epi
89c29600c7 removed cruft 2023-02-25 06:23:39 -06:00
epi
96375e7734 added minhash algo when resp too short for ssdeep 2023-02-25 06:20:30 -06:00
epi
3531b8c74b added gaoya dependency for minhash algo 2023-02-25 06:20:08 -06:00
epi
e8f4438a52 fixed bug in dynamic wildcards; reorded 404-like id strat 2023-02-24 20:09:29 -06:00
epi
02b25dc553 incremental save for testing 2023-02-23 17:21:48 -06:00
epi
551cf065f3 Merge pull request #793 from epi052/all-contributors/add-f3rn0s
docs: add f3rn0s as a contributor for bug
2023-02-16 20:30:41 -06:00
allcontributors[bot]
c81885cf5e docs: update .all-contributorsrc [skip ci] 2023-02-17 02:30:28 +00:00
allcontributors[bot]
6a3d250e3b docs: update README.md [skip ci] 2023-02-17 02:30:27 +00:00
epi
259fbcca74 Merge pull request #790 from epi052/all-contributors/add-joaociocca
docs: add joaociocca as a contributor for bug, and ideas
2023-02-15 20:50:31 -06:00
allcontributors[bot]
f3c9f8ed20 docs: update .all-contributorsrc [skip ci] 2023-02-16 02:49:52 +00:00
allcontributors[bot]
be400ce971 docs: update README.md [skip ci] 2023-02-16 02:49:51 +00:00
epi
b62c76bce3 updated deps 2023-02-15 20:44:07 -06:00
epi
990a471d71 Merge pull request #779 from epi052/fix-some-visuals
Fix some visuals; update deps
2023-02-15 20:39:58 -06:00
epi
ec47d6f934 updated deps 2023-02-15 20:31:54 -06:00
epi
da509bd208 clippy 2023-02-15 19:39:27 -06:00
epi
8568b340a9 clippy 2023-02-15 19:38:23 -06:00
epi
7c9d8f529d tweaked auto-tune behavior to more aggressively move upward 2023-02-15 19:36:16 -06:00
epi
6d47b4b68b fixed a case where the --dont-filter message wasnt shown 2023-02-15 07:04:48 -06:00
epi
be3290572e fallback to body.len when content-length header missing 2023-02-15 06:40:35 -06:00
epi
4f13fd7974 Merge branch 'fix-some-visuals' of github.com:epi052/feroxbuster into fix-some-visuals 2023-02-07 05:33:50 -06:00
epi
57b8117015 fixed stale file reference 2023-02-05 20:33:53 -06:00
epi
7b4900fa07 caught a comparison bug 2023-02-01 18:51:29 -06:00
epi
d1e47b0025 added messaging about state of auto-tune/bail 2023-01-30 16:46:29 -06:00
epi
98612e2256 changed auto-tune emoji to align across different terminals 2023-01-30 16:46:03 -06:00
epi
f08023b813 Update README.md 2023-01-13 05:53:18 -06:00
epi
98254e3cac Update README.md 2023-01-13 05:52:55 -06:00
epi
46cc64325f Update README.md 2023-01-13 05:51:02 -06:00
epi
fc034f0720 Update README.md 2023-01-13 05:49:51 -06:00
epi
ef4a597cb1 Update README.md 2023-01-13 05:48:56 -06:00
epi
bb6b12d168 Merge branch 'main' of github.com:epi052/feroxbuster 2023-01-13 05:32:53 -06:00
epi
21a9de2d39 fixed code coverage workflow 2023-01-13 05:32:48 -06:00
epi
7d8f3b0305 Merge pull request #764 from epi052/all-contributors/add-aidanhall34
docs: add aidanhall34 as a contributor for code, and infra
2023-01-13 05:13:39 -06:00
allcontributors[bot]
6b3fe48b4f docs: update .all-contributorsrc [skip ci] 2023-01-13 11:12:49 +00:00
allcontributors[bot]
7a79000d96 docs: update README.md [skip ci] 2023-01-13 11:12:48 +00:00
epi
d164034d3e Merge pull request #762 from aidanhall34/NA-dockerfile
Fixes #761 | Updated Dockerfile and CONTRIBUTING docs
2023-01-13 05:12:25 -06:00
aidan.hall34
e4dc7da756 Remove edge branch, update alpine 2023-01-12 23:34:03 +00:00
aidan.hall34
6090cefa4f Updated Dockerfile and CONTRIBUTING docs 2023-01-12 14:16:58 +00:00
epi
ec05644854 Merge pull request #753 from epi052/all-contributors/add-duokebei
docs: add duokebei as a contributor for ideas
2022-12-29 20:31:42 -06:00
allcontributors[bot]
567f927884 docs: update .all-contributorsrc [skip ci] 2022-12-30 02:31:17 +00:00
allcontributors[bot]
176a6a6426 docs: update README.md [skip ci] 2022-12-30 02:31:16 +00:00
epi
c99f6146e3 Update README.md 2022-12-29 20:29:56 -06:00
epi
fb34817509 Merge pull request #752 from epi052/all-contributors/add-hakdogpinas
docs: add hakdogpinas as a contributor for ideas
2022-12-29 20:29:26 -06:00
epi
0c8e5d51f0 Update .all-contributorsrc 2022-12-29 20:27:05 -06:00
allcontributors[bot]
ac24e507ac docs: update .all-contributorsrc [skip ci] 2022-12-30 02:24:54 +00:00
allcontributors[bot]
808c749f63 docs: update README.md [skip ci] 2022-12-30 02:24:53 +00:00
epi
b1f5ed507b Merge pull request #750 from epi052/742-748-state-file-bug-fixes
multiple bug fixes / small improvements
2022-12-29 19:57:23 -06:00
epi
79edc42b17 bumped version to 2.7.3 2022-12-29 15:56:56 -06:00
epi
1b223b0867 fixed #716; wordlist entries with leading slash are trimmed 2022-12-29 15:55:50 -06:00
epi
0c6d5193a9 fixed #743; redirects always show full url as Location 2022-12-29 15:43:57 -06:00
epi
c637355796 clippy 2022-12-29 15:32:22 -06:00
epi
a114cc8f85 fixed #748; cancelled scans persist across ctrl+c 2022-12-29 15:28:58 -06:00
epi
c8503faf02 updated dependencies 2022-12-29 07:03:03 -06:00
epi
cbbd642510 Merge pull request #749 from n0kovo/main
Fix incorrect username in Contributors
2022-12-29 06:59:34 -06:00
n0kovo
2c8e9bace9 Fix incorrect username in Contributors 2022-12-29 13:37:41 +01:00
epi
f4fe8c0544 Merge pull request #734 from epi052/all-contributors/add-kmanc
docs: add kmanc as a contributor for bug, and code
2022-12-14 06:09:50 -06:00
allcontributors[bot]
73109483fe docs: update .all-contributorsrc [skip ci] 2022-12-14 12:09:23 +00:00
allcontributors[bot]
aee33012b1 docs: update README.md [skip ci] 2022-12-14 12:09:22 +00:00
epi
eab95e0435 Merge pull request #733 from kmanc/bugfix-no-state-with-time-limit
FIX 732 ensure --no-state is respected even through --time-limit
2022-12-14 06:08:50 -06:00
koins
acb2f42f69 ensure --no-state is respected even through --time-limit 2022-12-13 22:37:27 -08:00
epi
ac20b213ec Merge branch 'main' of github.com:epi052/feroxbuster 2022-11-16 16:53:02 -06:00
epi
201873d7ac bumped version to 2.7.2 2022-11-16 16:52:35 -06:00
66 changed files with 2171 additions and 1149 deletions

View File

@@ -294,10 +294,10 @@
] ]
}, },
{ {
"login": "narkopolo", "login": "n0kovo",
"name": "narkopolo", "name": "n0kovo",
"avatar_url": "https://avatars.githubusercontent.com/u/16690056?v=4", "avatar_url": "https://avatars.githubusercontent.com/u/16690056?v=4",
"profile": "https://github.com/narkopolo", "profile": "https://github.com/n0kovo",
"contributions": [ "contributions": [
"ideas" "ideas"
] ]
@@ -458,6 +458,100 @@
"contributions": [ "contributions": [
"code" "code"
] ]
},
{
"login": "kmanc",
"name": "kmanc",
"avatar_url": "https://avatars.githubusercontent.com/u/14863147?v=4",
"profile": "https://github.com/kmanc",
"contributions": [
"bug",
"code"
]
},
{
"login": "hakdogpinas",
"name": "hakdogpinas",
"avatar_url": "https://avatars.githubusercontent.com/u/71529469?v=4",
"profile": "https://github.com/hakdogpinas",
"contributions": [
"ideas"
]
},
{
"login": "duokebei",
"name": "多可悲",
"avatar_url": "https://avatars.githubusercontent.com/u/75022552?v=4",
"profile": "https://github.com/duokebei",
"contributions": [
"ideas"
]
},
{
"login": "aidanhall34",
"name": "Aidan Hall",
"avatar_url": "https://avatars.githubusercontent.com/u/58670593?v=4",
"profile": "https://blog.ah34.net/",
"contributions": [
"code",
"infra"
]
},
{
"login": "joaociocca",
"name": "João Ciocca",
"avatar_url": "https://avatars.githubusercontent.com/u/6473725?v=4",
"profile": "https://hachyderm.io/@JohnnyCiocca",
"contributions": [
"bug",
"ideas"
]
},
{
"login": "f3rn0s",
"name": "f3rn0s",
"avatar_url": "https://avatars.githubusercontent.com/u/1351279?v=4",
"profile": "https://github.com/f3rn0s",
"contributions": [
"bug"
]
},
{
"login": "pich4ya",
"name": "LongCat",
"avatar_url": "https://avatars.githubusercontent.com/u/2099767?v=4",
"profile": "https://sth.sh",
"contributions": [
"ideas"
]
},
{
"login": "xaeroborg",
"name": "xaeroborg",
"avatar_url": "https://avatars.githubusercontent.com/u/33274680?v=4",
"profile": "https://github.com/xaeroborg",
"contributions": [
"ideas"
]
},
{
"login": "Luoooio",
"name": "Luoooio",
"avatar_url": "https://avatars.githubusercontent.com/u/26653157?v=4",
"profile": "https://github.com/Luoooio",
"contributions": [
"ideas"
]
},
{
"login": "aancw",
"name": "Aan",
"avatar_url": "https://avatars.githubusercontent.com/u/6284204?v=4",
"profile": "https://petruknisme.com",
"contributions": [
"code",
"infra"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,

View File

@@ -94,7 +94,7 @@ jobs:
env: env:
IN_PIPELINE: true IN_PIPELINE: true
runs-on: macos-latest runs-on: macos-latest
if: github.ref == 'refs/heads/main' # if: github.ref == 'refs/heads/main'
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
@@ -126,7 +126,7 @@ jobs:
env: env:
IN_PIPELINE: true IN_PIPELINE: true
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
if: github.ref == 'refs/heads/main' # if: github.ref == 'refs/heads/main'
strategy: strategy:
matrix: matrix:
type: [windows-x64, windows-x86] type: [windows-x64, windows-x86]

View File

@@ -7,18 +7,18 @@ jobs:
name: LLVM Coverage name: LLVM Coverage
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Install llvm-tools-preview - uses: dtolnay/rust-toolchain@stable
run: rustup toolchain install stable --component llvm-tools-preview with:
- name: Install cargo-llvm-cov components: llvm-tools-preview
uses: taiki-e/install-action@cargo-llvm-cov - name: Install cargo-llvm-cov and cargo-nextest
- name: Install cargo-nextest uses: taiki-e/install-action@v2
uses: taiki-e/install-action@nextest with:
- name: Generate code coverage tool: cargo-nextest,cargo-llvm-cov
run: cargo llvm-cov nextest --all-features --no-fail-fast --lcov --output-path lcov.info -- --retries 10 - name: Generate code coverage
- name: Upload coverage to Codecov run: cargo llvm-cov nextest --all-features --no-fail-fast --lcov --output-path lcov.info
uses: codecov/codecov-action@v1 - name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with: with:
token: ${{ secrets.CODECOV_TOKEN }}
files: lcov.info files: lcov.info
fail_ci_if_error: true fail_ci_if_error: true

View File

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

1175
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -23,3 +23,10 @@ clear = true
script = """ script = """
cargo clippy --all-targets --all-features -- -D warnings cargo clippy --all-targets --all-features -- -D warnings
""" """
# tests
[tasks.test]
clear = true
script = """
cargo nextest run --all-features --all-targets --retries 10
"""

120
README.md
View File

@@ -8,7 +8,7 @@
<p align="center"> <p align="center">
<a href="https://github.com/epi052/feroxbuster/actions?query=workflow%3A%22CI+Pipeline%22"> <a href="https://github.com/epi052/feroxbuster/actions?query=workflow%3A%22CI+Pipeline%22">
<img src="https://img.shields.io/github/workflow/status/epi052/feroxbuster/CI%20Pipeline/main?logo=github"> <img src="https://img.shields.io/github/actions/workflow/status/epi052/feroxbuster/.github/workflows/check.yml?branch=main&logo=github">
</a> </a>
<a href="https://github.com/epi052/feroxbuster/releases"> <a href="https://github.com/epi052/feroxbuster/releases">
@@ -167,6 +167,12 @@ cat targets | ./feroxbuster --stdin --silent -s 200 301 302 --redirects -x js |
./feroxbuster -u http://127.1 --query token=0123456789ABCDEF ./feroxbuster -u http://127.1 --query token=0123456789ABCDEF
``` ```
### Updating feroxbuster (new in v2.10.0)
```
./feroxbuster --update
```
## 🚀 Documentation has **moved** 🚀 ## 🚀 Documentation has **moved** 🚀
For realsies, there used to be over 1300 lines in this README, but it's all been moved to the [new documentation site](https://epi052.github.io/feroxbuster-docs/docs/). Go check it out! 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!
@@ -183,67 +189,81 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<table> <table>
<tbody> <tbody>
<tr> <tr>
<td align="center"><a href="https://io.fi"><img src="https://avatars.githubusercontent.com/u/5235109?v=4?s=100" width="100px;" alt="Joona Hoikkala"/><br /><sub><b>Joona Hoikkala</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=joohoi" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://io.fi"><img src="https://avatars.githubusercontent.com/u/5235109?v=4?s=100" width="100px;" alt="Joona Hoikkala"/><br /><sub><b>Joona Hoikkala</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=joohoi" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/jsav0"><img src="https://avatars.githubusercontent.com/u/20546041?v=4?s=100" width="100px;" alt="J Savage"/><br /><sub><b>J Savage</b></sub></a><br /><a href="#infra-jsav0" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=jsav0" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/jsav0"><img src="https://avatars.githubusercontent.com/u/20546041?v=4?s=100" width="100px;" alt="J Savage"/><br /><sub><b>J Savage</b></sub></a><br /><a href="#infra-jsav0" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=jsav0" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.tgotwig.dev"><img src="https://avatars.githubusercontent.com/u/30773779?v=4?s=100" width="100px;" alt="Thomas Gotwig"/><br /><sub><b>Thomas Gotwig</b></sub></a><br /><a href="#infra-TGotwig" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=TGotwig" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="http://www.tgotwig.dev"><img src="https://avatars.githubusercontent.com/u/30773779?v=4?s=100" width="100px;" alt="Thomas Gotwig"/><br /><sub><b>Thomas Gotwig</b></sub></a><br /><a href="#infra-TGotwig" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=TGotwig" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/spikecodes"><img src="https://avatars.githubusercontent.com/u/19519553?v=4?s=100" width="100px;" alt="Spike"/><br /><sub><b>Spike</b></sub></a><br /><a href="#infra-spikecodes" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=spikecodes" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/spikecodes"><img src="https://avatars.githubusercontent.com/u/19519553?v=4?s=100" width="100px;" alt="Spike"/><br /><sub><b>Spike</b></sub></a><br /><a href="#infra-spikecodes" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=spikecodes" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/evanrichter"><img src="https://avatars.githubusercontent.com/u/330292?v=4?s=100" width="100px;" alt="Evan Richter"/><br /><sub><b>Evan Richter</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=evanrichter" title="Code">💻</a> <a href="https://github.com/epi052/feroxbuster/commits?author=evanrichter" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/evanrichter"><img src="https://avatars.githubusercontent.com/u/330292?v=4?s=100" width="100px;" alt="Evan Richter"/><br /><sub><b>Evan Richter</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=evanrichter" title="Code">💻</a> <a href="https://github.com/epi052/feroxbuster/commits?author=evanrichter" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/mzpqnxow"><img src="https://avatars.githubusercontent.com/u/8016228?v=4?s=100" width="100px;" alt="AG"/><br /><sub><b>AG</b></sub></a><br /><a href="#ideas-mzpqnxow" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=mzpqnxow" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/mzpqnxow"><img src="https://avatars.githubusercontent.com/u/8016228?v=4?s=100" width="100px;" alt="AG"/><br /><sub><b>AG</b></sub></a><br /><a href="#ideas-mzpqnxow" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=mzpqnxow" title="Documentation">📖</a></td>
<td align="center"><a href="https://n-thumann.de/"><img src="https://avatars.githubusercontent.com/u/46975855?v=4?s=100" width="100px;" alt="Nicolas Thumann"/><br /><sub><b>Nicolas Thumann</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=n-thumann" title="Code">💻</a> <a href="https://github.com/epi052/feroxbuster/commits?author=n-thumann" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://n-thumann.de/"><img src="https://avatars.githubusercontent.com/u/46975855?v=4?s=100" width="100px;" alt="Nicolas Thumann"/><br /><sub><b>Nicolas Thumann</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=n-thumann" title="Code">💻</a> <a href="https://github.com/epi052/feroxbuster/commits?author=n-thumann" title="Documentation">📖</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://github.com/tomtastic"><img src="https://avatars.githubusercontent.com/u/302127?v=4?s=100" width="100px;" alt="Tom Matthews"/><br /><sub><b>Tom Matthews</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=tomtastic" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/tomtastic"><img src="https://avatars.githubusercontent.com/u/302127?v=4?s=100" width="100px;" alt="Tom Matthews"/><br /><sub><b>Tom Matthews</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=tomtastic" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/bsysop"><img src="https://avatars.githubusercontent.com/u/9998303?v=4?s=100" width="100px;" alt="bsysop"/><br /><sub><b>bsysop</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=bsysop" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/bsysop"><img src="https://avatars.githubusercontent.com/u/9998303?v=4?s=100" width="100px;" alt="bsysop"/><br /><sub><b>bsysop</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=bsysop" title="Documentation">📖</a></td>
<td align="center"><a href="http://bpsizemore.me"><img src="https://avatars.githubusercontent.com/u/11645898?v=4?s=100" width="100px;" alt="Brian Sizemore"/><br /><sub><b>Brian Sizemore</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=bpsizemore" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="http://bpsizemore.me"><img src="https://avatars.githubusercontent.com/u/11645898?v=4?s=100" width="100px;" alt="Brian Sizemore"/><br /><sub><b>Brian Sizemore</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=bpsizemore" title="Code">💻</a></td>
<td align="center"><a href="https://pwn.by/noraj"><img src="https://avatars.githubusercontent.com/u/16578570?v=4?s=100" width="100px;" alt="Alexandre ZANNI"/><br /><sub><b>Alexandre ZANNI</b></sub></a><br /><a href="#infra-noraj" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=noraj" title="Documentation">📖</a></td> <td align="center" valign="top" width="14.28%"><a href="https://pwn.by/noraj"><img src="https://avatars.githubusercontent.com/u/16578570?v=4?s=100" width="100px;" alt="Alexandre ZANNI"/><br /><sub><b>Alexandre ZANNI</b></sub></a><br /><a href="#infra-noraj" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=noraj" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/craig"><img src="https://avatars.githubusercontent.com/u/99729?v=4?s=100" width="100px;" alt="Craig"/><br /><sub><b>Craig</b></sub></a><br /><a href="#infra-craig" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/craig"><img src="https://avatars.githubusercontent.com/u/99729?v=4?s=100" width="100px;" alt="Craig"/><br /><sub><b>Craig</b></sub></a><br /><a href="#infra-craig" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://www.reddit.com/u/EONRaider"><img src="https://avatars.githubusercontent.com/u/15611424?v=4?s=100" width="100px;" alt="EONRaider"/><br /><sub><b>EONRaider</b></sub></a><br /><a href="#infra-EONRaider" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td> <td align="center" valign="top" width="14.28%"><a href="https://www.reddit.com/u/EONRaider"><img src="https://avatars.githubusercontent.com/u/15611424?v=4?s=100" width="100px;" alt="EONRaider"/><br /><sub><b>EONRaider</b></sub></a><br /><a href="#infra-EONRaider" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/wtwver"><img src="https://avatars.githubusercontent.com/u/53866088?v=4?s=100" width="100px;" alt="wtwver"/><br /><sub><b>wtwver</b></sub></a><br /><a href="#infra-wtwver" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/wtwver"><img src="https://avatars.githubusercontent.com/u/53866088?v=4?s=100" width="100px;" alt="wtwver"/><br /><sub><b>wtwver</b></sub></a><br /><a href="#infra-wtwver" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://tib3rius.com"><img src="https://avatars.githubusercontent.com/u/48113936?v=4?s=100" width="100px;" alt="Tib3rius"/><br /><sub><b>Tib3rius</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ATib3rius" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://tib3rius.com"><img src="https://avatars.githubusercontent.com/u/48113936?v=4?s=100" width="100px;" alt="Tib3rius"/><br /><sub><b>Tib3rius</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ATib3rius" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/0xdf"><img src="https://avatars.githubusercontent.com/u/1489045?v=4?s=100" width="100px;" alt="0xdf"/><br /><sub><b>0xdf</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3A0xdf" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/0xdf"><img src="https://avatars.githubusercontent.com/u/1489045?v=4?s=100" width="100px;" alt="0xdf"/><br /><sub><b>0xdf</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3A0xdf" title="Bug reports">🐛</a></td>
<td align="center"><a href="http://secure77.de"><img src="https://avatars.githubusercontent.com/u/31564517?v=4?s=100" width="100px;" alt="secure-77"/><br /><sub><b>secure-77</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asecure-77" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="http://secure77.de"><img src="https://avatars.githubusercontent.com/u/31564517?v=4?s=100" width="100px;" alt="secure-77"/><br /><sub><b>secure-77</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asecure-77" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/sbrun"><img src="https://avatars.githubusercontent.com/u/7712154?v=4?s=100" width="100px;" alt="Sophie Brun"/><br /><sub><b>Sophie Brun</b></sub></a><br /><a href="#infra-sbrun" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/sbrun"><img src="https://avatars.githubusercontent.com/u/7712154?v=4?s=100" width="100px;" alt="Sophie Brun"/><br /><sub><b>Sophie Brun</b></sub></a><br /><a href="#infra-sbrun" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/black-A"><img src="https://avatars.githubusercontent.com/u/30686803?v=4?s=100" width="100px;" alt="black-A"/><br /><sub><b>black-A</b></sub></a><br /><a href="#ideas-black-A" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/black-A"><img src="https://avatars.githubusercontent.com/u/30686803?v=4?s=100" width="100px;" alt="black-A"/><br /><sub><b>black-A</b></sub></a><br /><a href="#ideas-black-A" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/dinosn"><img src="https://avatars.githubusercontent.com/u/3851678?v=4?s=100" width="100px;" alt="Nicolas Krassas"/><br /><sub><b>Nicolas Krassas</b></sub></a><br /><a href="#ideas-dinosn" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/dinosn"><img src="https://avatars.githubusercontent.com/u/3851678?v=4?s=100" width="100px;" alt="Nicolas Krassas"/><br /><sub><b>Nicolas Krassas</b></sub></a><br /><a href="#ideas-dinosn" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/N0ur5"><img src="https://avatars.githubusercontent.com/u/24260009?v=4?s=100" width="100px;" alt="N0ur5"/><br /><sub><b>N0ur5</b></sub></a><br /><a href="#ideas-N0ur5" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/N0ur5"><img src="https://avatars.githubusercontent.com/u/24260009?v=4?s=100" width="100px;" alt="N0ur5"/><br /><sub><b>N0ur5</b></sub></a><br /><a href="#ideas-N0ur5" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://github.com/moscowchill"><img src="https://avatars.githubusercontent.com/u/72578879?v=4?s=100" width="100px;" alt="mchill"/><br /><sub><b>mchill</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Amoscowchill" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/moscowchill"><img src="https://avatars.githubusercontent.com/u/72578879?v=4?s=100" width="100px;" alt="mchill"/><br /><sub><b>mchill</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Amoscowchill" title="Bug reports">🐛</a></td>
<td align="center"><a href="http://BitThr3at.github.io"><img src="https://avatars.githubusercontent.com/u/45028933?v=4?s=100" width="100px;" alt="Naman"/><br /><sub><b>Naman</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ABitThr3at" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="http://BitThr3at.github.io"><img src="https://avatars.githubusercontent.com/u/45028933?v=4?s=100" width="100px;" alt="Naman"/><br /><sub><b>Naman</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ABitThr3at" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/Sicks3c"><img src="https://avatars.githubusercontent.com/u/32225186?v=4?s=100" width="100px;" alt="Ayoub Elaich"/><br /><sub><b>Ayoub Elaich</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asicks3c" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Sicks3c"><img src="https://avatars.githubusercontent.com/u/32225186?v=4?s=100" width="100px;" alt="Ayoub Elaich"/><br /><sub><b>Ayoub Elaich</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asicks3c" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/HenryHoggard"><img src="https://avatars.githubusercontent.com/u/1208121?v=4?s=100" width="100px;" alt="Henry"/><br /><sub><b>Henry</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AHenryHoggard" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/HenryHoggard"><img src="https://avatars.githubusercontent.com/u/1208121?v=4?s=100" width="100px;" alt="Henry"/><br /><sub><b>Henry</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AHenryHoggard" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/SleepiPanda"><img src="https://avatars.githubusercontent.com/u/6428561?v=4?s=100" width="100px;" alt="SleepiPanda"/><br /><sub><b>SleepiPanda</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ASleepiPanda" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/SleepiPanda"><img src="https://avatars.githubusercontent.com/u/6428561?v=4?s=100" width="100px;" alt="SleepiPanda"/><br /><sub><b>SleepiPanda</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ASleepiPanda" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/uBadRequest"><img src="https://avatars.githubusercontent.com/u/47282747?v=4?s=100" width="100px;" alt="Bad Requests"/><br /><sub><b>Bad Requests</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AuBadRequest" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/uBadRequest"><img src="https://avatars.githubusercontent.com/u/47282747?v=4?s=100" width="100px;" alt="Bad Requests"/><br /><sub><b>Bad Requests</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AuBadRequest" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://home.dnaka91.rocks"><img src="https://avatars.githubusercontent.com/u/36804488?v=4?s=100" width="100px;" alt="Dominik Nakamura"/><br /><sub><b>Dominik Nakamura</b></sub></a><br /><a href="#infra-dnaka91" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td> <td align="center" valign="top" width="14.28%"><a href="https://home.dnaka91.rocks"><img src="https://avatars.githubusercontent.com/u/36804488?v=4?s=100" width="100px;" alt="Dominik Nakamura"/><br /><sub><b>Dominik Nakamura</b></sub></a><br /><a href="#infra-dnaka91" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://github.com/hunter0x8"><img src="https://avatars.githubusercontent.com/u/46222314?v=4?s=100" width="100px;" alt="Muhammad Ahsan"/><br /><sub><b>Muhammad Ahsan</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Ahunter0x8" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/hunter0x8"><img src="https://avatars.githubusercontent.com/u/46222314?v=4?s=100" width="100px;" alt="Muhammad Ahsan"/><br /><sub><b>Muhammad Ahsan</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Ahunter0x8" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/cortantief"><img src="https://avatars.githubusercontent.com/u/34527333?v=4?s=100" width="100px;" alt="cortantief"/><br /><sub><b>cortantief</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Acortantief" title="Bug reports">🐛</a> <a href="https://github.com/epi052/feroxbuster/commits?author=cortantief" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/cortantief"><img src="https://avatars.githubusercontent.com/u/34527333?v=4?s=100" width="100px;" alt="cortantief"/><br /><sub><b>cortantief</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Acortantief" title="Bug reports">🐛</a> <a href="https://github.com/epi052/feroxbuster/commits?author=cortantief" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/dsaxton"><img src="https://avatars.githubusercontent.com/u/2658661?v=4?s=100" width="100px;" alt="Daniel Saxton"/><br /><sub><b>Daniel Saxton</b></sub></a><br /><a href="#ideas-dsaxton" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=dsaxton" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/dsaxton"><img src="https://avatars.githubusercontent.com/u/2658661?v=4?s=100" width="100px;" alt="Daniel Saxton"/><br /><sub><b>Daniel Saxton</b></sub></a><br /><a href="#ideas-dsaxton" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=dsaxton" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/narkopolo"><img src="https://avatars.githubusercontent.com/u/16690056?v=4?s=100" width="100px;" alt="narkopolo"/><br /><sub><b>narkopolo</b></sub></a><br /><a href="#ideas-narkopolo" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/n0kovo"><img src="https://avatars.githubusercontent.com/u/16690056?v=4?s=100" width="100px;" alt="n0kovo"/><br /><sub><b>n0kovo</b></sub></a><br /><a href="#ideas-n0kovo" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://ring0.lol"><img src="https://avatars.githubusercontent.com/u/1893909?v=4?s=100" width="100px;" alt="Justin Steven"/><br /><sub><b>Justin Steven</b></sub></a><br /><a href="#ideas-justinsteven" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="https://ring0.lol"><img src="https://avatars.githubusercontent.com/u/1893909?v=4?s=100" width="100px;" alt="Justin Steven"/><br /><sub><b>Justin Steven</b></sub></a><br /><a href="#ideas-justinsteven" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/7047payloads"><img src="https://avatars.githubusercontent.com/u/95562424?v=4?s=100" width="100px;" alt="7047payloads"/><br /><sub><b>7047payloads</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=7047payloads" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/7047payloads"><img src="https://avatars.githubusercontent.com/u/95562424?v=4?s=100" width="100px;" alt="7047payloads"/><br /><sub><b>7047payloads</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=7047payloads" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/unkn0wnsyst3m"><img src="https://avatars.githubusercontent.com/u/21272239?v=4?s=100" width="100px;" alt="unkn0wnsyst3m"/><br /><sub><b>unkn0wnsyst3m</b></sub></a><br /><a href="#ideas-unkn0wnsyst3m" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/unkn0wnsyst3m"><img src="https://avatars.githubusercontent.com/u/21272239?v=4?s=100" width="100px;" alt="unkn0wnsyst3m"/><br /><sub><b>unkn0wnsyst3m</b></sub></a><br /><a href="#ideas-unkn0wnsyst3m" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://ironwort.me/"><img src="https://avatars.githubusercontent.com/u/15280042?v=4?s=100" width="100px;" alt="0x08"/><br /><sub><b>0x08</b></sub></a><br /><a href="#ideas-its0x08" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="https://ironwort.me/"><img src="https://avatars.githubusercontent.com/u/15280042?v=4?s=100" width="100px;" alt="0x08"/><br /><sub><b>0x08</b></sub></a><br /><a href="#ideas-its0x08" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/MD-Levitan"><img src="https://avatars.githubusercontent.com/u/12116508?v=4?s=100" width="100px;" alt="kusok"/><br /><sub><b>kusok</b></sub></a><br /><a href="#ideas-MD-Levitan" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=MD-Levitan" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/MD-Levitan"><img src="https://avatars.githubusercontent.com/u/12116508?v=4?s=100" width="100px;" alt="kusok"/><br /><sub><b>kusok</b></sub></a><br /><a href="#ideas-MD-Levitan" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=MD-Levitan" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/godylockz"><img src="https://avatars.githubusercontent.com/u/81207744?v=4?s=100" width="100px;" alt="godylockz"/><br /><sub><b>godylockz</b></sub></a><br /><a href="#ideas-godylockz" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=godylockz" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/godylockz"><img src="https://avatars.githubusercontent.com/u/81207744?v=4?s=100" width="100px;" alt="godylockz"/><br /><sub><b>godylockz</b></sub></a><br /><a href="#ideas-godylockz" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=godylockz" title="Code">💻</a></td>
<td align="center"><a href="http://ryanmontgomery.me"><img src="https://avatars.githubusercontent.com/u/44453666?v=4?s=100" width="100px;" alt="Ryan Montgomery"/><br /><sub><b>Ryan Montgomery</b></sub></a><br /><a href="#ideas-0dayCTF" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="http://ryanmontgomery.me"><img src="https://avatars.githubusercontent.com/u/44453666?v=4?s=100" width="100px;" alt="Ryan Montgomery"/><br /><sub><b>Ryan Montgomery</b></sub></a><br /><a href="#ideas-0dayCTF" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/IppSec"><img src="https://avatars.githubusercontent.com/u/24677271?v=4?s=100" width="100px;" alt="ippsec"/><br /><sub><b>ippsec</b></sub></a><br /><a href="#ideas-ippsec" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/IppSec"><img src="https://avatars.githubusercontent.com/u/24677271?v=4?s=100" width="100px;" alt="ippsec"/><br /><sub><b>ippsec</b></sub></a><br /><a href="#ideas-ippsec" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/gtjamesa"><img src="https://avatars.githubusercontent.com/u/2078364?v=4?s=100" width="100px;" alt="James"/><br /><sub><b>James</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Agtjamesa" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/gtjamesa"><img src="https://avatars.githubusercontent.com/u/2078364?v=4?s=100" width="100px;" alt="James"/><br /><sub><b>James</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Agtjamesa" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://twitter.com/Jhaddix"><img src="https://avatars.githubusercontent.com/u/3488554?v=4?s=100" width="100px;" alt="Jason Haddix"/><br /><sub><b>Jason Haddix</b></sub></a><br /><a href="#ideas-jhaddix" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/issues?q=author%3Ajhaddix" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://twitter.com/Jhaddix"><img src="https://avatars.githubusercontent.com/u/3488554?v=4?s=100" width="100px;" alt="Jason Haddix"/><br /><sub><b>Jason Haddix</b></sub></a><br /><a href="#ideas-jhaddix" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/issues?q=author%3Ajhaddix" title="Bug reports">🐛</a></td>
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://github.com/ThisLimn0"><img src="https://avatars.githubusercontent.com/u/67125885?v=4?s=100" width="100px;" alt="Limn0"/><br /><sub><b>Limn0</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AThisLimn0" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/ThisLimn0"><img src="https://avatars.githubusercontent.com/u/67125885?v=4?s=100" width="100px;" alt="Limn0"/><br /><sub><b>Limn0</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AThisLimn0" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/0xdf223"><img src="https://avatars.githubusercontent.com/u/76954092?v=4?s=100" width="100px;" alt="0xdf"/><br /><sub><b>0xdf</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3A0xdf223" title="Bug reports">🐛</a> <a href="#ideas-0xdf223" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/0xdf223"><img src="https://avatars.githubusercontent.com/u/76954092?v=4?s=100" width="100px;" alt="0xdf"/><br /><sub><b>0xdf</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3A0xdf223" title="Bug reports">🐛</a> <a href="#ideas-0xdf223" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/Flangyver"><img src="https://avatars.githubusercontent.com/u/59575870?v=4?s=100" width="100px;" alt="Flangyver"/><br /><sub><b>Flangyver</b></sub></a><br /><a href="#ideas-Flangyver" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Flangyver"><img src="https://avatars.githubusercontent.com/u/59575870?v=4?s=100" width="100px;" alt="Flangyver"/><br /><sub><b>Flangyver</b></sub></a><br /><a href="#ideas-Flangyver" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/DonatoReis"><img src="https://avatars.githubusercontent.com/u/93531354?v=4?s=100" width="100px;" alt="PeakyBlinder"/><br /><sub><b>PeakyBlinder</b></sub></a><br /><a href="#ideas-DonatoReis" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/DonatoReis"><img src="https://avatars.githubusercontent.com/u/93531354?v=4?s=100" width="100px;" alt="PeakyBlinder"/><br /><sub><b>PeakyBlinder</b></sub></a><br /><a href="#ideas-DonatoReis" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://postmodern.github.io/"><img src="https://avatars.githubusercontent.com/u/12671?v=4?s=100" width="100px;" alt="Postmodern"/><br /><sub><b>Postmodern</b></sub></a><br /><a href="#ideas-postmodern" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="https://postmodern.github.io/"><img src="https://avatars.githubusercontent.com/u/12671?v=4?s=100" width="100px;" alt="Postmodern"/><br /><sub><b>Postmodern</b></sub></a><br /><a href="#ideas-postmodern" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/herrcykel"><img src="https://avatars.githubusercontent.com/u/1936757?v=4?s=100" width="100px;" alt="O"/><br /><sub><b>O</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=herrcykel" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/herrcykel"><img src="https://avatars.githubusercontent.com/u/1936757?v=4?s=100" width="100px;" alt="O"/><br /><sub><b>O</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=herrcykel" title="Code">💻</a></td>
<td align="center"><a href="http://udoprog.github.io/"><img src="https://avatars.githubusercontent.com/u/111092?v=4?s=100" width="100px;" alt="John-John Tedro"/><br /><sub><b>John-John Tedro</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=udoprog" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="http://udoprog.github.io/"><img src="https://avatars.githubusercontent.com/u/111092?v=4?s=100" width="100px;" alt="John-John Tedro"/><br /><sub><b>John-John Tedro</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=udoprog" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kmanc"><img src="https://avatars.githubusercontent.com/u/14863147?v=4?s=100" width="100px;" alt="kmanc"/><br /><sub><b>kmanc</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Akmanc" title="Bug reports">🐛</a> <a href="https://github.com/epi052/feroxbuster/commits?author=kmanc" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hakdogpinas"><img src="https://avatars.githubusercontent.com/u/71529469?v=4?s=100" width="100px;" alt="hakdogpinas"/><br /><sub><b>hakdogpinas</b></sub></a><br /><a href="#ideas-hakdogpinas" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/duokebei"><img src="https://avatars.githubusercontent.com/u/75022552?v=4?s=100" width="100px;" alt="多可悲"/><br /><sub><b>多可悲</b></sub></a><br /><a href="#ideas-duokebei" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://blog.ah34.net/"><img src="https://avatars.githubusercontent.com/u/58670593?v=4?s=100" width="100px;" alt="Aidan Hall"/><br /><sub><b>Aidan Hall</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=aidanhall34" title="Code">💻</a> <a href="#infra-aidanhall34" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://hachyderm.io/@JohnnyCiocca"><img src="https://avatars.githubusercontent.com/u/6473725?v=4?s=100" width="100px;" alt="João Ciocca"/><br /><sub><b>João Ciocca</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Ajoaociocca" title="Bug reports">🐛</a> <a href="#ideas-joaociocca" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/f3rn0s"><img src="https://avatars.githubusercontent.com/u/1351279?v=4?s=100" width="100px;" alt="f3rn0s"/><br /><sub><b>f3rn0s</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Af3rn0s" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://sth.sh"><img src="https://avatars.githubusercontent.com/u/2099767?v=4?s=100" width="100px;" alt="LongCat"/><br /><sub><b>LongCat</b></sub></a><br /><a href="#ideas-pich4ya" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xaeroborg"><img src="https://avatars.githubusercontent.com/u/33274680?v=4?s=100" width="100px;" alt="xaeroborg"/><br /><sub><b>xaeroborg</b></sub></a><br /><a href="#ideas-xaeroborg" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Luoooio"><img src="https://avatars.githubusercontent.com/u/26653157?v=4?s=100" width="100px;" alt="Luoooio"/><br /><sub><b>Luoooio</b></sub></a><br /><a href="#ideas-Luoooio" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://petruknisme.com"><img src="https://avatars.githubusercontent.com/u/6284204?v=4?s=100" width="100px;" alt="Aan"/><br /><sub><b>Aan</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=aancw" title="Code">💻</a> <a href="#infra-aancw" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -1,5 +1,5 @@
use std::fs::{copy, create_dir_all, OpenOptions}; use std::fs::{copy, create_dir_all, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write}; use std::io::{Read, Seek, Write};
use clap_complete::{generate_to, shells}; use clap_complete::{generate_to, shells};
@@ -30,7 +30,7 @@ fn main() {
let mut bash_file = OpenOptions::new() let mut bash_file = OpenOptions::new()
.read(true) .read(true)
.write(true) .write(true)
.open(format!("{}/feroxbuster.bash", outdir)) .open(format!("{outdir}/feroxbuster.bash"))
.expect("Couldn't open bash completion script"); .expect("Couldn't open bash completion script");
bash_file bash_file
@@ -40,7 +40,7 @@ fn main() {
contents = contents.replace("default feroxbuster", "default -o plusdirs feroxbuster"); contents = contents.replace("default feroxbuster", "default -o plusdirs feroxbuster");
bash_file bash_file
.seek(SeekFrom::Start(0)) .rewind()
.expect("Couldn't seek to position 0 in bash completion script"); .expect("Couldn't seek to position 0 in bash completion script");
bash_file bash_file

View File

@@ -54,6 +54,7 @@
# queries = [["name","value"], ["rick", "astley"]] # queries = [["name","value"], ["rick", "astley"]]
# save_state = false # save_state = false
# time_limit = "10m" # time_limit = "10m"
# update_app = false
# headers can be specified on multiple lines or as an inline table # headers can be specified on multiple lines or as an inline table
# #

BIN
img/logo/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -24,8 +24,8 @@ _feroxbuster() {
'--replay-proxy=[Send only unfiltered requests through a Replay Proxy, instead of all requests]:REPLAY_PROXY:_urls' \ '--replay-proxy=[Send only unfiltered requests through a Replay Proxy, instead of all requests]:REPLAY_PROXY:_urls' \
'*-R+[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \ '*-R+[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \
'*--replay-codes=[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \ '*--replay-codes=[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \
'-a+[Sets the User-Agent (default: feroxbuster/2.7.1)]:USER_AGENT: ' \ '-a+[Sets the User-Agent (default: feroxbuster/2.10.0)]:USER_AGENT: ' \
'--user-agent=[Sets the User-Agent (default: feroxbuster/2.7.1)]:USER_AGENT: ' \ '--user-agent=[Sets the User-Agent (default: feroxbuster/2.10.0)]:USER_AGENT: ' \
'*-x+[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \ '*-x+[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \
'*--extensions=[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \ '*--extensions=[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \
'*-m+[Which HTTP request method(s) should be sent (default: GET)]:HTTP_METHODS: ' \ '*-m+[Which HTTP request method(s) should be sent (default: GET)]:HTTP_METHODS: ' \
@@ -49,8 +49,8 @@ _feroxbuster() {
'(-s --status-codes)*-C+[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \ '(-s --status-codes)*-C+[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \
'(-s --status-codes)*--filter-status=[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \ '(-s --status-codes)*--filter-status=[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \
'*--filter-similar-to=[Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)]:UNWANTED_PAGE:_urls' \ '*--filter-similar-to=[Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)]:UNWANTED_PAGE:_urls' \
'*-s+[Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)]:STATUS_CODE: ' \ '*-s+[Status Codes to include (allow list) (default: All Status Codes)]:STATUS_CODE: ' \
'*--status-codes=[Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)]:STATUS_CODE: ' \ '*--status-codes=[Status Codes to include (allow list) (default: All Status Codes)]:STATUS_CODE: ' \
'-T+[Number of seconds before a client'\''s request times out (default: 7)]:SECONDS: ' \ '-T+[Number of seconds before a client'\''s request times out (default: 7)]:SECONDS: ' \
'--timeout=[Number of seconds before a client'\''s request times out (default: 7)]:SECONDS: ' \ '--timeout=[Number of seconds before a client'\''s request times out (default: 7)]:SECONDS: ' \
'-t+[Number of concurrent threads (default: 50)]:THREADS: ' \ '-t+[Number of concurrent threads (default: 50)]:THREADS: ' \
@@ -72,8 +72,8 @@ _feroxbuster() {
'(-u --url)--stdin[Read url(s) from STDIN]' \ '(-u --url)--stdin[Read url(s) from STDIN]' \
'(-p --proxy -k --insecure --burp-replay)--burp[Set --proxy to http://127.0.0.1:8080 and set --insecure to true]' \ '(-p --proxy -k --insecure --burp-replay)--burp[Set --proxy to http://127.0.0.1:8080 and set --insecure to true]' \
'(-P --replay-proxy -k --insecure)--burp-replay[Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true]' \ '(-P --replay-proxy -k --insecure)--burp-replay[Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true]' \
'--smart[Set --extract-links, --auto-tune, --collect-words, and --collect-backups to true]' \ '(--rate-limit --auto-bail)--smart[Set --extract-links, --auto-tune, --collect-words, and --collect-backups to true]' \
'--thorough[Use the same settings as --smart and set --collect-extensions to true]' \ '(--rate-limit --auto-bail)--thorough[Use the same settings as --smart and set --collect-extensions to true]' \
'-A[Use a random User-Agent]' \ '-A[Use a random User-Agent]' \
'--random-agent[Use a random User-Agent]' \ '--random-agent[Use a random User-Agent]' \
'-f[Append / to each request'\''s URL]' \ '-f[Append / to each request'\''s URL]' \
@@ -104,10 +104,12 @@ _feroxbuster() {
'--quiet[Hide progress bars and banner (good for tmux windows w/ notifications)]' \ '--quiet[Hide progress bars and banner (good for tmux windows w/ notifications)]' \
'--json[Emit JSON logs to --output and --debug-log instead of normal text]' \ '--json[Emit JSON logs to --output and --debug-log instead of normal text]' \
'--no-state[Disable state output file (*.state)]' \ '--no-state[Disable state output file (*.state)]' \
'-h[Print help information (use `--help` for more detail)]' \ '(-u --url -w --wordlist)-U[Update the app to the latest version]' \
'--help[Print help information (use `--help` for more detail)]' \ '(-u --url -w --wordlist)--update[Update the app to the latest version]' \
'-V[Print version information]' \ '-h[Print help (see more with '\''--help'\'')]' \
'--version[Print version information]' \ '--help[Print help (see more with '\''--help'\'')]' \
'-V[Print version]' \
'--version[Print version]' \
&& ret=0 && ret=0
} }
@@ -117,4 +119,8 @@ _feroxbuster_commands() {
_describe -t commands 'feroxbuster commands' commands "$@" _describe -t commands 'feroxbuster commands' commands "$@"
} }
_feroxbuster "$@" if [ "$funcstack[1]" = "_feroxbuster" ]; then
_feroxbuster "$@"
else
compdef _feroxbuster feroxbuster
fi

View File

@@ -30,8 +30,8 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('--replay-proxy', 'replay-proxy', [CompletionResultType]::ParameterName, 'Send only unfiltered requests through a Replay Proxy, instead of all requests') [CompletionResult]::new('--replay-proxy', 'replay-proxy', [CompletionResultType]::ParameterName, 'Send only unfiltered requests through a Replay Proxy, instead of all requests')
[CompletionResult]::new('-R', 'R', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)') [CompletionResult]::new('-R', 'R', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)')
[CompletionResult]::new('--replay-codes', 'replay-codes', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)') [CompletionResult]::new('--replay-codes', 'replay-codes', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)')
[CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.7.1)') [CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.10.0)')
[CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.7.1)') [CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.10.0)')
[CompletionResult]::new('-x', 'x', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)') [CompletionResult]::new('-x', 'x', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)')
[CompletionResult]::new('--extensions', 'extensions', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)') [CompletionResult]::new('--extensions', 'extensions', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)')
[CompletionResult]::new('-m', 'm', [CompletionResultType]::ParameterName, 'Which HTTP request method(s) should be sent (default: GET)') [CompletionResult]::new('-m', 'm', [CompletionResultType]::ParameterName, 'Which HTTP request method(s) should be sent (default: GET)')
@@ -55,8 +55,8 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('-C', 'C', [CompletionResultType]::ParameterName, 'Filter out status codes (deny list) (ex: -C 200 -C 401)') [CompletionResult]::new('-C', 'C', [CompletionResultType]::ParameterName, 'Filter out status codes (deny list) (ex: -C 200 -C 401)')
[CompletionResult]::new('--filter-status', 'filter-status', [CompletionResultType]::ParameterName, 'Filter out status codes (deny list) (ex: -C 200 -C 401)') [CompletionResult]::new('--filter-status', 'filter-status', [CompletionResultType]::ParameterName, 'Filter out status codes (deny list) (ex: -C 200 -C 401)')
[CompletionResult]::new('--filter-similar-to', 'filter-similar-to', [CompletionResultType]::ParameterName, 'Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)') [CompletionResult]::new('--filter-similar-to', 'filter-similar-to', [CompletionResultType]::ParameterName, 'Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)')
[CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)') [CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Status Codes to include (allow list) (default: All Status Codes)')
[CompletionResult]::new('--status-codes', 'status-codes', [CompletionResultType]::ParameterName, 'Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)') [CompletionResult]::new('--status-codes', 'status-codes', [CompletionResultType]::ParameterName, 'Status Codes to include (allow list) (default: All Status Codes)')
[CompletionResult]::new('-T', 'T', [CompletionResultType]::ParameterName, 'Number of seconds before a client''s request times out (default: 7)') [CompletionResult]::new('-T', 'T', [CompletionResultType]::ParameterName, 'Number of seconds before a client''s request times out (default: 7)')
[CompletionResult]::new('--timeout', 'timeout', [CompletionResultType]::ParameterName, 'Number of seconds before a client''s request times out (default: 7)') [CompletionResult]::new('--timeout', 'timeout', [CompletionResultType]::ParameterName, 'Number of seconds before a client''s request times out (default: 7)')
[CompletionResult]::new('-t', 't', [CompletionResultType]::ParameterName, 'Number of concurrent threads (default: 50)') [CompletionResult]::new('-t', 't', [CompletionResultType]::ParameterName, 'Number of concurrent threads (default: 50)')
@@ -110,10 +110,12 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('--quiet', 'quiet', [CompletionResultType]::ParameterName, 'Hide progress bars and banner (good for tmux windows w/ notifications)') [CompletionResult]::new('--quiet', 'quiet', [CompletionResultType]::ParameterName, 'Hide progress bars and banner (good for tmux windows w/ notifications)')
[CompletionResult]::new('--json', 'json', [CompletionResultType]::ParameterName, 'Emit JSON logs to --output and --debug-log instead of normal text') [CompletionResult]::new('--json', 'json', [CompletionResultType]::ParameterName, 'Emit JSON logs to --output and --debug-log instead of normal text')
[CompletionResult]::new('--no-state', 'no-state', [CompletionResultType]::ParameterName, 'Disable state output file (*.state)') [CompletionResult]::new('--no-state', 'no-state', [CompletionResultType]::ParameterName, 'Disable state output file (*.state)')
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information (use `--help` for more detail)') [CompletionResult]::new('-U', 'U', [CompletionResultType]::ParameterName, 'Update the app to the latest version')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information (use `--help` for more detail)') [CompletionResult]::new('--update', 'update', [CompletionResultType]::ParameterName, 'Update the app to the latest version')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information') [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information') [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version')
break break
} }
}) })

View File

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

View File

@@ -27,8 +27,8 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand --replay-proxy 'Send only unfiltered requests through a Replay Proxy, instead of all requests' cand --replay-proxy 'Send only unfiltered requests through a Replay Proxy, instead of all requests'
cand -R 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)' cand -R 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)'
cand --replay-codes 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)' cand --replay-codes 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)'
cand -a 'Sets the User-Agent (default: feroxbuster/2.7.1)' cand -a 'Sets the User-Agent (default: feroxbuster/2.10.0)'
cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.7.1)' cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.10.0)'
cand -x 'File extension(s) to search for (ex: -x php -x pdf js)' cand -x 'File extension(s) to search for (ex: -x php -x pdf js)'
cand --extensions 'File extension(s) to search for (ex: -x php -x pdf js)' cand --extensions 'File extension(s) to search for (ex: -x php -x pdf js)'
cand -m 'Which HTTP request method(s) should be sent (default: GET)' cand -m 'Which HTTP request method(s) should be sent (default: GET)'
@@ -52,8 +52,8 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand -C 'Filter out status codes (deny list) (ex: -C 200 -C 401)' cand -C 'Filter out status codes (deny list) (ex: -C 200 -C 401)'
cand --filter-status 'Filter out status codes (deny list) (ex: -C 200 -C 401)' cand --filter-status 'Filter out status codes (deny list) (ex: -C 200 -C 401)'
cand --filter-similar-to 'Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)' cand --filter-similar-to 'Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)'
cand -s 'Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)' cand -s 'Status Codes to include (allow list) (default: All Status Codes)'
cand --status-codes 'Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)' cand --status-codes 'Status Codes to include (allow list) (default: All Status Codes)'
cand -T 'Number of seconds before a client''s request times out (default: 7)' cand -T 'Number of seconds before a client''s request times out (default: 7)'
cand --timeout 'Number of seconds before a client''s request times out (default: 7)' cand --timeout 'Number of seconds before a client''s request times out (default: 7)'
cand -t 'Number of concurrent threads (default: 50)' cand -t 'Number of concurrent threads (default: 50)'
@@ -107,10 +107,12 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand --quiet 'Hide progress bars and banner (good for tmux windows w/ notifications)' cand --quiet 'Hide progress bars and banner (good for tmux windows w/ notifications)'
cand --json 'Emit JSON logs to --output and --debug-log instead of normal text' cand --json 'Emit JSON logs to --output and --debug-log instead of normal text'
cand --no-state 'Disable state output file (*.state)' cand --no-state 'Disable state output file (*.state)'
cand -h 'Print help information (use `--help` for more detail)' cand -U 'Update the app to the latest version'
cand --help 'Print help information (use `--help` for more detail)' cand --update 'Update the app to the latest version'
cand -V 'Print version information' cand -h 'Print help (see more with ''--help'')'
cand --version 'Print version information' cand --help 'Print help (see more with ''--help'')'
cand -V 'Print version'
cand --version 'Print version'
} }
] ]
$completions[$command] $completions[$command]

View File

@@ -3,7 +3,7 @@ use crate::{
config::Configuration, config::Configuration,
event_handlers::Handles, event_handlers::Handles,
utils::{logged_request, status_colorizer}, utils::{logged_request, status_colorizer},
DEFAULT_IGNORED_EXTENSIONS, DEFAULT_METHOD, VERSION, DEFAULT_IGNORED_EXTENSIONS, DEFAULT_METHOD, DEFAULT_STATUS_CODES, VERSION,
}; };
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use console::{style, Emoji}; use console::{style, Emoji};
@@ -166,6 +166,9 @@ pub struct Banner {
/// represents Configuration.collect_words /// represents Configuration.collect_words
force_recursion: BannerEntry, force_recursion: BannerEntry,
/// represents Configuration.update_app
update_app: BannerEntry,
} }
/// implementation of Banner /// implementation of Banner
@@ -204,12 +207,25 @@ impl Banner {
)); ));
} }
let mut codes = vec![]; // the +2 is for the 2 experimental status codes we add to the default list manually
for code in &config.status_codes { let status_codes = if config.status_codes.len() == DEFAULT_STATUS_CODES.len() + 2 {
codes.push(status_colorizer(&code.to_string())) let all_str = format!(
} "{} {} {}{}",
let status_codes = style("All").cyan(),
BannerEntry::new("👌", "Status Codes", &format!("[{}]", codes.join(", "))); style("Status").green(),
style("Codes").yellow(),
style("!").red()
);
BannerEntry::new("👌", "Status Codes", &all_str)
} else {
let mut codes = vec![];
for code in &config.status_codes {
codes.push(status_colorizer(&code.to_string()))
}
BannerEntry::new("👌", "Status Codes", &format!("[{}]", codes.join(", ")))
};
for code in &config.filter_status { for code in &config.filter_status {
code_filters.push(status_colorizer(&code.to_string())) code_filters.push(status_colorizer(&code.to_string()))
@@ -233,7 +249,7 @@ impl Banner {
headers.push(BannerEntry::new( headers.push(BannerEntry::new(
"🤯", "🤯",
"Header", "Header",
&format!("{}: {}", name, value), &format!("{name}: {value}"),
)); ));
} }
@@ -307,7 +323,7 @@ impl Banner {
BannerEntry::new("🤘", "Force Recursion", &config.force_recursion.to_string()); BannerEntry::new("🤘", "Force Recursion", &config.force_recursion.to_string());
let replay_proxy = BannerEntry::new("🎥", "Replay Proxy", &config.replay_proxy); let replay_proxy = BannerEntry::new("🎥", "Replay Proxy", &config.replay_proxy);
let auto_tune = BannerEntry::new("🎶", "Auto Tune", &config.auto_tune.to_string()); let auto_tune = BannerEntry::new("🎶", "Auto Tune", &config.auto_tune.to_string());
let auto_bail = BannerEntry::new("🪣", "Auto Bail", &config.auto_bail.to_string()); let auto_bail = BannerEntry::new("🙅", "Auto Bail", &config.auto_bail.to_string());
let cfg = BannerEntry::new("💉", "Config File", &config.config); let cfg = BannerEntry::new("💉", "Config File", &config.config);
let proxy = BannerEntry::new("💎", "Proxy", &config.proxy); let proxy = BannerEntry::new("💎", "Proxy", &config.proxy);
let threads = BannerEntry::new("🚀", "Threads", &config.threads.to_string()); let threads = BannerEntry::new("🚀", "Threads", &config.threads.to_string());
@@ -320,6 +336,7 @@ impl Banner {
let json = BannerEntry::new("🧔", "JSON Output", &config.json.to_string()); let json = BannerEntry::new("🧔", "JSON Output", &config.json.to_string());
let output = BannerEntry::new("💾", "Output File", &config.output); let output = BannerEntry::new("💾", "Output File", &config.output);
let debug_log = BannerEntry::new("🪲", "Debugging Log", &config.debug_log); let debug_log = BannerEntry::new("🪲", "Debugging Log", &config.debug_log);
let update_app = BannerEntry::new("🔥", "Update app", &config.update_app.to_string());
let extensions = BannerEntry::new( let extensions = BannerEntry::new(
"💲", "💲",
"Extensions", "Extensions",
@@ -424,6 +441,7 @@ impl Banner {
config: cfg, config: cfg,
version: VERSION.to_string(), version: VERSION.to_string(),
update_status: UpdateStatus::Unknown, update_status: UpdateStatus::Unknown,
update_app,
} }
} }
@@ -441,7 +459,7 @@ by Ben "epi" Risher {} ver: {}"#,
let top = "───────────────────────────┬──────────────────────"; let top = "───────────────────────────┬──────────────────────";
format!("{}\n{}", artwork, top) format!("{artwork}\n{top}")
} }
/// get a fancy footer for the banner /// get a fancy footer for the banner
@@ -455,7 +473,7 @@ by Ben "epi" Risher {} ver: {}"#,
style("Scan Management Menu").bright().yellow(), style("Scan Management Menu").bright().yellow(),
); );
format!("{}\n{}\n{}", bottom, instructions, addl_section) format!("{bottom}\n{instructions}\n{addl_section}")
} }
/// Makes a request to the given url, expecting to receive a JSON response that contains a field /// Makes a request to the given url, expecting to receive a JSON response that contains a field
@@ -508,11 +526,11 @@ by Ben "epi" Risher {} ver: {}"#,
// begin with always printed items // begin with always printed items
for target in &self.targets { for target in &self.targets {
writeln!(&mut writer, "{}", target)?; writeln!(&mut writer, "{target}")?;
} }
for denied_url in &self.url_denylist { for denied_url in &self.url_denylist {
writeln!(&mut writer, "{}", denied_url)?; writeln!(&mut writer, "{denied_url}")?;
} }
writeln!(&mut writer, "{}", self.threads)?; writeln!(&mut writer, "{}", self.threads)?;
@@ -551,27 +569,27 @@ by Ben "epi" Risher {} ver: {}"#,
} }
for header in &self.headers { for header in &self.headers {
writeln!(&mut writer, "{}", header)?; writeln!(&mut writer, "{header}")?;
} }
for filter in &self.filter_size { for filter in &self.filter_size {
writeln!(&mut writer, "{}", filter)?; writeln!(&mut writer, "{filter}")?;
} }
for filter in &self.filter_similar { for filter in &self.filter_similar {
writeln!(&mut writer, "{}", filter)?; writeln!(&mut writer, "{filter}")?;
} }
for filter in &self.filter_word_count { for filter in &self.filter_word_count {
writeln!(&mut writer, "{}", filter)?; writeln!(&mut writer, "{filter}")?;
} }
for filter in &self.filter_line_count { for filter in &self.filter_line_count {
writeln!(&mut writer, "{}", filter)?; writeln!(&mut writer, "{filter}")?;
} }
for filter in &self.filter_regex { for filter in &self.filter_regex {
writeln!(&mut writer, "{}", filter)?; writeln!(&mut writer, "{filter}")?;
} }
if config.extract_links { if config.extract_links {
@@ -583,7 +601,7 @@ by Ben "epi" Risher {} ver: {}"#,
} }
for query in &self.queries { for query in &self.queries {
writeln!(&mut writer, "{}", query)?; writeln!(&mut writer, "{query}")?;
} }
if !config.output.is_empty() { if !config.output.is_empty() {
@@ -653,6 +671,10 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(&mut writer, "{}", self.force_recursion)?; writeln!(&mut writer, "{}", self.force_recursion)?;
} }
if config.update_app {
writeln!(&mut writer, "{}", self.update_app)?;
}
if config.scan_limit > 0 { if config.scan_limit > 0 {
writeln!(&mut writer, "{}", self.scan_limit)?; writeln!(&mut writer, "{}", self.scan_limit)?;
} }
@@ -675,7 +697,7 @@ by Ben "epi" Risher {} ver: {}"#,
"New Version Available", "New Version Available",
"https://github.com/epi052/feroxbuster/releases/latest", "https://github.com/epi052/feroxbuster/releases/latest",
); );
writeln!(&mut writer, "{}", update)?; writeln!(&mut writer, "{update}")?;
} }
writeln!(&mut writer, "{}", self.footer())?; writeln!(&mut writer, "{}", self.footer())?;

View File

@@ -309,6 +309,10 @@ pub struct Configuration {
/// override recursion logic to always attempt recursion, still respects --depth /// override recursion logic to always attempt recursion, still respects --depth
#[serde(default)] #[serde(default)]
pub force_recursion: bool, pub force_recursion: bool,
/// Auto update app feature
#[serde(default)]
pub update_app: bool,
} }
impl Default for Configuration { impl Default for Configuration {
@@ -358,6 +362,7 @@ impl Default for Configuration {
collect_words: false, collect_words: false,
save_state: true, save_state: true,
force_recursion: false, force_recursion: false,
update_app: false,
proxy: String::new(), proxy: String::new(),
config: String::new(), config: String::new(),
output: String::new(), output: String::new(),
@@ -441,6 +446,7 @@ impl Configuration {
/// - **time_limit**: `None` (no limit on length of scan imposed) /// - **time_limit**: `None` (no limit on length of scan imposed)
/// - **replay_proxy**: `None` (no limit on concurrent scans imposed) /// - **replay_proxy**: `None` (no limit on concurrent scans imposed)
/// - **replay_codes**: [`DEFAULT_RESPONSE_CODES`](constant.DEFAULT_RESPONSE_CODES.html) /// - **replay_codes**: [`DEFAULT_RESPONSE_CODES`](constant.DEFAULT_RESPONSE_CODES.html)
/// - **update_app**: `false`
/// ///
/// After which, any values defined in a /// After which, any values defined in a
/// [ferox-config.toml](constant.DEFAULT_CONFIG_NAME.html) config file will override the /// [ferox-config.toml](constant.DEFAULT_CONFIG_NAME.html) config file will override the
@@ -790,7 +796,7 @@ impl Configuration {
if args.get_count("verbosity") > 0 { if args.get_count("verbosity") > 0 {
// occurrences_of returns 0 if none are found; this is protected in // occurrences_of returns 0 if none are found; this is protected in
// an if block for the same reason as the quiet option // an if block for the same reason as the quiet option
config.verbosity = args.get_count("verbosity") as u8; config.verbosity = args.get_count("verbosity");
} }
if came_from_cli!(args, "no_recursion") { if came_from_cli!(args, "no_recursion") {
@@ -816,6 +822,10 @@ impl Configuration {
config.force_recursion = true; config.force_recursion = true;
} }
if came_from_cli!(args, "update_app") {
config.update_app = true;
}
//// ////
// organizational breakpoint; all options below alter the Client configuration // organizational breakpoint; all options below alter the Client configuration
//// ////
@@ -992,6 +1002,7 @@ impl Configuration {
update_if_not_default!(&mut conf.methods, new.methods, methods()); update_if_not_default!(&mut conf.methods, new.methods, methods());
update_if_not_default!(&mut conf.data, new.data, Vec::<u8>::new()); update_if_not_default!(&mut conf.data, new.data, Vec::<u8>::new());
update_if_not_default!(&mut conf.url_denylist, new.url_denylist, Vec::<Url>::new()); update_if_not_default!(&mut conf.url_denylist, new.url_denylist, Vec::<Url>::new());
update_if_not_default!(&mut conf.update_app, new.update_app, false);
if !new.regex_denylist.is_empty() { if !new.regex_denylist.is_empty() {
// cant use the update_if_not_default macro due to the following error // cant use the update_if_not_default macro due to the following error
// //

View File

@@ -56,6 +56,7 @@ fn setup_config_test() -> Configuration {
filter_word_count = [994, 992] filter_word_count = [994, 992]
filter_line_count = [34] filter_line_count = [34]
filter_status = [201] filter_status = [201]
update_app = false
"#; "#;
let tmp_dir = TempDir::new().unwrap(); let tmp_dir = TempDir::new().unwrap();
let file = tmp_dir.path().join(DEFAULT_CONFIG_NAME); let file = tmp_dir.path().join(DEFAULT_CONFIG_NAME);
@@ -103,6 +104,7 @@ fn default_configuration() {
assert!(!config.collect_extensions); assert!(!config.collect_extensions);
assert!(!config.collect_backups); assert!(!config.collect_backups);
assert!(!config.collect_words); assert!(!config.collect_words);
assert!(!config.update_app);
assert!(config.regex_denylist.is_empty()); assert!(config.regex_denylist.is_empty());
assert_eq!(config.queries, Vec::new()); assert_eq!(config.queries, Vec::new());
assert_eq!(config.filter_size, Vec::<u64>::new()); assert_eq!(config.filter_size, Vec::<u64>::new());
@@ -470,6 +472,13 @@ fn config_default_not_random_agent() {
assert!(!config.random_agent); assert!(!config.random_agent);
} }
#[test]
/// parse the test config and see that the value parsed is correct
fn config_update_app() {
let config = setup_config_test();
assert!(!config.update_app);
}
#[test] #[test]
#[should_panic] #[should_panic]
/// test that an error message is printed and panic is called when report_and_exit is called /// test that an error message is printed and panic is called when report_and_exit is called
@@ -482,7 +491,7 @@ fn config_report_and_exit_works() {
fn as_str_returns_string_with_newline() { fn as_str_returns_string_with_newline() {
let config = Configuration::new().unwrap(); let config = Configuration::new().unwrap();
let config_str = config.as_str(); let config_str = config.as_str();
println!("{}", config_str); println!("{config_str}");
assert!(config_str.starts_with("Configuration {")); assert!(config_str.starts_with("Configuration {"));
assert!(config_str.ends_with("}\n")); assert!(config_str.ends_with("}\n"));
assert!(config_str.contains("replay_codes:")); assert!(config_str.contains("replay_codes:"));

View File

@@ -49,6 +49,10 @@ pub(super) fn status_codes() -> Vec<u16> {
DEFAULT_STATUS_CODES DEFAULT_STATUS_CODES
.iter() .iter()
.map(|code| code.as_u16()) .map(|code| code.as_u16())
// add experimental codes not found in reqwest
// - 103 - EARLY_HINTS
// - 425 - TOO_EARLY
.chain([103, 425])
.collect() .collect()
} }
@@ -72,7 +76,7 @@ pub(super) fn wordlist() -> String {
/// default user-agent /// default user-agent
pub(super) fn user_agent() -> String { pub(super) fn user_agent() -> String {
format!("feroxbuster/{}", VERSION) format!("feroxbuster/{VERSION}")
} }
/// default recursion depth /// default recursion depth

View File

@@ -24,7 +24,9 @@ pub enum Command {
AddStatus(StatusCode), AddStatus(StatusCode),
/// Create the progress bar (`BarType::Total`) that is updated from the stats thread /// Create the progress bar (`BarType::Total`) that is updated from the stats thread
CreateBar, ///
/// the u64 value is the offset at which to start the progress bar (can be 0)
CreateBar(u64),
/// Add to a `Stats` field that corresponds to the given `StatField` by the given `usize` value /// Add to a `Stats` field that corresponds to the given `StatField` by the given `usize` value
AddToUsizeField(StatField, usize), AddToUsizeField(StatField, usize),

View File

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

View File

@@ -248,7 +248,7 @@ impl TermOutHandler {
.unwrap() .unwrap()
.filters .filters
.data .data
.should_filter_response(&resp, self.handles.as_ref().unwrap().stats.tx.clone()); .should_filter_response(&resp, tx_stats.clone());
let contains_sentry = if !self.config.filter_status.is_empty() { let contains_sentry = if !self.config.filter_status.is_empty() {
// -C was used, meaning -s was not and we should ignore the defaults // -C was used, meaning -s was not and we should ignore the defaults
@@ -274,7 +274,7 @@ impl TermOutHandler {
self.tx_file self.tx_file
.send(Command::Report(resp.clone())) .send(Command::Report(resp.clone()))
.with_context(|| { .with_context(|| {
fmt_err(&format!("Could not send {} to file handler", resp)) fmt_err(&format!("Could not send {resp} to file handler"))
})?; })?;
} }
} }
@@ -394,11 +394,11 @@ impl TermOutHandler {
if !filename.is_empty() { if !filename.is_empty() {
// append rules // append rules
for suffix in ["~", ".bak", ".bak2", ".old", ".1"] { for suffix in ["~", ".bak", ".bak2", ".old", ".1"] {
self.add_new_url_to_vec(url, &format!("{}{}", filename, suffix), &mut urls); self.add_new_url_to_vec(url, &format!("{filename}{suffix}"), &mut urls);
} }
// vim swap rule // vim swap rule
self.add_new_url_to_vec(url, &format!(".{}.swp", filename), &mut urls); self.add_new_url_to_vec(url, &format!(".{filename}.swp"), &mut urls);
// replace original extension rule // replace original extension rule
let parts: Vec<_> = filename let parts: Vec<_> = filename
@@ -432,7 +432,7 @@ mod tests {
config, config,
receiver: rx, receiver: rx,
}; };
println!("{:?}", foh); println!("{foh:?}");
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
@@ -451,7 +451,7 @@ mod tests {
handles: Some(handles), handles: Some(handles),
}; };
println!("{:?}", toh); println!("{toh:?}");
tx.send(Command::Exit).unwrap(); tx.send(Command::Exit).unwrap();
} }

View File

@@ -6,7 +6,7 @@ use tokio::sync::{mpsc, Semaphore};
use crate::{ use crate::{
response::FeroxResponse, response::FeroxResponse,
scan_manager::{FeroxScan, FeroxScans, ScanOrder}, scan_manager::{FeroxScan, FeroxScans, ScanOrder},
scanner::FeroxScanner, scanner::{FeroxScanner, RESPONSES},
statistics::StatField::TotalScans, statistics::StatField::TotalScans,
url::FeroxUrl, url::FeroxUrl,
utils::should_deny_url, utils::should_deny_url,
@@ -218,7 +218,7 @@ impl ScanHandler {
// current number of requests expected per scan // current number of requests expected per scan
// ExpectedPerScan and TotalExpected are a += action, so we need the wordlist length to // ExpectedPerScan and TotalExpected are a += action, so we need the wordlist length to
// update them while the other updates use expected_num_requests_per_dir // update them while the other updates use expected_num_requests_per_dir
let num_words = self.get_wordlist()?.len(); let num_words = self.get_wordlist(0)?.len();
let current_expectation = self.handles.expected_num_requests_per_dir() as u64; let current_expectation = self.handles.expected_num_requests_per_dir() as u64;
// used in the calculation of bar width down below, see explanation there // used in the calculation of bar width down below, see explanation there
@@ -290,10 +290,19 @@ impl ScanHandler {
} }
/// Helper to easily get the (locked) underlying wordlist /// Helper to easily get the (locked) underlying wordlist
pub fn get_wordlist(&self) -> Result<Arc<Vec<String>>> { pub fn get_wordlist(&self, offset: usize) -> Result<Arc<Vec<String>>> {
if let Ok(guard) = self.wordlist.lock().as_ref() { if let Ok(guard) = self.wordlist.lock().as_ref() {
if let Some(list) = guard.as_ref() { if let Some(list) = guard.as_ref() {
return Ok(list.clone()); return if offset > 0 {
// the offset could be off a bit, so we'll adjust it backwards by 10%
// of the overall wordlist size to ensure we don't miss any words
// (hopefully)
let adjusted_offset = offset - ((offset as f64 * 0.10) as usize);
Ok(Arc::new(list[adjusted_offset..].to_vec()))
} else {
Ok(list.clone())
};
} }
} }
@@ -328,7 +337,7 @@ impl ScanHandler {
continue; continue;
} }
let list = self.get_wordlist()?; let list = self.get_wordlist(scan.requests() as usize)?;
log::info!("scan handler received {} - beginning scan", target); log::info!("scan handler received {} - beginning scan", target);
@@ -386,6 +395,58 @@ impl ScanHandler {
return Ok(()); return Ok(());
} }
if let Ok(responses) = RESPONSES.responses.read() {
for maybe_wild in responses.iter() {
if !maybe_wild.wildcard() || !maybe_wild.is_directory() {
// if the stored response isn't a wildcard, skip it
// if the stored response isn't a directory, skip it
// we're only interested in preventing recursion into wildcard directories
continue;
}
if maybe_wild.method() != response.method() {
// methods don't match, skip it
continue;
}
// methods match and is a directory wildcard
// need to check the wildcard's parent directory
// for equality with the incoming response's parent directory
//
// if the parent directories match, we need to prevent recursion
// into the wildcard directory
match (
maybe_wild.url().path_segments(),
response.url().path_segments(),
) {
// both urls must have path segments
(Some(mut maybe_wild_segments), Some(mut response_segments)) => {
match (
maybe_wild_segments.nth_back(1),
response_segments.nth_back(1),
) {
// both urls must have at least 2 path segments, the next to last being the parent
(Some(maybe_wild_parent), Some(response_parent)) => {
if maybe_wild_parent == response_parent {
// the parent directories match, so we need to prevent recursion
return Ok(());
}
}
_ => {
// we couldn't get the parent directory, so we'll skip this
continue;
}
}
}
_ => {
// we couldn't get the path segments, so we'll skip this
continue;
}
}
}
}
let targets = vec![response.url().to_string()]; let targets = vec![response.url().to_string()];
self.ordered_scan_url(targets, ScanOrder::Latest).await?; self.ordered_scan_url(targets, ScanOrder::Latest).await?;

View File

@@ -115,8 +115,9 @@ impl StatsHandler {
} }
} }
Command::AddToF64Field(field, value) => self.stats.update_f64_field(field, value), Command::AddToF64Field(field, value) => self.stats.update_f64_field(field, value),
Command::CreateBar => { Command::CreateBar(offset) => {
self.bar = add_bar("", self.stats.total_expected() as u64, BarType::Total); self.bar = add_bar("", self.stats.total_expected() as u64, BarType::Total);
self.bar.set_position(offset);
} }
Command::LoadStats(filename) => { Command::LoadStats(filename) => {
self.stats.merge_from(&filename)?; self.stats.merge_from(&filename)?;

View File

@@ -362,7 +362,7 @@ impl<'a> Extractor<'a> {
// this isn't the last index of the parts array // this isn't the last index of the parts array
// ex: /buried/misc/stupidfile.php // ex: /buried/misc/stupidfile.php
// this block skips the file but sees all parent folders // this block skips the file but sees all parent folders
possible_path = format!("{}/", possible_path); possible_path = format!("{possible_path}/");
} }
paths.push(possible_path); // good sub-path found paths.push(possible_path); // good sub-path found
@@ -395,9 +395,9 @@ impl<'a> Extractor<'a> {
let new_url = old_url let new_url = old_url
.join(link) .join(link)
.with_context(|| format!("Could not join {} with {}", old_url, link))?; .with_context(|| format!("Could not join {old_url} with {link}"))?;
if old_url.domain() != new_url.domain() || old_url.host() != old_url.host() { if old_url.domain() != new_url.domain() || old_url.host() != new_url.host() {
// domains/ips are not the same, don't scan things that aren't part of the original // domains/ips are not the same, don't scan things that aren't part of the original
// target url // target url
log::debug!( log::debug!(

View File

@@ -312,7 +312,7 @@ async fn request_robots_txt_without_proxy() -> Result<()> {
let resp = extractor.make_extract_request("/robots.txt").await?; let resp = extractor.make_extract_request("/robots.txt").await?;
assert!(matches!(resp.status(), &StatusCode::OK)); assert!(matches!(resp.status(), &StatusCode::OK));
println!("{}", resp); println!("{resp}");
assert_eq!(resp.content_length(), 14); assert_eq!(resp.content_length(), 14);
assert_eq!(mock.hits(), 1); assert_eq!(mock.hits(), 1);
Ok(()) Ok(())

View File

@@ -3,16 +3,16 @@ use std::sync::RwLock;
use anyhow::Result; use anyhow::Result;
use serde::{ser::SerializeSeq, Serialize, Serializer}; use serde::{ser::SerializeSeq, Serialize, Serializer};
use crate::{ use crate::response::FeroxResponse;
event_handlers::Command::AddToUsizeField, response::FeroxResponse,
statistics::StatField::WildcardsFiltered, CommandSender,
};
use super::{ use super::{
FeroxFilter, LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter, FeroxFilter, LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter,
WildcardFilter, WordsFilter, WildcardFilter, WordsFilter,
}; };
use crate::{
event_handlers::Command::AddToUsizeField, statistics::StatField::WildcardsFiltered,
CommandSender,
};
/// Container around a collection of `FeroxFilters`s /// Container around a collection of `FeroxFilters`s
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct FeroxFilters { pub struct FeroxFilters {
@@ -76,6 +76,7 @@ impl FeroxFilters {
for filter in filters.iter() { for filter in filters.iter() {
// wildcard.should_filter goes here // wildcard.should_filter goes here
if filter.should_filter_response(response) { if filter.should_filter_response(response) {
log::debug!("filtering response due to: {:?}", filter);
if filter.as_any().downcast_ref::<WildcardFilter>().is_some() { if filter.as_any().downcast_ref::<WildcardFilter>().is_some() {
tx_stats tx_stats
.send(AddToUsizeField(WildcardsFiltered, 1)) .send(AddToUsizeField(WildcardsFiltered, 1))
@@ -104,6 +105,10 @@ impl Serialize for FeroxFilters {
seq.serialize_element(word_filter).unwrap_or_default(); seq.serialize_element(word_filter).unwrap_or_default();
} else if let Some(size_filter) = filter.as_any().downcast_ref::<SizeFilter>() { } else if let Some(size_filter) = filter.as_any().downcast_ref::<SizeFilter>() {
seq.serialize_element(size_filter).unwrap_or_default(); seq.serialize_element(size_filter).unwrap_or_default();
} else if let Some(wildcard_filter) =
filter.as_any().downcast_ref::<WildcardFilter>()
{
seq.serialize_element(wildcard_filter).unwrap_or_default();
} else if let Some(status_filter) = } else if let Some(status_filter) =
filter.as_any().downcast_ref::<StatusCodeFilter>() filter.as_any().downcast_ref::<StatusCodeFilter>()
{ {
@@ -114,10 +119,6 @@ impl Serialize for FeroxFilters {
filter.as_any().downcast_ref::<SimilarityFilter>() filter.as_any().downcast_ref::<SimilarityFilter>()
{ {
seq.serialize_element(similarity_filter).unwrap_or_default(); seq.serialize_element(similarity_filter).unwrap_or_default();
} else if let Some(wildcard_filter) =
filter.as_any().downcast_ref::<WildcardFilter>()
{
seq.serialize_element(wildcard_filter).unwrap_or_default();
} }
} }
seq.end() seq.end()

View File

@@ -4,21 +4,20 @@ use std::any::Any;
use std::fmt::Debug; use std::fmt::Debug;
use crate::response::FeroxResponse; use crate::response::FeroxResponse;
use crate::traits::{FeroxFilter, FeroxSerialize}; use crate::traits::FeroxFilter;
pub use self::container::FeroxFilters; pub use self::container::FeroxFilters;
pub(crate) use self::empty::EmptyFilter; pub(crate) use self::empty::EmptyFilter;
pub use self::init::initialize; pub use self::init::initialize;
pub use self::lines::LinesFilter; pub use self::lines::LinesFilter;
pub use self::regex::RegexFilter; pub use self::regex::RegexFilter;
pub use self::similarity::SimilarityFilter; pub use self::similarity::{SimilarityFilter, SIM_HASHER};
pub use self::size::SizeFilter; pub use self::size::SizeFilter;
pub use self::status_code::StatusCodeFilter; pub use self::status_code::StatusCodeFilter;
pub(crate) use self::utils::{create_similarity_filter, filter_lookup}; pub(crate) use self::utils::{create_similarity_filter, filter_lookup};
pub use self::wildcard::WildcardFilter; pub use self::wildcard::WildcardFilter;
pub use self::words::WordsFilter; pub use self::words::WordsFilter;
mod wildcard;
mod status_code; mod status_code;
mod words; mod words;
mod lines; mod lines;
@@ -30,4 +29,5 @@ mod container;
mod tests; mod tests;
mod init; mod init;
mod utils; mod utils;
mod wildcard;
mod empty; mod empty;

View File

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

View File

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

View File

@@ -1,11 +1,12 @@
use super::FeroxFilter; use super::FeroxFilter;
use super::SimilarityFilter; use super::SimilarityFilter;
use crate::event_handlers::Handles; use crate::event_handlers::Handles;
use crate::filters::similarity::SIM_HASHER;
use crate::nlp::preprocess;
use crate::response::FeroxResponse; use crate::response::FeroxResponse;
use crate::utils::logged_request; use crate::utils::logged_request;
use crate::{DEFAULT_METHOD, SIMILARITY_THRESHOLD}; use crate::DEFAULT_METHOD;
use anyhow::Result; use anyhow::Result;
use fuzzyhash::FuzzyHash;
use regex::Regex; use regex::Regex;
use reqwest::Url; use reqwest::Url;
use std::sync::Arc; use std::sync::Arc;
@@ -40,12 +41,10 @@ pub(crate) async fn create_similarity_filter(
fr.parse_extension(handles.clone())?; fr.parse_extension(handles.clone())?;
} }
// hash the response body and store the resulting hash in the filter object let hash = SIM_HASHER.create_signature(preprocess(fr.text()).iter());
let hash = FuzzyHash::new(fr.text()).to_string();
Ok(SimilarityFilter { Ok(SimilarityFilter {
hash, hash,
threshold: SIMILARITY_THRESHOLD,
original_url: similarity_filter.to_string(), original_url: similarity_filter.to_string(),
}) })
} }
@@ -95,8 +94,7 @@ pub(crate) fn filter_lookup(filter_type: &str, filter_value: &str) -> Option<Box
} }
"similarity" => { "similarity" => {
return Some(Box::new(SimilarityFilter { return Some(Box::new(SimilarityFilter {
hash: String::new(), hash: 0,
threshold: SIMILARITY_THRESHOLD,
original_url: filter_value.to_string(), original_url: filter_value.to_string(),
})); }));
} }
@@ -157,8 +155,7 @@ mod tests {
assert_eq!( assert_eq!(
filter.as_any().downcast_ref::<SimilarityFilter>().unwrap(), filter.as_any().downcast_ref::<SimilarityFilter>().unwrap(),
&SimilarityFilter { &SimilarityFilter {
hash: String::new(), hash: 0,
threshold: SIMILARITY_THRESHOLD,
original_url: "http://localhost".to_string() original_url: "http://localhost".to_string()
} }
); );
@@ -195,8 +192,7 @@ mod tests {
assert_eq!( assert_eq!(
filter, filter,
SimilarityFilter { SimilarityFilter {
hash: "3:YKEpn:Yfp".to_string(), hash: 14897447612059286329,
threshold: SIMILARITY_THRESHOLD,
original_url: srv.url("/") original_url: srv.url("/")
} }
); );

View File

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

View File

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

View File

@@ -52,9 +52,6 @@ pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION");
/// Maximum number of file descriptors that can be opened during a scan /// Maximum number of file descriptors that can be opened during a scan
pub const DEFAULT_OPEN_FILE_LIMIT: u64 = 8192; pub const DEFAULT_OPEN_FILE_LIMIT: u64 = 8192;
/// Default value used to determine near-duplicate web pages (equivalent to 95%)
pub const SIMILARITY_THRESHOLD: u32 = 95;
/// Default set of extensions to Ignore when auto-collecting extensions during scans /// Default set of extensions to Ignore when auto-collecting extensions during scans
pub(crate) const DEFAULT_IGNORED_EXTENSIONS: [&str; 38] = [ pub(crate) const DEFAULT_IGNORED_EXTENSIONS: [&str; 38] = [
"tif", "tiff", "ico", "cur", "bmp", "webp", "svg", "png", "jpg", "jpeg", "jfif", "gif", "avif", "tif", "tiff", "ico", "cur", "bmp", "webp", "svg", "png", "jpg", "jpeg", "jfif", "gif", "avif",
@@ -85,29 +82,73 @@ pub(crate) const SLEEP_DURATION: u64 = 500;
/// The percentage of requests as errors it takes to be deemed too high /// The percentage of requests as errors it takes to be deemed too high
pub const HIGH_ERROR_RATIO: f64 = 0.90; pub const HIGH_ERROR_RATIO: f64 = 0.90;
/// Default list of status codes to report /// Default list of status codes to report (all of them)
/// pub const DEFAULT_STATUS_CODES: [StatusCode; 60] = [
/// * 200 Ok // all 1XX response codes
/// * 204 No Content StatusCode::CONTINUE,
/// * 301 Moved Permanently StatusCode::SWITCHING_PROTOCOLS,
/// * 302 Found StatusCode::PROCESSING,
/// * 307 Temporary Redirect // all 2XX response codes
/// * 308 Permanent Redirect
/// * 401 Unauthorized
/// * 403 Forbidden
/// * 405 Method Not Allowed
/// * 500 Internal Server Error
pub const DEFAULT_STATUS_CODES: [StatusCode; 10] = [
StatusCode::OK, StatusCode::OK,
StatusCode::CREATED,
StatusCode::ACCEPTED,
StatusCode::NON_AUTHORITATIVE_INFORMATION,
StatusCode::NO_CONTENT, StatusCode::NO_CONTENT,
StatusCode::RESET_CONTENT,
StatusCode::PARTIAL_CONTENT,
StatusCode::MULTI_STATUS,
StatusCode::ALREADY_REPORTED,
StatusCode::IM_USED,
// all 3XX response codes
StatusCode::MULTIPLE_CHOICES,
StatusCode::MOVED_PERMANENTLY, StatusCode::MOVED_PERMANENTLY,
StatusCode::FOUND, StatusCode::FOUND,
StatusCode::SEE_OTHER,
StatusCode::NOT_MODIFIED,
StatusCode::USE_PROXY,
StatusCode::TEMPORARY_REDIRECT, StatusCode::TEMPORARY_REDIRECT,
StatusCode::PERMANENT_REDIRECT, StatusCode::PERMANENT_REDIRECT,
// all 4XX response codes
StatusCode::BAD_REQUEST,
StatusCode::UNAUTHORIZED, StatusCode::UNAUTHORIZED,
StatusCode::PAYMENT_REQUIRED,
StatusCode::FORBIDDEN, StatusCode::FORBIDDEN,
StatusCode::NOT_FOUND,
StatusCode::METHOD_NOT_ALLOWED, StatusCode::METHOD_NOT_ALLOWED,
StatusCode::NOT_ACCEPTABLE,
StatusCode::PROXY_AUTHENTICATION_REQUIRED,
StatusCode::REQUEST_TIMEOUT,
StatusCode::CONFLICT,
StatusCode::GONE,
StatusCode::LENGTH_REQUIRED,
StatusCode::PRECONDITION_FAILED,
StatusCode::PAYLOAD_TOO_LARGE,
StatusCode::URI_TOO_LONG,
StatusCode::UNSUPPORTED_MEDIA_TYPE,
StatusCode::RANGE_NOT_SATISFIABLE,
StatusCode::EXPECTATION_FAILED,
StatusCode::IM_A_TEAPOT,
StatusCode::MISDIRECTED_REQUEST,
StatusCode::UNPROCESSABLE_ENTITY,
StatusCode::LOCKED,
StatusCode::FAILED_DEPENDENCY,
StatusCode::UPGRADE_REQUIRED,
StatusCode::PRECONDITION_REQUIRED,
StatusCode::TOO_MANY_REQUESTS,
StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE,
StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS,
// all 5XX response codes
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
StatusCode::NOT_IMPLEMENTED,
StatusCode::BAD_GATEWAY,
StatusCode::SERVICE_UNAVAILABLE,
StatusCode::GATEWAY_TIMEOUT,
StatusCode::HTTP_VERSION_NOT_SUPPORTED,
StatusCode::VARIANT_ALSO_NEGOTIATES,
StatusCode::INSUFFICIENT_STORAGE,
StatusCode::LOOP_DETECTED,
StatusCode::NOT_EXTENDED,
StatusCode::NETWORK_AUTHENTICATION_REQUIRED,
]; ];
/// Default method for requests /// Default method for requests

View File

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

View File

@@ -1,11 +1,14 @@
use std::io::stdin; use std::io::stdin;
use std::{ use std::{
env::args, env::{
args,
consts::{ARCH, OS},
},
fs::{create_dir, remove_file, File}, fs::{create_dir, remove_file, File},
io::{stderr, BufRead, BufReader}, io::{stderr, BufRead, BufReader},
ops::Index, ops::Index,
path::Path, path::Path,
process::Command, process::{exit, Command},
sync::{atomic::Ordering, Arc}, sync::{atomic::Ordering, Arc},
}; };
@@ -38,17 +41,19 @@ use feroxbuster::{
use feroxbuster::{utils::set_open_file_limit, DEFAULT_OPEN_FILE_LIMIT}; use feroxbuster::{utils::set_open_file_limit, DEFAULT_OPEN_FILE_LIMIT};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use regex::Regex; use regex::Regex;
use self_update::cargo_crate_version;
lazy_static! { lazy_static! {
/// Limits the number of parallel scans active at any given time when using --parallel /// Limits the number of parallel scans active at any given time when using --parallel
static ref PARALLEL_LIMITER: Semaphore = Semaphore::new(0); static ref PARALLEL_LIMITER: Semaphore = Semaphore::new(0);
} }
/// Create a HashSet of Strings from the given wordlist then stores it inside an Arc /// Create a Vec of Strings from the given wordlist then stores it inside an Arc
fn get_unique_words_from_wordlist(path: &str) -> Result<Arc<Vec<String>>> { fn get_unique_words_from_wordlist(path: &str) -> Result<Arc<Vec<String>>> {
log::trace!("enter: get_unique_words_from_wordlist({})", path); log::trace!("enter: get_unique_words_from_wordlist({})", path);
let mut trimmed_word = false;
let file = File::open(path).with_context(|| format!("Could not open {}", path))?; let file = File::open(path).with_context(|| format!("Could not open {path}"))?;
let reader = BufReader::new(file); let reader = BufReader::new(file);
@@ -61,12 +66,21 @@ fn get_unique_words_from_wordlist(path: &str) -> Result<Arc<Vec<String>>> {
for line in reader.lines() { for line in reader.lines() {
line.map(|result| { line.map(|result| {
if !result.starts_with('#') && !result.is_empty() { if !result.starts_with('#') && !result.is_empty() {
words.push(result); if result.starts_with('/') {
words.push(result.trim_start_matches('/').to_string());
trimmed_word = true;
} else {
words.push(result);
}
} }
}) })
.ok(); .ok();
} }
if trimmed_word {
log::warn!("Some words in the wordlist started with a leading forward-slash; those words were trimmed (i.e. /word -> word)");
}
log::trace!( log::trace!(
"exit: get_unique_words_from_wordlist -> Arc<wordlist[{} words...]>", "exit: get_unique_words_from_wordlist -> Arc<wordlist[{} words...]>",
words.len() words.len()
@@ -92,8 +106,20 @@ async fn scan(targets: Vec<String>, handles: Arc<Handles>) -> Result<()> {
// having been set, makes it so the progress bar doesn't flash as full before anything has // having been set, makes it so the progress bar doesn't flash as full before anything has
// even happened // even happened
if matches!(handles.config.output_level, OutputLevel::Default) { if matches!(handles.config.output_level, OutputLevel::Default) {
let mut total_offset = 0;
if let Ok(guard) = handles.scans.read() {
if let Some(handle) = &*guard {
if let Ok(scans) = handle.data.scans.read() {
for scan in scans.iter() {
total_offset += scan.requests_made_so_far();
}
}
}
}
// only create the bar if no --silent|--quiet // only create the bar if no --silent|--quiet
handles.stats.send(CreateBar)?; handles.stats.send(CreateBar(total_offset))?;
// blocks until the bar is created / avoids race condition in first two bars // blocks until the bar is created / avoids race condition in first two bars
handles.stats.sync().await?; handles.stats.sync().await?;
@@ -172,7 +198,7 @@ async fn get_targets(handles: Arc<Handles>) -> Result<Vec<String>> {
if !target.starts_with("http") && !target.starts_with("https") { if !target.starts_with("http") && !target.starts_with("https") {
// --url hackerone.com // --url hackerone.com
*target = format!("https://{}", target); *target = format!("https://{target}");
} }
} }
@@ -197,6 +223,30 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
PROGRESS_BAR.join().unwrap(); PROGRESS_BAR.join().unwrap();
}); });
// check if update_app is true
if config.update_app {
let target_os = format!("{}-{}", ARCH, OS);
tokio::task::spawn_blocking(move || {
let status = self_update::backends::github::Update::configure()
.repo_owner("epi052")
.repo_name("feroxbuster")
.bin_name("feroxbuster")
.target(target_os.as_str())
.show_download_progress(true)
.current_version(cargo_crate_version!())
.build()
.unwrap()
.update()
.unwrap();
println!("Updated version: `{}`!", status.version());
})
.await
.unwrap();
exit(0);
}
// cloning an Arc is cheap (it's basically a pointer into the heap) // cloning an Arc is cheap (it's basically a pointer into the heap)
// so that will allow for cheap/safe sharing of a single wordlist across multi-target scans // so that will allow for cheap/safe sharing of a single wordlist across multi-target scans
// as well as additional directories found as part of recursion // as well as additional directories found as part of recursion
@@ -328,7 +378,7 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
let new_folder = slugify_filename(&base_name.to_string_lossy(), "", "logs"); let new_folder = slugify_filename(&base_name.to_string_lossy(), "", "logs");
let final_path = output_path.with_file_name(&new_folder); let final_path = output_path.with_file_name(new_folder);
// create the directory or fail silently, assuming the reason for failure is that // create the directory or fail silently, assuming the reason for failure is that
// the path exists already // the path exists already
@@ -459,7 +509,7 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
clean_up(handles, tasks).await?; clean_up(handles, tasks).await?;
bail!(fmt_err(&format!("Failed while scanning: {}", e))); bail!(fmt_err(&format!("Failed while scanning: {e}")));
} }
} }
@@ -526,7 +576,7 @@ fn main() -> Result<()> {
{ {
let future = wrapped_main(config.clone()); let future = wrapped_main(config.clone());
if let Err(e) = runtime.block_on(future) { if let Err(e) = runtime.block_on(future) {
eprintln!("{}", e); eprintln!("{e}");
// the code below is to facilitate testing tests/test_banner entries. Since it's an // the code below is to facilitate testing tests/test_banner entries. Since it's an
// integration test, normal test detection (cfg!(test), etc...) won't work. So, in // integration test, normal test detection (cfg!(test), etc...) won't work. So, in

View File

@@ -85,7 +85,7 @@ impl Document {
// at this point, we have a non-empty Text element with a non-script|style parent; // at this point, we have a non-empty Text element with a non-script|style parent;
// now we can return the trimmed up string // now we can return the trimmed up string
return Some(format!("{} ", trimmed)); return Some(format!("{trimmed} "));
} }
// not an Element node // not an Element node

View File

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

View File

@@ -4,7 +4,7 @@ use std::borrow::Cow;
/// pre-processing pipeline wrapper that removes punctuation, normalizes word case (utf-8 included) /// pre-processing pipeline wrapper that removes punctuation, normalizes word case (utf-8 included)
/// to lowercase, and remove stop words /// to lowercase, and remove stop words
pub(super) fn preprocess(text: &str) -> Vec<String> { pub(crate) fn preprocess(text: &str) -> Vec<String> {
let text = remove_punctuation(text); let text = remove_punctuation(text);
let text = normalize_case(text); let text = normalize_case(text);
let text = remove_stop_words(&text); let text = remove_stop_words(&text);

View File

@@ -40,7 +40,7 @@ pub fn initialize() -> Command {
Arg::new("url") Arg::new("url")
.short('u') .short('u')
.long("url") .long("url")
.required_unless_present_any(["stdin", "resume_from"]) .required_unless_present_any(["stdin", "resume_from", "update_app"])
.help_heading("Target selection") .help_heading("Target selection")
.value_name("URL") .value_name("URL")
.use_value_delimiter(true) .use_value_delimiter(true)
@@ -91,12 +91,14 @@ pub fn initialize() -> Command {
.long("smart") .long("smart")
.num_args(0) .num_args(0)
.help_heading("Composite settings") .help_heading("Composite settings")
.conflicts_with_all(["rate_limit", "auto_bail"])
.help("Set --extract-links, --auto-tune, --collect-words, and --collect-backups to true"), .help("Set --extract-links, --auto-tune, --collect-words, and --collect-backups to true"),
).arg( ).arg(
Arg::new("thorough") Arg::new("thorough")
.long("thorough") .long("thorough")
.num_args(0) .num_args(0)
.help_heading("Composite settings") .help_heading("Composite settings")
.conflicts_with_all(["rate_limit", "auto_bail"])
.help("Use the same settings as --smart and set --collect-extensions to true"), .help("Use the same settings as --smart and set --collect-extensions to true"),
); );
@@ -355,7 +357,7 @@ pub fn initialize() -> Command {
.use_value_delimiter(true) .use_value_delimiter(true)
.help_heading("Response filters") .help_heading("Response filters")
.help( .help(
"Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)", "Status Codes to include (allow list) (default: All Status Codes)",
), ),
); );
@@ -473,6 +475,7 @@ pub fn initialize() -> Command {
Arg::new("wordlist") Arg::new("wordlist")
.short('w') .short('w')
.long("wordlist") .long("wordlist")
.required_unless_present_any(["update_app"])
.value_hint(ValueHint::FilePath) .value_hint(ValueHint::FilePath)
.value_name("FILE") .value_name("FILE")
.help("Path to the wordlist") .help("Path to the wordlist")
@@ -607,6 +610,15 @@ pub fn initialize() -> Command {
.args(["debug_log", "output"]) .args(["debug_log", "output"])
.multiple(true), .multiple(true),
) )
.arg(
Arg::new("update_app")
.short('U')
.long("update")
.conflicts_with_all(["url", "wordlist"])
.num_args(0)
.help_heading("Update settings")
.help("Update the app to the latest version"),
)
.after_long_help(EPILOGUE); .after_long_help(EPILOGUE);
///////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////
@@ -637,8 +649,7 @@ fn valid_time_spec(time_spec: &str) -> Result<String, String> {
true => Ok(time_spec.to_string()), true => Ok(time_spec.to_string()),
false => { false => {
let msg = format!( let msg = format!(
"Expected a non-negative, whole number followed by s, m, h, or d (case insensitive); received {}", "Expected a non-negative, whole number followed by s, m, h, or d (case insensitive); received {time_spec}"
time_spec
); );
Err(msg) Err(msg)
} }

View File

@@ -223,6 +223,14 @@ impl FeroxResponse {
.with_context(|| "Could not parse body from response") .with_context(|| "Could not parse body from response")
.unwrap_or_default(); .unwrap_or_default();
// in the event that the content_length was 0, we can try to get the length
// of the body we just parsed. At worst, it's still 0; at best we've accounted
// for sites that reply without a content-length header and yet still have
// contents in the body.
//
// thanks to twitter use @f3rn0s for pointing out the possibility
let content_length = content_length.max(text.len() as u64);
let line_count = text.lines().count(); let line_count = text.lines().count();
let word_count = text.lines().map(|s| s.split_whitespace().count()).sum(); let word_count = text.lines().map(|s| s.split_whitespace().count()).sum();
@@ -415,7 +423,18 @@ impl FeroxSerialize for FeroxResponse {
.get("Location") .get("Location")
.unwrap() // known Some() already .unwrap() // known Some() already
.to_str() .to_str()
.unwrap_or("Unknown"); .unwrap_or("Unknown")
.to_string();
let loc = if loc.starts_with('/') {
if let Ok(joined) = self.url().join(&loc) {
joined.to_string()
} else {
loc
}
} else {
loc
};
// prettify the redirect target // prettify the redirect target
let loc = style(loc).yellow(); let loc = style(loc).yellow();
@@ -434,7 +453,7 @@ impl FeroxSerialize for FeroxResponse {
// create the base message // create the base message
let mut message = format!( let mut message = format!(
"{} {:>8} {:>8}l {:>8}w {:>8}c Got {} for {} (url length: {})\n", "{} {:>8} {:>8}l {:>8}w {:>8}c Got {} for {}\n",
wild_status, wild_status,
method, method,
lines, lines,
@@ -442,7 +461,6 @@ impl FeroxSerialize for FeroxResponse {
chars, chars,
status_colorizer(status), status_colorizer(status),
self.url(), self.url(),
FeroxUrl::path_length_of_url(&self.url)
); );
if self.status().is_redirection() { if self.status().is_redirection() {

View File

@@ -116,8 +116,8 @@ impl Menu {
let padded_name = pad_str(&name, longest, Alignment::Center, None); let padded_name = pad_str(&name, longest, Alignment::Center, None);
let header = format!("{}\n{}\n{}", border, padded_name, border); let header = format!("{border}\n{padded_name}\n{border}");
let footer = format!("{}\n{}", commands, border); let footer = format!("{commands}\n{border}");
Self { Self {
header, header,
@@ -174,7 +174,7 @@ impl Menu {
.to_string() .to_string()
.parse::<usize>() .parse::<usize>()
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
self.println(&format!("Found non-numeric input: {}: {:?}", e, value)); self.println(&format!("Found non-numeric input: {e}: {value:?}"));
0 0
}) })
} }
@@ -198,7 +198,7 @@ impl Menu {
if range.len() != 2 { if range.len() != 2 {
// expecting [1, 4] or similar, if a 0 was used, we'd be left with a vec of size 1 // expecting [1, 4] or similar, if a 0 was used, we'd be left with a vec of size 1
self.println(&format!("Found invalid range of scans: {}", value)); self.println(&format!("Found invalid range of scans: {value}"));
continue; continue;
} }
@@ -290,8 +290,7 @@ impl Menu {
/// Given a url, confirm with user that we should cancel /// Given a url, confirm with user that we should cancel
pub(super) fn confirm_cancellation(&self, url: &str) -> char { pub(super) fn confirm_cancellation(&self, url: &str) -> char {
self.println(&format!( self.println(&format!(
"You sure you wanna cancel this scan: {}? [Y/n]", "You sure you wanna cancel this scan: {url}? [Y/n]"
url
)); ));
self.term.read_char().unwrap_or('n') self.term.read_char().unwrap_or('n')

View File

@@ -44,6 +44,12 @@ pub struct FeroxScan {
/// Number of requests to populate the progress bar with /// Number of requests to populate the progress bar with
pub(super) num_requests: u64, pub(super) num_requests: u64,
/// Number of requests made so far, only used during deserialization
///
/// serialization: saves self.requests() to this field
/// deserialization: sets self.requests_made_so_far to this field
pub(super) requests_made_so_far: u64,
/// Status of this scan /// Status of this scan
pub status: Mutex<ScanStatus>, pub status: Mutex<ScanStatus>,
@@ -80,6 +86,7 @@ impl Default for FeroxScan {
task: sync::Mutex::new(None), // tokio mutex task: sync::Mutex::new(None), // tokio mutex
status: Mutex::new(ScanStatus::default()), status: Mutex::new(ScanStatus::default()),
num_requests: 0, num_requests: 0,
requests_made_so_far: 0,
scan_order: ScanOrder::Latest, scan_order: ScanOrder::Latest,
url: String::new(), url: String::new(),
normalized_url: String::new(), normalized_url: String::new(),
@@ -122,6 +129,11 @@ impl FeroxScan {
&self.url &self.url
} }
/// getter for number of requests made during previously saved scans (i.e. --resume-from used)
pub fn requests_made_so_far(&self) -> u64 {
self.requests_made_so_far
}
/// small wrapper to set the JoinHandle /// small wrapper to set the JoinHandle
pub async fn set_task(&self, task: JoinHandle<()>) -> Result<()> { pub async fn set_task(&self, task: JoinHandle<()>) -> Result<()> {
let mut guard = self.task.lock().await; let mut guard = self.task.lock().await;
@@ -162,6 +174,8 @@ impl FeroxScan {
let pb = add_bar(&self.url, self.num_requests, bar_type); let pb = add_bar(&self.url, self.num_requests, bar_type);
pb.reset_elapsed(); pb.reset_elapsed();
pb.set_position(self.requests_made_so_far);
let _ = std::mem::replace(&mut *guard, Some(pb.clone())); let _ = std::mem::replace(&mut *guard, Some(pb.clone()));
pb pb
@@ -233,6 +247,14 @@ impl FeroxScan {
false false
} }
/// small wrapper to inspect ScanStatus and see if it's Cancelled
pub fn is_cancelled(&self) -> bool {
if let Ok(guard) = self.status.lock() {
return matches!(*guard, ScanStatus::Cancelled);
}
false
}
/// await a task's completion, similar to a thread's join; perform necessary bookkeeping /// await a task's completion, similar to a thread's join; perform necessary bookkeeping
pub async fn join(&self) { pub async fn join(&self) {
log::trace!("enter join({:?})", self); log::trace!("enter join({:?})", self);
@@ -269,6 +291,7 @@ impl FeroxScan {
PolicyTrigger::Status403 => self.status_403s(), PolicyTrigger::Status403 => self.status_403s(),
PolicyTrigger::Status429 => self.status_429s(), PolicyTrigger::Status429 => self.status_429s(),
PolicyTrigger::Errors => self.errors(), PolicyTrigger::Errors => self.errors(),
PolicyTrigger::TryAdjustUp => 0,
} }
} }
@@ -345,6 +368,7 @@ impl Serialize for FeroxScan {
state.serialize_field("scan_type", &self.scan_type)?; state.serialize_field("scan_type", &self.scan_type)?;
state.serialize_field("status", &self.status)?; state.serialize_field("status", &self.status)?;
state.serialize_field("num_requests", &self.num_requests)?; state.serialize_field("num_requests", &self.num_requests)?;
state.serialize_field("requests_made_so_far", &self.requests())?;
state.end() state.end()
} }
@@ -403,6 +427,11 @@ impl<'de> Deserialize<'de> for FeroxScan {
scan.num_requests = num_requests; scan.num_requests = num_requests;
} }
} }
"requests_made_so_far" => {
if let Some(requests_made_so_far) = value.as_u64() {
scan.requests_made_so_far = requests_made_so_far;
}
}
_ => {} _ => {}
} }
} }
@@ -495,6 +524,7 @@ mod tests {
scan_type: ScanType::Directory, scan_type: ScanType::Directory,
scan_order: ScanOrder::Initial, scan_order: ScanOrder::Initial,
num_requests: 0, num_requests: 0,
requests_made_so_far: 0,
status: Mutex::new(ScanStatus::Running), status: Mutex::new(ScanStatus::Running),
task: Default::default(), task: Default::default(),
progress_bar: Mutex::new(None), progress_bar: Mutex::new(None),

View File

@@ -8,6 +8,7 @@ use crate::filters::{
use crate::traits::FeroxFilter; use crate::traits::FeroxFilter;
use crate::Command::AddFilter; use crate::Command::AddFilter;
use crate::{ use crate::{
banner::Banner,
config::OutputLevel, config::OutputLevel,
progress::PROGRESS_PRINTER, progress::PROGRESS_PRINTER,
progress::{add_bar, BarType}, progress::{add_bar, BarType},
@@ -138,6 +139,15 @@ impl FeroxScans {
let mut deser_scan: FeroxScan = let mut deser_scan: FeroxScan =
serde_json::from_value(scan.clone()).unwrap_or_default(); serde_json::from_value(scan.clone()).unwrap_or_default();
if deser_scan.is_cancelled() {
// if the scan was cancelled by the user, mark it as complete. This will
// prevent the scan from being resumed as well as prevent the wordlist
// from requesting it again
if let Ok(mut guard) = deser_scan.status.lock() {
*guard = ScanStatus::Complete;
}
}
// FeroxScans gets -q value from config as usual; the FeroxScans themselves // FeroxScans gets -q value from config as usual; the FeroxScans themselves
// rely on that value being passed in. If the user starts a scan without -q // rely on that value being passed in. If the user starts a scan without -q
// and resumes the scan but adds -q, FeroxScan will not have the proper value // and resumes the scan but adds -q, FeroxScan will not have the proper value
@@ -262,7 +272,7 @@ impl FeroxScans {
for (idx, _) in &matches { for (idx, _) in &matches {
for scan in guard.iter() { for scan in guard.iter() {
let slice = url.index(0..*idx); let slice = url.index(0..*idx);
if slice == scan.url || format!("{}/", slice).as_str() == scan.url { if slice == scan.url || format!("{slice}/").as_str() == scan.url {
log::trace!("enter: get_base_scan_by_url -> {}", scan); log::trace!("enter: get_base_scan_by_url -> {}", scan);
return Some(scan.clone()); return Some(scan.clone());
} }
@@ -327,7 +337,7 @@ impl FeroxScans {
} }
// we're only interested in displaying directory scans, as those are // we're only interested in displaying directory scans, as those are
// the only ones that make sense to be stopped // the only ones that make sense to be stopped
let scan_msg = format!("{:3}: {}", i, scan); let scan_msg = format!("{i:3}: {scan}");
self.menu.println(&scan_msg); self.menu.println(&scan_msg);
printed += 1; printed += 1;
} }
@@ -351,7 +361,7 @@ impl FeroxScans {
if num >= u_scans.len() { if num >= u_scans.len() {
// usize can't be negative, just need to handle exceeding bounds // usize can't be negative, just need to handle exceeding bounds
self.menu self.menu
.println(&format!("The number {} is not a valid choice.", num)); .println(&format!("The number {num} is not a valid choice."));
sleep(menu_pause_duration); sleep(menu_pause_duration);
continue; continue;
} }
@@ -441,6 +451,12 @@ impl FeroxScans {
}; };
self.menu.clear_screen(); self.menu.clear_screen();
let banner = Banner::new(&[handles.config.target_url.clone()], &handles.config);
banner
.print_to(&self.menu.term, handles.config.clone())
.unwrap_or_default();
self.menu.show_progress_bars(); self.menu.show_progress_bars();
result result

View File

@@ -58,7 +58,7 @@ impl FeroxState {
impl FeroxSerialize for FeroxState { impl FeroxSerialize for FeroxState {
/// Simply return debug format of FeroxState to satisfy as_str /// Simply return debug format of FeroxState to satisfy as_str
fn as_str(&self) -> String { fn as_str(&self) -> String {
format!("{:?}", self) format!("{self:?}")
} }
/// Simple call to produce a JSON string using the given FeroxState /// Simple call to produce a JSON string using the given FeroxState

View File

@@ -10,7 +10,7 @@ use crate::{
scanner::RESPONSES, scanner::RESPONSES,
statistics::Stats, statistics::Stats,
traits::FeroxSerialize, traits::FeroxSerialize,
SIMILARITY_THRESHOLD, SLEEP_DURATION, VERSION, SLEEP_DURATION, VERSION,
}; };
use indicatif::ProgressBar; use indicatif::ProgressBar;
use predicates::prelude::*; use predicates::prelude::*;
@@ -224,7 +224,7 @@ fn ferox_scan_get_progress_bar_when_none_is_set() {
/// given a JSON entry representing a FeroxScan, test that it deserializes into the proper type /// given a JSON entry representing a FeroxScan, test that it deserializes into the proper type
/// with the right attributes /// with the right attributes
fn ferox_scan_deserialize() { fn ferox_scan_deserialize() {
let fs_json = r#"{"id":"057016a14769414aac9a7a62707598cb","url":"https://spiritanimal.com","scan_type":"Directory","status":"Complete"}"#; let fs_json = r#"{"id":"057016a14769414aac9a7a62707598cb","url":"https://spiritanimal.com","scan_type":"Directory","status":"Complete","requests_made_so_far":500}"#;
let fs_json_two = r#"{"id":"057016a14769414aac9a7a62707598cb","url":"https://spiritanimal.com","scan_type":"Not Correct","status":"Cancelled"}"#; let fs_json_two = r#"{"id":"057016a14769414aac9a7a62707598cb","url":"https://spiritanimal.com","scan_type":"Not Correct","status":"Cancelled"}"#;
let fs_json_three = r#"{"id":"057016a14769414aac9a7a62707598cb","url":"https://spiritanimal.com","scan_type":"Not Correct","status":"","num_requests":42}"#; let fs_json_three = r#"{"id":"057016a14769414aac9a7a62707598cb","url":"https://spiritanimal.com","scan_type":"Not Correct","status":"","num_requests":42}"#;
@@ -246,9 +246,13 @@ fn ferox_scan_deserialize() {
ScanType::File => {} ScanType::File => {}
} }
match *fs.progress_bar.lock().unwrap() { match fs.progress_bar.lock() {
None => {} Ok(guard) => {
Some(_) => { if guard.is_some() {
panic!();
}
}
Err(_) => {
panic!(); panic!();
} }
} }
@@ -277,7 +281,7 @@ fn ferox_scan_serialize() {
None, None,
); );
let fs_json = format!( let fs_json = format!(
r#"{{"id":"{}","url":"https://spiritanimal.com","normalized_url":"https://spiritanimal.com/","scan_type":"Directory","status":"NotStarted","num_requests":0}}"#, r#"{{"id":"{}","url":"https://spiritanimal.com","normalized_url":"https://spiritanimal.com/","scan_type":"Directory","status":"NotStarted","num_requests":0,"requests_made_so_far":0}}"#,
fs.id fs.id
); );
assert_eq!(fs_json, serde_json::to_string(&*fs).unwrap()); assert_eq!(fs_json, serde_json::to_string(&*fs).unwrap());
@@ -296,7 +300,7 @@ fn ferox_scans_serialize() {
); );
let ferox_scans = FeroxScans::default(); let ferox_scans = FeroxScans::default();
let ferox_scans_json = format!( let ferox_scans_json = format!(
r#"[{{"id":"{}","url":"https://spiritanimal.com","normalized_url":"https://spiritanimal.com/","scan_type":"Directory","status":"NotStarted","num_requests":0}}]"#, r#"[{{"id":"{}","url":"https://spiritanimal.com","normalized_url":"https://spiritanimal.com/","scan_type":"Directory","status":"NotStarted","num_requests":0,"requests_made_so_far":0}}]"#,
ferox_scan.id ferox_scan.id
); );
ferox_scans.scans.write().unwrap().push(ferox_scan); ferox_scans.scans.write().unwrap().push(ferox_scan);
@@ -317,7 +321,7 @@ fn ferox_responses_serialize() {
// responses has a response now // responses has a response now
// serialized should be a list of responses // serialized should be a list of responses
let expected = format!("[{}]", json_response); let expected = format!("[{json_response}]");
let serialized = serde_json::to_string(&responses).unwrap(); let serialized = serde_json::to_string(&responses).unwrap();
assert_eq!(expected, serialized); assert_eq!(expected, serialized);
@@ -399,8 +403,7 @@ fn feroxstates_feroxserialize_implementation() {
.unwrap(); .unwrap();
filters filters
.push(Box::new(SimilarityFilter { .push(Box::new(SimilarityFilter {
hash: "3:YKEpn:Yfp".to_string(), hash: 1,
threshold: SIMILARITY_THRESHOLD,
original_url: "http://localhost:12345/".to_string(), original_url: "http://localhost:12345/".to_string(),
})) }))
.unwrap(); .unwrap();
@@ -426,11 +429,11 @@ fn feroxstates_feroxserialize_implementation() {
let json_state = ferox_state.as_json().unwrap(); let json_state = ferox_state.as_json().unwrap();
println!("echo '{}'|jq", json_state); // for debugging, if the test fails, can see what's going on println!("echo '{json_state}'|jq"); // for debugging, if the test fails, can see what's going on
for expected in [ for expected in [
r#""scans""#, r#""scans""#,
&format!(r#""id":"{}""#, saved_id), &format!(r#""id":"{saved_id}""#),
r#""url":"https://spiritanimal.com""#, r#""url":"https://spiritanimal.com""#,
r#""scan_type":"Directory""#, r#""scan_type":"Directory""#,
r#""status":"NotStarted""#, r#""status":"NotStarted""#,
@@ -442,8 +445,8 @@ fn feroxstates_feroxserialize_implementation() {
r#""proxy":"""#, r#""proxy":"""#,
r#""replay_proxy":"""#, r#""replay_proxy":"""#,
r#""target_url":"""#, r#""target_url":"""#,
r#""status_codes":[200,204,301,302,307,308,401,403,405,500]"#, r#""status_codes":[100,101,102,200,201,202,203,204,205,206,207,208,226,300,301,302,303,304,305,307,308,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,421,422,423,424,426,428,429,431,451,500,501,502,503,504,505,506,507,508,510,511,103,425]"#,
r#""replay_codes":[200,204,301,302,307,308,401,403,405,500]"#, r#""replay_codes":[100,101,102,200,201,202,203,204,205,206,207,208,226,300,301,302,303,304,305,307,308,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,421,422,423,424,426,428,429,431,451,500,501,502,503,504,505,506,507,508,510,511,103,425]"#,
r#""filter_status":[]"#, r#""filter_status":[]"#,
r#""threads":50"#, r#""threads":50"#,
r#""timeout":7"#, r#""timeout":7"#,
@@ -456,7 +459,7 @@ fn feroxstates_feroxserialize_implementation() {
r#""json":false"#, r#""json":false"#,
r#""output":"""#, r#""output":"""#,
r#""debug_log":"""#, r#""debug_log":"""#,
&format!(r#""user_agent":"feroxbuster/{}""#, VERSION), &format!(r#""user_agent":"feroxbuster/{VERSION}""#),
r#""random_agent":false"#, r#""random_agent":false"#,
r#""redirects":false"#, r#""redirects":false"#,
r#""insecure":false"#, r#""insecure":false"#,
@@ -499,7 +502,7 @@ fn feroxstates_feroxserialize_implementation() {
r#""collect_extensions":true"#, r#""collect_extensions":true"#,
r#""collect_backups":false"#, r#""collect_backups":false"#,
r#""collect_words":false"#, r#""collect_words":false"#,
r#""filters":[{"filter_code":100},{"word_count":200},{"content_length":300},{"line_count":400},{"compiled":".*","raw_string":".*"},{"hash":"3:YKEpn:Yfp","threshold":95,"original_url":"http://localhost:12345/"}]"#, r#""filters":[{"filter_code":100},{"word_count":200},{"content_length":300},{"line_count":400},{"compiled":".*","raw_string":".*"},{"hash":1,"original_url":"http://localhost:12345/"}]"#,
r#""collected_extensions":["php"]"#, r#""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":["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"]"#,
] ]
@@ -560,6 +563,7 @@ fn feroxscan_display() {
scan_order: ScanOrder::Latest, scan_order: ScanOrder::Latest,
scan_type: Default::default(), scan_type: Default::default(),
num_requests: 0, num_requests: 0,
requests_made_so_far: 0,
start_time: Instant::now(), start_time: Instant::now(),
output_level: OutputLevel::Default, output_level: OutputLevel::Default,
status_403s: Default::default(), status_403s: Default::default(),
@@ -570,26 +574,26 @@ fn feroxscan_display() {
errors: Default::default(), errors: Default::default(),
}; };
let not_started = format!("{}", scan); let not_started = format!("{scan}");
assert!(predicate::str::contains("not started") assert!(predicate::str::contains("not started")
.and(predicate::str::contains("localhost")) .and(predicate::str::contains("localhost"))
.eval(&not_started)); .eval(&not_started));
scan.set_status(ScanStatus::Complete).unwrap(); scan.set_status(ScanStatus::Complete).unwrap();
let complete = format!("{}", scan); let complete = format!("{scan}");
assert!(predicate::str::contains("complete") assert!(predicate::str::contains("complete")
.and(predicate::str::contains("localhost")) .and(predicate::str::contains("localhost"))
.eval(&complete)); .eval(&complete));
scan.set_status(ScanStatus::Cancelled).unwrap(); scan.set_status(ScanStatus::Cancelled).unwrap();
let cancelled = format!("{}", scan); let cancelled = format!("{scan}");
assert!(predicate::str::contains("cancelled") assert!(predicate::str::contains("cancelled")
.and(predicate::str::contains("localhost")) .and(predicate::str::contains("localhost"))
.eval(&cancelled)); .eval(&cancelled));
scan.set_status(ScanStatus::Running).unwrap(); scan.set_status(ScanStatus::Running).unwrap();
let running = format!("{}", scan); let running = format!("{scan}");
assert!(predicate::str::contains("running") assert!(predicate::str::contains("running")
.and(predicate::str::contains("localhost")) .and(predicate::str::contains("localhost"))
.eval(&running)); .eval(&running));
@@ -605,6 +609,7 @@ async fn ferox_scan_abort() {
scan_order: ScanOrder::Latest, scan_order: ScanOrder::Latest,
scan_type: Default::default(), scan_type: Default::default(),
num_requests: 0, num_requests: 0,
requests_made_so_far: 0,
start_time: Instant::now(), start_time: Instant::now(),
output_level: OutputLevel::Default, output_level: OutputLevel::Default,
status_403s: Default::default(), status_403s: Default::default(),
@@ -636,10 +641,7 @@ fn menu_print_header_and_footer() {
let menu_cmd_2 = MenuCmd::Cancel(vec![0], false); let menu_cmd_2 = MenuCmd::Cancel(vec![0], false);
let menu_cmd_res_1 = MenuCmdResult::Url(String::from("http://localhost")); let menu_cmd_res_1 = MenuCmdResult::Url(String::from("http://localhost"));
let menu_cmd_res_2 = MenuCmdResult::NumCancelled(2); let menu_cmd_res_2 = MenuCmdResult::NumCancelled(2);
println!( println!("{menu_cmd_1:?}{menu_cmd_2:?}{menu_cmd_res_1:?}{menu_cmd_res_2:?}");
"{:?}{:?}{:?}{:?}",
menu_cmd_1, menu_cmd_2, menu_cmd_res_1, menu_cmd_res_2
);
menu.clear_screen(); menu.clear_screen();
menu.print_header(); menu.print_header();
menu.print_footer(); menu.print_footer();
@@ -656,9 +658,9 @@ fn menu_get_command_input_from_user_returns_cancel() {
let force = idx % 2 == 0; let force = idx % 2 == 0;
let full_cmd = if force { let full_cmd = if force {
format!("{} -f {}\n", cmd, idx) format!("{cmd} -f {idx}\n")
} else { } else {
format!("{} {}\n", cmd, idx) format!("{cmd} {idx}\n")
}; };
let result = menu.get_command_input_from_user(&full_cmd).unwrap(); let result = menu.get_command_input_from_user(&full_cmd).unwrap();
@@ -683,7 +685,7 @@ fn menu_get_command_input_from_user_returns_add() {
for cmd in ["add", "Addd", "a", "A", "None"] { for cmd in ["add", "Addd", "a", "A", "None"] {
let test_url = "http://happyfuntimes.commmm"; let test_url = "http://happyfuntimes.commmm";
let full_cmd = format!("{} {}\n", cmd, test_url); let full_cmd = format!("{cmd} {test_url}\n");
if cmd != "None" { if cmd != "None" {
let result = menu.get_command_input_from_user(&full_cmd).unwrap(); let result = menu.get_command_input_from_user(&full_cmd).unwrap();

View File

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

View File

@@ -10,6 +10,7 @@ use lazy_static::lazy_static;
use tokio::sync::Semaphore; use tokio::sync::Semaphore;
use crate::filters::{create_similarity_filter, EmptyFilter, SimilarityFilter}; use crate::filters::{create_similarity_filter, EmptyFilter, SimilarityFilter};
use crate::heuristics::WildcardResult;
use crate::Command::AddFilter; use crate::Command::AddFilter;
use crate::{ use crate::{
event_handlers::{ event_handlers::{
@@ -182,6 +183,7 @@ impl FeroxScanner {
Err(e) => { Err(e) => {
log::warn!("error awaiting a response: {}", e); log::warn!("error awaiting a response: {}", e);
self.handles.stats.send(AddError(Other)).unwrap_or_default(); self.handles.stats.send(AddError(Other)).unwrap_or_default();
std::process::exit(1);
} }
} }
}); });
@@ -243,13 +245,9 @@ impl FeroxScanner {
} }
{ {
// heuristics test block // heuristics test block:
let test = heuristics::HeuristicTests::new(self.handles.clone()); let test = heuristics::HeuristicTests::new(self.handles.clone());
if let Ok(num_reqs) = test.wildcard(&self.target_url).await {
progress_bar.inc(num_reqs);
}
if let Ok(dirlist_result) = test.directory_listing(&self.target_url).await { if let Ok(dirlist_result) = test.directory_listing(&self.target_url).await {
if dirlist_result.is_some() { if dirlist_result.is_some() {
let dirlist_result = dirlist_result.unwrap(); let dirlist_result = dirlist_result.unwrap();
@@ -293,9 +291,34 @@ impl FeroxScanner {
ferox_scan.finish()?; ferox_scan.finish()?;
return Ok(()); return Ok(()); // nothing left to do if we found a dir listing
} }
} }
// now that we haven't found a directory listing, we'll attempt to derive whatever
// the server is using to respond to resources that don't exist (could be a
// traditional 404, or a custom response)
//
// `detect_404_like_responses` will make the requests that the wildcard test used to
// perform pre-2.8 in addition to new detection techniques, superseding the old
// wildcard test
let num_reqs_made = test.detect_404_like_responses(&self.target_url).await?;
match num_reqs_made {
Some(WildcardResult::WildcardDirectory(num_reqs)) => {
let message = format!(
"=> {} dir! {} recursion",
style("Wildcard").blue().bright(),
style("stopped").red()
);
progress_bar.set_message(&message);
progress_bar.inc(num_reqs as u64);
}
Some(WildcardResult::FourOhFourLike(num_reqs)) => {
progress_bar.inc(num_reqs as u64);
}
_ => {}
}
} }
// Arc clones to be passed around to the various scans // Arc clones to be passed around to the various scans
@@ -328,7 +351,7 @@ impl FeroxScanner {
log::info!( log::info!(
"requesting {} collected words: {:?}...", "requesting {} collected words: {:?}...",
new_words_len, new_words_len,
&new_words[..new_words_len.min(3) as usize] &new_words[..new_words_len.min(3)]
); );
self.stream_requests( self.stream_requests(

View File

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

View File

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

View File

@@ -1,10 +1,15 @@
use std::{ use std::{
cmp::max, cmp::max,
collections::HashSet, collections::HashSet,
sync::{self, atomic::Ordering, Arc, Mutex}, sync::{
self,
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
}; };
use anyhow::Result; use anyhow::Result;
use console::style;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use leaky_bucket::RateLimiter; use leaky_bucket::RateLimiter;
use tokio::{ use tokio::{
@@ -64,6 +69,8 @@ pub(super) struct Requester {
/// seen; this will satisfy the non-mut self constraint (due to us being behind an Arc, and /// seen; this will satisfy the non-mut self constraint (due to us being behind an Arc, and
/// the need for a counter) /// the need for a counter)
tuning_lock: Mutex<usize>, tuning_lock: Mutex<usize>,
policy_triggered: AtomicBool,
} }
/// Requester implementation /// Requester implementation
@@ -91,6 +98,7 @@ impl Requester {
handles: scanner.handles.clone(), handles: scanner.handles.clone(),
target_url: scanner.target_url.to_owned(), target_url: scanner.target_url.to_owned(),
tuning_lock: Mutex::new(0), tuning_lock: Mutex::new(0),
policy_triggered: AtomicBool::new(false),
}) })
} }
@@ -118,6 +126,7 @@ impl Requester {
atomic_store!(self.policy_data.cooling_down, true, Ordering::SeqCst); atomic_store!(self.policy_data.cooling_down, true, Ordering::SeqCst);
sleep(Duration::from_millis(self.policy_data.wait_time)).await; sleep(Duration::from_millis(self.policy_data.wait_time)).await;
self.ferox_scan.progress_bar().set_message("");
atomic_store!(self.policy_data.cooling_down, false, Ordering::SeqCst); atomic_store!(self.policy_data.cooling_down, false, Ordering::SeqCst);
} }
@@ -203,18 +212,34 @@ impl Requester {
*guard = 0; // reset streak counter to 0 *guard = 0; // reset streak counter to 0
if atomic_load!(self.policy_data.errors) != 0 { if atomic_load!(self.policy_data.errors) != 0 {
self.policy_data.adjust_down(); self.policy_data.adjust_down();
let styled_direction = style("reduced").red();
self.ferox_scan
.progress_bar()
.set_message(&format!("=> 🚦 {styled_direction} scan speed",));
} }
self.policy_data.set_errors(scan_errors); self.policy_data.set_errors(scan_errors);
} else { } else {
// errors can only be incremented, so an else is sufficient // errors can only be incremented, so an else is sufficient
*guard += 1; *guard += 1;
self.policy_data.adjust_up(&guard); self.policy_data.adjust_up(&guard);
let styled_direction = style("increased").green();
self.ferox_scan
.progress_bar()
.set_message(&format!("=> 🚦 {styled_direction} scan speed",));
} }
} }
if atomic_load!(self.policy_data.remove_limit) { if atomic_load!(self.policy_data.remove_limit) {
self.set_rate_limiter(None).await?; self.set_rate_limiter(None).await?;
atomic_store!(self.policy_data.remove_limit, false); atomic_store!(self.policy_data.remove_limit, false);
self.ferox_scan
.progress_bar()
.set_message("=> 🚦 removed rate limiter 🚀");
} else if create_limiter { } else if create_limiter {
// create_limiter is really just used for unit testing situations, it's true anytime // create_limiter is really just used for unit testing situations, it's true anytime
// during actual execution // during actual execution
@@ -253,8 +278,15 @@ impl Requester {
let reqs_sec = self.ferox_scan.requests_per_second() as usize; let reqs_sec = self.ferox_scan.requests_per_second() as usize;
self.policy_data.set_reqs_sec(reqs_sec); self.policy_data.set_reqs_sec(reqs_sec);
// set the flag to indicate that we have triggered the rate limiter
// at least once
atomic_store!(self.policy_triggered, true);
let new_limit = self.policy_data.get_limit(); let new_limit = self.policy_data.get_limit();
self.set_rate_limiter(Some(new_limit)).await?; self.set_rate_limiter(Some(new_limit)).await?;
self.ferox_scan
.progress_bar()
.set_message(&format!("=> 🚦 set rate limit ({new_limit}/s)"));
} }
self.adjust_limit(trigger, true).await?; self.adjust_limit(trigger, true).await?;
@@ -291,6 +323,14 @@ impl Requester {
let pb = self.ferox_scan.progress_bar(); let pb = self.ferox_scan.progress_bar();
let num_skipped = pb.length().saturating_sub(pb.position()) as usize; let num_skipped = pb.length().saturating_sub(pb.position()) as usize;
let styled_trigger = style(format!("{trigger:?}")).red();
pb.set_message(&format!(
"=> 💀 too many {} ({}) 💀 bailing",
styled_trigger,
self.ferox_scan.num_errors(trigger),
));
// update the overall scan bar by subtracting the number of skipped requests from // update the overall scan bar by subtracting the number of skipped requests from
// the total // the total
self.handles self.handles
@@ -357,6 +397,9 @@ impl Requester {
RequesterPolicy::AutoTune => { RequesterPolicy::AutoTune => {
if let Some(trigger) = self.should_enforce_policy() { if let Some(trigger) = self.should_enforce_policy() {
self.tune(trigger).await?; self.tune(trigger).await?;
} else if atomic_load!(self.policy_triggered) {
self.adjust_limit(PolicyTrigger::TryAdjustUp, true).await?;
self.cool_down().await;
} }
} }
RequesterPolicy::AutoBail => { RequesterPolicy::AutoBail => {
@@ -548,7 +591,7 @@ mod tests {
let scans = handles.ferox_scans().unwrap(); let scans = handles.ferox_scans().unwrap();
for _ in 0..num_errors { for _ in 0..num_errors {
scans.increment_error(format!("{}/", url).as_str()); scans.increment_error(format!("{url}/").as_str());
} }
} }
@@ -561,7 +604,7 @@ mod tests {
) { ) {
let scans = handles.ferox_scans().unwrap(); let scans = handles.ferox_scans().unwrap();
for _ in 0..num_errors { for _ in 0..num_errors {
scans.increment_status_code(format!("{}/", url).as_str(), code); scans.increment_status_code(format!("{url}/").as_str(), code);
} }
} }
@@ -627,6 +670,7 @@ mod tests {
PolicyTrigger::Errors => { PolicyTrigger::Errors => {
increment_scan_errors(handles.clone(), url, num_errors).await; increment_scan_errors(handles.clone(), url, num_errors).await;
} }
_ => {}
} }
assert_eq!(scan.num_errors(trigger), num_errors); assert_eq!(scan.num_errors(trigger), num_errors);
@@ -647,6 +691,7 @@ mod tests {
ferox_scan: Arc::new(FeroxScan::default()), ferox_scan: Arc::new(FeroxScan::default()),
rate_limiter: RwLock::new(None), rate_limiter: RwLock::new(None),
policy_data: Default::default(), policy_data: Default::default(),
policy_triggered: AtomicBool::new(false),
}; };
let ferox_scan = Arc::new(FeroxScan::default()); let ferox_scan = Arc::new(FeroxScan::default());
@@ -675,6 +720,7 @@ mod tests {
target_url: "http://localhost".to_string(), target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None), rate_limiter: RwLock::new(None),
policy_data: Default::default(), policy_data: Default::default(),
policy_triggered: AtomicBool::new(false),
}; };
increment_errors(requester.handles.clone(), ferox_scan.clone(), 25).await; increment_errors(requester.handles.clone(), ferox_scan.clone(), 25).await;
@@ -700,6 +746,7 @@ mod tests {
target_url: "http://localhost".to_string(), target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None), rate_limiter: RwLock::new(None),
policy_data: Default::default(), policy_data: Default::default(),
policy_triggered: AtomicBool::new(false),
}; };
increment_status_codes( increment_status_codes(
@@ -740,6 +787,7 @@ mod tests {
target_url: "http://localhost".to_string(), target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None), rate_limiter: RwLock::new(None),
policy_data: Default::default(), policy_data: Default::default(),
policy_triggered: AtomicBool::new(false),
}; };
increment_status_codes( increment_status_codes(
@@ -795,6 +843,7 @@ mod tests {
target_url: "http://one/one/stuff.php".to_string(), target_url: "http://one/one/stuff.php".to_string(),
rate_limiter: RwLock::new(None), rate_limiter: RwLock::new(None),
policy_data: Default::default(), policy_data: Default::default(),
policy_triggered: AtomicBool::new(false),
}; };
requester.bail(PolicyTrigger::Errors).await.unwrap(); requester.bail(PolicyTrigger::Errors).await.unwrap();
@@ -829,6 +878,7 @@ mod tests {
target_url: "http://one/one/stuff.php".to_string(), target_url: "http://one/one/stuff.php".to_string(),
rate_limiter: RwLock::new(None), rate_limiter: RwLock::new(None),
policy_data: Default::default(), policy_data: Default::default(),
policy_triggered: AtomicBool::new(false),
}; };
let result = requester.bail(PolicyTrigger::Status403).await; let result = requester.bail(PolicyTrigger::Status403).await;
@@ -851,6 +901,7 @@ mod tests {
target_url: "http://localhost".to_string(), target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None), rate_limiter: RwLock::new(None),
policy_data: Default::default(), policy_data: Default::default(),
policy_triggered: AtomicBool::new(false),
}; };
requester requester
@@ -874,6 +925,7 @@ mod tests {
target_url: "http://localhost".to_string(), target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None), rate_limiter: RwLock::new(None),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7), policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
policy_triggered: AtomicBool::new(false),
}); });
let start = Instant::now(); let start = Instant::now();
@@ -904,6 +956,7 @@ mod tests {
target_url: "http://localhost".to_string(), target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None), rate_limiter: RwLock::new(None),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7), policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
policy_triggered: AtomicBool::new(false),
}; };
requester.policy_data.set_reqs_sec(400); requester.policy_data.set_reqs_sec(400);
@@ -942,6 +995,7 @@ mod tests {
target_url: "http://localhost".to_string(), target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(Some(limiter)), rate_limiter: RwLock::new(Some(limiter)),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7), policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
policy_triggered: AtomicBool::new(false),
}; };
requester.policy_data.set_reqs_sec(400); requester.policy_data.set_reqs_sec(400);
@@ -979,6 +1033,7 @@ mod tests {
target_url: "http://localhost".to_string(), target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None), rate_limiter: RwLock::new(None),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7), policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
policy_triggered: AtomicBool::new(false),
}; };
requester.policy_data.set_reqs_sec(400); requester.policy_data.set_reqs_sec(400);
@@ -1007,6 +1062,7 @@ mod tests {
target_url: "http://localhost".to_string(), target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None), rate_limiter: RwLock::new(None),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7), policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
policy_triggered: AtomicBool::new(false),
}; };
assert!(!requester.too_many_status_errors(PolicyTrigger::Errors)); assert!(!requester.too_many_status_errors(PolicyTrigger::Errors));
@@ -1050,6 +1106,7 @@ mod tests {
target_url: "http://localhost".to_string(), target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(Some(limiter)), rate_limiter: RwLock::new(Some(limiter)),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7), policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
policy_triggered: AtomicBool::new(false),
}; };
requester.set_rate_limiter(Some(200)).await.unwrap(); requester.set_rate_limiter(Some(200)).await.unwrap();
@@ -1093,6 +1150,7 @@ mod tests {
target_url: "http://localhost".to_string(), target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(Some(limiter)), rate_limiter: RwLock::new(Some(limiter)),
policy_data: PolicyData::new(RequesterPolicy::AutoTune, 4), policy_data: PolicyData::new(RequesterPolicy::AutoTune, 4),
policy_triggered: AtomicBool::new(false),
}; };
let start = Instant::now(); let start = Instant::now();

View File

@@ -9,4 +9,7 @@ pub enum PolicyTrigger {
/// excessive general errors /// excessive general errors
Errors, Errors,
/// dummy error for upward rate adjustment
TryAdjustUp,
} }

View File

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

View File

@@ -4,6 +4,7 @@ use crate::filters::{
WordsFilter, WordsFilter,
}; };
use crate::response::FeroxResponse; use crate::response::FeroxResponse;
use crate::utils::status_colorizer;
use anyhow::Result; use anyhow::Result;
use crossterm::style::{style, Stylize}; use crossterm::style::{style, Stylize};
use serde::Serialize; use serde::Serialize;
@@ -38,11 +39,43 @@ impl Display for dyn FeroxFilter {
} else if let Some(filter) = self.as_any().downcast_ref::<RegexFilter>() { } else if let Some(filter) = self.as_any().downcast_ref::<RegexFilter>() {
write!(f, "Regex: {}", style(&filter.raw_string).cyan()) write!(f, "Regex: {}", style(&filter.raw_string).cyan())
} else if let Some(filter) = self.as_any().downcast_ref::<WildcardFilter>() { } else if let Some(filter) = self.as_any().downcast_ref::<WildcardFilter>() {
if filter.dynamic != u64::MAX { let mut msg = format!(
write!(f, "Dynamic wildcard: {}", style(filter.dynamic).cyan()) "{} requests with {} responses ",
} else { style(&filter.method).cyan(),
write!(f, "Static wildcard: {}", style(filter.size).cyan()) status_colorizer(&filter.status_code.to_string())
);
match (filter.content_length, filter.word_count, filter.line_count) {
(None, None, None) => {
unreachable!("wildcard filter without any filters set");
}
(None, None, Some(lc)) => {
msg.push_str(&format!("containing {} lines", lc));
}
(None, Some(wc), None) => {
msg.push_str(&format!("containing {} words", wc));
}
(None, Some(wc), Some(lc)) => {
msg.push_str(&format!("containing {} words and {} lines", wc, lc));
}
(Some(cl), None, None) => {
msg.push_str(&format!("containing {} bytes", cl));
}
(Some(cl), None, Some(lc)) => {
msg.push_str(&format!("containing {} bytes and {} lines", cl, lc));
}
(Some(cl), Some(wc), None) => {
msg.push_str(&format!("containing {} bytes and {} words", cl, wc));
}
(Some(cl), Some(wc), Some(lc)) => {
msg.push_str(&format!(
"containing {} bytes, {} words, and {} lines",
cl, wc, lc
));
}
} }
write!(f, "{}", msg)
} else if let Some(filter) = self.as_any().downcast_ref::<StatusCodeFilter>() { } else if let Some(filter) = self.as_any().downcast_ref::<StatusCodeFilter>() {
write!(f, "Status code: {}", style(filter.filter_code).cyan()) write!(f, "Status code: {}", style(filter.filter_code).cyan())
} else if let Some(filter) = self.as_any().downcast_ref::<SimilarityFilter>() { } else if let Some(filter) = self.as_any().downcast_ref::<SimilarityFilter>() {
@@ -52,7 +85,7 @@ impl Display for dyn FeroxFilter {
style(&filter.original_url).cyan() style(&filter.original_url).cyan()
) )
} else { } else {
write!(f, "Filter: {:?}", self) write!(f, "Filter: {self:?}")
} }
} }
} }

View File

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

View File

@@ -41,7 +41,7 @@ pub fn open_file(filename: &str) -> Result<BufWriter<fs::File>> {
.create(true) .create(true)
.append(true) .append(true)
.open(filename) .open(filename)
.with_context(|| fmt_err(&format!("Could not open {}", filename)))?; .with_context(|| fmt_err(&format!("Could not open {filename}")))?;
let writer = BufWriter::new(file); // std io let writer = BufWriter::new(file); // std io
@@ -104,7 +104,7 @@ pub fn ferox_print(msg: &str, bar: &ProgressBar) {
bar.println(msg); bar.println(msg);
} else { } else {
let stripped = strip_ansi_codes(msg); let stripped = strip_ansi_codes(msg);
println!("{}", stripped); println!("{stripped}");
} }
} }
@@ -265,19 +265,17 @@ pub fn create_report_string(
) -> String { ) -> String {
if matches!(output_level, OutputLevel::Silent) { if matches!(output_level, OutputLevel::Silent) {
// --silent used, just need the url // --silent used, just need the url
format!("{}\n", url) format!("{url}\n")
} else { } else {
// normal printing with status and sizes // normal printing with status and sizes
let color_status = status_colorizer(status); let color_status = status_colorizer(status);
if status.contains("MSG") { if status.contains("MSG") {
format!( format!(
"{} {:>8} {:>9} {:>9} {:>9} {}\n", "{color_status} {method:>8} {line_count:>9} {word_count:>9} {content_length:>9} {url}\n"
color_status, method, line_count, word_count, content_length, url
) )
} else { } else {
format!( format!(
"{} {:>8} {:>8}l {:>8}w {:>8}c {}\n", "{color_status} {method:>8} {line_count:>8}l {word_count:>8}w {content_length:>8}c {url}\n"
color_status, method, line_count, word_count, content_length, url
) )
} }
} }
@@ -423,7 +421,7 @@ fn should_deny_absolute(url_to_test: &Url, denier: &Url, handles: Arc<Handles>)
// to a scanned url that is also a parent of the given url // to a scanned url that is also a parent of the given url
for ferox_scan in handles.ferox_scans()?.get_active_scans() { for ferox_scan in handles.ferox_scans()?.get_active_scans() {
let scanner = Url::parse(ferox_scan.url().trim_end_matches('/')) let scanner = Url::parse(ferox_scan.url().trim_end_matches('/'))
.with_context(|| format!("Could not parse {} as a url", ferox_scan))?; .with_context(|| format!("Could not parse {ferox_scan} as a url"))?;
if let Some(scan_host) = scanner.host() { if let Some(scan_host) = scanner.host() {
// same domain/ip check we perform on the denier above // same domain/ip check we perform on the denier above
@@ -521,14 +519,14 @@ pub fn slugify_filename(url: &str, prefix: &str, suffix: &str) -> String {
.as_secs(); .as_secs();
let altered_prefix = if !prefix.is_empty() { let altered_prefix = if !prefix.is_empty() {
format!("{}-", prefix) format!("{prefix}-")
} else { } else {
String::new() String::new()
}; };
let slug = url.replace("://", "_").replace(['/', '.'], "_"); let slug = url.replace("://", "_").replace(['/', '.'], "_");
let filename = format!("{}{}-{}.{}", altered_prefix, slug, ts, suffix); let filename = format!("{altered_prefix}{slug}-{ts}.{suffix}");
log::trace!("exit: slugify -> {}", filename); log::trace!("exit: slugify -> {}", filename);
filename filename

View File

@@ -1420,3 +1420,19 @@ fn banner_prints_force_recursion() {
.and(predicate::str::contains("─┴─")), .and(predicate::str::contains("─┴─")),
); );
} }
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + force recursion
fn banner_prints_update_app() {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--update")
.assert()
.success()
.stderr(
predicate::str::contains("─┬─")
.and(predicate::str::contains("Update app"))
.and(predicate::str::contains("─┴─")),
);
}

View File

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

View File

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

View File

@@ -180,68 +180,18 @@ fn test_static_wildcard_request_found() -> Result<(), Box<dyn std::error::Error>
teardown_tmp_directory(tmp_dir); teardown_tmp_directory(tmp_dir);
cmd.assert().success().stdout( cmd.assert().success().stdout(
predicate::str::contains("WLD") predicate::str::contains("GET")
.and(predicate::str::contains("Got")) .and(predicate::str::contains(
"Auto-filtering found 404-like response and created new filter",
))
.and(predicate::str::contains("200")) .and(predicate::str::contains("200"))
.and(predicate::str::contains("(url length: 32)")), .and(predicate::str::contains("1l")),
); );
assert_eq!(mock.hits(), 1); assert_eq!(mock.hits(), 1);
Ok(()) Ok(())
} }
#[test]
/// test finds a dynamic wildcard and reports as much to stdout and a file
fn test_dynamic_wildcard_request_found() {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
let outfile = tmp_dir.path().join("outfile");
let mock = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
let mock2 = srv.mock(|when, then| {
when.method(GET).path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
then.status(200).body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--add-slash")
.arg("--output")
.arg(outfile.as_os_str())
.unwrap();
let contents = std::fs::read_to_string(outfile).unwrap();
teardown_tmp_directory(tmp_dir);
assert!(contents.contains("WLD"));
assert!(contents.contains("Got"));
assert!(contents.contains("200"));
assert!(contents.contains("(url length: 32)"));
assert!(contents.contains("(url length: 96)"));
cmd.assert().success().stdout(
predicate::str::contains("WLD")
.and(predicate::str::contains("Got"))
.and(predicate::str::contains("200"))
.and(predicate::str::contains("(url length: 32)"))
.and(predicate::str::contains("(url length: 96)")),
);
assert_eq!(mock.hits(), 1);
assert_eq!(mock2.hits(), 1);
}
#[test] #[test]
/// uses dont_filter, so the normal wildcard test should never happen /// uses dont_filter, so the normal wildcard test should never happen
fn heuristics_static_wildcard_request_with_dont_filter() -> Result<(), Box<dyn std::error::Error>> { fn heuristics_static_wildcard_request_with_dont_filter() -> Result<(), Box<dyn std::error::Error>> {
@@ -326,14 +276,14 @@ fn heuristics_wildcard_test_with_two_static_wildcards_with_silent_enabled(
let mock = srv.mock(|when, then| { let mock = srv.mock(|when, then| {
when.method(GET) when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap()); .path_matches(Regex::new("/.?[a-zA-Z0-9]{32,}").unwrap());
then.status(200) then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
}); });
let mock2 = srv.mock(|when, then| { let mock2 = srv.mock(|when, then| {
when.method(GET) when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap()); .path_matches(Regex::new("/LICENSE").unwrap());
then.status(200) then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
}); });
@@ -344,7 +294,6 @@ fn heuristics_wildcard_test_with_two_static_wildcards_with_silent_enabled(
.arg(srv.url("/")) .arg(srv.url("/"))
.arg("--wordlist") .arg("--wordlist")
.arg(file.as_os_str()) .arg(file.as_os_str())
.arg("--add-slash")
.arg("--silent") .arg("--silent")
.arg("--threads") .arg("--threads")
.arg("1") .arg("1")
@@ -352,9 +301,11 @@ fn heuristics_wildcard_test_with_two_static_wildcards_with_silent_enabled(
teardown_tmp_directory(tmp_dir); teardown_tmp_directory(tmp_dir);
cmd.assert().success().stdout(predicate::str::is_empty()); cmd.assert()
.success()
.stdout(predicate::str::contains(srv.url("/")));
assert_eq!(mock.hits(), 1); assert_eq!(mock.hits(), 4);
assert_eq!(mock2.hits(), 1); assert_eq!(mock2.hits(), 1);
Ok(()) Ok(())
} }

View File

@@ -127,7 +127,7 @@ fn main_parallel_spawns_children() -> Result<(), Box<dyn std::error::Error>> {
); );
let contents = read_to_string(outfile).unwrap(); let contents = read_to_string(outfile).unwrap();
println!("contents: {}", contents); println!("contents: {contents}");
assert!(contents.contains("parallel branch && wrapped main")); // exits parallel branch assert!(contents.contains("parallel branch && wrapped main")); // exits parallel branch
@@ -196,7 +196,7 @@ fn main_parallel_creates_output_directory() -> Result<(), Box<dyn std::error::Er
let file_regex = Regex::new("ferox-[a-zA-Z_:0-9]+-[0-9]+.log").unwrap(); let file_regex = Regex::new("ferox-[a-zA-Z_:0-9]+-[0-9]+.log").unwrap();
let dir_regex = Regex::new("output-file-[0-9]+.logs").unwrap(); let dir_regex = Regex::new("output-file-[0-9]+.logs").unwrap();
let sub_dir = output_dir.as_ref().join(&sub_dir); let sub_dir = output_dir.as_ref().join(sub_dir);
// created directory like output-file-1627845741.logs/ // created directory like output-file-1627845741.logs/
assert!(dir_regex.is_match(&sub_dir.to_string_lossy())); assert!(dir_regex.is_match(&sub_dir.to_string_lossy()));

View File

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

View File

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

View File

@@ -53,17 +53,17 @@ fn scanner_recursive_request_scan() -> Result<(), Box<dyn std::error::Error>> {
let js_mock = srv.mock(|when, then| { let js_mock = srv.mock(|when, then| {
when.method(GET).path("/js"); when.method(GET).path("/js");
then.status(301).header("Location", &srv.url("/js/")); then.status(301).header("Location", srv.url("/js/"));
}); });
let js_prod_mock = srv.mock(|when, then| { let js_prod_mock = srv.mock(|when, then| {
when.method(GET).path("/js/prod"); when.method(GET).path("/js/prod");
then.status(301).header("Location", &srv.url("/js/prod/")); then.status(301).header("Location", srv.url("/js/prod/"));
}); });
let js_dev_mock = srv.mock(|when, then| { let js_dev_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev"); when.method(GET).path("/js/dev");
then.status(301).header("Location", &srv.url("/js/dev/")); then.status(301).header("Location", srv.url("/js/dev/"));
}); });
let js_dev_file_mock = srv.mock(|when, then| { let js_dev_file_mock = srv.mock(|when, then| {
@@ -116,17 +116,17 @@ fn scanner_recursive_request_scan_using_only_success_responses(
let js_mock = srv.mock(|when, then| { let js_mock = srv.mock(|when, then| {
when.method(GET).path("/js/"); when.method(GET).path("/js/");
then.status(200).header("Location", &srv.url("/js/")); then.status(200).header("Location", srv.url("/js/"));
}); });
let js_prod_mock = srv.mock(|when, then| { let js_prod_mock = srv.mock(|when, then| {
when.method(GET).path("/js/prod/"); when.method(GET).path("/js/prod/");
then.status(200).header("Location", &srv.url("/js/prod/")); then.status(200).header("Location", srv.url("/js/prod/"));
}); });
let js_dev_mock = srv.mock(|when, then| { let js_dev_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev/"); when.method(GET).path("/js/dev/");
then.status(200).header("Location", &srv.url("/js/dev/")); then.status(200).header("Location", srv.url("/js/dev/"));
}); });
let js_dev_file_mock = srv.mock(|when, then| { let js_dev_file_mock = srv.mock(|when, then| {
@@ -454,7 +454,7 @@ fn scanner_single_request_scan_with_debug_logging() {
.unwrap(); .unwrap();
let contents = std::fs::read_to_string(outfile).unwrap(); let contents = std::fs::read_to_string(outfile).unwrap();
println!("{}", contents); println!("{contents}");
assert!(contents.starts_with("Configuration {")); assert!(contents.starts_with("Configuration {"));
assert!(contents.contains("TRC")); assert!(contents.contains("TRC"));
assert!(contents.contains("DBG")); assert!(contents.contains("DBG"));
@@ -492,7 +492,7 @@ fn scanner_single_request_scan_with_debug_logging_as_json() {
.unwrap(); .unwrap();
let contents = std::fs::read_to_string(outfile).unwrap(); let contents = std::fs::read_to_string(outfile).unwrap();
println!("{}", contents); println!("{contents}");
assert!(contents.starts_with("{\"type\":\"configuration\"")); assert!(contents.starts_with("{\"type\":\"configuration\""));
assert!(contents.contains("\"level\":\"TRACE\"")); assert!(contents.contains("\"level\":\"TRACE\""));
assert!(contents.contains("\"level\":\"DEBUG\"")); assert!(contents.contains("\"level\":\"DEBUG\""));
@@ -587,9 +587,12 @@ fn scanner_recursion_works_with_403_directories() {
cmd.assert().success().stdout( cmd.assert().success().stdout(
predicate::str::contains("/LICENSE") predicate::str::contains("/LICENSE")
.count(2) .count(2)
.and(predicate::str::contains("200").count(2)) .and(predicate::str::contains("200"))
.and(predicate::str::contains("403")) .and(predicate::str::contains("404"))
.and(predicate::str::contains("53c")) .and(predicate::str::contains("53c Auto-filtering"))
.and(predicate::str::contains(
"Auto-filtering found 404-like response and created new filter;",
))
.and(predicate::str::contains("14c")) .and(predicate::str::contains("14c"))
.and(predicate::str::contains("0c")) .and(predicate::str::contains("0c"))
.and(predicate::str::contains("ignored").count(2)) .and(predicate::str::contains("ignored").count(2))
@@ -651,7 +654,7 @@ fn add_discovered_extension_updates_bars_and_stats() {
) )
.unwrap(); .unwrap();
srv.mock(|when, then| { let mock = srv.mock(|when, then| {
when.method(GET).path("/stuff.php"); when.method(GET).path("/stuff.php");
then.status(200).body("cool... coolcoolcool"); then.status(200).body("cool... coolcoolcool");
}); });
@@ -675,10 +678,11 @@ fn add_discovered_extension_updates_bars_and_stats() {
.assert() .assert()
.success(); .success();
mock.assert_hits(1);
let contents = std::fs::read_to_string(file_path).unwrap(); let contents = std::fs::read_to_string(file_path).unwrap();
println!("{}", contents); println!("{contents}");
assert!(contents.contains("discovered new extension: php")); assert!(contents.contains("discovered new extension: php"));
assert!(contents.contains("extensions_collected: 1")); // assert!(contents.contains("extensions_collected: 1")); // this is racy
assert!(contents.contains("expected_per_scan: 6")); assert!(contents.contains("expected_per_scan: 6"));
} }
@@ -864,7 +868,7 @@ fn scanner_forced_recursion_ignores_normal_redirect_logic() -> Result<(), Box<dy
when.method(GET).path("/LICENSE"); when.method(GET).path("/LICENSE");
then.status(301) then.status(301)
.body("this is a test") .body("this is a test")
.header("Location", &srv.url("/LICENSE")); .header("Location", srv.url("/LICENSE"));
}); });
let mock2 = srv.mock(|when, then| { let mock2 = srv.mock(|when, then| {
@@ -891,12 +895,16 @@ fn scanner_forced_recursion_ignores_normal_redirect_logic() -> Result<(), Box<dy
.arg("--wordlist") .arg("--wordlist")
.arg(file.as_os_str()) .arg(file.as_os_str())
.arg("--force-recursion") .arg("--force-recursion")
.arg("--dont-filter")
.arg("--status-codes")
.arg("301")
.arg("200")
.arg("-o") .arg("-o")
.arg(outfile.as_os_str()) .arg(outfile.as_os_str())
.unwrap(); .unwrap();
let contents = std::fs::read_to_string(outfile)?; let contents = std::fs::read_to_string(outfile)?;
println!("{}", contents); println!("{contents}");
assert!(contents.contains("/LICENSE")); assert!(contents.contains("/LICENSE"));
assert!(contents.contains("301")); assert!(contents.contains("301"));