mirror of
https://github.com/epi052/feroxbuster.git
synced 2026-05-22 12:11:13 -03:00
Compare commits
164 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1f5ed507b | ||
|
|
79edc42b17 | ||
|
|
1b223b0867 | ||
|
|
0c6d5193a9 | ||
|
|
c637355796 | ||
|
|
a114cc8f85 | ||
|
|
c8503faf02 | ||
|
|
cbbd642510 | ||
|
|
2c8e9bace9 | ||
|
|
f4fe8c0544 | ||
|
|
73109483fe | ||
|
|
aee33012b1 | ||
|
|
eab95e0435 | ||
|
|
acb2f42f69 | ||
|
|
ac20b213ec | ||
|
|
201873d7ac | ||
|
|
9678b8f31c | ||
|
|
20a826fc0f | ||
|
|
56b78a4e04 | ||
|
|
4b6bf3645d | ||
|
|
6fd201b717 | ||
|
|
5f39d71fe8 | ||
|
|
c23850208b | ||
|
|
d5605efb08 | ||
|
|
5b8d3f5661 | ||
|
|
ce7f3b79b8 | ||
|
|
c9c63bebd0 | ||
|
|
1f60e06247 | ||
|
|
04e3ad69cc | ||
|
|
fd5b1f6f25 | ||
|
|
a9dde3f7e1 | ||
|
|
7a9ee39941 | ||
|
|
6befae1a93 | ||
|
|
e6b422b92a | ||
|
|
fb4bfa27fd | ||
|
|
2795ec4e72 | ||
|
|
e9d283bc59 | ||
|
|
3a128df2fc | ||
|
|
38f1b917c4 | ||
|
|
afa7d6804c | ||
|
|
28c3e25eeb | ||
|
|
55e22467ce | ||
|
|
bbfaddaedd | ||
|
|
53e3420efd | ||
|
|
d390bbc12d | ||
|
|
0c3b91855a | ||
|
|
48f5362f5f | ||
|
|
24514faf9e | ||
|
|
a424057166 | ||
|
|
7d483b6edd | ||
|
|
1a717e878d | ||
|
|
e35a6dda9f | ||
|
|
f3b2193b2f | ||
|
|
07a7ac652e | ||
|
|
f51993cde0 | ||
|
|
9093ffb92a | ||
|
|
d550448229 | ||
|
|
492665154e | ||
|
|
c14e617076 | ||
|
|
6bb748af17 | ||
|
|
863ea089cc | ||
|
|
ad3091a7db | ||
|
|
b2bdea71dd | ||
|
|
f478700b86 | ||
|
|
1f2aad5e52 | ||
|
|
3a6a61cc24 | ||
|
|
0311a846b3 | ||
|
|
3066efa848 | ||
|
|
a8fae65d63 | ||
|
|
970886a68b | ||
|
|
494eed81e8 | ||
|
|
c8a577b1e7 | ||
|
|
ccb10c1c68 | ||
|
|
20ab0aade3 | ||
|
|
02ad0b1d85 | ||
|
|
09aad922c1 | ||
|
|
697f947bfa | ||
|
|
d300d68737 | ||
|
|
c2c6854db4 | ||
|
|
63be575d89 | ||
|
|
0d25fda11e | ||
|
|
b0341c2432 | ||
|
|
62352db152 | ||
|
|
3090edc49c | ||
|
|
85d686d1aa | ||
|
|
17138f4ef7 | ||
|
|
1d30b7db31 | ||
|
|
4c0d3c91a0 | ||
|
|
96fc6b232a | ||
|
|
9b306aad34 | ||
|
|
10eee184d0 | ||
|
|
986161f05f | ||
|
|
4a19dbfd7d | ||
|
|
b8ceeaff0f | ||
|
|
d04e58036e | ||
|
|
d1a74207f4 | ||
|
|
03a36f0b60 | ||
|
|
f2d9269643 | ||
|
|
bba7cba02e | ||
|
|
fffd1e5c82 | ||
|
|
3c6da0f782 | ||
|
|
5ecd937c0e | ||
|
|
9f6221daf6 | ||
|
|
af49fd8e62 | ||
|
|
d1daefd8ba | ||
|
|
3e8255d5b7 | ||
|
|
5af18e83d8 | ||
|
|
d1d0757d56 | ||
|
|
f5f9344a81 | ||
|
|
fd52e39188 | ||
|
|
22377dc9a3 | ||
|
|
1cf7dff734 | ||
|
|
7c9eb900b7 | ||
|
|
8480b3cc2c | ||
|
|
9d29142046 | ||
|
|
38c194b222 | ||
|
|
72dc14bf3d | ||
|
|
9a7c690c17 | ||
|
|
de4514e381 | ||
|
|
2be8aaf2bf | ||
|
|
4db3a0b056 | ||
|
|
a9ef7f180f | ||
|
|
ac7cb5d6b6 | ||
|
|
f8e18abb48 | ||
|
|
1d805aca5a | ||
|
|
fa09266804 | ||
|
|
15592c3dfd | ||
|
|
53c171aeb5 | ||
|
|
5167d24c4b | ||
|
|
81ff62c53d | ||
|
|
433f68458e | ||
|
|
d32720a90a | ||
|
|
0ceef975e6 | ||
|
|
6d7d6c4e7b | ||
|
|
5f21953bc1 | ||
|
|
6906ac0ee8 | ||
|
|
cafe766d9e | ||
|
|
98d0d177df | ||
|
|
23e10833d0 | ||
|
|
7f51d0f7bf | ||
|
|
9515a5da99 | ||
|
|
b417ab41a5 | ||
|
|
b946565c2f | ||
|
|
714b054360 | ||
|
|
30544eaf7d | ||
|
|
99e2d46aa2 | ||
|
|
82a4f16252 | ||
|
|
6e0fe1eced | ||
|
|
1ffc93a337 | ||
|
|
7ab0453eb7 | ||
|
|
50b29a2b74 | ||
|
|
bf78fea926 | ||
|
|
56267726cc | ||
|
|
01844cffd8 | ||
|
|
2abc0b78ee | ||
|
|
fdb8774bfd | ||
|
|
8d3cd2471b | ||
|
|
da22371c87 | ||
|
|
ca494ca801 | ||
|
|
376804aa59 | ||
|
|
56a769c197 | ||
|
|
9922eb5124 | ||
|
|
7a58f8fcf8 | ||
|
|
8d1872fb3f |
@@ -384,6 +384,90 @@
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jhaddix",
|
||||
"name": "Jason Haddix",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3488554?v=4",
|
||||
"profile": "https://twitter.com/Jhaddix",
|
||||
"contributions": [
|
||||
"ideas",
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ThisLimn0",
|
||||
"name": "Limn0",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/67125885?v=4",
|
||||
"profile": "https://github.com/ThisLimn0",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "0xdf223",
|
||||
"name": "0xdf",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/76954092?v=4",
|
||||
"profile": "https://github.com/0xdf223",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"ideas"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Flangyver",
|
||||
"name": "Flangyver",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/59575870?v=4",
|
||||
"profile": "https://github.com/Flangyver",
|
||||
"contributions": [
|
||||
"ideas"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "DonatoReis",
|
||||
"name": "PeakyBlinder",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/93531354?v=4",
|
||||
"profile": "https://github.com/DonatoReis",
|
||||
"contributions": [
|
||||
"ideas"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "postmodern",
|
||||
"name": "Postmodern",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/12671?v=4",
|
||||
"profile": "https://postmodern.github.io/",
|
||||
"contributions": [
|
||||
"ideas"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "herrcykel",
|
||||
"name": "O",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1936757?v=4",
|
||||
"profile": "https://github.com/herrcykel",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "udoprog",
|
||||
"name": "John-John Tedro",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/111092?v=4",
|
||||
"profile": "http://udoprog.github.io/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "kmanc",
|
||||
"name": "kmanc",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/14863147?v=4",
|
||||
"profile": "https://github.com/kmanc",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
@@ -391,5 +475,6 @@
|
||||
"projectOwner": "epi052",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"skipCi": true
|
||||
"skipCi": true,
|
||||
"commitConvention": "angular"
|
||||
}
|
||||
|
||||
28
.github/workflows/build.yml
vendored
28
.github/workflows/build.yml
vendored
@@ -73,18 +73,22 @@ jobs:
|
||||
name: ${{ matrix.name }}.tar.gz
|
||||
path: ${{ matrix.name }}.tar.gz
|
||||
|
||||
build-deb:
|
||||
needs: [build-nix]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Deb Build
|
||||
uses: ebbflow-io/cargo-deb-amd64-ubuntu@1.0
|
||||
- name: Upload Deb Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: feroxbuster_amd64.deb
|
||||
path: ./target/x86_64-unknown-linux-musl/debian/*
|
||||
# build-deb:
|
||||
# needs: [build-nix]
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@master
|
||||
# - name: Install cargo-deb
|
||||
# run: cargo install -f cargo-deb
|
||||
# - name: Install musl toolchain
|
||||
# run: rustup target add x86_64-unknown-linux-musl
|
||||
# - name: Deb Build
|
||||
# run: cargo deb --target=x86_64-unknown-linux-musl
|
||||
# - name: Upload Deb Artifact
|
||||
# uses: actions/upload-artifact@v2
|
||||
# with:
|
||||
# name: feroxbuster_amd64.deb
|
||||
# path: ./target/x86_64-unknown-linux-musl/debian/*
|
||||
|
||||
build-macos:
|
||||
env:
|
||||
|
||||
32
.github/workflows/check.yml
vendored
32
.github/workflows/check.yml
vendored
@@ -8,11 +8,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
@@ -22,26 +17,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- name: Install latest nextest release
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: Test with latest nextest release
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
command: nextest
|
||||
args: run --all-features --all-targets --retries 10
|
||||
|
||||
fmt:
|
||||
name: Rust fmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- run: rustup component add rustfmt
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
@@ -52,13 +40,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- run: rustup component add clippy
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --all-targets --all-features -- -D warnings -A clippy::deref_addrof -A clippy::mutex-atomic
|
||||
args: --all-targets --all-features -- -D warnings
|
||||
|
||||
48
.github/workflows/coverage.yml
vendored
48
.github/workflows/coverage.yml
vendored
@@ -3,42 +3,22 @@ on: [push]
|
||||
name: Code Coverage Pipeline
|
||||
|
||||
jobs:
|
||||
upload-coverage:
|
||||
coverage:
|
||||
name: LLVM Coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clean
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all-features --no-fail-fast
|
||||
env:
|
||||
CARGO_INCREMENTAL: '0'
|
||||
RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort'
|
||||
RUSTDOCFLAGS: '-Cpanic=abort'
|
||||
- uses: actions-rs/grcov@v0.1
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: lcov.info
|
||||
path: lcov.info
|
||||
- name: Convert lcov to xml
|
||||
run: |
|
||||
curl -O https://raw.githubusercontent.com/epi052/lcov-to-cobertura-xml/master/lcov_cobertura/lcov_cobertura.py
|
||||
chmod +x lcov_cobertura.py
|
||||
./lcov_cobertura.py ./lcov.info
|
||||
- uses: codecov/codecov-action@v1
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install llvm-tools-preview
|
||||
run: rustup toolchain install stable --component llvm-tools-preview
|
||||
- name: Install cargo-llvm-cov
|
||||
uses: taiki-e/install-action@cargo-llvm-cov
|
||||
- name: Install cargo-nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: Generate code coverage
|
||||
run: cargo llvm-cov nextest --all-features --no-fail-fast --lcov --output-path lcov.info -- --retries 10
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: ./coverage.xml
|
||||
name: codecov-umbrella
|
||||
files: lcov.info
|
||||
fail_ci_if_error: true
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: coverage.xml
|
||||
path: ./coverage.xml
|
||||
|
||||
1170
Cargo.lock
generated
1170
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
84
Cargo.toml
84
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "feroxbuster"
|
||||
version = "2.6.1"
|
||||
version = "2.7.3"
|
||||
authors = ["Ben 'epi' Risher (@epi052)"]
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
@@ -8,7 +8,13 @@ homepage = "https://github.com/epi052/feroxbuster"
|
||||
repository = "https://github.com/epi052/feroxbuster"
|
||||
description = "A fast, simple, recursive content discovery tool."
|
||||
categories = ["command-line-utilities"]
|
||||
keywords = ["pentest", "enumeration", "url-bruteforce", "content-discovery", "web"]
|
||||
keywords = [
|
||||
"pentest",
|
||||
"enumeration",
|
||||
"url-bruteforce",
|
||||
"content-discovery",
|
||||
"web",
|
||||
]
|
||||
exclude = [".github/*", "img/*", "check-coverage.sh"]
|
||||
build = "build.rs"
|
||||
|
||||
@@ -16,40 +22,40 @@ build = "build.rs"
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[build-dependencies]
|
||||
clap = { version = "3.1.5", features = ["wrap_help", "cargo"] }
|
||||
clap_complete = "3.1.1"
|
||||
regex = "1.5.4"
|
||||
clap = { version = "4.0.8", features = ["wrap_help", "cargo"] }
|
||||
clap_complete = "4.0.2"
|
||||
regex = "1.5.5"
|
||||
lazy_static = "1.4.0"
|
||||
dirs = "4.0.0"
|
||||
|
||||
[dependencies]
|
||||
scraper = "0.12.0"
|
||||
scraper = "0.14.0"
|
||||
futures = "0.3.21"
|
||||
tokio = { version = "1.17.0", features = ["full"] }
|
||||
tokio-util = { version = "0.7.0", features = ["codec"] }
|
||||
log = "0.4.14"
|
||||
env_logger = "0.9.0"
|
||||
reqwest = { version = "0.11.9", features = ["socks"] }
|
||||
tokio = { version = "1.18.2", features = ["full"] }
|
||||
tokio-util = { version = "0.7.1", features = ["codec"] }
|
||||
log = "0.4.17"
|
||||
env_logger = "0.10.0"
|
||||
reqwest = { version = "0.11.10", features = ["socks"] }
|
||||
# uses feature unification to add 'serde' to reqwest::Url
|
||||
url = { version = "2.2.2", features = ["serde"] }
|
||||
serde_regex = "1.1.0"
|
||||
clap = { version = "3.1.5", features = ["wrap_help", "cargo"] }
|
||||
clap = { version = "4.0.8", features = ["wrap_help", "cargo"] }
|
||||
lazy_static = "1.4.0"
|
||||
toml = "0.5.8"
|
||||
serde = { version = "1.0.136", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.79"
|
||||
uuid = { version = "0.8.2", features = ["v4"] }
|
||||
toml = "0.5.9"
|
||||
serde = { version = "1.0.137", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.81"
|
||||
uuid = { version = "1.0.0", features = ["v4"] }
|
||||
indicatif = "0.15"
|
||||
console = "0.15.0"
|
||||
openssl = { version = "0.10.38", features = ["vendored"] }
|
||||
console = "0.15.2"
|
||||
openssl = { version = "0.10.40", features = ["vendored"] }
|
||||
dirs = "4.0.0"
|
||||
regex = "1.5.4"
|
||||
crossterm = "0.23.0"
|
||||
rlimit = "0.7.0"
|
||||
ctrlc = "3.2.1"
|
||||
regex = "1.5.5"
|
||||
crossterm = "0.25.0"
|
||||
rlimit = "0.9.0"
|
||||
ctrlc = "3.2.2"
|
||||
fuzzyhash = "0.2.1"
|
||||
anyhow = "1.0.55"
|
||||
leaky-bucket = "0.10.0" # todo: upgrade, will take a little work/thought since api changed
|
||||
anyhow = "1.0.57"
|
||||
leaky-bucket = "0.12.1"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.3.0"
|
||||
@@ -67,9 +73,29 @@ section = "utility"
|
||||
license-file = ["LICENSE", "4"]
|
||||
conf-files = ["/etc/feroxbuster/ferox-config.toml"]
|
||||
assets = [
|
||||
["target/release/feroxbuster", "/usr/bin/", "755"],
|
||||
["ferox-config.toml.example", "/etc/feroxbuster/ferox-config.toml", "644"],
|
||||
["shell_completions/feroxbuster.bash", "/usr/share/bash-completion/completions/feroxbuster.bash", "644"],
|
||||
["shell_completions/feroxbuster.fish", "/usr/share/fish/completions/feroxbuster.fish", "644"],
|
||||
["shell_completions/_feroxbuster", "/usr/share/zsh/vendor-completions/_feroxbuster", "644"],
|
||||
[
|
||||
"target/release/feroxbuster",
|
||||
"/usr/bin/",
|
||||
"755",
|
||||
],
|
||||
[
|
||||
"ferox-config.toml.example",
|
||||
"/etc/feroxbuster/ferox-config.toml",
|
||||
"644",
|
||||
],
|
||||
[
|
||||
"shell_completions/feroxbuster.bash",
|
||||
"/usr/share/bash-completion/completions/feroxbuster.bash",
|
||||
"644",
|
||||
],
|
||||
[
|
||||
"shell_completions/feroxbuster.fish",
|
||||
"/usr/share/fish/completions/feroxbuster.fish",
|
||||
"644",
|
||||
],
|
||||
[
|
||||
"shell_completions/_feroxbuster",
|
||||
"/usr/share/zsh/vendor-completions/_feroxbuster",
|
||||
"644",
|
||||
],
|
||||
]
|
||||
|
||||
@@ -11,8 +11,15 @@ rm ferox-*.state
|
||||
# dependency management
|
||||
[tasks.upgrade-deps]
|
||||
command = "cargo"
|
||||
args = ["upgrade", "--exclude", "indicatif", "leaky-bucket"]
|
||||
args = ["upgrade", "--exclude", "indicatif"]
|
||||
|
||||
[tasks.update]
|
||||
command = "cargo"
|
||||
args = ["update"]
|
||||
|
||||
# clippy / lint
|
||||
[tasks.clippy]
|
||||
clear = true
|
||||
script = """
|
||||
cargo clippy --all-targets --all-features -- -D warnings
|
||||
"""
|
||||
|
||||
123
README.md
123
README.md
@@ -181,59 +181,74 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="https://io.fi"><img src="https://avatars.githubusercontent.com/u/5235109?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Joona Hoikkala</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=joohoi" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/jsav0"><img src="https://avatars.githubusercontent.com/u/20546041?v=4?s=100" width="100px;" alt=""/><br /><sub><b>J Savage</b></sub></a><br /><a href="#infra-jsav0" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=jsav0" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://www.tgotwig.dev"><img src="https://avatars.githubusercontent.com/u/30773779?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Thomas Gotwig</b></sub></a><br /><a href="#infra-TGotwig" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=TGotwig" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/spikecodes"><img src="https://avatars.githubusercontent.com/u/19519553?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Spike</b></sub></a><br /><a href="#infra-spikecodes" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=spikecodes" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/evanrichter"><img src="https://avatars.githubusercontent.com/u/330292?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Evan Richter</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=evanrichter" title="Code">💻</a> <a href="https://github.com/epi052/feroxbuster/commits?author=evanrichter" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/mzpqnxow"><img src="https://avatars.githubusercontent.com/u/8016228?v=4?s=100" width="100px;" alt=""/><br /><sub><b>AG</b></sub></a><br /><a href="#ideas-mzpqnxow" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=mzpqnxow" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://n-thumann.de/"><img src="https://avatars.githubusercontent.com/u/46975855?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nicolas Thumann</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=n-thumann" title="Code">💻</a> <a href="https://github.com/epi052/feroxbuster/commits?author=n-thumann" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/tomtastic"><img src="https://avatars.githubusercontent.com/u/302127?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tom Matthews</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=tomtastic" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/bsysop"><img src="https://avatars.githubusercontent.com/u/9998303?v=4?s=100" width="100px;" alt=""/><br /><sub><b>bsysop</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=bsysop" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://bpsizemore.me"><img src="https://avatars.githubusercontent.com/u/11645898?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Brian Sizemore</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=bpsizemore" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://pwn.by/noraj"><img src="https://avatars.githubusercontent.com/u/16578570?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alexandre ZANNI</b></sub></a><br /><a href="#infra-noraj" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=noraj" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/craig"><img src="https://avatars.githubusercontent.com/u/99729?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Craig</b></sub></a><br /><a href="#infra-craig" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center"><a href="https://www.reddit.com/u/EONRaider"><img src="https://avatars.githubusercontent.com/u/15611424?v=4?s=100" width="100px;" alt=""/><br /><sub><b>EONRaider</b></sub></a><br /><a href="#infra-EONRaider" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center"><a href="https://github.com/wtwver"><img src="https://avatars.githubusercontent.com/u/53866088?v=4?s=100" width="100px;" alt=""/><br /><sub><b>wtwver</b></sub></a><br /><a href="#infra-wtwver" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://tib3rius.com"><img src="https://avatars.githubusercontent.com/u/48113936?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tib3rius</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ATib3rius" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://github.com/0xdf"><img src="https://avatars.githubusercontent.com/u/1489045?v=4?s=100" width="100px;" alt=""/><br /><sub><b>0xdf</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3A0xdf" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="http://secure77.de"><img src="https://avatars.githubusercontent.com/u/31564517?v=4?s=100" width="100px;" alt=""/><br /><sub><b>secure-77</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asecure-77" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://github.com/sbrun"><img src="https://avatars.githubusercontent.com/u/7712154?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sophie Brun</b></sub></a><br /><a href="#infra-sbrun" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center"><a href="https://github.com/black-A"><img src="https://avatars.githubusercontent.com/u/30686803?v=4?s=100" width="100px;" alt=""/><br /><sub><b>black-A</b></sub></a><br /><a href="#ideas-black-A" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://github.com/dinosn"><img src="https://avatars.githubusercontent.com/u/3851678?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nicolas Krassas</b></sub></a><br /><a href="#ideas-dinosn" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://github.com/N0ur5"><img src="https://avatars.githubusercontent.com/u/24260009?v=4?s=100" width="100px;" alt=""/><br /><sub><b>N0ur5</b></sub></a><br /><a href="#ideas-N0ur5" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/moscowchill"><img src="https://avatars.githubusercontent.com/u/72578879?v=4?s=100" width="100px;" alt=""/><br /><sub><b>mchill</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Amoscowchill" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="http://BitThr3at.github.io"><img src="https://avatars.githubusercontent.com/u/45028933?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Naman</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ABitThr3at" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://github.com/Sicks3c"><img src="https://avatars.githubusercontent.com/u/32225186?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ayoub Elaich</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asicks3c" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://github.com/HenryHoggard"><img src="https://avatars.githubusercontent.com/u/1208121?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Henry</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AHenryHoggard" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://github.com/SleepiPanda"><img src="https://avatars.githubusercontent.com/u/6428561?v=4?s=100" width="100px;" alt=""/><br /><sub><b>SleepiPanda</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ASleepiPanda" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://github.com/uBadRequest"><img src="https://avatars.githubusercontent.com/u/47282747?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Bad Requests</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AuBadRequest" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://home.dnaka91.rocks"><img src="https://avatars.githubusercontent.com/u/36804488?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dominik Nakamura</b></sub></a><br /><a href="#infra-dnaka91" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/hunter0x8"><img src="https://avatars.githubusercontent.com/u/46222314?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Muhammad Ahsan</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Ahunter0x8" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://github.com/cortantief"><img src="https://avatars.githubusercontent.com/u/34527333?v=4?s=100" width="100px;" alt=""/><br /><sub><b>cortantief</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Acortantief" title="Bug reports">🐛</a> <a href="https://github.com/epi052/feroxbuster/commits?author=cortantief" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/dsaxton"><img src="https://avatars.githubusercontent.com/u/2658661?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Daniel Saxton</b></sub></a><br /><a href="#ideas-dsaxton" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=dsaxton" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/narkopolo"><img src="https://avatars.githubusercontent.com/u/16690056?v=4?s=100" width="100px;" alt=""/><br /><sub><b>narkopolo</b></sub></a><br /><a href="#ideas-narkopolo" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://ring0.lol"><img src="https://avatars.githubusercontent.com/u/1893909?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Justin Steven</b></sub></a><br /><a href="#ideas-justinsteven" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://github.com/7047payloads"><img src="https://avatars.githubusercontent.com/u/95562424?v=4?s=100" width="100px;" alt=""/><br /><sub><b>7047payloads</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=7047payloads" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/unkn0wnsyst3m"><img src="https://avatars.githubusercontent.com/u/21272239?v=4?s=100" width="100px;" alt=""/><br /><sub><b>unkn0wnsyst3m</b></sub></a><br /><a href="#ideas-unkn0wnsyst3m" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://ironwort.me/"><img src="https://avatars.githubusercontent.com/u/15280042?v=4?s=100" width="100px;" alt=""/><br /><sub><b>0x08</b></sub></a><br /><a href="#ideas-its0x08" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://github.com/MD-Levitan"><img src="https://avatars.githubusercontent.com/u/12116508?v=4?s=100" width="100px;" alt=""/><br /><sub><b>kusok</b></sub></a><br /><a href="#ideas-MD-Levitan" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=MD-Levitan" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/godylockz"><img src="https://avatars.githubusercontent.com/u/81207744?v=4?s=100" width="100px;" alt=""/><br /><sub><b>godylockz</b></sub></a><br /><a href="#ideas-godylockz" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=godylockz" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://ryanmontgomery.me"><img src="https://avatars.githubusercontent.com/u/44453666?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ryan Montgomery</b></sub></a><br /><a href="#ideas-0dayCTF" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://github.com/IppSec"><img src="https://avatars.githubusercontent.com/u/24677271?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ippsec</b></sub></a><br /><a href="#ideas-ippsec" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://github.com/gtjamesa"><img src="https://avatars.githubusercontent.com/u/2078364?v=4?s=100" width="100px;" alt=""/><br /><sub><b>James</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Agtjamesa" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tbody>
|
||||
<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"><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"><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"><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>
|
||||
</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"><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"><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"><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>
|
||||
</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"><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"><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"><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>
|
||||
</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"><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"><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"><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>
|
||||
</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"><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"><a href="https://github.com/n0kovo"><img src="https://avatars.githubusercontent.com/u/16690056?v=4?s=100" width="100px;" alt="narkopolo"/><br /><sub><b>n0kovo</b></sub></a><br /><a href="#ideas-n0kovo" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://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"><a href="https://github.com/unkn0wnsyst3m"><img src="https://avatars.githubusercontent.com/u/21272239?v=4?s=100" width="100px;" alt="unkn0wnsyst3m"/><br /><sub><b>unkn0wnsyst3m</b></sub></a><br /><a href="#ideas-unkn0wnsyst3m" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://ironwort.me/"><img src="https://avatars.githubusercontent.com/u/15280042?v=4?s=100" width="100px;" alt="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"><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"><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"><a href="https://twitter.com/Jhaddix"><img src="https://avatars.githubusercontent.com/u/3488554?v=4?s=100" width="100px;" alt="Jason Haddix"/><br /><sub><b>Jason Haddix</b></sub></a><br /><a href="#ideas-jhaddix" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/issues?q=author%3Ajhaddix" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/ThisLimn0"><img src="https://avatars.githubusercontent.com/u/67125885?v=4?s=100" width="100px;" alt="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"><a href="https://github.com/Flangyver"><img src="https://avatars.githubusercontent.com/u/59575870?v=4?s=100" width="100px;" alt="Flangyver"/><br /><sub><b>Flangyver</b></sub></a><br /><a href="#ideas-Flangyver" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://github.com/DonatoReis"><img src="https://avatars.githubusercontent.com/u/93531354?v=4?s=100" width="100px;" alt="PeakyBlinder"/><br /><sub><b>PeakyBlinder</b></sub></a><br /><a href="#ideas-DonatoReis" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://postmodern.github.io/"><img src="https://avatars.githubusercontent.com/u/12671?v=4?s=100" width="100px;" alt="Postmodern"/><br /><sub><b>Postmodern</b></sub></a><br /><a href="#ideas-postmodern" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://github.com/herrcykel"><img src="https://avatars.githubusercontent.com/u/1936757?v=4?s=100" width="100px;" alt="O"/><br /><sub><b>O</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=herrcykel" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://udoprog.github.io/"><img src="https://avatars.githubusercontent.com/u/111092?v=4?s=100" width="100px;" alt="John-John Tedro"/><br /><sub><b>John-John Tedro</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=udoprog" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/kmanc"><img src="https://avatars.githubusercontent.com/u/14863147?v=4?s=100" width="100px;" alt="kmanc"/><br /><sub><b>kmanc</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Akmanc" title="Bug reports">🐛</a> <a href="https://github.com/epi052/feroxbuster/commits?author=kmanc" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
@@ -241,4 +256,4 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||
|
||||
14
build.rs
14
build.rs
@@ -59,15 +59,11 @@ fn main() {
|
||||
if !config_dir.exists() {
|
||||
// recursively create the feroxbuster directory and all of its parent components if
|
||||
// they are missing
|
||||
if !config_dir.exists() {
|
||||
// recursively create the feroxbuster directory and all of its parent components if
|
||||
// they are missing
|
||||
if create_dir_all(&config_dir).is_err() {
|
||||
// only copy the config file when we're not running in the CI/CD pipeline
|
||||
// which fails with permission denied
|
||||
eprintln!("Couldn't create one or more directories needed to copy the config file");
|
||||
return;
|
||||
}
|
||||
if create_dir_all(&config_dir).is_err() {
|
||||
// only copy the config file when we're not running in the CI/CD pipeline
|
||||
// which fails with permission denied
|
||||
eprintln!("Couldn't create one or more directories needed to copy the config file");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
# dont_filter = true
|
||||
# extract_links = true
|
||||
# depth = 1
|
||||
# force_recursion = true
|
||||
# filter_size = [5174]
|
||||
# filter_regex = ["^ignore me$"]
|
||||
# filter_similar = ["https://somesite.com/soft404"]
|
||||
|
||||
@@ -24,8 +24,8 @@ _feroxbuster() {
|
||||
'--replay-proxy=[Send only unfiltered requests through a Replay Proxy, instead of all requests]:REPLAY_PROXY:_urls' \
|
||||
'*-R+[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \
|
||||
'*--replay-codes=[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \
|
||||
'-a+[Sets the User-Agent (default: feroxbuster/2.6.1)]:USER_AGENT: ' \
|
||||
'--user-agent=[Sets the User-Agent (default: feroxbuster/2.6.1)]:USER_AGENT: ' \
|
||||
'-a+[Sets the User-Agent (default: feroxbuster/2.7.3)]:USER_AGENT: ' \
|
||||
'--user-agent=[Sets the User-Agent (default: feroxbuster/2.7.3)]:USER_AGENT: ' \
|
||||
'*-x+[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \
|
||||
'*--extensions=[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \
|
||||
'*-m+[Which HTTP request method(s) should be sent (default: GET)]:HTTP_METHODS: ' \
|
||||
@@ -46,8 +46,8 @@ _feroxbuster() {
|
||||
'*--filter-words=[Filter out messages of a particular word count (ex: -W 312 -W 91,82)]:WORDS: ' \
|
||||
'*-N+[Filter out messages of a particular line count (ex: -N 20 -N 31,30)]:LINES: ' \
|
||||
'*--filter-lines=[Filter out messages of a particular line count (ex: -N 20 -N 31,30)]:LINES: ' \
|
||||
'*-C+[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \
|
||||
'*--filter-status=[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \
|
||||
'(-s --status-codes)*-C+[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \
|
||||
'(-s --status-codes)*--filter-status=[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \
|
||||
'*--filter-similar-to=[Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)]:UNWANTED_PAGE:_urls' \
|
||||
'*-s+[Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)]:STATUS_CODE: ' \
|
||||
'*--status-codes=[Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)]:STATUS_CODE: ' \
|
||||
@@ -69,10 +69,6 @@ _feroxbuster() {
|
||||
'-o+[Output file to write results to (use w/ --json for JSON entries)]:FILE:_files' \
|
||||
'--output=[Output file to write results to (use w/ --json for JSON entries)]:FILE:_files' \
|
||||
'--debug-log=[Output file to write log entries (use w/ --json for JSON entries)]:FILE:_files' \
|
||||
'-h[Print help information]' \
|
||||
'--help[Print help information]' \
|
||||
'-V[Print version information]' \
|
||||
'--version[Print version information]' \
|
||||
'(-u --url)--stdin[Read url(s) from STDIN]' \
|
||||
'(-p --proxy -k --insecure --burp-replay)--burp[Set --proxy to http://127.0.0.1:8080 and set --insecure to true]' \
|
||||
'(-P --replay-proxy -k --insecure)--burp-replay[Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true]' \
|
||||
@@ -88,6 +84,7 @@ _feroxbuster() {
|
||||
'--insecure[Disables TLS certificate validation in the client]' \
|
||||
'-n[Do not scan recursively]' \
|
||||
'--no-recursion[Do not scan recursively]' \
|
||||
'(-n --no-recursion)--force-recursion[Force recursion attempts on all '\''found'\'' endpoints (still respects recursion depth)]' \
|
||||
'-e[Extract links from response body (html, javascript, etc...); make new requests based on findings]' \
|
||||
'--extract-links[Extract links from response body (html, javascript, etc...); make new requests based on findings]' \
|
||||
'(--auto-bail)--auto-tune[Automatically lower scan rate when an excessive amount of errors are encountered]' \
|
||||
@@ -107,6 +104,10 @@ _feroxbuster() {
|
||||
'--quiet[Hide progress bars and banner (good for tmux windows w/ notifications)]' \
|
||||
'--json[Emit JSON logs to --output and --debug-log instead of normal text]' \
|
||||
'--no-state[Disable state output file (*.state)]' \
|
||||
'-h[Print help information (use `--help` for more detail)]' \
|
||||
'--help[Print help information (use `--help` for more detail)]' \
|
||||
'-V[Print version information]' \
|
||||
'--version[Print version information]' \
|
||||
&& ret=0
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
|
||||
[CompletionResult]::new('--replay-proxy', 'replay-proxy', [CompletionResultType]::ParameterName, 'Send only unfiltered requests through a Replay Proxy, instead of all requests')
|
||||
[CompletionResult]::new('-R', 'R', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)')
|
||||
[CompletionResult]::new('--replay-codes', 'replay-codes', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)')
|
||||
[CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.6.1)')
|
||||
[CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.6.1)')
|
||||
[CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.7.3)')
|
||||
[CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.7.3)')
|
||||
[CompletionResult]::new('-x', 'x', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)')
|
||||
[CompletionResult]::new('--extensions', 'extensions', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)')
|
||||
[CompletionResult]::new('-m', 'm', [CompletionResultType]::ParameterName, 'Which HTTP request method(s) should be sent (default: GET)')
|
||||
@@ -75,10 +75,6 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
|
||||
[CompletionResult]::new('-o', 'o', [CompletionResultType]::ParameterName, 'Output file to write results to (use w/ --json for JSON entries)')
|
||||
[CompletionResult]::new('--output', 'output', [CompletionResultType]::ParameterName, 'Output file to write results to (use w/ --json for JSON entries)')
|
||||
[CompletionResult]::new('--debug-log', 'debug-log', [CompletionResultType]::ParameterName, 'Output file to write log entries (use w/ --json for JSON entries)')
|
||||
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information')
|
||||
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information')
|
||||
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information')
|
||||
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information')
|
||||
[CompletionResult]::new('--stdin', 'stdin', [CompletionResultType]::ParameterName, 'Read url(s) from STDIN')
|
||||
[CompletionResult]::new('--burp', 'burp', [CompletionResultType]::ParameterName, 'Set --proxy to http://127.0.0.1:8080 and set --insecure to true')
|
||||
[CompletionResult]::new('--burp-replay', 'burp-replay', [CompletionResultType]::ParameterName, 'Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true')
|
||||
@@ -94,6 +90,7 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
|
||||
[CompletionResult]::new('--insecure', 'insecure', [CompletionResultType]::ParameterName, 'Disables TLS certificate validation in the client')
|
||||
[CompletionResult]::new('-n', 'n', [CompletionResultType]::ParameterName, 'Do not scan recursively')
|
||||
[CompletionResult]::new('--no-recursion', 'no-recursion', [CompletionResultType]::ParameterName, 'Do not scan recursively')
|
||||
[CompletionResult]::new('--force-recursion', 'force-recursion', [CompletionResultType]::ParameterName, 'Force recursion attempts on all ''found'' endpoints (still respects recursion depth)')
|
||||
[CompletionResult]::new('-e', 'e', [CompletionResultType]::ParameterName, 'Extract links from response body (html, javascript, etc...); make new requests based on findings')
|
||||
[CompletionResult]::new('--extract-links', 'extract-links', [CompletionResultType]::ParameterName, 'Extract links from response body (html, javascript, etc...); make new requests based on findings')
|
||||
[CompletionResult]::new('--auto-tune', 'auto-tune', [CompletionResultType]::ParameterName, 'Automatically lower scan rate when an excessive amount of errors are encountered')
|
||||
@@ -113,6 +110,10 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
|
||||
[CompletionResult]::new('--quiet', 'quiet', [CompletionResultType]::ParameterName, 'Hide progress bars and banner (good for tmux windows w/ notifications)')
|
||||
[CompletionResult]::new('--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('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information (use `--help` for more detail)')
|
||||
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information (use `--help` for more detail)')
|
||||
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information')
|
||||
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information')
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
@@ -8,8 +8,8 @@ _feroxbuster() {
|
||||
|
||||
for i in ${COMP_WORDS[@]}
|
||||
do
|
||||
case "${i}" in
|
||||
"$1")
|
||||
case "${cmd},${i}" in
|
||||
",$1")
|
||||
cmd="feroxbuster"
|
||||
;;
|
||||
*)
|
||||
@@ -19,7 +19,7 @@ _feroxbuster() {
|
||||
|
||||
case "${cmd}" in
|
||||
feroxbuster)
|
||||
opts="-h -V -u -p -P -R -a -A -x -m -H -b -Q -f -S -X -W -N -C -s -T -r -k -t -n -d -e -L -w -D -E -B -g -I -v -q -o --help --version --url --stdin --resume-from --burp --burp-replay --smart --thorough --proxy --replay-proxy --replay-codes --user-agent --random-agent --extensions --methods --data --headers --cookies --query --add-slash --dont-scan --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --status-codes --timeout --redirects --insecure --threads --no-recursion --depth --extract-links --scan-limit --parallel --rate-limit --time-limit --wordlist --auto-tune --auto-bail --dont-filter --collect-extensions --collect-backups --collect-words --dont-collect --verbosity --silent --quiet --json --output --debug-log --no-state"
|
||||
opts="-u -p -P -R -a -A -x -m -H -b -Q -f -S -X -W -N -C -s -T -r -k -t -n -d -e -L -w -D -E -B -g -I -v -q -o -h -V --url --stdin --resume-from --burp --burp-replay --smart --thorough --proxy --replay-proxy --replay-codes --user-agent --random-agent --extensions --methods --data --headers --cookies --query --add-slash --dont-scan --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --status-codes --timeout --redirects --insecure --threads --no-recursion --depth --force-recursion --extract-links --scan-limit --parallel --rate-limit --time-limit --wordlist --auto-tune --auto-bail --dont-filter --collect-extensions --collect-backups --collect-words --dont-collect --verbosity --silent --quiet --json --output --debug-log --no-state --help --version"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
|
||||
@@ -27,8 +27,8 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
|
||||
cand --replay-proxy 'Send only unfiltered requests through a Replay Proxy, instead of all requests'
|
||||
cand -R 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)'
|
||||
cand --replay-codes 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)'
|
||||
cand -a 'Sets the User-Agent (default: feroxbuster/2.6.1)'
|
||||
cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.6.1)'
|
||||
cand -a 'Sets the User-Agent (default: feroxbuster/2.7.3)'
|
||||
cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.7.3)'
|
||||
cand -x 'File extension(s) to search for (ex: -x php -x pdf js)'
|
||||
cand --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)'
|
||||
@@ -72,10 +72,6 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
|
||||
cand -o 'Output file to write results to (use w/ --json for JSON entries)'
|
||||
cand --output 'Output file to write results to (use w/ --json for JSON entries)'
|
||||
cand --debug-log 'Output file to write log entries (use w/ --json for JSON entries)'
|
||||
cand -h 'Print help information'
|
||||
cand --help 'Print help information'
|
||||
cand -V 'Print version information'
|
||||
cand --version 'Print version information'
|
||||
cand --stdin 'Read url(s) from STDIN'
|
||||
cand --burp 'Set --proxy to http://127.0.0.1:8080 and set --insecure to true'
|
||||
cand --burp-replay 'Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true'
|
||||
@@ -91,6 +87,7 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
|
||||
cand --insecure 'Disables TLS certificate validation in the client'
|
||||
cand -n 'Do not scan recursively'
|
||||
cand --no-recursion 'Do not scan recursively'
|
||||
cand --force-recursion 'Force recursion attempts on all ''found'' endpoints (still respects recursion depth)'
|
||||
cand -e 'Extract links from response body (html, javascript, etc...); make new requests based on findings'
|
||||
cand --extract-links 'Extract links from response body (html, javascript, etc...); make new requests based on findings'
|
||||
cand --auto-tune 'Automatically lower scan rate when an excessive amount of errors are encountered'
|
||||
@@ -110,6 +107,10 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
|
||||
cand --quiet 'Hide progress bars and banner (good for tmux windows w/ notifications)'
|
||||
cand --json 'Emit JSON logs to --output and --debug-log instead of normal text'
|
||||
cand --no-state 'Disable state output file (*.state)'
|
||||
cand -h 'Print help information (use `--help` for more detail)'
|
||||
cand --help 'Print help information (use `--help` for more detail)'
|
||||
cand -V 'Print version information'
|
||||
cand --version 'Print version information'
|
||||
}
|
||||
]
|
||||
$completions[$command]
|
||||
|
||||
@@ -163,6 +163,9 @@ pub struct Banner {
|
||||
|
||||
/// represents Configuration.collect_words
|
||||
collect_words: BannerEntry,
|
||||
|
||||
/// represents Configuration.collect_words
|
||||
force_recursion: BannerEntry,
|
||||
}
|
||||
|
||||
/// implementation of Banner
|
||||
@@ -300,6 +303,8 @@ impl Banner {
|
||||
&config.scan_limit.to_string(),
|
||||
);
|
||||
|
||||
let force_recursion =
|
||||
BannerEntry::new("🤘", "Force Recursion", &config.force_recursion.to_string());
|
||||
let replay_proxy = BannerEntry::new("🎥", "Replay Proxy", &config.replay_proxy);
|
||||
let auto_tune = BannerEntry::new("🎶", "Auto Tune", &config.auto_tune.to_string());
|
||||
let auto_bail = BannerEntry::new("🪣", "Auto Bail", &config.auto_bail.to_string());
|
||||
@@ -409,6 +414,7 @@ impl Banner {
|
||||
no_recursion,
|
||||
rate_limit,
|
||||
scan_limit,
|
||||
force_recursion,
|
||||
time_limit,
|
||||
url_denylist,
|
||||
collect_extensions,
|
||||
@@ -511,11 +517,12 @@ by Ben "epi" Risher {} ver: {}"#,
|
||||
|
||||
writeln!(&mut writer, "{}", self.threads)?;
|
||||
writeln!(&mut writer, "{}", self.wordlist)?;
|
||||
writeln!(&mut writer, "{}", self.status_codes)?;
|
||||
|
||||
if !config.filter_status.is_empty() {
|
||||
// exception here for an optional print in the middle of always printed values is due
|
||||
// to me wanting the allows and denys to be printed one after the other
|
||||
if config.filter_status.is_empty() {
|
||||
// -C and -s are mutually exclusive, and -s meaning changes when -C is used
|
||||
// so only print one or the other
|
||||
writeln!(&mut writer, "{}", self.status_codes)?;
|
||||
} else {
|
||||
writeln!(&mut writer, "{}", self.filter_status)?;
|
||||
}
|
||||
|
||||
@@ -642,6 +649,10 @@ by Ben "epi" Risher {} ver: {}"#,
|
||||
|
||||
writeln!(&mut writer, "{}", self.no_recursion)?;
|
||||
|
||||
if config.force_recursion {
|
||||
writeln!(&mut writer, "{}", self.force_recursion)?;
|
||||
}
|
||||
|
||||
if config.scan_limit > 0 {
|
||||
writeln!(&mut writer, "{}", self.scan_limit)?;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{
|
||||
DEFAULT_CONFIG_NAME,
|
||||
};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use clap::ArgMatches;
|
||||
use clap::{parser::ValueSource, ArgMatches};
|
||||
use regex::Regex;
|
||||
use reqwest::{Client, Method, StatusCode, Url};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -22,15 +22,10 @@ use std::{
|
||||
|
||||
/// macro helper to abstract away repetitive configuration updates
|
||||
macro_rules! update_config_if_present {
|
||||
($conf_val:expr, $matches:ident, $arg_name:expr) => {
|
||||
match $matches.value_of_t($arg_name) {
|
||||
Ok(value) => *$conf_val = value, // Update value
|
||||
Err(err) => {
|
||||
if !matches!(err.kind(), clap::ErrorKind::ArgumentNotFound) {
|
||||
// Do nothing if argument not found
|
||||
err.exit() // Exit with error on any other parse error
|
||||
}
|
||||
}
|
||||
($conf_val:expr, $matches:ident, $arg_name:expr, $arg_type:ty) => {
|
||||
match $matches.get_one::<$arg_type>($arg_name) {
|
||||
Some(value) => *$conf_val = value.to_owned(), // Update value
|
||||
None => {}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -44,6 +39,35 @@ macro_rules! update_if_not_default {
|
||||
};
|
||||
}
|
||||
|
||||
/// macro helper to abstract away repetitive checks to see if the user has specified a value
|
||||
/// for a given argument from the commandline or if we just had a default value in the parser
|
||||
macro_rules! came_from_cli {
|
||||
($matches:ident, $arg_name:expr) => {
|
||||
matches!(
|
||||
$matches.value_source($arg_name),
|
||||
Some(ValueSource::CommandLine)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/// macro helper to abstract away repetitive if not default: update checks, specifically for
|
||||
/// values that are number types, i.e. usize, u64, etc
|
||||
macro_rules! update_config_with_num_type_if_present {
|
||||
($conf_val:expr, $matches:ident, $arg_name:expr, $arg_type:ty) => {
|
||||
if let Some(val) = $matches.get_one::<String>($arg_name) {
|
||||
match val.parse::<$arg_type>() {
|
||||
Ok(v) => *$conf_val = v,
|
||||
Err(_) => {
|
||||
report_and_exit(&format!(
|
||||
"Invalid value for --{}, must be a positive integer",
|
||||
$arg_name
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Represents the final, global configuration of the program.
|
||||
///
|
||||
/// This struct is the combination of the following:
|
||||
@@ -281,6 +305,10 @@ pub struct Configuration {
|
||||
/// Automatically discover important words from within responses and add them to the wordlist
|
||||
#[serde(default)]
|
||||
pub collect_words: bool,
|
||||
|
||||
/// override recursion logic to always attempt recursion, still respects --depth
|
||||
#[serde(default)]
|
||||
pub force_recursion: bool,
|
||||
}
|
||||
|
||||
impl Default for Configuration {
|
||||
@@ -329,6 +357,7 @@ impl Default for Configuration {
|
||||
collect_backups: false,
|
||||
collect_words: false,
|
||||
save_state: true,
|
||||
force_recursion: false,
|
||||
proxy: String::new(),
|
||||
config: String::new(),
|
||||
output: String::new(),
|
||||
@@ -405,6 +434,7 @@ impl Configuration {
|
||||
/// - **json**: `false`
|
||||
/// - **dont_filter**: `false` (auto filter wildcard responses)
|
||||
/// - **depth**: `4` (maximum recursion depth)
|
||||
/// - **force_recursion**: `false` (still respects recursion depth)
|
||||
/// - **scan_limit**: `0` (no limit on concurrent scans imposed)
|
||||
/// - **parallel**: `0` (no limit on parallel scans imposed)
|
||||
/// - **rate_limit**: `0` (no limit on requests per second imposed)
|
||||
@@ -454,7 +484,7 @@ impl Configuration {
|
||||
|
||||
// --resume-from used, need to first read the Configuration from disk, and then
|
||||
// merge the cli_config into the resumed config
|
||||
if let Some(filename) = args.value_of("resume_from") {
|
||||
if let Some(filename) = args.get_one::<String>("resume_from") {
|
||||
// when resuming a scan, instead of normal configuration loading, we just
|
||||
// load the config from disk by calling resume_scan
|
||||
let mut previous_config = resume_scan(filename);
|
||||
@@ -540,18 +570,21 @@ impl Configuration {
|
||||
fn parse_cli_args(args: &ArgMatches) -> Self {
|
||||
let mut config = Configuration::default();
|
||||
|
||||
update_config_if_present!(&mut config.threads, args, "threads");
|
||||
update_config_if_present!(&mut config.depth, args, "depth");
|
||||
update_config_if_present!(&mut config.scan_limit, args, "scan_limit");
|
||||
update_config_if_present!(&mut config.parallel, args, "parallel");
|
||||
update_config_if_present!(&mut config.rate_limit, args, "rate_limit");
|
||||
update_config_if_present!(&mut config.wordlist, args, "wordlist");
|
||||
update_config_if_present!(&mut config.output, args, "output");
|
||||
update_config_if_present!(&mut config.debug_log, args, "debug_log");
|
||||
update_config_if_present!(&mut config.time_limit, args, "time_limit");
|
||||
update_config_if_present!(&mut config.resume_from, args, "resume_from");
|
||||
update_config_with_num_type_if_present!(&mut config.threads, args, "threads", usize);
|
||||
update_config_with_num_type_if_present!(&mut config.parallel, args, "parallel", usize);
|
||||
update_config_with_num_type_if_present!(&mut config.depth, args, "depth", usize);
|
||||
update_config_with_num_type_if_present!(&mut config.scan_limit, args, "scan_limit", usize);
|
||||
update_config_with_num_type_if_present!(&mut config.rate_limit, args, "rate_limit", usize);
|
||||
update_config_if_present!(&mut config.wordlist, args, "wordlist", String);
|
||||
update_config_if_present!(&mut config.output, args, "output", String);
|
||||
update_config_if_present!(&mut config.debug_log, args, "debug_log", String);
|
||||
update_config_if_present!(&mut config.resume_from, args, "resume_from", String);
|
||||
|
||||
if let Some(arg) = args.values_of("status_codes") {
|
||||
if let Ok(Some(inner)) = args.try_get_one::<String>("time_limit") {
|
||||
config.time_limit = inner.to_owned();
|
||||
}
|
||||
|
||||
if let Some(arg) = args.get_many::<String>("status_codes") {
|
||||
config.status_codes = arg
|
||||
.map(|code| {
|
||||
StatusCode::from_bytes(code.as_bytes())
|
||||
@@ -561,7 +594,7 @@ impl Configuration {
|
||||
.collect();
|
||||
}
|
||||
|
||||
if let Some(arg) = args.values_of("replay_codes") {
|
||||
if let Some(arg) = args.get_many::<String>("replay_codes") {
|
||||
// replay codes passed in by the user
|
||||
config.replay_codes = arg
|
||||
.map(|code| {
|
||||
@@ -575,7 +608,7 @@ impl Configuration {
|
||||
config.replay_codes = config.status_codes.clone();
|
||||
}
|
||||
|
||||
if let Some(arg) = args.values_of("filter_status") {
|
||||
if let Some(arg) = args.get_many::<String>("filter_status") {
|
||||
config.filter_status = arg
|
||||
.map(|code| {
|
||||
StatusCode::from_bytes(code.as_bytes())
|
||||
@@ -585,15 +618,17 @@ impl Configuration {
|
||||
.collect();
|
||||
}
|
||||
|
||||
if let Some(arg) = args.values_of("extensions") {
|
||||
config.extensions = arg.map(|val| val.to_string()).collect();
|
||||
if let Some(arg) = args.get_many::<String>("extensions") {
|
||||
config.extensions = arg
|
||||
.map(|val| val.trim_start_matches('.').to_string())
|
||||
.collect();
|
||||
}
|
||||
|
||||
if let Some(arg) = args.values_of("dont_collect") {
|
||||
if let Some(arg) = args.get_many::<String>("dont_collect") {
|
||||
config.dont_collect = arg.map(|val| val.to_string()).collect();
|
||||
}
|
||||
|
||||
if let Some(arg) = args.values_of("methods") {
|
||||
if let Some(arg) = args.get_many::<String>("methods") {
|
||||
config.methods = arg
|
||||
.map(|val| {
|
||||
// Check methods if they are correct
|
||||
@@ -605,7 +640,7 @@ impl Configuration {
|
||||
.collect();
|
||||
}
|
||||
|
||||
if let Some(arg) = args.value_of("data") {
|
||||
if let Some(arg) = args.get_one::<String>("data") {
|
||||
if let Some(stripped) = arg.strip_prefix('@') {
|
||||
config.data =
|
||||
std::fs::read(stripped).unwrap_or_else(|e| report_and_exit(&e.to_string()));
|
||||
@@ -614,13 +649,13 @@ impl Configuration {
|
||||
}
|
||||
}
|
||||
|
||||
if args.is_present("stdin") {
|
||||
if came_from_cli!(args, "stdin") {
|
||||
config.stdin = true;
|
||||
} else if let Some(url) = args.value_of("url") {
|
||||
config.target_url = String::from(url);
|
||||
} else if let Some(url) = args.get_one::<String>("url") {
|
||||
config.target_url = url.into();
|
||||
}
|
||||
|
||||
if let Some(arg) = args.values_of("url_denylist") {
|
||||
if let Some(arg) = args.get_many::<String>("url_denylist") {
|
||||
// compile all regular expressions and absolute urls used for --dont-scan
|
||||
//
|
||||
// when --dont-scan is used, the should_deny_url function is called at least once per
|
||||
@@ -664,15 +699,15 @@ impl Configuration {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(arg) = args.values_of("filter_regex") {
|
||||
if let Some(arg) = args.get_many::<String>("filter_regex") {
|
||||
config.filter_regex = arg.map(|val| val.to_string()).collect();
|
||||
}
|
||||
|
||||
if let Some(arg) = args.values_of("filter_similar") {
|
||||
if let Some(arg) = args.get_many::<String>("filter_similar") {
|
||||
config.filter_similar = arg.map(|val| val.to_string()).collect();
|
||||
}
|
||||
|
||||
if let Some(arg) = args.values_of("filter_size") {
|
||||
if let Some(arg) = args.get_many::<String>("filter_size") {
|
||||
config.filter_size = arg
|
||||
.map(|size| {
|
||||
size.parse::<u64>()
|
||||
@@ -681,7 +716,7 @@ impl Configuration {
|
||||
.collect();
|
||||
}
|
||||
|
||||
if let Some(arg) = args.values_of("filter_words") {
|
||||
if let Some(arg) = args.get_many::<String>("filter_words") {
|
||||
config.filter_word_count = arg
|
||||
.map(|size| {
|
||||
size.parse::<usize>()
|
||||
@@ -690,7 +725,7 @@ impl Configuration {
|
||||
.collect();
|
||||
}
|
||||
|
||||
if let Some(arg) = args.values_of("filter_lines") {
|
||||
if let Some(arg) = args.get_many::<String>("filter_lines") {
|
||||
config.filter_line_count = arg
|
||||
.map(|size| {
|
||||
size.parse::<usize>()
|
||||
@@ -699,7 +734,7 @@ impl Configuration {
|
||||
.collect();
|
||||
}
|
||||
|
||||
if args.is_present("silent") {
|
||||
if came_from_cli!(args, "silent") {
|
||||
// the reason this is protected by an if statement:
|
||||
// consider a user specifying silent = true in ferox-config.toml
|
||||
// if the line below is outside of the if, we'd overwrite true with
|
||||
@@ -708,102 +743,111 @@ impl Configuration {
|
||||
config.output_level = OutputLevel::Silent;
|
||||
}
|
||||
|
||||
if args.is_present("quiet") {
|
||||
if came_from_cli!(args, "quiet") {
|
||||
config.quiet = true;
|
||||
config.output_level = OutputLevel::Quiet;
|
||||
}
|
||||
|
||||
if args.is_present("auto_tune") || args.is_present("smart") || args.is_present("thorough") {
|
||||
if came_from_cli!(args, "auto_tune")
|
||||
|| came_from_cli!(args, "smart")
|
||||
|| came_from_cli!(args, "thorough")
|
||||
{
|
||||
config.auto_tune = true;
|
||||
config.requester_policy = RequesterPolicy::AutoTune;
|
||||
}
|
||||
|
||||
if args.is_present("auto_bail") {
|
||||
if came_from_cli!(args, "auto_bail") {
|
||||
config.auto_bail = true;
|
||||
config.requester_policy = RequesterPolicy::AutoBail;
|
||||
}
|
||||
|
||||
if args.is_present("no_state") {
|
||||
if came_from_cli!(args, "no_state") {
|
||||
config.save_state = false;
|
||||
}
|
||||
|
||||
if args.is_present("dont_filter") {
|
||||
if came_from_cli!(args, "dont_filter") {
|
||||
config.dont_filter = true;
|
||||
}
|
||||
|
||||
if args.is_present("collect_extensions") || args.is_present("thorough") {
|
||||
if came_from_cli!(args, "collect_extensions") || came_from_cli!(args, "thorough") {
|
||||
config.collect_extensions = true;
|
||||
}
|
||||
|
||||
if args.is_present("collect_backups")
|
||||
|| args.is_present("smart")
|
||||
|| args.is_present("thorough")
|
||||
if came_from_cli!(args, "collect_backups")
|
||||
|| came_from_cli!(args, "smart")
|
||||
|| came_from_cli!(args, "thorough")
|
||||
{
|
||||
config.collect_backups = true;
|
||||
}
|
||||
|
||||
if args.is_present("collect_words")
|
||||
|| args.is_present("smart")
|
||||
|| args.is_present("thorough")
|
||||
if came_from_cli!(args, "collect_words")
|
||||
|| came_from_cli!(args, "smart")
|
||||
|| came_from_cli!(args, "thorough")
|
||||
{
|
||||
config.collect_words = true;
|
||||
}
|
||||
|
||||
if args.occurrences_of("verbosity") > 0 {
|
||||
if args.get_count("verbosity") > 0 {
|
||||
// occurrences_of returns 0 if none are found; this is protected in
|
||||
// an if block for the same reason as the quiet option
|
||||
config.verbosity = args.occurrences_of("verbosity") as u8;
|
||||
config.verbosity = args.get_count("verbosity");
|
||||
}
|
||||
|
||||
if args.is_present("no_recursion") {
|
||||
if came_from_cli!(args, "no_recursion") {
|
||||
config.no_recursion = true;
|
||||
}
|
||||
|
||||
if args.is_present("add_slash") {
|
||||
if came_from_cli!(args, "add_slash") {
|
||||
config.add_slash = true;
|
||||
}
|
||||
|
||||
if args.is_present("extract_links")
|
||||
|| args.is_present("smart")
|
||||
|| args.is_present("thorough")
|
||||
if came_from_cli!(args, "extract_links")
|
||||
|| came_from_cli!(args, "smart")
|
||||
|| came_from_cli!(args, "thorough")
|
||||
{
|
||||
config.extract_links = true;
|
||||
}
|
||||
|
||||
if args.is_present("json") {
|
||||
if came_from_cli!(args, "json") {
|
||||
config.json = true;
|
||||
}
|
||||
|
||||
if came_from_cli!(args, "force_recursion") {
|
||||
config.force_recursion = true;
|
||||
}
|
||||
|
||||
////
|
||||
// organizational breakpoint; all options below alter the Client configuration
|
||||
////
|
||||
update_config_if_present!(&mut config.proxy, args, "proxy");
|
||||
update_config_if_present!(&mut config.replay_proxy, args, "replay_proxy");
|
||||
update_config_if_present!(&mut config.user_agent, args, "user_agent");
|
||||
update_config_if_present!(&mut config.timeout, args, "timeout");
|
||||
update_config_if_present!(&mut config.proxy, args, "proxy", String);
|
||||
update_config_if_present!(&mut config.replay_proxy, args, "replay_proxy", String);
|
||||
update_config_if_present!(&mut config.user_agent, args, "user_agent", String);
|
||||
update_config_with_num_type_if_present!(&mut config.timeout, args, "timeout", u64);
|
||||
|
||||
if args.is_present("burp") {
|
||||
if came_from_cli!(args, "burp") {
|
||||
config.proxy = String::from("http://127.0.0.1:8080");
|
||||
}
|
||||
|
||||
if args.is_present("burp_replay") {
|
||||
if came_from_cli!(args, "burp_replay") {
|
||||
config.replay_proxy = String::from("http://127.0.0.1:8080");
|
||||
}
|
||||
|
||||
if args.is_present("random_agent") {
|
||||
if came_from_cli!(args, "random_agent") {
|
||||
config.random_agent = true;
|
||||
}
|
||||
|
||||
if args.is_present("redirects") {
|
||||
if came_from_cli!(args, "redirects") {
|
||||
config.redirects = true;
|
||||
}
|
||||
|
||||
if args.is_present("insecure") || args.is_present("burp") || args.is_present("burp_replay")
|
||||
if came_from_cli!(args, "insecure")
|
||||
|| came_from_cli!(args, "burp")
|
||||
|| came_from_cli!(args, "burp_replay")
|
||||
{
|
||||
config.insecure = true;
|
||||
}
|
||||
|
||||
if let Some(headers) = args.values_of("headers") {
|
||||
if let Some(headers) = args.get_many::<String>("headers") {
|
||||
for val in headers {
|
||||
let mut split_val = val.split(':');
|
||||
|
||||
@@ -817,7 +861,7 @@ impl Configuration {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cookies) = args.values_of("cookies") {
|
||||
if let Some(cookies) = args.get_many::<String>("cookies") {
|
||||
config.headers.insert(
|
||||
// we know the header name is always "cookie"
|
||||
"Cookie".to_string(),
|
||||
@@ -833,7 +877,7 @@ impl Configuration {
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(queries) = args.values_of("queries") {
|
||||
if let Some(queries) = args.get_many::<String>("queries") {
|
||||
for val in queries {
|
||||
// same basic logic used as reading in the headers HashMap above
|
||||
let mut split_val = val.split('=');
|
||||
@@ -942,9 +986,10 @@ impl Configuration {
|
||||
update_if_not_default!(&mut conf.output, new.output, "");
|
||||
update_if_not_default!(&mut conf.redirects, new.redirects, false);
|
||||
update_if_not_default!(&mut conf.insecure, new.insecure, false);
|
||||
update_if_not_default!(&mut conf.force_recursion, new.force_recursion, false);
|
||||
update_if_not_default!(&mut conf.extract_links, new.extract_links, false);
|
||||
update_if_not_default!(&mut conf.extensions, new.extensions, Vec::<String>::new());
|
||||
update_if_not_default!(&mut conf.methods, new.methods, Vec::<String>::new());
|
||||
update_if_not_default!(&mut conf.methods, new.methods, methods());
|
||||
update_if_not_default!(&mut conf.data, new.data, Vec::<u8>::new());
|
||||
update_if_not_default!(&mut conf.url_denylist, new.url_denylist, Vec::<Url>::new());
|
||||
if !new.regex_denylist.is_empty() {
|
||||
@@ -1017,7 +1062,17 @@ impl Configuration {
|
||||
/// uses serde to deserialize the toml into a `Configuration` struct
|
||||
pub(super) fn parse_config(config_file: PathBuf) -> Result<Self> {
|
||||
let content = read_to_string(config_file)?;
|
||||
let config: Self = toml::from_str(content.as_str())?;
|
||||
let mut config: Self = toml::from_str(content.as_str())?;
|
||||
|
||||
if !config.extensions.is_empty() {
|
||||
// remove leading periods, if any are found
|
||||
config.extensions = config
|
||||
.extensions
|
||||
.iter()
|
||||
.map(|ext| ext.trim_start_matches('.').to_string())
|
||||
.collect();
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ fn setup_config_test() -> Configuration {
|
||||
json = true
|
||||
save_state = false
|
||||
depth = 1
|
||||
force_recursion = true
|
||||
filter_size = [4120]
|
||||
filter_regex = ["^ignore me$"]
|
||||
filter_similar = ["https://somesite.com/soft404"]
|
||||
@@ -95,6 +96,7 @@ fn default_configuration() {
|
||||
assert!(config.save_state);
|
||||
assert!(!config.stdin);
|
||||
assert!(!config.add_slash);
|
||||
assert!(!config.force_recursion);
|
||||
assert!(!config.redirects);
|
||||
assert!(!config.extract_links);
|
||||
assert!(!config.insecure);
|
||||
@@ -208,6 +210,13 @@ fn config_reads_silent() {
|
||||
assert!(config.silent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_force_recursion() {
|
||||
let config = setup_config_test();
|
||||
assert!(config.force_recursion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_quiet() {
|
||||
|
||||
@@ -81,7 +81,7 @@ pub(super) fn depth() -> usize {
|
||||
}
|
||||
|
||||
/// enum representing the three possible states for informational output (not logging verbosity)
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum OutputLevel {
|
||||
/// normal scan, no --quiet|--silent
|
||||
Default,
|
||||
@@ -116,7 +116,7 @@ pub fn determine_output_level(quiet: bool, silent: bool) -> OutputLevel {
|
||||
}
|
||||
|
||||
/// represents actions the Requester should take in certain situations
|
||||
#[derive(Debug, PartialEq, Copy, Clone)]
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
pub enum RequesterPolicy {
|
||||
/// automatically try to lower request rate in order to reduce errors
|
||||
AutoTune,
|
||||
|
||||
@@ -5,6 +5,7 @@ use tokio::sync::oneshot::Sender;
|
||||
|
||||
use crate::response::FeroxResponse;
|
||||
use crate::{
|
||||
event_handlers::Handles,
|
||||
message::FeroxMessage,
|
||||
statistics::{StatError, StatField},
|
||||
traits::FeroxFilter,
|
||||
@@ -43,6 +44,9 @@ pub enum Command {
|
||||
/// Add a `FeroxFilter` implementor to `FilterHandler`'s instance of `FeroxFilters`
|
||||
AddFilter(Box<dyn FeroxFilter>),
|
||||
|
||||
/// Remove a set of `FeroxFilter` implementors from `FeroxFilters` by index
|
||||
RemoveFilters(Vec<usize>),
|
||||
|
||||
/// Send a `FeroxResponse` to the output handler for reporting
|
||||
Report(Box<FeroxResponse>),
|
||||
|
||||
@@ -75,4 +79,8 @@ pub enum Command {
|
||||
|
||||
/// Break out of the (infinite) mpsc receive loop
|
||||
Exit,
|
||||
|
||||
/// Give a handler access to an Arc<Handles> instance after the handler has
|
||||
/// already been initialized
|
||||
AddHandles(Arc<Handles>),
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::filters::EmptyFilter;
|
||||
use crate::{filters::FeroxFilters, CommandSender, FeroxChannel, Joiner};
|
||||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
@@ -84,8 +85,12 @@ impl FiltersHandler {
|
||||
while let Some(command) = self.receiver.recv().await {
|
||||
match command {
|
||||
Command::AddFilter(filter) => {
|
||||
self.data.push(filter)?;
|
||||
if filter.as_any().downcast_ref::<EmptyFilter>().is_none() {
|
||||
// don't add an empty filter
|
||||
self.data.push(filter)?;
|
||||
}
|
||||
}
|
||||
Command::RemoveFilters(mut indices) => self.data.remove(&mut indices),
|
||||
Command::Sync(sender) => {
|
||||
log::debug!("filters: {:?}", self);
|
||||
sender.send(true).unwrap_or_default();
|
||||
@@ -99,3 +104,41 @@ impl FiltersHandler {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::filters::WordsFilter;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn empty_filter_skipped() {
|
||||
let data = Arc::new(FeroxFilters::default());
|
||||
let (tx, rx): FeroxChannel<Command> = mpsc::unbounded_channel();
|
||||
|
||||
let mut handler = FiltersHandler::new(data.clone(), rx);
|
||||
|
||||
let event_handle = FiltersHandle::new(data, tx);
|
||||
|
||||
let _task = tokio::spawn(async move { handler.start().await });
|
||||
|
||||
event_handle
|
||||
.send(Command::AddFilter(Box::new(EmptyFilter {})))
|
||||
.unwrap();
|
||||
|
||||
let (tx, rx) = oneshot::channel::<bool>();
|
||||
event_handle.send(Command::Sync(tx)).unwrap();
|
||||
rx.await.unwrap();
|
||||
|
||||
assert!(event_handle.data.filters.read().unwrap().is_empty());
|
||||
|
||||
event_handle
|
||||
.send(Command::AddFilter(Box::new(WordsFilter { word_count: 1 })))
|
||||
.unwrap();
|
||||
|
||||
let (tx, rx) = oneshot::channel::<bool>();
|
||||
event_handle.send(Command::Sync(tx)).unwrap();
|
||||
rx.await.unwrap();
|
||||
|
||||
assert_eq!(event_handle.data.filters.read().unwrap().len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,12 +98,16 @@ impl TermInputHandler {
|
||||
handles.config.clone(),
|
||||
&RESPONSES,
|
||||
handles.stats.data.clone(),
|
||||
handles.filters.data.clone(),
|
||||
);
|
||||
|
||||
let state_file = open_file(&filename);
|
||||
// User didn't set the --no-state flag (so saved_state is still the default true)
|
||||
if handles.config.save_state {
|
||||
let state_file = open_file(&filename);
|
||||
|
||||
let mut buffered_file = state_file?;
|
||||
write_to(&state, &mut buffered_file, true)?;
|
||||
let mut buffered_file = state_file?;
|
||||
write_to(&state, &mut buffered_file, true)?;
|
||||
}
|
||||
|
||||
log::trace!("exit: sigint_handler (end of program)");
|
||||
std::process::exit(1);
|
||||
|
||||
@@ -5,14 +5,13 @@ use anyhow::{Context, Result};
|
||||
use futures::future::{BoxFuture, FutureExt};
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
|
||||
use crate::statistics::StatField::TotalExpected;
|
||||
use crate::{
|
||||
config::Configuration,
|
||||
progress::PROGRESS_PRINTER,
|
||||
response::FeroxResponse,
|
||||
scanner::RESPONSES,
|
||||
send_command, skip_fail,
|
||||
statistics::StatField::ResourcesDiscovered,
|
||||
statistics::StatField::{ResourcesDiscovered, TotalExpected},
|
||||
traits::FeroxSerialize,
|
||||
utils::{ferox_print, fmt_err, make_request, open_file, write_to},
|
||||
CommandReceiver, CommandSender, Joiner,
|
||||
@@ -144,6 +143,9 @@ pub struct TermOutHandler {
|
||||
|
||||
/// pointer to "global" configuration struct
|
||||
config: Arc<Configuration>,
|
||||
|
||||
/// handles instance
|
||||
handles: Option<Arc<Handles>>,
|
||||
}
|
||||
|
||||
/// implementation of TermOutHandler
|
||||
@@ -161,6 +163,7 @@ impl TermOutHandler {
|
||||
tx_file,
|
||||
file_task,
|
||||
config,
|
||||
handles: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,6 +215,9 @@ impl TermOutHandler {
|
||||
Command::Sync(sender) => {
|
||||
sender.send(true).unwrap_or_default();
|
||||
}
|
||||
Command::AddHandles(handles) => {
|
||||
self.handles = Some(handles);
|
||||
}
|
||||
Command::Exit => {
|
||||
if self.file_task.is_some() && self.tx_file.send(Command::Exit).is_ok() {
|
||||
self.file_task.as_mut().unwrap().await??; // wait for death
|
||||
@@ -236,9 +242,26 @@ impl TermOutHandler {
|
||||
log::trace!("enter: process_response({:?}, {:?})", resp, call_type);
|
||||
|
||||
async move {
|
||||
let contains_sentry = self.config.status_codes.contains(&resp.status().as_u16());
|
||||
let should_filter = self
|
||||
.handles
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.filters
|
||||
.data
|
||||
.should_filter_response(&resp, self.handles.as_ref().unwrap().stats.tx.clone());
|
||||
|
||||
let contains_sentry = if !self.config.filter_status.is_empty() {
|
||||
// -C was used, meaning -s was not and we should ignore the defaults
|
||||
// https://github.com/epi052/feroxbuster/issues/535
|
||||
// -C indicates that we should filter that status code, but allow all others
|
||||
!self.config.filter_status.contains(&resp.status().as_u16())
|
||||
} else {
|
||||
// -C wasn't used, so, we defer to checking the -s values
|
||||
self.config.status_codes.contains(&resp.status().as_u16())
|
||||
};
|
||||
|
||||
let unknown_sentry = !RESPONSES.contains(&resp); // !contains == unknown
|
||||
let should_process_response = contains_sentry && unknown_sentry;
|
||||
let should_process_response = contains_sentry && unknown_sentry && !should_filter;
|
||||
|
||||
if should_process_response {
|
||||
// print to stdout
|
||||
@@ -284,7 +307,7 @@ impl TermOutHandler {
|
||||
&& matches!(call_type, ProcessResponseCall::Recursive)
|
||||
{
|
||||
// --collect-backups was used; the response is one we care about, and the function
|
||||
// call came from the loop in `.start` (i.e. recursive was specified
|
||||
// call came from the loop in `.start` (i.e. recursive was specified)
|
||||
let backup_urls = self.generate_backup_urls(&resp).await;
|
||||
|
||||
// need to manually adjust stats
|
||||
@@ -398,6 +421,7 @@ impl TermOutHandler {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::event_handlers::Command;
|
||||
|
||||
#[test]
|
||||
/// try to hit struct field coverage of FileOutHandler
|
||||
@@ -417,12 +441,14 @@ mod tests {
|
||||
let (tx, rx) = mpsc::unbounded_channel::<Command>();
|
||||
let (tx_file, _) = mpsc::unbounded_channel::<Command>();
|
||||
let config = Arc::new(Configuration::new().unwrap());
|
||||
let handles = Arc::new(Handles::for_testing(None, None).0);
|
||||
|
||||
let toh = TermOutHandler {
|
||||
config,
|
||||
file_task: None,
|
||||
receiver: rx,
|
||||
tx_file,
|
||||
handles: Some(handles),
|
||||
};
|
||||
|
||||
println!("{:?}", toh);
|
||||
@@ -435,12 +461,14 @@ mod tests {
|
||||
let (tx, rx) = mpsc::unbounded_channel::<Command>();
|
||||
let (tx_file, _) = mpsc::unbounded_channel::<Command>();
|
||||
let config = Arc::new(Configuration::new().unwrap());
|
||||
let handles = Arc::new(Handles::for_testing(None, None).0);
|
||||
|
||||
let toh = TermOutHandler {
|
||||
config,
|
||||
file_task: None,
|
||||
receiver: rx,
|
||||
tx_file,
|
||||
handles: Some(handles),
|
||||
};
|
||||
|
||||
let expected: Vec<_> = vec![
|
||||
@@ -478,12 +506,14 @@ mod tests {
|
||||
let (tx, rx) = mpsc::unbounded_channel::<Command>();
|
||||
let (tx_file, _) = mpsc::unbounded_channel::<Command>();
|
||||
let config = Arc::new(Configuration::new().unwrap());
|
||||
let handles = Arc::new(Handles::for_testing(None, None).0);
|
||||
|
||||
let toh = TermOutHandler {
|
||||
config,
|
||||
file_task: None,
|
||||
receiver: rx,
|
||||
tx_file,
|
||||
handles: Some(handles),
|
||||
};
|
||||
|
||||
let expected: Vec<_> = vec![
|
||||
@@ -521,12 +551,14 @@ mod tests {
|
||||
let (tx, rx) = mpsc::unbounded_channel::<Command>();
|
||||
let (tx_file, _) = mpsc::unbounded_channel::<Command>();
|
||||
let config = Arc::new(Configuration::new().unwrap());
|
||||
let handles = Arc::new(Handles::for_testing(None, None).0);
|
||||
|
||||
let toh = TermOutHandler {
|
||||
config,
|
||||
file_task: None,
|
||||
receiver: rx,
|
||||
tx_file,
|
||||
handles: Some(handles),
|
||||
};
|
||||
|
||||
let expected: Vec<_> = vec![
|
||||
|
||||
@@ -368,8 +368,8 @@ impl ScanHandler {
|
||||
async fn try_recursion(&mut self, response: Box<FeroxResponse>) -> Result<()> {
|
||||
log::trace!("enter: try_recursion({:?})", response,);
|
||||
|
||||
if !response.is_directory() {
|
||||
// not a directory, quick exit
|
||||
if !self.handles.config.force_recursion && !response.is_directory() {
|
||||
// not a directory and --force-recursion wasn't used, quick exit
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,11 @@ pub(super) const LINKFINDER_REGEX: &str = r#"(?:"|')(((?:[a-zA-Z]{1,10}://|//)[^
|
||||
pub(super) const ROBOTS_TXT_REGEX: &str =
|
||||
r#"(?m)^ *(Allow|Disallow): *(?P<url_path>[a-zA-Z0-9._/?#@!&'()+,;%=-]+?)$"#; // multi-line (?m)
|
||||
|
||||
/// Regular expression to filter bad characters from extracted url paths
|
||||
///
|
||||
/// ref: https://www.rfc-editor.org/rfc/rfc3986#section-2
|
||||
pub(super) const URL_CHARS_REGEX: &str = r#"["<>\\^`{|} ]"#;
|
||||
|
||||
/// Which type of extraction should be performed
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum ExtractionTarget {
|
||||
@@ -90,6 +95,7 @@ impl<'a> ExtractorBuilder<'a> {
|
||||
Ok(Extractor {
|
||||
links_regex: Regex::new(LINKFINDER_REGEX).unwrap(),
|
||||
robots_regex: Regex::new(ROBOTS_TXT_REGEX).unwrap(),
|
||||
url_regex: Regex::new(URL_CHARS_REGEX).unwrap(),
|
||||
response: if self.response.is_some() {
|
||||
Some(self.response.unwrap())
|
||||
} else {
|
||||
|
||||
@@ -2,7 +2,6 @@ use super::*;
|
||||
use crate::{
|
||||
client,
|
||||
event_handlers::{
|
||||
Command,
|
||||
Command::{AddError, AddToUsizeField},
|
||||
Handles,
|
||||
},
|
||||
@@ -12,14 +11,13 @@ use crate::{
|
||||
StatField::{LinksExtracted, TotalExpected},
|
||||
},
|
||||
url::FeroxUrl,
|
||||
utils::{logged_request, make_request, should_deny_url},
|
||||
utils::{logged_request, make_request, send_try_recursion_command, should_deny_url},
|
||||
ExtractionResult, DEFAULT_METHOD,
|
||||
};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use reqwest::{Client, StatusCode, Url};
|
||||
use scraper::{Html, Selector};
|
||||
use std::collections::HashSet;
|
||||
use tokio::sync::oneshot;
|
||||
use std::{borrow::Cow, collections::HashSet};
|
||||
|
||||
/// Whether an active scan is recursive or not
|
||||
#[derive(Debug)]
|
||||
@@ -40,6 +38,9 @@ pub struct Extractor<'a> {
|
||||
/// `ROBOTS_TXT_REGEX` as a regex::Regex type
|
||||
pub(super) robots_regex: Regex,
|
||||
|
||||
/// regex to validate a url
|
||||
pub(super) url_regex: Regex,
|
||||
|
||||
/// Response from which to extract links
|
||||
pub(super) response: Option<&'a FeroxResponse>,
|
||||
|
||||
@@ -186,11 +187,21 @@ impl<'a> Extractor<'a> {
|
||||
resp.set_url(&format!("{}/", resp.url()));
|
||||
}
|
||||
|
||||
self.handles
|
||||
.send_scan_command(Command::TryRecursion(Box::new(resp)))?;
|
||||
let (tx, rx) = oneshot::channel::<bool>();
|
||||
self.handles.send_scan_command(Command::Sync(tx))?;
|
||||
rx.await?;
|
||||
if self.handles.config.filter_status.is_empty() {
|
||||
// -C wasn't used, so -s is the only 'filter' left to account for
|
||||
if self
|
||||
.handles
|
||||
.config
|
||||
.status_codes
|
||||
.contains(&resp.status().as_u16())
|
||||
{
|
||||
send_try_recursion_command(self.handles.clone(), resp).await?;
|
||||
}
|
||||
} else {
|
||||
// -C was used, that means the filters above would have removed
|
||||
// those responses, and anything else should be let through
|
||||
send_try_recursion_command(self.handles.clone(), resp).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
log::trace!("exit: request_links");
|
||||
@@ -212,7 +223,7 @@ impl<'a> Extractor<'a> {
|
||||
self.extract_links_by_attr(resp_url, links, html, "div", "src");
|
||||
self.extract_links_by_attr(resp_url, links, html, "frame", "src");
|
||||
self.extract_links_by_attr(resp_url, links, html, "embed", "src");
|
||||
self.extract_links_by_attr(resp_url, links, html, "script", "src");
|
||||
self.extract_links_by_attr(resp_url, links, html, "link", "href");
|
||||
}
|
||||
|
||||
/// Given the body of a `reqwest::Response`, perform the following actions
|
||||
@@ -325,8 +336,9 @@ impl<'a> Extractor<'a> {
|
||||
let normalized_path = self.normalize_url_path(path);
|
||||
|
||||
// filter out any empty strings caused by .split
|
||||
let mut parts: Vec<&str> = normalized_path
|
||||
let mut parts: Vec<Cow<_>> = normalized_path
|
||||
.split('/')
|
||||
.map(|s| self.url_regex.replace_all(s, ""))
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
|
||||
@@ -385,6 +397,17 @@ impl<'a> Extractor<'a> {
|
||||
.join(link)
|
||||
.with_context(|| format!("Could not join {} with {}", old_url, link))?;
|
||||
|
||||
if old_url.domain() != new_url.domain() || old_url.host() != old_url.host() {
|
||||
// domains/ips are not the same, don't scan things that aren't part of the original
|
||||
// target url
|
||||
log::debug!(
|
||||
"Skipping {} because it's not part of the original target",
|
||||
new_url
|
||||
);
|
||||
log::trace!("exit: add_link_to_set_of_links");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
links.insert(new_url.to_string());
|
||||
|
||||
log::trace!("exit: add_link_to_set_of_links");
|
||||
@@ -406,7 +429,7 @@ impl<'a> Extractor<'a> {
|
||||
|
||||
let scanned_urls = self.handles.ferox_scans()?;
|
||||
|
||||
if scanned_urls.get_scan_by_url(&new_url.to_string()).is_some() {
|
||||
if scanned_urls.get_scan_by_url(new_url.as_ref()).is_some() {
|
||||
//we've seen the url before and don't need to scan again
|
||||
log::trace!("exit: request_link -> None");
|
||||
bail!("previously seen url");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::builder::{LINKFINDER_REGEX, ROBOTS_TXT_REGEX};
|
||||
use super::builder::{LINKFINDER_REGEX, ROBOTS_TXT_REGEX, URL_CHARS_REGEX};
|
||||
use super::*;
|
||||
use crate::config::{Configuration, OutputLevel};
|
||||
use crate::scan_manager::ScanOrder;
|
||||
@@ -273,6 +273,7 @@ async fn extractor_get_links_with_absolute_url_that_differs_from_target_domain()
|
||||
let extractor = Extractor {
|
||||
links_regex: Regex::new(LINKFINDER_REGEX).unwrap(),
|
||||
robots_regex: Regex::new(ROBOTS_TXT_REGEX).unwrap(),
|
||||
url_regex: Regex::new(URL_CHARS_REGEX).unwrap(),
|
||||
response: Some(&ferox_response),
|
||||
url: String::new(),
|
||||
target: ExtractionTarget::ResponseBody,
|
||||
@@ -301,6 +302,7 @@ async fn request_robots_txt_without_proxy() -> Result<()> {
|
||||
let extractor = Extractor {
|
||||
links_regex: Regex::new(LINKFINDER_REGEX).unwrap(),
|
||||
robots_regex: Regex::new(ROBOTS_TXT_REGEX).unwrap(),
|
||||
url_regex: Regex::new(URL_CHARS_REGEX).unwrap(),
|
||||
response: None,
|
||||
url: srv.url("/api/users/stuff/things"),
|
||||
target: ExtractionTarget::RobotsTxt,
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
use std::sync::Mutex;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{ser::SerializeSeq, Serialize, Serializer};
|
||||
|
||||
use crate::response::FeroxResponse;
|
||||
use crate::{
|
||||
event_handlers::Command::AddToUsizeField, statistics::StatField::WildcardsFiltered,
|
||||
CommandSender,
|
||||
event_handlers::Command::AddToUsizeField, response::FeroxResponse,
|
||||
statistics::StatField::WildcardsFiltered, CommandSender,
|
||||
};
|
||||
|
||||
use super::{FeroxFilter, WildcardFilter};
|
||||
use super::{
|
||||
FeroxFilter, LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter,
|
||||
WildcardFilter, WordsFilter,
|
||||
};
|
||||
|
||||
/// Container around a collection of `FeroxFilters`s
|
||||
#[derive(Debug, Default)]
|
||||
pub struct FeroxFilters {
|
||||
/// collection of `FeroxFilters`
|
||||
pub filters: Mutex<Vec<Box<dyn FeroxFilter>>>,
|
||||
pub filters: RwLock<Vec<Box<dyn FeroxFilter>>>,
|
||||
}
|
||||
|
||||
/// implementation of FeroxFilter collection
|
||||
impl FeroxFilters {
|
||||
/// add a single FeroxFilter to the collection
|
||||
pub fn push(&self, filter: Box<dyn FeroxFilter>) -> Result<()> {
|
||||
if let Ok(mut guard) = self.filters.lock() {
|
||||
if let Ok(mut guard) = self.filters.write() {
|
||||
if guard.contains(&filter) {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -31,6 +34,37 @@ impl FeroxFilters {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// remove items from the underlying collection by their index
|
||||
///
|
||||
/// note: indexes passed in should be index-to-remove+1. This is built for the scan mgt menu
|
||||
/// so indexes aren't 0-based whehn the user enters them.
|
||||
///
|
||||
pub fn remove(&self, indices: &mut [usize]) {
|
||||
// since we're removing by index, indices must be sorted and then reversed.
|
||||
// this allows us to iterate over the collection from the rear, allowing any shifting
|
||||
// of the vector to happen on sections that we no longer care about, as we're moving
|
||||
// in the opposite direction
|
||||
indices.sort_unstable();
|
||||
indices.reverse();
|
||||
|
||||
if let Ok(mut guard) = self.filters.write() {
|
||||
for index in indices {
|
||||
// numbering of the menu starts at 1, so we'll need to reduce the index by 1
|
||||
// to account for that. if they've provided 0 as an offset, we'll set the
|
||||
// result to a gigantic number and skip it in the loop with a bounds check
|
||||
let reduced_idx = index.checked_sub(1).unwrap_or(usize::MAX);
|
||||
|
||||
// check if number provided is out of range
|
||||
if reduced_idx >= guard.len() {
|
||||
// usize can't be negative, just need to handle exceeding bounds
|
||||
continue;
|
||||
}
|
||||
|
||||
guard.remove(reduced_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple helper to stay DRY; determines whether or not a given `FeroxResponse` should be reported
|
||||
/// to the user or not.
|
||||
pub fn should_filter_response(
|
||||
@@ -38,7 +72,7 @@ impl FeroxFilters {
|
||||
response: &FeroxResponse,
|
||||
tx_stats: CommandSender,
|
||||
) -> bool {
|
||||
if let Ok(filters) = self.filters.lock() {
|
||||
if let Ok(filters) = self.filters.read() {
|
||||
for filter in filters.iter() {
|
||||
// wildcard.should_filter goes here
|
||||
if filter.should_filter_response(response) {
|
||||
@@ -54,3 +88,43 @@ impl FeroxFilters {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for FeroxFilters {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
if let Ok(guard) = self.filters.read() {
|
||||
let mut seq = serializer.serialize_seq(Some(guard.len()))?;
|
||||
|
||||
for filter in &*guard {
|
||||
if let Some(line_filter) = filter.as_any().downcast_ref::<LinesFilter>() {
|
||||
seq.serialize_element(line_filter).unwrap_or_default();
|
||||
} else if let Some(word_filter) = filter.as_any().downcast_ref::<WordsFilter>() {
|
||||
seq.serialize_element(word_filter).unwrap_or_default();
|
||||
} else if let Some(size_filter) = filter.as_any().downcast_ref::<SizeFilter>() {
|
||||
seq.serialize_element(size_filter).unwrap_or_default();
|
||||
} else if let Some(status_filter) =
|
||||
filter.as_any().downcast_ref::<StatusCodeFilter>()
|
||||
{
|
||||
seq.serialize_element(status_filter).unwrap_or_default();
|
||||
} else if let Some(regex_filter) = filter.as_any().downcast_ref::<RegexFilter>() {
|
||||
seq.serialize_element(regex_filter).unwrap_or_default();
|
||||
} else if let Some(similarity_filter) =
|
||||
filter.as_any().downcast_ref::<SimilarityFilter>()
|
||||
{
|
||||
seq.serialize_element(similarity_filter).unwrap_or_default();
|
||||
} else if let Some(wildcard_filter) =
|
||||
filter.as_any().downcast_ref::<WildcardFilter>()
|
||||
{
|
||||
seq.serialize_element(wildcard_filter).unwrap_or_default();
|
||||
}
|
||||
}
|
||||
seq.end()
|
||||
} else {
|
||||
// if for some reason we can't unlock the mutex, just write an empty list
|
||||
let seq = serializer.serialize_seq(Some(0))?;
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
src/filters/empty.rs
Normal file
22
src/filters/empty.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use super::*;
|
||||
|
||||
/// Dummy filter for internal shenanigans
|
||||
#[derive(Default, Debug, PartialEq, Eq)]
|
||||
pub struct EmptyFilter {}
|
||||
|
||||
impl FeroxFilter for EmptyFilter {
|
||||
/// `EmptyFilter` always returns false
|
||||
fn should_filter_response(&self, _response: &FeroxResponse) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Compare one EmptyFilter to another
|
||||
fn box_eq(&self, other: &dyn Any) -> bool {
|
||||
other.downcast_ref::<Self>().map_or(false, |a| self == a)
|
||||
}
|
||||
|
||||
/// Return self as Any for dynamic dispatch purposes
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,10 @@
|
||||
use super::{
|
||||
LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter, WordsFilter,
|
||||
};
|
||||
use crate::{
|
||||
event_handlers::Handles,
|
||||
response::FeroxResponse,
|
||||
skip_fail,
|
||||
utils::{fmt_err, logged_request},
|
||||
Command::AddFilter,
|
||||
DEFAULT_METHOD, SIMILARITY_THRESHOLD,
|
||||
utils::create_similarity_filter, LinesFilter, RegexFilter, SizeFilter, StatusCodeFilter,
|
||||
WordsFilter,
|
||||
};
|
||||
use crate::{event_handlers::Handles, skip_fail, utils::fmt_err, Command::AddFilter};
|
||||
use anyhow::Result;
|
||||
use fuzzyhash::FuzzyHash;
|
||||
use regex::Regex;
|
||||
use reqwest::Url;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// add all user-supplied filters to the (already started) filters handler
|
||||
@@ -68,32 +60,7 @@ pub async fn initialize(handles: Arc<Handles>) -> Result<()> {
|
||||
|
||||
// add any similarity filters to filters handler's FeroxFilters (--filter-similar-to)
|
||||
for similarity_filter in &handles.config.filter_similar {
|
||||
// url as-is based on input, ignores user-specified url manipulation options (add-slash etc)
|
||||
let url = skip_fail!(Url::parse(similarity_filter));
|
||||
|
||||
// attempt to request the given url
|
||||
let resp = skip_fail!(logged_request(&url, DEFAULT_METHOD, None, handles.clone()).await);
|
||||
|
||||
// if successful, create a filter based on the response's body
|
||||
let mut fr = FeroxResponse::from(
|
||||
resp,
|
||||
similarity_filter,
|
||||
DEFAULT_METHOD,
|
||||
handles.config.output_level,
|
||||
)
|
||||
.await;
|
||||
|
||||
if handles.config.collect_extensions {
|
||||
fr.parse_extension(handles.clone())?;
|
||||
}
|
||||
|
||||
// hash the response body and store the resulting hash in the filter object
|
||||
let hash = FuzzyHash::new(&fr.text()).to_string();
|
||||
|
||||
let filter = SimilarityFilter {
|
||||
text: hash,
|
||||
threshold: SIMILARITY_THRESHOLD,
|
||||
};
|
||||
let filter = skip_fail!(create_similarity_filter(similarity_filter, handles.clone()).await);
|
||||
|
||||
let boxed_filter = Box::new(filter);
|
||||
skip_fail!(handles.filters.send(AddFilter(boxed_filter)));
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::*;
|
||||
|
||||
/// Simple implementor of FeroxFilter; used to filter out responses based on the number of lines
|
||||
/// in a Response body; specified using -N|--filter-lines
|
||||
#[derive(Default, Debug, PartialEq)]
|
||||
#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct LinesFilter {
|
||||
/// Number of lines in a Response's body that should be filtered
|
||||
pub line_count: usize,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! contains all of feroxbuster's filters
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::any::Any;
|
||||
use std::fmt::Debug;
|
||||
|
||||
@@ -6,12 +7,14 @@ use crate::response::FeroxResponse;
|
||||
use crate::traits::{FeroxFilter, FeroxSerialize};
|
||||
|
||||
pub use self::container::FeroxFilters;
|
||||
pub(crate) use self::empty::EmptyFilter;
|
||||
pub use self::init::initialize;
|
||||
pub use self::lines::LinesFilter;
|
||||
pub use self::regex::RegexFilter;
|
||||
pub use self::similarity::SimilarityFilter;
|
||||
pub use self::size::SizeFilter;
|
||||
pub use self::status_code::StatusCodeFilter;
|
||||
pub(crate) use self::utils::{create_similarity_filter, filter_lookup};
|
||||
pub use self::wildcard::WildcardFilter;
|
||||
pub use self::words::WordsFilter;
|
||||
|
||||
@@ -26,3 +29,5 @@ mod container;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod init;
|
||||
mod utils;
|
||||
mod empty;
|
||||
|
||||
@@ -3,15 +3,25 @@ use ::regex::Regex;
|
||||
|
||||
/// Simple implementor of FeroxFilter; used to filter out responses based on a given regular
|
||||
/// expression; specified using -X|--filter-regex
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RegexFilter {
|
||||
/// Regular expression to be applied to the response body for filtering, compiled
|
||||
#[serde(with = "serde_regex")]
|
||||
pub compiled: Regex,
|
||||
|
||||
/// Regular expression as passed in on the command line, not compiled
|
||||
pub raw_string: String,
|
||||
}
|
||||
|
||||
impl Default for RegexFilter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
compiled: Regex::new("").unwrap(),
|
||||
raw_string: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// implementation of FeroxFilter for RegexFilter
|
||||
impl FeroxFilter for RegexFilter {
|
||||
/// Check `expression` against the response body, if the expression matches, the response
|
||||
|
||||
@@ -3,13 +3,16 @@ use fuzzyhash::FuzzyHash;
|
||||
|
||||
/// Simple implementor of FeroxFilter; used to filter out responses based on the similarity of a
|
||||
/// Response body with a known response; specified using --filter-similar-to
|
||||
#[derive(Default, Debug, PartialEq)]
|
||||
#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SimilarityFilter {
|
||||
/// Response's body to be used for comparison for similarity
|
||||
pub text: String,
|
||||
/// Hash of Response's body to be used during similarity comparison
|
||||
pub hash: String,
|
||||
|
||||
/// Percentage of similarity at which a page is determined to be a near-duplicate of another
|
||||
pub threshold: u32,
|
||||
|
||||
/// Url originally requested for the similarity filter
|
||||
pub original_url: String,
|
||||
}
|
||||
|
||||
/// implementation of FeroxFilter for SimilarityFilter
|
||||
@@ -17,9 +20,9 @@ impl FeroxFilter for SimilarityFilter {
|
||||
/// Check `FeroxResponse::text` against what was requested from the site passed in via
|
||||
/// --filter-similar-to
|
||||
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
|
||||
let other = FuzzyHash::new(&response.text());
|
||||
let other = FuzzyHash::new(response.text());
|
||||
|
||||
if let Ok(result) = FuzzyHash::compare(&self.text, &other.to_string()) {
|
||||
if let Ok(result) = FuzzyHash::compare(&self.hash, other.to_string()) {
|
||||
return result >= self.threshold;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::*;
|
||||
|
||||
/// Simple implementor of FeroxFilter; used to filter out responses based on the length of a
|
||||
/// Response body; specified using -S|--filter-size
|
||||
#[derive(Default, Debug, PartialEq)]
|
||||
#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SizeFilter {
|
||||
/// Overall length of a Response's body that should be filtered
|
||||
pub content_length: u64,
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::*;
|
||||
|
||||
/// Simple implementor of FeroxFilter; used to filter out status codes specified using
|
||||
/// -C|--filter-status
|
||||
#[derive(Default, Debug, PartialEq)]
|
||||
#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct StatusCodeFilter {
|
||||
/// Status code that should not be displayed to the user
|
||||
pub filter_code: u16,
|
||||
|
||||
@@ -186,22 +186,23 @@ fn similarity_filter_is_accurate() {
|
||||
resp.set_text("sitting");
|
||||
|
||||
let mut filter = SimilarityFilter {
|
||||
text: FuzzyHash::new("kitten").to_string(),
|
||||
hash: FuzzyHash::new("kitten").to_string(),
|
||||
threshold: 95,
|
||||
original_url: "".to_string(),
|
||||
};
|
||||
|
||||
// kitten/sitting is 57% similar, so a threshold of 95 should not be filtered
|
||||
assert!(!filter.should_filter_response(&resp));
|
||||
|
||||
resp.set_text("");
|
||||
filter.text = String::new();
|
||||
filter.hash = String::new();
|
||||
filter.threshold = 100;
|
||||
|
||||
// two empty strings are the same, however ssdeep doesn't accept empty strings, expect false
|
||||
assert!(!filter.should_filter_response(&resp));
|
||||
|
||||
resp.set_text("some data to hash for the purposes of running a test");
|
||||
filter.text = FuzzyHash::new("some data to hash for the purposes of running a te").to_string();
|
||||
filter.hash = FuzzyHash::new("some data to hash for the purposes of running a te").to_string();
|
||||
filter.threshold = 17;
|
||||
|
||||
assert!(filter.should_filter_response(&resp));
|
||||
@@ -211,20 +212,58 @@ fn similarity_filter_is_accurate() {
|
||||
/// just a simple test to increase code coverage by hitting as_any and the inner value
|
||||
fn similarity_filter_as_any() {
|
||||
let filter = SimilarityFilter {
|
||||
text: String::from("stuff"),
|
||||
hash: String::from("stuff"),
|
||||
threshold: 95,
|
||||
original_url: "".to_string(),
|
||||
};
|
||||
|
||||
let filter2 = SimilarityFilter {
|
||||
text: String::from("stuff"),
|
||||
hash: String::from("stuff"),
|
||||
threshold: 95,
|
||||
original_url: "".to_string(),
|
||||
};
|
||||
|
||||
assert!(filter.box_eq(filter2.as_any()));
|
||||
|
||||
assert_eq!(filter.text, "stuff");
|
||||
assert_eq!(filter.hash, "stuff");
|
||||
assert_eq!(
|
||||
*filter.as_any().downcast_ref::<SimilarityFilter>().unwrap(),
|
||||
filter
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test correctness of FeroxFilters::remove
|
||||
fn remove_function_works_as_expected() {
|
||||
let data = FeroxFilters::default();
|
||||
assert!(data.filters.read().unwrap().is_empty());
|
||||
|
||||
(0..8).for_each(|i| {
|
||||
data.push(Box::new(WordsFilter { word_count: i })).unwrap();
|
||||
});
|
||||
|
||||
// remove removes index-1 from the vec, zero is skipped, and out-of-bounds indices are skipped
|
||||
data.remove(&mut [0]);
|
||||
assert_eq!(data.filters.read().unwrap().len(), 8);
|
||||
|
||||
data.remove(&mut [10000]);
|
||||
assert_eq!(data.filters.read().unwrap().len(), 8);
|
||||
|
||||
// removing 0, 2, 4
|
||||
data.remove(&mut [1, 3, 5]);
|
||||
|
||||
assert_eq!(data.filters.read().unwrap().len(), 5);
|
||||
|
||||
let expected = vec![
|
||||
WordsFilter { word_count: 1 },
|
||||
WordsFilter { word_count: 3 },
|
||||
WordsFilter { word_count: 5 },
|
||||
WordsFilter { word_count: 6 },
|
||||
WordsFilter { word_count: 7 },
|
||||
];
|
||||
|
||||
for filter in data.filters.read().unwrap().iter() {
|
||||
let downcast = filter.as_any().downcast_ref::<WordsFilter>().unwrap();
|
||||
assert!(expected.contains(downcast));
|
||||
}
|
||||
}
|
||||
|
||||
204
src/filters/utils.rs
Normal file
204
src/filters/utils.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
use super::FeroxFilter;
|
||||
use super::SimilarityFilter;
|
||||
use crate::event_handlers::Handles;
|
||||
use crate::response::FeroxResponse;
|
||||
use crate::utils::logged_request;
|
||||
use crate::{DEFAULT_METHOD, SIMILARITY_THRESHOLD};
|
||||
use anyhow::Result;
|
||||
use fuzzyhash::FuzzyHash;
|
||||
use regex::Regex;
|
||||
use reqwest::Url;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// wrapper around logic necessary to create a SimilarityFilter
|
||||
///
|
||||
/// - parses given url
|
||||
/// - makes request to the parsed url
|
||||
/// - gathers extensions from the url, if configured to do so
|
||||
/// - computes hash of response body
|
||||
/// - creates filter with hash
|
||||
pub(crate) async fn create_similarity_filter(
|
||||
similarity_filter: &str,
|
||||
handles: Arc<Handles>,
|
||||
) -> Result<SimilarityFilter> {
|
||||
// url as-is based on input, ignores user-specified url manipulation options (add-slash etc)
|
||||
let url = Url::parse(similarity_filter)?;
|
||||
|
||||
// attempt to request the given url
|
||||
let resp = logged_request(&url, DEFAULT_METHOD, None, handles.clone()).await?;
|
||||
|
||||
// if successful, create a filter based on the response's body
|
||||
let mut fr = FeroxResponse::from(
|
||||
resp,
|
||||
similarity_filter,
|
||||
DEFAULT_METHOD,
|
||||
handles.config.output_level,
|
||||
)
|
||||
.await;
|
||||
|
||||
if handles.config.collect_extensions {
|
||||
fr.parse_extension(handles.clone())?;
|
||||
}
|
||||
|
||||
// hash the response body and store the resulting hash in the filter object
|
||||
let hash = FuzzyHash::new(fr.text()).to_string();
|
||||
|
||||
Ok(SimilarityFilter {
|
||||
hash,
|
||||
threshold: SIMILARITY_THRESHOLD,
|
||||
original_url: similarity_filter.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// used in conjunction with the Scan Management Menu
|
||||
///
|
||||
/// when a user uses the n[ew-filter] command in the menu, the two params are passed here for
|
||||
/// processing.
|
||||
///
|
||||
/// an example command may be `new-filter lines 40`. `lines` and `40` are passed here as &str's
|
||||
///
|
||||
/// once here, the type and value are used to create an appropriate FeroxFilter. If anything
|
||||
/// goes wrong during creation, None is returned.
|
||||
pub(crate) fn filter_lookup(filter_type: &str, filter_value: &str) -> Option<Box<dyn FeroxFilter>> {
|
||||
match filter_type {
|
||||
"status" => {
|
||||
if let Ok(parsed) = filter_value.parse() {
|
||||
return Some(Box::new(super::StatusCodeFilter {
|
||||
filter_code: parsed,
|
||||
}));
|
||||
}
|
||||
}
|
||||
"lines" => {
|
||||
if let Ok(parsed) = filter_value.parse() {
|
||||
return Some(Box::new(super::LinesFilter { line_count: parsed }));
|
||||
}
|
||||
}
|
||||
"size" => {
|
||||
if let Ok(parsed) = filter_value.parse() {
|
||||
return Some(Box::new(super::SizeFilter {
|
||||
content_length: parsed,
|
||||
}));
|
||||
}
|
||||
}
|
||||
"words" => {
|
||||
if let Ok(parsed) = filter_value.parse() {
|
||||
return Some(Box::new(super::WordsFilter { word_count: parsed }));
|
||||
}
|
||||
}
|
||||
"regex" => {
|
||||
if let Ok(parsed) = Regex::new(filter_value) {
|
||||
return Some(Box::new(super::RegexFilter {
|
||||
compiled: parsed,
|
||||
raw_string: filter_value.to_string(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
"similarity" => {
|
||||
return Some(Box::new(SimilarityFilter {
|
||||
hash: String::new(),
|
||||
threshold: SIMILARITY_THRESHOLD,
|
||||
original_url: filter_value.to_string(),
|
||||
}));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::Configuration;
|
||||
use crate::filters::{LinesFilter, RegexFilter, SizeFilter, StatusCodeFilter, WordsFilter};
|
||||
use crate::scan_manager::FeroxScans;
|
||||
use httpmock::Method::GET;
|
||||
use httpmock::MockServer;
|
||||
|
||||
#[test]
|
||||
/// filter_lookup returns correct filters
|
||||
fn filter_lookup_returns_correct_filters() {
|
||||
let filter = filter_lookup("status", "200").unwrap();
|
||||
assert_eq!(
|
||||
filter.as_any().downcast_ref::<StatusCodeFilter>().unwrap(),
|
||||
&StatusCodeFilter { filter_code: 200 }
|
||||
);
|
||||
|
||||
let filter = filter_lookup("lines", "10").unwrap();
|
||||
assert_eq!(
|
||||
filter.as_any().downcast_ref::<LinesFilter>().unwrap(),
|
||||
&LinesFilter { line_count: 10 }
|
||||
);
|
||||
|
||||
let filter = filter_lookup("size", "20").unwrap();
|
||||
assert_eq!(
|
||||
filter.as_any().downcast_ref::<SizeFilter>().unwrap(),
|
||||
&SizeFilter { content_length: 20 }
|
||||
);
|
||||
|
||||
let filter = filter_lookup("words", "30").unwrap();
|
||||
assert_eq!(
|
||||
filter.as_any().downcast_ref::<WordsFilter>().unwrap(),
|
||||
&WordsFilter { word_count: 30 }
|
||||
);
|
||||
|
||||
let filter = filter_lookup("regex", "stuff.*").unwrap();
|
||||
let compiled = Regex::new("stuff.*").unwrap();
|
||||
let raw_string = String::from("stuff.*");
|
||||
assert_eq!(
|
||||
filter.as_any().downcast_ref::<RegexFilter>().unwrap(),
|
||||
&RegexFilter {
|
||||
compiled,
|
||||
raw_string
|
||||
}
|
||||
);
|
||||
|
||||
let filter = filter_lookup("similarity", "http://localhost").unwrap();
|
||||
assert_eq!(
|
||||
filter.as_any().downcast_ref::<SimilarityFilter>().unwrap(),
|
||||
&SimilarityFilter {
|
||||
hash: String::new(),
|
||||
threshold: SIMILARITY_THRESHOLD,
|
||||
original_url: "http://localhost".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
assert!(filter_lookup("non-existent", "").is_none());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
/// ensure create_similarity_filter correctness of return value and side-effects
|
||||
async fn create_similarity_filter_is_correct() {
|
||||
let srv = MockServer::start();
|
||||
|
||||
let mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/");
|
||||
then.status(200).body("this is a test");
|
||||
});
|
||||
|
||||
let scans = FeroxScans::default();
|
||||
let config = Configuration {
|
||||
collect_extensions: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (test_handles, _) = Handles::for_testing(Some(Arc::new(scans)), Some(Arc::new(config)));
|
||||
|
||||
let handles = Arc::new(test_handles);
|
||||
|
||||
let filter = create_similarity_filter(&srv.url("/"), handles.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(mock.hits(), 1);
|
||||
|
||||
assert_eq!(
|
||||
filter,
|
||||
SimilarityFilter {
|
||||
hash: "3:YKEpn:Yfp".to_string(),
|
||||
threshold: SIMILARITY_THRESHOLD,
|
||||
original_url: srv.url("/")
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ use crate::{url::FeroxUrl, DEFAULT_METHOD};
|
||||
///
|
||||
/// `size` is size of the response that should be included with filters passed via runtime
|
||||
/// configuration and any static wildcard lengths.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct WildcardFilter {
|
||||
/// size of the response that will later be combined with the length of the path of the url
|
||||
/// requested
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::*;
|
||||
|
||||
/// Simple implementor of FeroxFilter; used to filter out responses based on the number of words
|
||||
/// in a Response body; specified using -W|--filter-words
|
||||
#[derive(Default, Debug, PartialEq)]
|
||||
#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct WordsFilter {
|
||||
/// Number of words in a Response's body that should be filtered
|
||||
pub word_count: usize,
|
||||
|
||||
@@ -91,7 +91,7 @@ impl HeuristicTests {
|
||||
let mut ids = vec![];
|
||||
|
||||
for _ in 0..length {
|
||||
ids.push(Uuid::new_v4().to_simple().to_string());
|
||||
ids.push(Uuid::new_v4().as_simple().to_string());
|
||||
}
|
||||
|
||||
let unique_id = ids.join("");
|
||||
|
||||
11
src/lib.rs
11
src/lib.rs
@@ -65,10 +65,19 @@ pub(crate) const DEFAULT_IGNORED_EXTENSIONS: [&str; 38] = [
|
||||
/// Default wordlist to use when `-w|--wordlist` isn't specified and not `wordlist` isn't set
|
||||
/// in a [ferox-config.toml](constant.DEFAULT_CONFIG_NAME.html) config file.
|
||||
///
|
||||
/// defaults to kali's default install location:
|
||||
/// defaults to kali's default install location on linux:
|
||||
/// - `/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt`
|
||||
///
|
||||
/// and to the current directory on windows
|
||||
/// - `.\seclists\Discovery\Web-Content\raft-medium-directories.txt`
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub const DEFAULT_WORDLIST: &str =
|
||||
"/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt";
|
||||
#[cfg(target_os = "windows")]
|
||||
pub const DEFAULT_WORDLIST: &str =
|
||||
".\\SecLists\\Discovery\\Web-Content\\raft-medium-directories.txt";
|
||||
pub const SECONDARY_WORDLIST: &str =
|
||||
"/usr/local/share/seclists/Discovery/Web-Content/raft-medium-directories.txt";
|
||||
|
||||
/// Number of milliseconds to wait between polls of `PAUSE_SCAN` when user pauses a scan
|
||||
pub(crate) const SLEEP_DURATION: u64 = 500;
|
||||
|
||||
@@ -65,7 +65,7 @@ pub fn initialize(config: Arc<Configuration>) -> Result<()> {
|
||||
kind: "log".to_string(),
|
||||
};
|
||||
|
||||
PROGRESS_PRINTER.println(&log_entry.as_str());
|
||||
PROGRESS_PRINTER.println(log_entry.as_str());
|
||||
|
||||
if let Some(buffered_file) = file.clone() {
|
||||
if let Ok(mut unlocked) = buffered_file.write() {
|
||||
|
||||
50
src/main.rs
50
src/main.rs
@@ -17,20 +17,22 @@ use tokio::{
|
||||
};
|
||||
use tokio_util::codec::{FramedRead, LinesCodec};
|
||||
|
||||
use feroxbuster::scan_manager::ScanType;
|
||||
use feroxbuster::{
|
||||
banner::{Banner, UPDATE_URL},
|
||||
config::{Configuration, OutputLevel},
|
||||
event_handlers::{
|
||||
Command::{CreateBar, Exit, JoinTasks, LoadStats, ScanInitialUrls, UpdateWordlist},
|
||||
Command::{
|
||||
AddHandles, CreateBar, Exit, JoinTasks, LoadStats, ScanInitialUrls, UpdateWordlist,
|
||||
},
|
||||
FiltersHandler, Handles, ScanHandler, StatsHandler, Tasks, TermInputHandler,
|
||||
TermOutHandler, SCAN_COMPLETE,
|
||||
},
|
||||
filters, heuristics, logger,
|
||||
progress::{PROGRESS_BAR, PROGRESS_PRINTER},
|
||||
scan_manager::{self},
|
||||
scan_manager::{self, ScanType},
|
||||
scanner,
|
||||
utils::{fmt_err, slugify_filename},
|
||||
SECONDARY_WORDLIST,
|
||||
};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use feroxbuster::{utils::set_open_file_limit, DEFAULT_OPEN_FILE_LIMIT};
|
||||
@@ -42,11 +44,12 @@ lazy_static! {
|
||||
static ref PARALLEL_LIMITER: Semaphore = Semaphore::new(0);
|
||||
}
|
||||
|
||||
/// Create a HashSet of Strings from the given wordlist then stores it inside an Arc
|
||||
/// Create a Vec of Strings from the given wordlist then stores it inside an Arc
|
||||
fn get_unique_words_from_wordlist(path: &str) -> Result<Arc<Vec<String>>> {
|
||||
log::trace!("enter: get_unique_words_from_wordlist({})", path);
|
||||
let mut trimmed_word = false;
|
||||
|
||||
let file = File::open(&path).with_context(|| format!("Could not open {}", path))?;
|
||||
let file = File::open(path).with_context(|| format!("Could not open {}", path))?;
|
||||
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
@@ -59,12 +62,21 @@ fn get_unique_words_from_wordlist(path: &str) -> Result<Arc<Vec<String>>> {
|
||||
for line in reader.lines() {
|
||||
line.map(|result| {
|
||||
if !result.starts_with('#') && !result.is_empty() {
|
||||
words.push(result);
|
||||
if result.starts_with('/') {
|
||||
words.push(result.trim_start_matches('/').to_string());
|
||||
trimmed_word = true;
|
||||
} else {
|
||||
words.push(result);
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
if trimmed_word {
|
||||
log::warn!("Some words in the wordlist started with a leading forward-slash; those words were trimmed (i.e. /word -> word)");
|
||||
}
|
||||
|
||||
log::trace!(
|
||||
"exit: get_unique_words_from_wordlist -> Arc<wordlist[{} words...]>",
|
||||
words.len()
|
||||
@@ -148,7 +160,7 @@ async fn get_targets(handles: Arc<Handles>) -> Result<Vec<String>> {
|
||||
}
|
||||
|
||||
// remove footgun that arises if a --dont-scan value matches on a base url
|
||||
for target in &targets {
|
||||
for target in targets.iter_mut() {
|
||||
for denier in &handles.config.regex_denylist {
|
||||
if denier.is_match(target) {
|
||||
bail!(
|
||||
@@ -167,6 +179,11 @@ async fn get_targets(handles: Arc<Handles>) -> Result<Vec<String>> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !target.starts_with("http") && !target.starts_with("https") {
|
||||
// --url hackerone.com
|
||||
*target = format!("https://{}", target);
|
||||
}
|
||||
}
|
||||
|
||||
log::trace!("exit: get_targets -> {:?}", targets);
|
||||
@@ -193,7 +210,19 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
|
||||
// cloning an Arc is cheap (it's basically a pointer into the heap)
|
||||
// so that will allow for cheap/safe sharing of a single wordlist across multi-target scans
|
||||
// as well as additional directories found as part of recursion
|
||||
let words = get_unique_words_from_wordlist(&config.wordlist)?;
|
||||
let words = match get_unique_words_from_wordlist(&config.wordlist) {
|
||||
Ok(w) => w,
|
||||
Err(err) => {
|
||||
let secondary = Path::new(SECONDARY_WORDLIST);
|
||||
|
||||
if secondary.exists() {
|
||||
eprintln!("Found wordlist in secondary location");
|
||||
get_unique_words_from_wordlist(SECONDARY_WORDLIST)?
|
||||
} else {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if words.len() <= 1 {
|
||||
// the check is now <= 1 due to the initial empty string added in 2.6.0
|
||||
@@ -220,6 +249,7 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
|
||||
let (scan_task, scan_handle) = ScanHandler::initialize(handles.clone());
|
||||
|
||||
handles.set_scan_handle(scan_handle); // must be done after Handles initialization
|
||||
handles.output.send(AddHandles(handles.clone()))?;
|
||||
|
||||
filters::initialize(handles.clone()).await?; // send user-supplied filters to the handler
|
||||
|
||||
@@ -247,7 +277,7 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
|
||||
let from_here = config.resume_from.clone();
|
||||
|
||||
// populate FeroxScans object with previously seen scans
|
||||
scanned_urls.add_serialized_scans(&from_here)?;
|
||||
scanned_urls.add_serialized_scans(&from_here, handles.clone())?;
|
||||
|
||||
// populate Stats object with previously known statistics
|
||||
handles.stats.send(LoadStats(from_here))?;
|
||||
@@ -308,7 +338,7 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
|
||||
|
||||
let new_folder = slugify_filename(&base_name.to_string_lossy(), "", "logs");
|
||||
|
||||
let final_path = output_path.with_file_name(&new_folder);
|
||||
let final_path = output_path.with_file_name(new_folder);
|
||||
|
||||
// create the directory or fail silently, assuming the reason for failure is that
|
||||
// the path exists already
|
||||
|
||||
@@ -23,7 +23,7 @@ impl Document {
|
||||
document.number_of_terms += processed.len();
|
||||
|
||||
for normalized in processed {
|
||||
if normalized.len() > 2 {
|
||||
if normalized.len() >= 2 {
|
||||
document.add_term(&normalized)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,19 +38,16 @@ fn normalize_case<'a, S: Into<Cow<'a, str>>>(input: S) -> Cow<'a, str> {
|
||||
}
|
||||
}
|
||||
|
||||
/// remove ascii and some utf-8 punctuation characters from the given string
|
||||
/// replace ascii and some utf-8 punctuation characters with ' ' (space) in the given string
|
||||
fn remove_punctuation(text: &str) -> String {
|
||||
// non-separator type chars can be replaced with an empty string, while separators are replaced
|
||||
// with a space. This attempts to keep things like
|
||||
// 'aboutblogfaqcontactpresstermslexicondisclosure' from happening
|
||||
text.replace(
|
||||
[
|
||||
'!', '\\', '"', '#', '$', '%', '&', '(', ')', '*', '+', ':', ';', '<', '=', '>', '?',
|
||||
'@', '[', ']', '^', '{', '}', '|', '~', ',', '\'', '“', '”', '’', '‘', '’', '‘',
|
||||
'@', '[', ']', '^', '{', '}', '|', '~', ',', '\'', '“', '”', '’', '‘', '’', '‘', '/',
|
||||
'–', '—', '.',
|
||||
],
|
||||
"",
|
||||
" ",
|
||||
)
|
||||
.replace(['/', '–', '—', '.'], " ")
|
||||
}
|
||||
|
||||
/// remove stop words from the given string
|
||||
@@ -86,7 +83,10 @@ mod tests {
|
||||
fn test_remove_punctuation() {
|
||||
let tester = "!\\\"#$%&()*+/:;<=>?@[]^{}|~,.'“”’‘–—\n‘’";
|
||||
// the `" \n"` is because of the things like / getting replaced with a space
|
||||
assert_eq!(remove_punctuation(tester), " \n");
|
||||
assert_eq!(
|
||||
remove_punctuation(tester),
|
||||
" \n "
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -115,7 +115,7 @@ mod tests {
|
||||
/// ensure preprocess
|
||||
fn test_preprocess_results() {
|
||||
let tester = "WHY are Y'all YELLing?";
|
||||
assert_eq!(&preprocess(tester), &["yall", "yelling"]);
|
||||
assert_eq!(&preprocess(tester), &["y", "all", "yelling"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
176
src/parser.rs
176
src/parser.rs
@@ -1,3 +1,4 @@
|
||||
use clap::ArgAction;
|
||||
use clap::{
|
||||
crate_authors, crate_description, crate_name, crate_version, Arg, ArgGroup, Command, ValueHint,
|
||||
};
|
||||
@@ -25,7 +26,7 @@ lazy_static! {
|
||||
}
|
||||
|
||||
/// Create and return an instance of [clap::App](https://docs.rs/clap/latest/clap/struct.App.html), i.e. the Command Line Interface's configuration
|
||||
pub fn initialize() -> Command<'static> {
|
||||
pub fn initialize() -> Command {
|
||||
let app = Command::new(crate_name!())
|
||||
.version(crate_version!())
|
||||
.author(crate_authors!())
|
||||
@@ -39,7 +40,7 @@ pub fn initialize() -> Command<'static> {
|
||||
Arg::new("url")
|
||||
.short('u')
|
||||
.long("url")
|
||||
.required_unless_present_any(&["stdin", "resume_from"])
|
||||
.required_unless_present_any(["stdin", "resume_from"])
|
||||
.help_heading("Target selection")
|
||||
.value_name("URL")
|
||||
.use_value_delimiter(true)
|
||||
@@ -50,7 +51,7 @@ pub fn initialize() -> Command<'static> {
|
||||
Arg::new("stdin")
|
||||
.long("stdin")
|
||||
.help_heading("Target selection")
|
||||
.takes_value(false)
|
||||
.num_args(0)
|
||||
.help("Read url(s) from STDIN")
|
||||
.conflicts_with("url")
|
||||
)
|
||||
@@ -62,7 +63,7 @@ pub fn initialize() -> Command<'static> {
|
||||
.help_heading("Target selection")
|
||||
.help("State file from which to resume a partially complete scan (ex. --resume-from ferox-1606586780.state)")
|
||||
.conflicts_with("url")
|
||||
.takes_value(true),
|
||||
.num_args(1),
|
||||
);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
@@ -72,25 +73,29 @@ pub fn initialize() -> Command<'static> {
|
||||
.arg(
|
||||
Arg::new("burp")
|
||||
.long("burp")
|
||||
.num_args(0)
|
||||
.help_heading("Composite settings")
|
||||
.conflicts_with_all(&["proxy", "insecure", "burp_replay"])
|
||||
.conflicts_with_all(["proxy", "insecure", "burp_replay"])
|
||||
.help("Set --proxy to http://127.0.0.1:8080 and set --insecure to true"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("burp_replay")
|
||||
.long("burp-replay")
|
||||
.num_args(0)
|
||||
.help_heading("Composite settings")
|
||||
.conflicts_with_all(&["replay_proxy", "insecure"])
|
||||
.conflicts_with_all(["replay_proxy", "insecure"])
|
||||
.help("Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("smart")
|
||||
.long("smart")
|
||||
.num_args(0)
|
||||
.help_heading("Composite settings")
|
||||
.help("Set --extract-links, --auto-tune, --collect-words, and --collect-backups to true"),
|
||||
).arg(
|
||||
Arg::new("thorough")
|
||||
.long("thorough")
|
||||
.num_args(0)
|
||||
.help_heading("Composite settings")
|
||||
.help("Use the same settings as --smart and set --collect-extensions to true"),
|
||||
);
|
||||
@@ -103,7 +108,7 @@ pub fn initialize() -> Command<'static> {
|
||||
Arg::new("proxy")
|
||||
.short('p')
|
||||
.long("proxy")
|
||||
.takes_value(true)
|
||||
.num_args(1)
|
||||
.value_name("PROXY")
|
||||
.value_hint(ValueHint::Url)
|
||||
.help_heading("Proxy settings")
|
||||
@@ -115,7 +120,7 @@ pub fn initialize() -> Command<'static> {
|
||||
Arg::new("replay_proxy")
|
||||
.short('P')
|
||||
.long("replay-proxy")
|
||||
.takes_value(true)
|
||||
.num_args(1)
|
||||
.value_hint(ValueHint::Url)
|
||||
.value_name("REPLAY_PROXY")
|
||||
.help_heading("Proxy settings")
|
||||
@@ -128,9 +133,8 @@ pub fn initialize() -> Command<'static> {
|
||||
.short('R')
|
||||
.long("replay-codes")
|
||||
.value_name("REPLAY_CODE")
|
||||
.takes_value(true)
|
||||
.multiple_values(true)
|
||||
.multiple_occurrences(true)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.use_value_delimiter(true)
|
||||
.requires("replay_proxy")
|
||||
.help_heading("Proxy settings")
|
||||
@@ -148,7 +152,7 @@ pub fn initialize() -> Command<'static> {
|
||||
.short('a')
|
||||
.long("user-agent")
|
||||
.value_name("USER_AGENT")
|
||||
.takes_value(true)
|
||||
.num_args(1)
|
||||
.help_heading("Request settings")
|
||||
.help(&**DEFAULT_USER_AGENT),
|
||||
)
|
||||
@@ -156,7 +160,7 @@ pub fn initialize() -> Command<'static> {
|
||||
Arg::new("random_agent")
|
||||
.short('A')
|
||||
.long("random-agent")
|
||||
.takes_value(false)
|
||||
.num_args(0)
|
||||
.help_heading("Request settings")
|
||||
.help("Use a random User-Agent"),
|
||||
)
|
||||
@@ -165,9 +169,8 @@ pub fn initialize() -> Command<'static> {
|
||||
.short('x')
|
||||
.long("extensions")
|
||||
.value_name("FILE_EXTENSION")
|
||||
.takes_value(true)
|
||||
.multiple_values(true)
|
||||
.multiple_occurrences(true)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.use_value_delimiter(true)
|
||||
.help_heading("Request settings")
|
||||
.help(
|
||||
@@ -179,9 +182,8 @@ pub fn initialize() -> Command<'static> {
|
||||
.short('m')
|
||||
.long("methods")
|
||||
.value_name("HTTP_METHODS")
|
||||
.takes_value(true)
|
||||
.multiple_values(true)
|
||||
.multiple_occurrences(true)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.use_value_delimiter(true)
|
||||
.help_heading("Request settings")
|
||||
.help(
|
||||
@@ -192,7 +194,7 @@ pub fn initialize() -> Command<'static> {
|
||||
Arg::new("data")
|
||||
.long("data")
|
||||
.value_name("DATA")
|
||||
.takes_value(true)
|
||||
.num_args(1)
|
||||
.help_heading("Request settings")
|
||||
.help(
|
||||
"Request's Body; can read data from a file if input starts with an @ (ex: @post.bin)",
|
||||
@@ -203,10 +205,9 @@ pub fn initialize() -> Command<'static> {
|
||||
.short('H')
|
||||
.long("headers")
|
||||
.value_name("HEADER")
|
||||
.takes_value(true)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.help_heading("Request settings")
|
||||
.multiple_values(true)
|
||||
.multiple_occurrences(true)
|
||||
.use_value_delimiter(true)
|
||||
.help(
|
||||
"Specify HTTP headers to be used in each request (ex: -H Header:val -H 'stuff: things')",
|
||||
@@ -217,9 +218,8 @@ pub fn initialize() -> Command<'static> {
|
||||
.short('b')
|
||||
.long("cookies")
|
||||
.value_name("COOKIE")
|
||||
.takes_value(true)
|
||||
.multiple_values(true)
|
||||
.multiple_occurrences(true)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.use_value_delimiter(true)
|
||||
.help_heading("Request settings")
|
||||
.help(
|
||||
@@ -231,9 +231,8 @@ pub fn initialize() -> Command<'static> {
|
||||
.short('Q')
|
||||
.long("query")
|
||||
.value_name("QUERY")
|
||||
.takes_value(true)
|
||||
.multiple_values(true)
|
||||
.multiple_occurrences(true)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.use_value_delimiter(true)
|
||||
.help_heading("Request settings")
|
||||
.help(
|
||||
@@ -245,7 +244,7 @@ pub fn initialize() -> Command<'static> {
|
||||
.short('f')
|
||||
.long("add-slash")
|
||||
.help_heading("Request settings")
|
||||
.takes_value(false)
|
||||
.num_args(0)
|
||||
.help("Append / to each request's URL")
|
||||
);
|
||||
|
||||
@@ -256,9 +255,8 @@ pub fn initialize() -> Command<'static> {
|
||||
Arg::new("url_denylist")
|
||||
.long("dont-scan")
|
||||
.value_name("URL")
|
||||
.takes_value(true)
|
||||
.multiple_values(true)
|
||||
.multiple_occurrences(true)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.use_value_delimiter(true)
|
||||
.help_heading("Request filters")
|
||||
.help("URL(s) or Regex Pattern(s) to exclude from recursion/scans"),
|
||||
@@ -273,9 +271,8 @@ pub fn initialize() -> Command<'static> {
|
||||
.short('S')
|
||||
.long("filter-size")
|
||||
.value_name("SIZE")
|
||||
.takes_value(true)
|
||||
.multiple_values(true)
|
||||
.multiple_occurrences(true)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.use_value_delimiter(true)
|
||||
.help_heading("Response filters")
|
||||
.help(
|
||||
@@ -287,9 +284,8 @@ pub fn initialize() -> Command<'static> {
|
||||
.short('X')
|
||||
.long("filter-regex")
|
||||
.value_name("REGEX")
|
||||
.takes_value(true)
|
||||
.multiple_values(true)
|
||||
.multiple_occurrences(true)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.use_value_delimiter(true)
|
||||
.help_heading("Response filters")
|
||||
.help(
|
||||
@@ -301,9 +297,8 @@ pub fn initialize() -> Command<'static> {
|
||||
.short('W')
|
||||
.long("filter-words")
|
||||
.value_name("WORDS")
|
||||
.takes_value(true)
|
||||
.multiple_values(true)
|
||||
.multiple_occurrences(true)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.use_value_delimiter(true)
|
||||
.help_heading("Response filters")
|
||||
.help(
|
||||
@@ -315,9 +310,8 @@ pub fn initialize() -> Command<'static> {
|
||||
.short('N')
|
||||
.long("filter-lines")
|
||||
.value_name("LINES")
|
||||
.takes_value(true)
|
||||
.multiple_values(true)
|
||||
.multiple_occurrences(true)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.use_value_delimiter(true)
|
||||
.help_heading("Response filters")
|
||||
.help(
|
||||
@@ -329,10 +323,10 @@ pub fn initialize() -> Command<'static> {
|
||||
.short('C')
|
||||
.long("filter-status")
|
||||
.value_name("STATUS_CODE")
|
||||
.takes_value(true)
|
||||
.multiple_values(true)
|
||||
.multiple_occurrences(true)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.use_value_delimiter(true)
|
||||
.conflicts_with("status_codes")
|
||||
.help_heading("Response filters")
|
||||
.help(
|
||||
"Filter out status codes (deny list) (ex: -C 200 -C 401)",
|
||||
@@ -342,9 +336,8 @@ pub fn initialize() -> Command<'static> {
|
||||
Arg::new("filter_similar")
|
||||
.long("filter-similar-to")
|
||||
.value_name("UNWANTED_PAGE")
|
||||
.takes_value(true)
|
||||
.multiple_values(true)
|
||||
.multiple_occurrences(true)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.value_hint(ValueHint::Url)
|
||||
.use_value_delimiter(true)
|
||||
.help_heading("Response filters")
|
||||
@@ -357,9 +350,8 @@ pub fn initialize() -> Command<'static> {
|
||||
.short('s')
|
||||
.long("status-codes")
|
||||
.value_name("STATUS_CODE")
|
||||
.takes_value(true)
|
||||
.multiple_values(true)
|
||||
.multiple_occurrences(true)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.use_value_delimiter(true)
|
||||
.help_heading("Response filters")
|
||||
.help(
|
||||
@@ -376,7 +368,7 @@ pub fn initialize() -> Command<'static> {
|
||||
.short('T')
|
||||
.long("timeout")
|
||||
.value_name("SECONDS")
|
||||
.takes_value(true)
|
||||
.num_args(1)
|
||||
.help_heading("Client settings")
|
||||
.help("Number of seconds before a client's request times out (default: 7)"),
|
||||
)
|
||||
@@ -384,7 +376,7 @@ pub fn initialize() -> Command<'static> {
|
||||
Arg::new("redirects")
|
||||
.short('r')
|
||||
.long("redirects")
|
||||
.takes_value(false)
|
||||
.num_args(0)
|
||||
.help_heading("Client settings")
|
||||
.help("Allow client to follow redirects"),
|
||||
)
|
||||
@@ -392,7 +384,7 @@ pub fn initialize() -> Command<'static> {
|
||||
Arg::new("insecure")
|
||||
.short('k')
|
||||
.long("insecure")
|
||||
.takes_value(false)
|
||||
.num_args(0)
|
||||
.help_heading("Client settings")
|
||||
.help("Disables TLS certificate validation in the client"),
|
||||
);
|
||||
@@ -406,7 +398,7 @@ pub fn initialize() -> Command<'static> {
|
||||
.short('t')
|
||||
.long("threads")
|
||||
.value_name("THREADS")
|
||||
.takes_value(true)
|
||||
.num_args(1)
|
||||
.help_heading("Scan settings")
|
||||
.help("Number of concurrent threads (default: 50)"),
|
||||
)
|
||||
@@ -414,7 +406,7 @@ pub fn initialize() -> Command<'static> {
|
||||
Arg::new("no_recursion")
|
||||
.short('n')
|
||||
.long("no-recursion")
|
||||
.takes_value(false)
|
||||
.num_args(0)
|
||||
.help_heading("Scan settings")
|
||||
.help("Do not scan recursively"),
|
||||
)
|
||||
@@ -423,14 +415,21 @@ pub fn initialize() -> Command<'static> {
|
||||
.short('d')
|
||||
.long("depth")
|
||||
.value_name("RECURSION_DEPTH")
|
||||
.takes_value(true)
|
||||
.num_args(1)
|
||||
.help_heading("Scan settings")
|
||||
.help("Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)"),
|
||||
).arg(
|
||||
Arg::new("force_recursion")
|
||||
.long("force-recursion")
|
||||
.num_args(0)
|
||||
.conflicts_with("no_recursion")
|
||||
.help_heading("Scan settings")
|
||||
.help("Force recursion attempts on all 'found' endpoints (still respects recursion depth)"),
|
||||
).arg(
|
||||
Arg::new("extract_links")
|
||||
.short('e')
|
||||
.long("extract-links")
|
||||
.takes_value(false)
|
||||
.num_args(0)
|
||||
.help_heading("Scan settings")
|
||||
.help("Extract links from response body (html, javascript, etc...); make new requests based on findings")
|
||||
)
|
||||
@@ -439,7 +438,7 @@ pub fn initialize() -> Command<'static> {
|
||||
.short('L')
|
||||
.long("scan-limit")
|
||||
.value_name("SCAN_LIMIT")
|
||||
.takes_value(true)
|
||||
.num_args(1)
|
||||
.help_heading("Scan settings")
|
||||
.help("Limit total number of concurrent scans (default: 0, i.e. no limit)")
|
||||
)
|
||||
@@ -447,7 +446,7 @@ pub fn initialize() -> Command<'static> {
|
||||
Arg::new("parallel")
|
||||
.long("parallel")
|
||||
.value_name("PARALLEL_SCANS")
|
||||
.takes_value(true)
|
||||
.num_args(1)
|
||||
.requires("stdin")
|
||||
.help_heading("Scan settings")
|
||||
.help("Run parallel feroxbuster instances (one child process per url passed via stdin)")
|
||||
@@ -456,7 +455,7 @@ pub fn initialize() -> Command<'static> {
|
||||
Arg::new("rate_limit")
|
||||
.long("rate-limit")
|
||||
.value_name("RATE_LIMIT")
|
||||
.takes_value(true)
|
||||
.num_args(1)
|
||||
.conflicts_with("auto_tune")
|
||||
.help_heading("Scan settings")
|
||||
.help("Limit number of requests per second (per directory) (default: 0, i.e. no limit)")
|
||||
@@ -465,8 +464,8 @@ pub fn initialize() -> Command<'static> {
|
||||
Arg::new("time_limit")
|
||||
.long("time-limit")
|
||||
.value_name("TIME_SPEC")
|
||||
.takes_value(true)
|
||||
.validator(valid_time_spec)
|
||||
.num_args(1)
|
||||
.value_parser(valid_time_spec)
|
||||
.help_heading("Scan settings")
|
||||
.help("Limit total run time of all scans (ex: --time-limit 10m)")
|
||||
)
|
||||
@@ -478,11 +477,11 @@ pub fn initialize() -> Command<'static> {
|
||||
.value_name("FILE")
|
||||
.help("Path to the wordlist")
|
||||
.help_heading("Scan settings")
|
||||
.takes_value(true),
|
||||
.num_args(1),
|
||||
).arg(
|
||||
Arg::new("auto_tune")
|
||||
.long("auto-tune")
|
||||
.takes_value(false)
|
||||
.num_args(0)
|
||||
.conflicts_with("auto_bail")
|
||||
.help_heading("Scan settings")
|
||||
.help("Automatically lower scan rate when an excessive amount of errors are encountered")
|
||||
@@ -490,35 +489,35 @@ pub fn initialize() -> Command<'static> {
|
||||
.arg(
|
||||
Arg::new("auto_bail")
|
||||
.long("auto-bail")
|
||||
.takes_value(false)
|
||||
.num_args(0)
|
||||
.help_heading("Scan settings")
|
||||
.help("Automatically stop scanning when an excessive amount of errors are encountered")
|
||||
).arg(
|
||||
Arg::new("dont_filter")
|
||||
.short('D')
|
||||
.long("dont-filter")
|
||||
.takes_value(false)
|
||||
.num_args(0)
|
||||
.help_heading("Scan settings")
|
||||
.help("Don't auto-filter wildcard responses")
|
||||
).arg(
|
||||
Arg::new("collect_extensions")
|
||||
.short('E')
|
||||
.long("collect-extensions")
|
||||
.takes_value(false)
|
||||
.num_args(0)
|
||||
.help_heading("Dynamic collection settings")
|
||||
.help("Automatically discover extensions and add them to --extensions (unless they're in --dont-collect)")
|
||||
).arg(
|
||||
Arg::new("collect_backups")
|
||||
.short('B')
|
||||
.long("collect-backups")
|
||||
.takes_value(false)
|
||||
.num_args(0)
|
||||
.help_heading("Dynamic collection settings")
|
||||
.help("Automatically request likely backup extensions for \"found\" urls")
|
||||
).arg(
|
||||
Arg::new("collect_words")
|
||||
.short('g')
|
||||
.long("collect-words")
|
||||
.takes_value(false)
|
||||
.num_args(0)
|
||||
.help_heading("Dynamic collection settings")
|
||||
.help("Automatically discover important words from within responses and add them to the wordlist")
|
||||
).arg(
|
||||
@@ -526,9 +525,8 @@ pub fn initialize() -> Command<'static> {
|
||||
.short('I')
|
||||
.long("dont-collect")
|
||||
.value_name("FILE_EXTENSION")
|
||||
.takes_value(true)
|
||||
.multiple_values(true)
|
||||
.multiple_occurrences(true)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
.use_value_delimiter(true)
|
||||
.help_heading("Dynamic collection settings")
|
||||
.help(
|
||||
@@ -544,15 +542,15 @@ pub fn initialize() -> Command<'static> {
|
||||
Arg::new("verbosity")
|
||||
.short('v')
|
||||
.long("verbosity")
|
||||
.takes_value(false)
|
||||
.multiple_occurrences(true)
|
||||
.num_args(0)
|
||||
.action(ArgAction::Count)
|
||||
.conflicts_with("silent")
|
||||
.help_heading("Output settings")
|
||||
.help("Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v's is probably too much)"),
|
||||
).arg(
|
||||
Arg::new("silent")
|
||||
.long("silent")
|
||||
.takes_value(false)
|
||||
.num_args(0)
|
||||
.conflicts_with("quiet")
|
||||
.help_heading("Output settings")
|
||||
.help("Only print URLs + turn off logging (good for piping a list of urls to other commands)")
|
||||
@@ -561,7 +559,7 @@ pub fn initialize() -> Command<'static> {
|
||||
Arg::new("quiet")
|
||||
.short('q')
|
||||
.long("quiet")
|
||||
.takes_value(false)
|
||||
.num_args(0)
|
||||
.help_heading("Output settings")
|
||||
.help("Hide progress bars and banner (good for tmux windows w/ notifications)")
|
||||
)
|
||||
@@ -569,7 +567,7 @@ pub fn initialize() -> Command<'static> {
|
||||
.arg(
|
||||
Arg::new("json")
|
||||
.long("json")
|
||||
.takes_value(false)
|
||||
.num_args(0)
|
||||
.requires("output_files")
|
||||
.help_heading("Output settings")
|
||||
.help("Emit JSON logs to --output and --debug-log instead of normal text")
|
||||
@@ -581,7 +579,7 @@ pub fn initialize() -> Command<'static> {
|
||||
.value_name("FILE")
|
||||
.help_heading("Output settings")
|
||||
.help("Output file to write results to (use w/ --json for JSON entries)")
|
||||
.takes_value(true),
|
||||
.num_args(1),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("debug_log")
|
||||
@@ -590,12 +588,12 @@ pub fn initialize() -> Command<'static> {
|
||||
.value_hint(ValueHint::FilePath)
|
||||
.help_heading("Output settings")
|
||||
.help("Output file to write log entries (use w/ --json for JSON entries)")
|
||||
.takes_value(true),
|
||||
.num_args(1),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no_state")
|
||||
.long("no-state")
|
||||
.takes_value(false)
|
||||
.num_args(0)
|
||||
.help_heading("Output settings")
|
||||
.help("Disable state output file (*.state)")
|
||||
);
|
||||
@@ -606,7 +604,7 @@ pub fn initialize() -> Command<'static> {
|
||||
let mut app = app
|
||||
.group(
|
||||
ArgGroup::new("output_files")
|
||||
.args(&["debug_log", "output"])
|
||||
.args(["debug_log", "output"])
|
||||
.multiple(true),
|
||||
)
|
||||
.after_long_help(EPILOGUE);
|
||||
@@ -634,9 +632,9 @@ pub fn initialize() -> Command<'static> {
|
||||
}
|
||||
|
||||
/// Validate that a string is formatted as a number followed by s, m, h, or d (10d, 30s, etc...)
|
||||
fn valid_time_spec(time_spec: &str) -> Result<(), String> {
|
||||
fn valid_time_spec(time_spec: &str) -> Result<String, String> {
|
||||
match TIMESPEC_REGEX.is_match(time_spec) {
|
||||
true => Ok(()),
|
||||
true => Ok(time_spec.to_string()),
|
||||
false => {
|
||||
let msg = format!(
|
||||
"Expected a non-negative, whole number followed by s, m, h, or d (case insensitive); received {}",
|
||||
@@ -668,7 +666,7 @@ EXAMPLES:
|
||||
cat targets | ./feroxbuster --stdin --silent -s 200 301 302 --redirects -x js | fff -s 200 -o js-files
|
||||
|
||||
Proxy traffic through Burp
|
||||
./feroxbuster -u http://127.1 --insecure --proxy http://127.0.0.1:8080
|
||||
./feroxbuster -u http://127.1 --burp
|
||||
|
||||
Proxy traffic through a SOCKS proxy
|
||||
./feroxbuster -u http://127.1 --proxy socks5://127.0.0.1:9050
|
||||
@@ -680,7 +678,7 @@ EXAMPLES:
|
||||
./feroxbuster -u http://127.1 --extract-links
|
||||
|
||||
Ludicrous speed... go!
|
||||
./feroxbuster -u http://127.1 -threads 200
|
||||
./feroxbuster -u http://127.1 --threads 200
|
||||
|
||||
Limit to a total of 60 active requests at any given time (threads * scan limit)
|
||||
./feroxbuster -u http://127.1 --threads 30 --scan-limit 2
|
||||
|
||||
@@ -279,7 +279,9 @@ impl FeroxResponse {
|
||||
if handles
|
||||
.config
|
||||
.status_codes
|
||||
.contains(&self.status().as_u16())
|
||||
.contains(&self.status().as_u16()) // in -s list
|
||||
// or -C was used, and -s should be all responses that aren't filtered
|
||||
|| !handles.config.filter_status.is_empty()
|
||||
{
|
||||
// only add extensions to those responses that pass our checks; filtered out
|
||||
// status codes are handled by should_filter, but we need to still check against
|
||||
@@ -413,7 +415,18 @@ impl FeroxSerialize for FeroxResponse {
|
||||
.get("Location")
|
||||
.unwrap() // known Some() already
|
||||
.to_str()
|
||||
.unwrap_or("Unknown");
|
||||
.unwrap_or("Unknown")
|
||||
.to_string();
|
||||
|
||||
let loc = if loc.starts_with('/') {
|
||||
if let Ok(joined) = self.url().join(&loc) {
|
||||
joined.to_string()
|
||||
} else {
|
||||
loc
|
||||
}
|
||||
} else {
|
||||
loc
|
||||
};
|
||||
|
||||
// prettify the redirect target
|
||||
let loc = style(loc).yellow();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::filters::filter_lookup;
|
||||
use crate::progress::PROGRESS_BAR;
|
||||
use crate::traits::FeroxFilter;
|
||||
use console::{measure_text_width, pad_str, style, Alignment, Term};
|
||||
use indicatif::ProgressDrawTarget;
|
||||
use regex::Regex;
|
||||
@@ -7,10 +9,16 @@ use regex::Regex;
|
||||
#[derive(Debug)]
|
||||
pub enum MenuCmd {
|
||||
/// user wants to add a url to be scanned
|
||||
Add(String),
|
||||
AddUrl(String),
|
||||
|
||||
/// user wants to cancel one or more active scans
|
||||
Cancel(Vec<usize>, bool),
|
||||
|
||||
/// user wants to create a new filter
|
||||
AddFilter(Box<dyn FeroxFilter>),
|
||||
|
||||
/// user wants to remove one or more active filters
|
||||
RemoveFilter(Vec<usize>),
|
||||
}
|
||||
|
||||
/// Data container for a command result to be used internally by the ferox_scanner
|
||||
@@ -21,6 +29,9 @@ pub enum MenuCmdResult {
|
||||
|
||||
/// Number of scans that were actually cancelled, can be 0
|
||||
NumCancelled(usize),
|
||||
|
||||
/// Filter to be added to current list of `FeroxFilters`
|
||||
Filter(Box<dyn FeroxFilter>),
|
||||
}
|
||||
|
||||
/// Interactive scan cancellation menu
|
||||
@@ -32,6 +43,9 @@ pub(super) struct Menu {
|
||||
/// footer: instructions surrounded by separators
|
||||
footer: String,
|
||||
|
||||
/// unicode line border, matched to longest displayed line
|
||||
border: String,
|
||||
|
||||
/// target for output
|
||||
pub(super) term: Term,
|
||||
}
|
||||
@@ -57,16 +71,44 @@ impl Menu {
|
||||
);
|
||||
|
||||
let canx_cmd = format!(
|
||||
" {}[{}] [-f] SCAN_ID[-SCAN_ID[,...]] (ex: {} 1-4,8,9-13 or {} -f 3)",
|
||||
" {}[{}] [-f] SCAN_ID[-SCAN_ID[,...]] (ex: {} 1-4,8,9-13 or {} -f 3)\n",
|
||||
style("c").red(),
|
||||
style("ancel").red(),
|
||||
style("cancel").red(),
|
||||
style("c").red(),
|
||||
);
|
||||
|
||||
let mut commands = String::from("Commands:\n");
|
||||
let new_filter_cmd = format!(
|
||||
" {}[{}] FILTER_TYPE FILTER_VALUE (ex: {} lines 40)\n",
|
||||
style("n").green(),
|
||||
style("ew-filter").green(),
|
||||
style("n").green(),
|
||||
);
|
||||
|
||||
let valid_filters = format!(
|
||||
" FILTER_TYPEs: {}, {}, {}, {}, {}, {}\n",
|
||||
style("status").yellow(),
|
||||
style("lines").yellow(),
|
||||
style("size").yellow(),
|
||||
style("words").yellow(),
|
||||
style("regex").yellow(),
|
||||
style("similarity").yellow()
|
||||
);
|
||||
|
||||
let rm_filter_cmd = format!(
|
||||
" {}[{}] FILTER_ID[-FILTER_ID[,...]] (ex: {} 1-4,8,9-13 or {} 3)",
|
||||
style("r").red(),
|
||||
style("m-filter").red(),
|
||||
style("rm-filter").red(),
|
||||
style("r").red(),
|
||||
);
|
||||
|
||||
let mut commands = format!("{}:\n", style("Commands").bright().blue());
|
||||
commands.push_str(&add_cmd);
|
||||
commands.push_str(&canx_cmd);
|
||||
commands.push_str(&new_filter_cmd);
|
||||
commands.push_str(&valid_filters);
|
||||
commands.push_str(&rm_filter_cmd);
|
||||
|
||||
let longest = measure_text_width(&canx_cmd).max(measure_text_width(&name));
|
||||
|
||||
@@ -75,11 +117,12 @@ impl Menu {
|
||||
let padded_name = pad_str(&name, longest, Alignment::Center, None);
|
||||
|
||||
let header = format!("{}\n{}\n{}", border, padded_name, border);
|
||||
let footer = format!("{}\n{}\n{}", border, commands, border);
|
||||
let footer = format!("{}\n{}", commands, border);
|
||||
|
||||
Self {
|
||||
header,
|
||||
footer,
|
||||
border,
|
||||
term: Term::stderr(),
|
||||
}
|
||||
}
|
||||
@@ -89,6 +132,11 @@ impl Menu {
|
||||
self.println(&self.header);
|
||||
}
|
||||
|
||||
/// print menu unicode border line
|
||||
pub(super) fn print_border(&self) {
|
||||
self.println(&self.border);
|
||||
}
|
||||
|
||||
/// print menu footer
|
||||
pub(super) fn print_footer(&self) {
|
||||
self.println(&self.footer);
|
||||
@@ -198,7 +246,39 @@ impl Menu {
|
||||
let re = Regex::new(r"^[aA][dD]*").unwrap();
|
||||
let line = re.replace(line, "").to_string().trim().to_string();
|
||||
|
||||
Some(MenuCmd::Add(line))
|
||||
Some(MenuCmd::AddUrl(line))
|
||||
}
|
||||
'n' => {
|
||||
// new filter command
|
||||
let mut line = line.split_whitespace();
|
||||
line.next(); // 'n' or 'new-filter'
|
||||
|
||||
if let Some(filter_type) = line.next() {
|
||||
// have a string in the filter_type position
|
||||
if let Some(filter_value) = line.next() {
|
||||
// have a string in the filter_value position
|
||||
if let Some(result) = filter_lookup(filter_type, filter_value) {
|
||||
// lookup was successful, return the new filter
|
||||
return Some(MenuCmd::AddFilter(result));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
'r' => {
|
||||
// remove filter command
|
||||
|
||||
// remove r[m-filter] from the command so it can be passed to the number
|
||||
// splitter
|
||||
let re = Regex::new(r"^[rR][mfilterMFILTER-]*").unwrap();
|
||||
// we don't respect a -f or lack thereof in this command, but in case the user
|
||||
// doesn't realize / thinks its the same as cancel -f, just remove it
|
||||
let line = line.replace("-f", "");
|
||||
let line = re.replace(&line, "").to_string();
|
||||
|
||||
let indices = self.split_to_nums(&line);
|
||||
|
||||
Some(MenuCmd::RemoveFilter(indices))
|
||||
}
|
||||
_ => {
|
||||
// invalid input
|
||||
|
||||
@@ -45,7 +45,7 @@ impl FeroxResponses {
|
||||
pub fn contains(&self, other: &FeroxResponse) -> bool {
|
||||
if let Ok(responses) = self.responses.read() {
|
||||
for response in responses.iter() {
|
||||
if response.url() == other.url() {
|
||||
if response.url() == other.url() && response.method() == other.method() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ pub struct FeroxScan {
|
||||
/// The URL that to be scanned
|
||||
pub(super) url: String,
|
||||
|
||||
/// A url used solely for comparison to other URLs
|
||||
pub(super) normalized_url: String,
|
||||
|
||||
/// The type of scan
|
||||
pub scan_type: ScanType,
|
||||
|
||||
@@ -70,7 +73,7 @@ pub struct FeroxScan {
|
||||
impl Default for FeroxScan {
|
||||
/// Create a default FeroxScan, populates ID with a new UUID
|
||||
fn default() -> Self {
|
||||
let new_id = Uuid::new_v4().to_simple().to_string();
|
||||
let new_id = Uuid::new_v4().as_simple().to_string();
|
||||
|
||||
FeroxScan {
|
||||
id: new_id,
|
||||
@@ -79,6 +82,7 @@ impl Default for FeroxScan {
|
||||
num_requests: 0,
|
||||
scan_order: ScanOrder::Latest,
|
||||
url: String::new(),
|
||||
normalized_url: String::new(),
|
||||
progress_bar: Mutex::new(None),
|
||||
scan_type: ScanType::File,
|
||||
output_level: Default::default(),
|
||||
@@ -191,6 +195,7 @@ impl FeroxScan {
|
||||
) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
url: url.to_string(),
|
||||
normalized_url: format!("{}/", url.trim_end_matches('/')),
|
||||
scan_type,
|
||||
scan_order,
|
||||
num_requests,
|
||||
@@ -228,6 +233,14 @@ impl FeroxScan {
|
||||
false
|
||||
}
|
||||
|
||||
/// small wrapper to inspect ScanStatus and see if it's Cancelled
|
||||
pub fn is_cancelled(&self) -> bool {
|
||||
if let Ok(guard) = self.status.lock() {
|
||||
return matches!(*guard, ScanStatus::Cancelled);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// await a task's completion, similar to a thread's join; perform necessary bookkeeping
|
||||
pub async fn join(&self) {
|
||||
log::trace!("enter join({:?})", self);
|
||||
@@ -332,10 +345,11 @@ impl Serialize for FeroxScan {
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut state = serializer.serialize_struct("FeroxScan", 4)?;
|
||||
let mut state = serializer.serialize_struct("FeroxScan", 6)?;
|
||||
|
||||
state.serialize_field("id", &self.id)?;
|
||||
state.serialize_field("url", &self.url)?;
|
||||
state.serialize_field("normalized_url", &self.normalized_url)?;
|
||||
state.serialize_field("scan_type", &self.scan_type)?;
|
||||
state.serialize_field("status", &self.status)?;
|
||||
state.serialize_field("num_requests", &self.num_requests)?;
|
||||
@@ -387,6 +401,11 @@ impl<'de> Deserialize<'de> for FeroxScan {
|
||||
scan.url = url.to_string();
|
||||
}
|
||||
}
|
||||
"normalized_url" => {
|
||||
if let Some(normalized_url) = value.as_str() {
|
||||
scan.normalized_url = normalized_url.to_string();
|
||||
}
|
||||
}
|
||||
"num_requests" => {
|
||||
if let Some(num_requests) = value.as_u64() {
|
||||
scan.num_requests = num_requests;
|
||||
@@ -480,6 +499,7 @@ mod tests {
|
||||
let scan = FeroxScan {
|
||||
id: "".to_string(),
|
||||
url: "".to_string(),
|
||||
normalized_url: String::from("/"),
|
||||
scan_type: ScanType::Directory,
|
||||
scan_order: ScanOrder::Initial,
|
||||
num_requests: 0,
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
use super::scan::ScanType;
|
||||
use super::*;
|
||||
use crate::event_handlers::Handles;
|
||||
use crate::filters::{
|
||||
EmptyFilter, LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter,
|
||||
WildcardFilter, WordsFilter,
|
||||
};
|
||||
use crate::traits::FeroxFilter;
|
||||
use crate::Command::AddFilter;
|
||||
use crate::{
|
||||
config::OutputLevel,
|
||||
progress::PROGRESS_PRINTER,
|
||||
@@ -7,9 +14,10 @@ use crate::{
|
||||
scan_manager::{MenuCmd, MenuCmdResult},
|
||||
scanner::RESPONSES,
|
||||
traits::FeroxSerialize,
|
||||
SLEEP_DURATION,
|
||||
Command, SLEEP_DURATION,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use console::style;
|
||||
use reqwest::StatusCode;
|
||||
use serde::{ser::SerializeSeq, Serialize, Serializer};
|
||||
use std::{
|
||||
@@ -67,7 +75,7 @@ impl Serialize for FeroxScans {
|
||||
let mut seq = serializer.serialize_seq(Some(scans.len() + 1))?;
|
||||
|
||||
for scan in scans.iter() {
|
||||
seq.serialize_element(&*scan).unwrap_or_default();
|
||||
seq.serialize_element(scan).unwrap_or_default();
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
@@ -117,7 +125,7 @@ impl FeroxScans {
|
||||
}
|
||||
|
||||
/// load serialized FeroxScan(s) and any previously collected extensions into this FeroxScans
|
||||
pub fn add_serialized_scans(&self, filename: &str) -> Result<()> {
|
||||
pub fn add_serialized_scans(&self, filename: &str, handles: Arc<Handles>) -> Result<()> {
|
||||
log::trace!("enter: add_serialized_scans({})", filename);
|
||||
let file = File::open(filename)?;
|
||||
|
||||
@@ -130,6 +138,15 @@ impl FeroxScans {
|
||||
let mut deser_scan: FeroxScan =
|
||||
serde_json::from_value(scan.clone()).unwrap_or_default();
|
||||
|
||||
if deser_scan.is_cancelled() {
|
||||
// if the scan was cancelled by the user, mark it as complete. This will
|
||||
// prevent the scan from being resumed as well as prevent the wordlist
|
||||
// from requesting it again
|
||||
if let Ok(mut guard) = deser_scan.status.lock() {
|
||||
*guard = ScanStatus::Complete;
|
||||
}
|
||||
}
|
||||
|
||||
// FeroxScans gets -q value from config as usual; the FeroxScans themselves
|
||||
// rely on that value being passed in. If the user starts a scan without -q
|
||||
// and resumes the scan but adds -q, FeroxScan will not have the proper value
|
||||
@@ -154,6 +171,49 @@ impl FeroxScans {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(filters) = state.get("filters") {
|
||||
if let Some(arr_filters) = filters.as_array() {
|
||||
for filter in arr_filters {
|
||||
let final_filter: Box<dyn FeroxFilter> = if let Ok(deserialized) =
|
||||
serde_json::from_value::<RegexFilter>(filter.clone())
|
||||
{
|
||||
Box::new(deserialized)
|
||||
} else if let Ok(deserialized) =
|
||||
serde_json::from_value::<WordsFilter>(filter.clone())
|
||||
{
|
||||
Box::new(deserialized)
|
||||
} else if let Ok(deserialized) =
|
||||
serde_json::from_value::<WildcardFilter>(filter.clone())
|
||||
{
|
||||
Box::new(deserialized)
|
||||
} else if let Ok(deserialized) =
|
||||
serde_json::from_value::<SizeFilter>(filter.clone())
|
||||
{
|
||||
Box::new(deserialized)
|
||||
} else if let Ok(deserialized) =
|
||||
serde_json::from_value::<LinesFilter>(filter.clone())
|
||||
{
|
||||
Box::new(deserialized)
|
||||
} else if let Ok(deserialized) =
|
||||
serde_json::from_value::<SimilarityFilter>(filter.clone())
|
||||
{
|
||||
Box::new(deserialized)
|
||||
} else if let Ok(deserialized) =
|
||||
serde_json::from_value::<StatusCodeFilter>(filter.clone())
|
||||
{
|
||||
Box::new(deserialized)
|
||||
} else {
|
||||
Box::new(EmptyFilter {})
|
||||
};
|
||||
|
||||
handles
|
||||
.filters
|
||||
.send(AddFilter(final_filter))
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::trace!("exit: add_serialized_scans");
|
||||
Ok(())
|
||||
}
|
||||
@@ -162,8 +222,10 @@ impl FeroxScans {
|
||||
/// on the given URL
|
||||
pub fn contains(&self, url: &str) -> bool {
|
||||
if let Ok(scans) = self.scans.read() {
|
||||
let normalized = format!("{}/", url.trim_end_matches('/'));
|
||||
|
||||
for scan in scans.iter() {
|
||||
if scan.url == url {
|
||||
if scan.normalized_url == normalized {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -174,8 +236,10 @@ impl FeroxScans {
|
||||
/// Find and return a `FeroxScan` based on the given URL
|
||||
pub fn get_scan_by_url(&self, url: &str) -> Option<Arc<FeroxScan>> {
|
||||
if let Ok(guard) = self.scans.read() {
|
||||
let normalized = format!("{}/", url.trim_end_matches('/'));
|
||||
|
||||
for scan in guard.iter() {
|
||||
if scan.url == url {
|
||||
if scan.normalized_url == normalized {
|
||||
return Some(scan.clone());
|
||||
}
|
||||
}
|
||||
@@ -257,6 +321,8 @@ impl FeroxScans {
|
||||
.clone()
|
||||
};
|
||||
|
||||
let mut printed = 0;
|
||||
|
||||
for (i, scan) in scans.iter().enumerate() {
|
||||
if matches!(scan.scan_order, ScanOrder::Initial) || scan.task.try_lock().is_err() {
|
||||
// original target passed in via either -u or --stdin
|
||||
@@ -264,12 +330,21 @@ impl FeroxScans {
|
||||
}
|
||||
|
||||
if matches!(scan.scan_type, ScanType::Directory) {
|
||||
if printed == 0 {
|
||||
self.menu
|
||||
.println(&format!("{}:", style("Scans").bright().blue()));
|
||||
}
|
||||
// we're only interested in displaying directory scans, as those are
|
||||
// the only ones that make sense to be stopped
|
||||
let scan_msg = format!("{:3}: {}", i, scan);
|
||||
self.menu.println(&scan_msg);
|
||||
printed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if printed > 0 {
|
||||
self.menu.print_border();
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a list of indexes, cancel their associated FeroxScans
|
||||
@@ -320,12 +395,34 @@ impl FeroxScans {
|
||||
num_cancelled
|
||||
}
|
||||
|
||||
fn display_filters(&self, handles: Arc<Handles>) {
|
||||
let mut printed = 0;
|
||||
|
||||
if let Ok(guard) = handles.filters.data.filters.read() {
|
||||
for (i, filter) in guard.iter().enumerate() {
|
||||
if i == 0 {
|
||||
self.menu
|
||||
.println(&format!("{}:", style("Filters").bright().blue()));
|
||||
}
|
||||
|
||||
let filter_msg = format!("{:3}: {}", i + 1, filter);
|
||||
self.menu.println(&filter_msg);
|
||||
printed += 1;
|
||||
}
|
||||
|
||||
if printed > 0 {
|
||||
self.menu.print_border();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// CLI menu that allows for interactive cancellation of recursed-into directories
|
||||
async fn interactive_menu(&self) -> Option<MenuCmdResult> {
|
||||
async fn interactive_menu(&self, handles: Arc<Handles>) -> Option<MenuCmdResult> {
|
||||
self.menu.hide_progress_bars();
|
||||
self.menu.clear_screen();
|
||||
self.menu.print_header();
|
||||
self.display_scans().await;
|
||||
self.display_filters(handles.clone());
|
||||
self.menu.print_footer();
|
||||
|
||||
let menu_cmd = if let Ok(line) = self.menu.term.read_line() {
|
||||
@@ -340,7 +437,15 @@ impl FeroxScans {
|
||||
let num_cancelled = self.cancel_scans(indices, should_force).await;
|
||||
Some(MenuCmdResult::NumCancelled(num_cancelled))
|
||||
}
|
||||
Some(MenuCmd::Add(url)) => Some(MenuCmdResult::Url(url)),
|
||||
Some(MenuCmd::AddUrl(url)) => Some(MenuCmdResult::Url(url)),
|
||||
Some(MenuCmd::AddFilter(filter)) => Some(MenuCmdResult::Filter(filter)),
|
||||
Some(MenuCmd::RemoveFilter(indices)) => {
|
||||
handles
|
||||
.filters
|
||||
.send(Command::RemoveFilters(indices))
|
||||
.unwrap_or_default();
|
||||
None
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
@@ -395,7 +500,11 @@ impl FeroxScans {
|
||||
///
|
||||
/// When the value stored in `PAUSE_SCAN` becomes `false`, the function returns, exiting the busy
|
||||
/// loop
|
||||
pub async fn pause(&self, get_user_input: bool) -> Option<MenuCmdResult> {
|
||||
pub async fn pause(
|
||||
&self,
|
||||
get_user_input: bool,
|
||||
handles: Arc<Handles>,
|
||||
) -> Option<MenuCmdResult> {
|
||||
// function uses tokio::time, not std
|
||||
|
||||
// local testing showed a pretty slow increase (less than linear) in CPU usage as # of
|
||||
@@ -407,7 +516,7 @@ impl FeroxScans {
|
||||
INTERACTIVE_BARRIER.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
if get_user_input {
|
||||
command_result = self.interactive_menu().await;
|
||||
command_result = self.interactive_menu(handles).await;
|
||||
PAUSE_SCAN.store(false, Ordering::Relaxed);
|
||||
self.print_known_responses();
|
||||
}
|
||||
@@ -493,7 +602,8 @@ impl FeroxScans {
|
||||
///
|
||||
/// Also return a reference to the new `FeroxScan`
|
||||
pub fn add_directory_scan(&self, url: &str, scan_order: ScanOrder) -> (bool, Arc<FeroxScan>) {
|
||||
self.add_scan(url, ScanType::Directory, scan_order)
|
||||
let normalized = format!("{}/", url.trim_end_matches('/'));
|
||||
self.add_scan(&normalized, ScanType::Directory, scan_order)
|
||||
}
|
||||
|
||||
/// Given a url, create a new `FeroxScan` and add it to `FeroxScans` as a File Scan
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::filters::FeroxFilters;
|
||||
use crate::{config::Configuration, statistics::Stats, traits::FeroxSerialize, utils::fmt_err};
|
||||
use anyhow::{Context, Result};
|
||||
use serde::Serialize;
|
||||
@@ -22,6 +23,9 @@ pub struct FeroxState {
|
||||
|
||||
/// collected extensions
|
||||
collected_extensions: HashSet<String>,
|
||||
|
||||
/// runtime filters, as they may differ from original config
|
||||
filters: Arc<FeroxFilters>,
|
||||
}
|
||||
|
||||
/// implementation of FeroxState
|
||||
@@ -32,6 +36,7 @@ impl FeroxState {
|
||||
config: Arc<Configuration>,
|
||||
responses: &'static FeroxResponses,
|
||||
statistics: Arc<Stats>,
|
||||
filters: Arc<FeroxFilters>,
|
||||
) -> Self {
|
||||
let collected_extensions = match scans.collected_extensions.read() {
|
||||
Ok(extensions) => extensions.clone(),
|
||||
@@ -44,6 +49,7 @@ impl FeroxState {
|
||||
responses,
|
||||
statistics,
|
||||
collected_extensions,
|
||||
filters,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use super::*;
|
||||
use crate::filters::{
|
||||
FeroxFilters, LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter,
|
||||
WordsFilter,
|
||||
};
|
||||
use crate::{
|
||||
config::{Configuration, OutputLevel},
|
||||
event_handlers::Handles,
|
||||
@@ -6,10 +10,11 @@ use crate::{
|
||||
scanner::RESPONSES,
|
||||
statistics::Stats,
|
||||
traits::FeroxSerialize,
|
||||
SLEEP_DURATION, VERSION,
|
||||
SIMILARITY_THRESHOLD, SLEEP_DURATION, VERSION,
|
||||
};
|
||||
use indicatif::ProgressBar;
|
||||
use predicates::prelude::*;
|
||||
use regex::Regex;
|
||||
use std::sync::{atomic::Ordering, Arc};
|
||||
use std::thread::sleep;
|
||||
use std::time::Instant;
|
||||
@@ -31,6 +36,7 @@ fn default_scantype_is_file() {
|
||||
async fn scanner_pause_scan_with_finished_spinner() {
|
||||
let now = time::Instant::now();
|
||||
let urls = FeroxScans::default();
|
||||
let handles = Arc::new(Handles::for_testing(None, None).0);
|
||||
|
||||
PAUSE_SCAN.store(true, Ordering::Relaxed);
|
||||
|
||||
@@ -41,7 +47,7 @@ async fn scanner_pause_scan_with_finished_spinner() {
|
||||
PAUSE_SCAN.store(false, Ordering::Relaxed);
|
||||
});
|
||||
|
||||
urls.pause(false).await;
|
||||
urls.pause(false, handles).await;
|
||||
|
||||
assert!(now.elapsed() > expected);
|
||||
}
|
||||
@@ -271,7 +277,7 @@ fn ferox_scan_serialize() {
|
||||
None,
|
||||
);
|
||||
let fs_json = format!(
|
||||
r#"{{"id":"{}","url":"https://spiritanimal.com","scan_type":"Directory","status":"NotStarted","num_requests":0}}"#,
|
||||
r#"{{"id":"{}","url":"https://spiritanimal.com","normalized_url":"https://spiritanimal.com/","scan_type":"Directory","status":"NotStarted","num_requests":0}}"#,
|
||||
fs.id
|
||||
);
|
||||
assert_eq!(fs_json, serde_json::to_string(&*fs).unwrap());
|
||||
@@ -290,7 +296,7 @@ fn ferox_scans_serialize() {
|
||||
);
|
||||
let ferox_scans = FeroxScans::default();
|
||||
let ferox_scans_json = format!(
|
||||
r#"[{{"id":"{}","url":"https://spiritanimal.com","scan_type":"Directory","status":"NotStarted","num_requests":0}}]"#,
|
||||
r#"[{{"id":"{}","url":"https://spiritanimal.com","normalized_url":"https://spiritanimal.com/","scan_type":"Directory","status":"NotStarted","num_requests":0}}]"#,
|
||||
ferox_scan.id
|
||||
);
|
||||
ferox_scans.scans.write().unwrap().push(ferox_scan);
|
||||
@@ -370,7 +376,42 @@ fn feroxstates_feroxserialize_implementation() {
|
||||
let response: FeroxResponse = serde_json::from_str(json_response).unwrap();
|
||||
RESPONSES.insert(response);
|
||||
|
||||
let ferox_state = FeroxState::new(Arc::new(ferox_scans), Arc::new(config), &RESPONSES, stats);
|
||||
let filters = FeroxFilters::default();
|
||||
filters
|
||||
.push(Box::new(StatusCodeFilter { filter_code: 100 }))
|
||||
.unwrap();
|
||||
filters
|
||||
.push(Box::new(WordsFilter { word_count: 200 }))
|
||||
.unwrap();
|
||||
filters
|
||||
.push(Box::new(SizeFilter {
|
||||
content_length: 300,
|
||||
}))
|
||||
.unwrap();
|
||||
filters
|
||||
.push(Box::new(LinesFilter { line_count: 400 }))
|
||||
.unwrap();
|
||||
filters
|
||||
.push(Box::new(RegexFilter {
|
||||
raw_string: ".*".to_string(),
|
||||
compiled: Regex::new(".*").unwrap(),
|
||||
}))
|
||||
.unwrap();
|
||||
filters
|
||||
.push(Box::new(SimilarityFilter {
|
||||
hash: "3:YKEpn:Yfp".to_string(),
|
||||
threshold: SIMILARITY_THRESHOLD,
|
||||
original_url: "http://localhost:12345/".to_string(),
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
let ferox_state = FeroxState::new(
|
||||
Arc::new(ferox_scans),
|
||||
Arc::new(config),
|
||||
&RESPONSES,
|
||||
stats,
|
||||
Arc::new(filters),
|
||||
);
|
||||
|
||||
let expected_strs = predicates::str::contains("scans: FeroxScans").and(
|
||||
predicate::str::contains("config: Configuration")
|
||||
@@ -411,6 +452,7 @@ fn feroxstates_feroxserialize_implementation() {
|
||||
r#""quiet":false"#,
|
||||
r#""auto_bail":false"#,
|
||||
r#""auto_tune":false"#,
|
||||
r#""force_recursion":false"#,
|
||||
r#""json":false"#,
|
||||
r#""output":"""#,
|
||||
r#""debug_log":"""#,
|
||||
@@ -457,6 +499,7 @@ fn feroxstates_feroxserialize_implementation() {
|
||||
r#""collect_extensions":true"#,
|
||||
r#""collect_backups":false"#,
|
||||
r#""collect_words":false"#,
|
||||
r#""filters":[{"filter_code":100},{"word_count":200},{"content_length":300},{"line_count":400},{"compiled":".*","raw_string":".*"},{"hash":"3:YKEpn:Yfp","threshold":95,"original_url":"http://localhost:12345/"}]"#,
|
||||
r#""collected_extensions":["php"]"#,
|
||||
r#""dont_collect":["tif","tiff","ico","cur","bmp","webp","svg","png","jpg","jpeg","jfif","gif","avif","apng","pjpeg","pjp","mov","wav","mpg","mpeg","mp3","mp4","m4a","m4p","m4v","ogg","webm","ogv","oga","flac","aac","3gp","css","zip","xls","xml","gz","tgz"]"#,
|
||||
]
|
||||
@@ -513,6 +556,7 @@ fn feroxscan_display() {
|
||||
let scan = FeroxScan {
|
||||
id: "".to_string(),
|
||||
url: String::from("http://localhost"),
|
||||
normalized_url: String::from("http://localhost/"),
|
||||
scan_order: ScanOrder::Latest,
|
||||
scan_type: Default::default(),
|
||||
num_requests: 0,
|
||||
@@ -557,6 +601,7 @@ async fn ferox_scan_abort() {
|
||||
let scan = FeroxScan {
|
||||
id: "".to_string(),
|
||||
url: String::from("http://localhost"),
|
||||
normalized_url: String::from("http://localhost/"),
|
||||
scan_order: ScanOrder::Latest,
|
||||
scan_type: Default::default(),
|
||||
num_requests: 0,
|
||||
@@ -587,7 +632,7 @@ async fn ferox_scan_abort() {
|
||||
/// and their correctness can be verified easily manually; just calling for now
|
||||
fn menu_print_header_and_footer() {
|
||||
let menu = Menu::new();
|
||||
let menu_cmd_1 = MenuCmd::Add(String::from("http://localhost"));
|
||||
let menu_cmd_1 = MenuCmd::AddUrl(String::from("http://localhost"));
|
||||
let menu_cmd_2 = MenuCmd::Cancel(vec![0], false);
|
||||
let menu_cmd_res_1 = MenuCmdResult::Url(String::from("http://localhost"));
|
||||
let menu_cmd_res_2 = MenuCmdResult::NumCancelled(2);
|
||||
@@ -602,8 +647,8 @@ fn menu_print_header_and_footer() {
|
||||
menu.show_progress_bars();
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// ensure command parsing from user input results int he correct MenuCmd returned
|
||||
#[test]
|
||||
fn menu_get_command_input_from_user_returns_cancel() {
|
||||
let menu = Menu::new();
|
||||
|
||||
@@ -631,8 +676,8 @@ fn menu_get_command_input_from_user_returns_cancel() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// ensure command parsing from user input results int he correct MenuCmd returned
|
||||
#[test]
|
||||
fn menu_get_command_input_from_user_returns_add() {
|
||||
let menu = Menu::new();
|
||||
|
||||
@@ -642,9 +687,9 @@ fn menu_get_command_input_from_user_returns_add() {
|
||||
|
||||
if cmd != "None" {
|
||||
let result = menu.get_command_input_from_user(&full_cmd).unwrap();
|
||||
assert!(matches!(result, MenuCmd::Add(_)));
|
||||
assert!(matches!(result, MenuCmd::AddUrl(_)));
|
||||
|
||||
if let MenuCmd::Add(url) = result {
|
||||
if let MenuCmd::AddUrl(url) = result {
|
||||
assert_eq!(url, test_url);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::fmt::Write as _;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::{ops::Deref, sync::atomic::Ordering, sync::Arc, time::Instant};
|
||||
|
||||
@@ -8,6 +9,8 @@ use indicatif::ProgressBar;
|
||||
use lazy_static::lazy_static;
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
use crate::filters::{create_similarity_filter, EmptyFilter, SimilarityFilter};
|
||||
use crate::Command::AddFilter;
|
||||
use crate::{
|
||||
event_handlers::{
|
||||
Command::{AddError, AddToF64Field, AddToUsizeField, SubtractFromUsizeField},
|
||||
@@ -47,7 +50,7 @@ async fn check_for_user_input(
|
||||
|
||||
// todo write a test or two for this function at some point...
|
||||
if pause_flag.load(Ordering::Acquire) {
|
||||
match scanned_urls.pause(true).await {
|
||||
match scanned_urls.pause(true, handles.clone()).await {
|
||||
Some(MenuCmdResult::Url(url)) => {
|
||||
// user wants to add a new url to be scanned, need to send
|
||||
// it over to the event handler for processing
|
||||
@@ -63,6 +66,38 @@ async fn check_for_user_input(
|
||||
.unwrap_or_else(|e| log::warn!("Could not update overall scan bar: {}", e));
|
||||
}
|
||||
}
|
||||
Some(MenuCmdResult::Filter(mut filter)) => {
|
||||
let url = if let Some(SimilarityFilter { original_url, .. }) =
|
||||
filter.as_any().downcast_ref::<SimilarityFilter>()
|
||||
{
|
||||
original_url.to_owned()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
if !url.is_empty() {
|
||||
// filter was a SimilarityFilter and now we have a url to request.
|
||||
//
|
||||
// The reason for this janky structure is that `filter.as_any().downcast_ref`
|
||||
// isn't Send so we can't call create_similarity_filter(...).await, within
|
||||
// the if let Some ipso-facto, janky code /shrug
|
||||
let real_filter = create_similarity_filter(&url, handles.clone())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if real_filter.original_url.is_empty() {
|
||||
// failed to create filter
|
||||
filter = Box::new(EmptyFilter {});
|
||||
} else {
|
||||
filter = Box::new(real_filter)
|
||||
}
|
||||
}
|
||||
|
||||
handles
|
||||
.filters
|
||||
.send(AddFilter(filter))
|
||||
.unwrap_or_else(|e| log::warn!("Could not add new filter: {}", e));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -250,8 +285,7 @@ impl FeroxScanner {
|
||||
let mut message = format!("=> {}", style("Directory listing").blue().bright());
|
||||
|
||||
if !self.handles.config.extract_links {
|
||||
message
|
||||
.push_str(&format!(" (add {} to scan)", style("-e").bright().yellow()))
|
||||
write!(message, " (add {} to scan)", style("-e").bright().yellow())?;
|
||||
}
|
||||
|
||||
progress_bar.reset_eta();
|
||||
@@ -294,7 +328,7 @@ impl FeroxScanner {
|
||||
log::info!(
|
||||
"requesting {} collected words: {:?}...",
|
||||
new_words_len,
|
||||
&new_words[..new_words_len.min(3) as usize]
|
||||
&new_words[..new_words_len.min(3)]
|
||||
);
|
||||
|
||||
self.stream_requests(
|
||||
|
||||
@@ -6,9 +6,9 @@ use std::{
|
||||
|
||||
use anyhow::Result;
|
||||
use lazy_static::lazy_static;
|
||||
use leaky_bucket::LeakyBucket;
|
||||
use leaky_bucket::RateLimiter;
|
||||
use tokio::{
|
||||
sync::{oneshot, RwLock},
|
||||
sync::RwLock,
|
||||
time::{sleep, Duration},
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::{
|
||||
atomic_load, atomic_store,
|
||||
config::RequesterPolicy,
|
||||
event_handlers::{
|
||||
Command::{self, AddError, SubtractFromUsizeField},
|
||||
Command::{AddError, SubtractFromUsizeField},
|
||||
Handles,
|
||||
},
|
||||
extractor::{ExtractionTarget, ExtractorBuilder},
|
||||
@@ -25,7 +25,7 @@ use crate::{
|
||||
scan_manager::{FeroxScan, ScanStatus},
|
||||
statistics::{StatError::Other, StatField::TotalExpected},
|
||||
url::FeroxUrl,
|
||||
utils::{logged_request, should_deny_url},
|
||||
utils::{logged_request, send_try_recursion_command, should_deny_url},
|
||||
HIGH_ERROR_RATIO,
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ pub(super) struct Requester {
|
||||
target_url: String,
|
||||
|
||||
/// limits requests per second if present
|
||||
rate_limiter: RwLock<Option<LeakyBucket>>,
|
||||
rate_limiter: RwLock<Option<RateLimiter>>,
|
||||
|
||||
/// data regarding policy and metadata about last enforced trigger etc...
|
||||
policy_data: PolicyData,
|
||||
@@ -94,18 +94,18 @@ impl Requester {
|
||||
})
|
||||
}
|
||||
|
||||
/// build a LeakyBucket, given a rate limit (as requests per second)
|
||||
fn build_a_bucket(limit: usize) -> Result<LeakyBucket> {
|
||||
/// build a RateLimiter, given a rate limit (as requests per second)
|
||||
fn build_a_bucket(limit: usize) -> Result<RateLimiter> {
|
||||
let refill = max((limit as f64 / 10.0).round() as usize, 1); // minimum of 1 per second
|
||||
let tokens = max((limit as f64 / 2.0).round() as usize, 1);
|
||||
let interval = if refill == 1 { 1000 } else { 100 }; // 1 second if refill is 1
|
||||
|
||||
Ok(LeakyBucket::builder()
|
||||
.refill_interval(Duration::from_millis(interval)) // add tokens every 0.1s
|
||||
.refill_amount(refill) // ex: 100 req/s -> 10 tokens per 0.1s
|
||||
.tokens(tokens) // reduce initial burst, 2 is arbitrary, but felt good
|
||||
Ok(RateLimiter::builder()
|
||||
.interval(Duration::from_millis(interval)) // add tokens every 0.1s
|
||||
.refill(refill) // ex: 100 req/s -> 10 tokens per 0.1s
|
||||
.initial(tokens) // reduce initial burst, 2 is arbitrary, but felt good
|
||||
.max(limit)
|
||||
.build()?)
|
||||
.build())
|
||||
}
|
||||
|
||||
/// sleep and set a flag that can be checked by other threads
|
||||
@@ -124,13 +124,12 @@ impl Requester {
|
||||
|
||||
/// limit the number of requests per second
|
||||
pub async fn limit(&self) -> Result<()> {
|
||||
self.rate_limiter
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.acquire_one()
|
||||
.await?;
|
||||
let guard = self.rate_limiter.read().await;
|
||||
|
||||
if guard.is_some() {
|
||||
guard.as_ref().unwrap().acquire_one().await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -209,7 +208,7 @@ impl Requester {
|
||||
} else {
|
||||
// errors can only be incremented, so an else is sufficient
|
||||
*guard += 1;
|
||||
self.policy_data.adjust_up(&*guard);
|
||||
self.policy_data.adjust_up(&guard);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,14 +378,14 @@ impl Requester {
|
||||
.await;
|
||||
|
||||
// do recursion if appropriate
|
||||
if !self.handles.config.no_recursion {
|
||||
self.handles
|
||||
.send_scan_command(Command::TryRecursion(Box::new(
|
||||
ferox_response.clone(),
|
||||
)))?;
|
||||
let (tx, rx) = oneshot::channel::<bool>();
|
||||
self.handles.send_scan_command(Command::Sync(tx))?;
|
||||
rx.await?;
|
||||
if !self.handles.config.no_recursion && !self.handles.config.force_recursion {
|
||||
// to support --force-recursion, we want to limit recursive calls to only
|
||||
// 'found' assets. That means we need to either gate or delay the call.
|
||||
//
|
||||
// this branch will retain the 'old' behavior by checking that
|
||||
// --force-recursion isn't turned on
|
||||
send_try_recursion_command(self.handles.clone(), ferox_response.clone())
|
||||
.await?;
|
||||
}
|
||||
|
||||
// purposefully doing recursion before filtering. the thought process is that
|
||||
@@ -400,6 +399,33 @@ impl Requester {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !self.handles.config.no_recursion && self.handles.config.force_recursion {
|
||||
// in this branch, we're saying that both recursion AND force recursion
|
||||
// are turned on. It comes after should_filter_response, so those cases
|
||||
// are handled. Now we need to account for -s/-C options.
|
||||
|
||||
if self.handles.config.filter_status.is_empty() {
|
||||
// -C wasn't used, so -s is the only 'filter' left to account for
|
||||
if self
|
||||
.handles
|
||||
.config
|
||||
.status_codes
|
||||
.contains(&ferox_response.status().as_u16())
|
||||
{
|
||||
send_try_recursion_command(
|
||||
self.handles.clone(),
|
||||
ferox_response.clone(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
} else {
|
||||
// -C was used, that means the filters above would have removed
|
||||
// those responses, and anything else should be let through
|
||||
send_try_recursion_command(self.handles.clone(), ferox_response.clone())
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
if self.handles.config.collect_extensions {
|
||||
ferox_response.parse_extension(self.handles.clone())?;
|
||||
}
|
||||
@@ -469,6 +495,7 @@ mod tests {
|
||||
use crate::{
|
||||
config::Configuration,
|
||||
config::OutputLevel,
|
||||
event_handlers::Command::AddStatus,
|
||||
event_handlers::{FiltersHandler, ScanHandler, StatsHandler, Tasks, TermOutHandler},
|
||||
filters,
|
||||
scan_manager::{ScanOrder, ScanType},
|
||||
@@ -509,10 +536,7 @@ mod tests {
|
||||
/// helper to stay DRY
|
||||
async fn increment_errors(handles: Arc<Handles>, scan: Arc<FeroxScan>, num_errors: usize) {
|
||||
for _ in 0..num_errors {
|
||||
handles
|
||||
.stats
|
||||
.send(Command::AddError(StatError::Other))
|
||||
.unwrap();
|
||||
handles.stats.send(AddError(StatError::Other)).unwrap();
|
||||
scan.add_error();
|
||||
}
|
||||
|
||||
@@ -549,7 +573,7 @@ mod tests {
|
||||
code: StatusCode,
|
||||
) {
|
||||
for _ in 0..num_codes {
|
||||
handles.stats.send(Command::AddStatus(code)).unwrap();
|
||||
handles.stats.send(AddStatus(code)).unwrap();
|
||||
if code == StatusCode::FORBIDDEN {
|
||||
scan.add_403();
|
||||
} else {
|
||||
@@ -901,10 +925,10 @@ mod tests {
|
||||
/// decrease the scan rate
|
||||
async fn adjust_limit_resets_streak_counter_on_downward_movement() {
|
||||
let (handles, _) = setup_requester_test(None).await;
|
||||
let mut buckets = leaky_bucket::LeakyBuckets::new();
|
||||
let coordinator = buckets.coordinate().unwrap();
|
||||
tokio::spawn(async move { coordinator.await.expect("coordinator errored") });
|
||||
let limiter = buckets.rate_limiter().max(200).build().unwrap();
|
||||
let limiter = RateLimiter::builder()
|
||||
.interval(Duration::from_secs(1))
|
||||
.max(200)
|
||||
.build();
|
||||
|
||||
let scan = FeroxScan::default();
|
||||
scan.add_error();
|
||||
@@ -923,9 +947,10 @@ mod tests {
|
||||
requester.policy_data.set_reqs_sec(400);
|
||||
requester.policy_data.set_errors(1);
|
||||
|
||||
let mut guard = requester.tuning_lock.lock().unwrap();
|
||||
*guard = 2;
|
||||
drop(guard);
|
||||
{
|
||||
let mut guard = requester.tuning_lock.lock().unwrap();
|
||||
*guard = 2;
|
||||
}
|
||||
|
||||
requester
|
||||
.adjust_limit(PolicyTrigger::Errors, false)
|
||||
@@ -1012,10 +1037,10 @@ mod tests {
|
||||
/// set_rate_limiter should exit early when new limit equals the current bucket's max
|
||||
async fn set_rate_limiter_early_exit() {
|
||||
let (handles, _) = setup_requester_test(None).await;
|
||||
let mut buckets = leaky_bucket::LeakyBuckets::new();
|
||||
let coordinator = buckets.coordinate().unwrap();
|
||||
tokio::spawn(async move { coordinator.await.expect("coordinator errored") });
|
||||
let limiter = buckets.rate_limiter().max(200).build().unwrap();
|
||||
let limiter = RateLimiter::builder()
|
||||
.interval(Duration::from_secs(1))
|
||||
.max(200)
|
||||
.build();
|
||||
|
||||
let requester = Requester {
|
||||
handles,
|
||||
@@ -1044,10 +1069,10 @@ mod tests {
|
||||
async fn tune_sets_expected_values_and_then_waits() {
|
||||
let (handles, _) = setup_requester_test(None).await;
|
||||
|
||||
let mut buckets = leaky_bucket::LeakyBuckets::new();
|
||||
let coordinator = buckets.coordinate().unwrap();
|
||||
tokio::spawn(async move { coordinator.await.expect("coordinator errored") });
|
||||
let limiter = buckets.rate_limiter().max(200).build().unwrap();
|
||||
let limiter = RateLimiter::builder()
|
||||
.interval(Duration::from_secs(1))
|
||||
.max(200)
|
||||
.build();
|
||||
|
||||
let scan = FeroxScan::new(
|
||||
"http://localhost",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
/// represents different situations where different criteria can trigger auto-tune/bail behavior
|
||||
pub enum PolicyTrigger {
|
||||
/// excessive 403 trigger
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
//! collection of all traits used
|
||||
use crate::filters::{
|
||||
LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter, WildcardFilter,
|
||||
WordsFilter,
|
||||
};
|
||||
use crate::response::FeroxResponse;
|
||||
use anyhow::Result;
|
||||
use crossterm::style::{style, Stylize};
|
||||
use serde::Serialize;
|
||||
use std::any::Any;
|
||||
use std::fmt::Debug;
|
||||
use std::fmt::{self, Debug, Display, Formatter};
|
||||
|
||||
// references:
|
||||
// https://dev.to/magnusstrale/rust-trait-objects-in-a-vector-non-trivial-4co5
|
||||
@@ -22,6 +27,36 @@ pub trait FeroxFilter: Debug + Send + Sync {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
}
|
||||
|
||||
impl Display for dyn FeroxFilter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
if let Some(filter) = self.as_any().downcast_ref::<LinesFilter>() {
|
||||
write!(f, "Line count: {}", style(filter.line_count).cyan())
|
||||
} else if let Some(filter) = self.as_any().downcast_ref::<WordsFilter>() {
|
||||
write!(f, "Word count: {}", style(filter.word_count).cyan())
|
||||
} else if let Some(filter) = self.as_any().downcast_ref::<SizeFilter>() {
|
||||
write!(f, "Response size: {}", style(filter.content_length).cyan())
|
||||
} else if let Some(filter) = self.as_any().downcast_ref::<RegexFilter>() {
|
||||
write!(f, "Regex: {}", style(&filter.raw_string).cyan())
|
||||
} else if let Some(filter) = self.as_any().downcast_ref::<WildcardFilter>() {
|
||||
if filter.dynamic != u64::MAX {
|
||||
write!(f, "Dynamic wildcard: {}", style(filter.dynamic).cyan())
|
||||
} else {
|
||||
write!(f, "Static wildcard: {}", style(filter.size).cyan())
|
||||
}
|
||||
} else if let Some(filter) = self.as_any().downcast_ref::<StatusCodeFilter>() {
|
||||
write!(f, "Status code: {}", style(filter.filter_code).cyan())
|
||||
} else if let Some(filter) = self.as_any().downcast_ref::<SimilarityFilter>() {
|
||||
write!(
|
||||
f,
|
||||
"Pages similar to: {}",
|
||||
style(&filter.original_url).cyan()
|
||||
)
|
||||
} else {
|
||||
write!(f, "Filter: {:?}", self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// implementation of PartialEq, necessary long-form due to "trait cannot be made into an object"
|
||||
/// error when attempting to derive PartialEq on the trait itself
|
||||
impl PartialEq for Box<dyn FeroxFilter> {
|
||||
|
||||
38
src/utils.rs
38
src/utils.rs
@@ -12,16 +12,17 @@ use std::{
|
||||
time::Duration,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use tokio::sync::{mpsc::UnboundedSender, oneshot};
|
||||
|
||||
use crate::config::Configuration;
|
||||
use crate::{
|
||||
config::Configuration,
|
||||
config::OutputLevel,
|
||||
event_handlers::{
|
||||
Command::{self, AddError, AddStatus},
|
||||
Handles,
|
||||
},
|
||||
progress::PROGRESS_PRINTER,
|
||||
response::FeroxResponse,
|
||||
send_command,
|
||||
statistics::StatError::{Connection, Other, Redirection, Request, Timeout},
|
||||
traits::FeroxSerialize,
|
||||
@@ -67,6 +68,20 @@ pub fn fmt_err(msg: &str) -> String {
|
||||
format!("{}: {}", status_colorizer("ERROR"), msg)
|
||||
}
|
||||
|
||||
/// given a FeroxResponse, send a TryRecursion command
|
||||
///
|
||||
/// moved to utils to allow for calls from extractor and scanner
|
||||
pub(crate) async fn send_try_recursion_command(
|
||||
handles: Arc<Handles>,
|
||||
response: FeroxResponse,
|
||||
) -> Result<()> {
|
||||
handles.send_scan_command(Command::TryRecursion(Box::new(response.clone())))?;
|
||||
let (tx, rx) = oneshot::channel::<bool>();
|
||||
handles.send_scan_command(Command::Sync(tx))?;
|
||||
rx.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Takes in a string and colors it using console::style
|
||||
///
|
||||
/// mainly putting this here in case i want to change the color later, making any changes easy
|
||||
@@ -146,7 +161,7 @@ pub async fn make_request(
|
||||
|
||||
let mut request = client.request(Method::from_bytes(method.as_bytes())?, url.to_owned());
|
||||
|
||||
if (!config.proxy.is_empty() || config.replay_proxy.is_empty())
|
||||
if (!config.proxy.is_empty() || !config.replay_proxy.is_empty())
|
||||
&& data.is_none()
|
||||
&& ["post", "put", "patch"].contains(&method.to_ascii_lowercase().as_str())
|
||||
{
|
||||
@@ -254,10 +269,17 @@ pub fn create_report_string(
|
||||
} else {
|
||||
// normal printing with status and sizes
|
||||
let color_status = status_colorizer(status);
|
||||
format!(
|
||||
"{} {:>8} {:>8}l {:>8}w {:>8}c {}\n",
|
||||
color_status, method, line_count, word_count, content_length, url
|
||||
)
|
||||
if status.contains("MSG") {
|
||||
format!(
|
||||
"{} {:>8} {:>9} {:>9} {:>9} {}\n",
|
||||
color_status, method, line_count, word_count, content_length, url
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{} {:>8} {:>8}l {:>8}w {:>8}c {}\n",
|
||||
color_status, method, line_count, word_count, content_length, url
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,7 +526,7 @@ pub fn slugify_filename(url: &str, prefix: &str, suffix: &str) -> String {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let slug = url.replace("://", "_").replace('/', "_").replace('.', "_");
|
||||
let slug = url.replace("://", "_").replace(['/', '.'], "_");
|
||||
|
||||
let filename = format!("{}{}-{}.{}", altered_prefix, slug, ts, suffix);
|
||||
|
||||
|
||||
@@ -784,7 +784,6 @@ fn banner_prints_filter_status() {
|
||||
.and(predicate::str::contains("http://localhost"))
|
||||
.and(predicate::str::contains("Threads"))
|
||||
.and(predicate::str::contains("Wordlist"))
|
||||
.and(predicate::str::contains("Status Codes"))
|
||||
.and(predicate::str::contains("Timeout (secs)"))
|
||||
.and(predicate::str::contains("User-Agent"))
|
||||
.and(predicate::str::contains("Status Code Filters"))
|
||||
@@ -1394,3 +1393,30 @@ fn banner_prints_all_composite_settings_burp_replay() {
|
||||
.and(predicate::str::contains("─┴─")),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
||||
/// expect to see all mandatory prints + force recursion
|
||||
fn banner_prints_force_recursion() {
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg("http://localhost")
|
||||
.arg("--force-recursion")
|
||||
.arg("--wordlist")
|
||||
.arg("/definitely/doesnt/exist/0cd7fed0-47f4-4b18-a1b0-ac39708c1676")
|
||||
.assert()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
.and(predicate::str::contains("http://localhost"))
|
||||
.and(predicate::str::contains("Threads"))
|
||||
.and(predicate::str::contains("Wordlist"))
|
||||
.and(predicate::str::contains("Status Codes"))
|
||||
.and(predicate::str::contains("Timeout (secs)"))
|
||||
.and(predicate::str::contains("User-Agent"))
|
||||
.and(predicate::str::contains("Force Recursion"))
|
||||
.and(predicate::str::contains("─┴─")),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ fn deny_list_works_during_extraction() {
|
||||
let mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/LICENSE");
|
||||
then.status(200)
|
||||
.body(&srv.url("'/homepage/assets/img/icons/handshake.svg'"));
|
||||
.body(srv.url("'/homepage/assets/img/icons/handshake.svg'"));
|
||||
});
|
||||
|
||||
let mock_two = srv.mock(|when, then| {
|
||||
@@ -90,17 +90,17 @@ fn deny_list_works_during_recursion() {
|
||||
|
||||
let js_mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/js");
|
||||
then.status(301).header("Location", &srv.url("/js/"));
|
||||
then.status(301).header("Location", srv.url("/js/"));
|
||||
});
|
||||
|
||||
let js_prod_mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/js/prod");
|
||||
then.status(301).header("Location", &srv.url("/js/prod/"));
|
||||
then.status(301).header("Location", srv.url("/js/prod/"));
|
||||
});
|
||||
|
||||
let js_dev_mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/js/dev");
|
||||
then.status(301).header("Location", &srv.url("/js/dev/"));
|
||||
then.status(301).header("Location", srv.url("/js/dev/"));
|
||||
});
|
||||
|
||||
let js_dev_file_mock = srv.mock(|when, then| {
|
||||
@@ -155,7 +155,7 @@ fn deny_list_works_during_recursion_with_inverted_parents() {
|
||||
|
||||
let js_mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/js");
|
||||
then.status(301).header("Location", &srv.url("/js/"));
|
||||
then.status(301).header("Location", srv.url("/js/"));
|
||||
});
|
||||
|
||||
let api_mock = srv.mock(|when, then| {
|
||||
@@ -165,12 +165,12 @@ fn deny_list_works_during_recursion_with_inverted_parents() {
|
||||
|
||||
let js_prod_mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/js/prod");
|
||||
then.status(301).header("Location", &srv.url("/js/prod/"));
|
||||
then.status(301).header("Location", srv.url("/js/prod/"));
|
||||
});
|
||||
|
||||
let js_dev_mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/js/dev");
|
||||
then.status(301).header("Location", &srv.url("/js/dev/"));
|
||||
then.status(301).header("Location", srv.url("/js/dev/"));
|
||||
});
|
||||
|
||||
let js_dev_file_mock = srv.mock(|when, then| {
|
||||
|
||||
@@ -16,7 +16,7 @@ fn extractor_finds_absolute_url() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/LICENSE");
|
||||
then.status(200)
|
||||
.body(&srv.url("'/homepage/assets/img/icons/handshake.svg'"));
|
||||
.body(srv.url("'/homepage/assets/img/icons/handshake.svg'"));
|
||||
});
|
||||
|
||||
let mock_two = srv.mock(|when, then| {
|
||||
@@ -136,13 +136,13 @@ fn extractor_finds_same_relative_url_twice() {
|
||||
let mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/LICENSE");
|
||||
then.status(200)
|
||||
.body(&srv.url("\"/homepage/assets/img/icons/handshake.svg\""));
|
||||
.body(srv.url("\"/homepage/assets/img/icons/handshake.svg\""));
|
||||
});
|
||||
|
||||
let mock_two = srv.mock(|when, then| {
|
||||
when.method(GET).path("/README");
|
||||
then.status(200)
|
||||
.body(&srv.url("\"/homepage/assets/img/icons/handshake.svg\""));
|
||||
.body(srv.url("\"/homepage/assets/img/icons/handshake.svg\""));
|
||||
});
|
||||
|
||||
let mock_three = srv.mock(|when, then| {
|
||||
@@ -185,7 +185,7 @@ fn extractor_finds_filtered_content() -> Result<(), Box<dyn std::error::Error>>
|
||||
let mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/LICENSE");
|
||||
then.status(200)
|
||||
.body(&srv.url("\"/homepage/assets/img/icons/handshake.svg\""));
|
||||
.body(srv.url("\"/homepage/assets/img/icons/handshake.svg\""));
|
||||
});
|
||||
|
||||
let mock_two = srv.mock(|when, then| {
|
||||
@@ -413,7 +413,7 @@ fn extractor_finds_directory_listing_links_and_displays_files() {
|
||||
|
||||
let mock_dir_redir = srv.mock(|when, then| {
|
||||
when.method(GET).path("/misc");
|
||||
then.status(301).header("Location", &srv.url("/misc/"));
|
||||
then.status(301).header("Location", srv.url("/misc/"));
|
||||
});
|
||||
let mock_dir = srv.mock(|when, then| {
|
||||
when.method(GET).path("/misc/");
|
||||
@@ -522,7 +522,7 @@ fn extractor_finds_directory_listing_links_and_displays_files_non_recursive() {
|
||||
|
||||
let mock_dir_redir = srv.mock(|when, then| {
|
||||
when.method(GET).path("/misc");
|
||||
then.status(301).header("Location", &srv.url("/misc/"));
|
||||
then.status(301).header("Location", srv.url("/misc/"));
|
||||
});
|
||||
let mock_dir = srv.mock(|when, then| {
|
||||
when.method(GET).path("/misc/");
|
||||
@@ -600,7 +600,7 @@ fn extractor_recurses_into_403_directories() -> Result<(), Box<dyn std::error::E
|
||||
let mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/LICENSE");
|
||||
then.status(200)
|
||||
.body(&srv.url("'/homepage/assets/img/icons/handshake.svg'"));
|
||||
.body(srv.url("'/homepage/assets/img/icons/handshake.svg'"));
|
||||
});
|
||||
|
||||
let mock_two = srv.mock(|when, then| {
|
||||
|
||||
@@ -269,51 +269,53 @@ fn heuristics_static_wildcard_request_with_dont_filter() -> Result<(), Box<dyn s
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test finds a static wildcard and reports as much to stdout
|
||||
fn heuristics_wildcard_test_with_two_static_wildcards() {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
|
||||
// #[test]
|
||||
// /// test finds a static wildcard and reports as much to stdout
|
||||
// fn heuristics_wildcard_test_with_two_static_wildcards() {
|
||||
// let srv = MockServer::start();
|
||||
// let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
|
||||
|
||||
let mock = srv.mock(|when, then| {
|
||||
when.method(GET)
|
||||
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
|
||||
then.status(200)
|
||||
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
});
|
||||
// let mock = srv.mock(|when, then| {
|
||||
// when.method(GET)
|
||||
// .path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
|
||||
// then.status(200)
|
||||
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
// });
|
||||
|
||||
let mock2 = srv.mock(|when, then| {
|
||||
when.method(GET)
|
||||
.path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
|
||||
then.status(200)
|
||||
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
});
|
||||
// let mock2 = srv.mock(|when, then| {
|
||||
// when.method(GET)
|
||||
// .path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
|
||||
// then.status(200)
|
||||
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
// });
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--add-slash")
|
||||
.unwrap();
|
||||
// let cmd = Command::cargo_bin("feroxbuster")
|
||||
// .unwrap()
|
||||
// .arg("--url")
|
||||
// .arg(srv.url("/"))
|
||||
// .arg("--wordlist")
|
||||
// .arg(file.as_os_str())
|
||||
// .arg("--add-slash")
|
||||
// .arg("--threads")
|
||||
// .arg("1")
|
||||
// .unwrap();
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
// teardown_tmp_directory(tmp_dir);
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::contains("WLD")
|
||||
.and(predicate::str::contains("Got"))
|
||||
.and(predicate::str::contains("200"))
|
||||
.and(predicate::str::contains("(url length: 32)"))
|
||||
.and(predicate::str::contains("(url length: 96)"))
|
||||
.and(predicate::str::contains(
|
||||
"Wildcard response is static; auto-filtering 46",
|
||||
)),
|
||||
);
|
||||
// cmd.assert().success().stdout(
|
||||
// predicate::str::contains("WLD")
|
||||
// .and(predicate::str::contains("Got"))
|
||||
// .and(predicate::str::contains("200"))
|
||||
// .and(predicate::str::contains("(url length: 32)"))
|
||||
// .and(predicate::str::contains("(url length: 96)"))
|
||||
// .and(predicate::str::contains(
|
||||
// "Wildcard response is static; auto-filtering 46",
|
||||
// )),
|
||||
// );
|
||||
|
||||
assert_eq!(mock.hits(), 1);
|
||||
assert_eq!(mock2.hits(), 1);
|
||||
}
|
||||
// assert_eq!(mock.hits(), 1);
|
||||
// assert_eq!(mock2.hits(), 1);
|
||||
// }
|
||||
|
||||
#[test]
|
||||
/// test finds a static wildcard and reports nothing to stdout
|
||||
@@ -344,6 +346,8 @@ fn heuristics_wildcard_test_with_two_static_wildcards_with_silent_enabled(
|
||||
.arg(file.as_os_str())
|
||||
.arg("--add-slash")
|
||||
.arg("--silent")
|
||||
.arg("--threads")
|
||||
.arg("1")
|
||||
.unwrap();
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
@@ -355,119 +359,126 @@ fn heuristics_wildcard_test_with_two_static_wildcards_with_silent_enabled(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test finds a static wildcard and reports as much to stdout and a file
|
||||
fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file() {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
|
||||
let outfile = tmp_dir.path().join("outfile");
|
||||
// #[test]
|
||||
// /// test finds a static wildcard and reports as much to stdout and a file
|
||||
// fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file() {
|
||||
// let srv = MockServer::start();
|
||||
// let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
|
||||
// let outfile = tmp_dir.path().join("outfile");
|
||||
|
||||
let mock = srv.mock(|when, then| {
|
||||
when.method(GET)
|
||||
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
|
||||
then.status(200)
|
||||
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
});
|
||||
// let mock = srv.mock(|when, then| {
|
||||
// when.method(GET)
|
||||
// .path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
|
||||
// then.status(200)
|
||||
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
// });
|
||||
|
||||
let mock2 = srv.mock(|when, then| {
|
||||
when.method(GET)
|
||||
.path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
|
||||
then.status(200)
|
||||
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
});
|
||||
// let mock2 = srv.mock(|when, then| {
|
||||
// when.method(GET)
|
||||
// .path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
|
||||
// then.status(200)
|
||||
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
// });
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--add-slash")
|
||||
.arg("--output")
|
||||
.arg(outfile.as_os_str())
|
||||
.unwrap();
|
||||
// let cmd = Command::cargo_bin("feroxbuster")
|
||||
// .unwrap()
|
||||
// .arg("--url")
|
||||
// .arg(srv.url("/"))
|
||||
// .arg("--wordlist")
|
||||
// .arg(file.as_os_str())
|
||||
// .arg("--add-slash")
|
||||
// .arg("--output")
|
||||
// .arg(outfile.as_os_str())
|
||||
// .arg("--threads")
|
||||
// .arg("1")
|
||||
// .unwrap();
|
||||
|
||||
let contents = std::fs::read_to_string(outfile).unwrap();
|
||||
// let contents = std::fs::read_to_string(outfile).unwrap();
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
// teardown_tmp_directory(tmp_dir);
|
||||
|
||||
assert!(contents.contains("WLD"));
|
||||
assert!(contents.contains("Got"));
|
||||
assert!(contents.contains("200"));
|
||||
assert!(contents.contains("(url length: 32)"));
|
||||
assert!(contents.contains("(url length: 96)"));
|
||||
// assert!(contents.contains("WLD"));
|
||||
// assert!(contents.contains("Got"));
|
||||
// assert!(contents.contains("200"));
|
||||
// assert!(contents.contains("(url length: 32)"));
|
||||
// assert!(contents.contains("(url length: 96)"));
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::contains("WLD")
|
||||
.and(predicate::str::contains("Got"))
|
||||
.and(predicate::str::contains("200"))
|
||||
.and(predicate::str::contains("(url length: 32)"))
|
||||
.and(predicate::str::contains("(url length: 96)"))
|
||||
.and(predicate::str::contains(
|
||||
"Wildcard response is static; auto-filtering 46",
|
||||
)),
|
||||
);
|
||||
// cmd.assert().success().stdout(
|
||||
// predicate::str::contains("WLD")
|
||||
// .and(predicate::str::contains("Got"))
|
||||
// .and(predicate::str::contains("200"))
|
||||
// .and(predicate::str::contains("(url length: 32)"))
|
||||
// .and(predicate::str::contains("(url length: 96)"))
|
||||
// .and(predicate::str::contains(
|
||||
// "Wildcard response is static; auto-filtering 46",
|
||||
// )),
|
||||
// );
|
||||
|
||||
assert_eq!(mock.hits(), 1);
|
||||
assert_eq!(mock2.hits(), 1);
|
||||
}
|
||||
// assert_eq!(mock.hits(), 1);
|
||||
// assert_eq!(mock2.hits(), 1);
|
||||
// }
|
||||
|
||||
#[test]
|
||||
/// test finds a static wildcard that returns 3xx, expect redirects to => in response as well as
|
||||
/// in the output file
|
||||
fn heuristics_wildcard_test_with_redirect_as_response_code(
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||
let outfile = tmp_dir.path().join("outfile");
|
||||
// #[test]
|
||||
// /// test finds a static wildcard that returns 3xx, expect redirects to => in response as well as
|
||||
// /// in the output file
|
||||
// fn heuristics_wildcard_test_with_redirect_as_response_code(
|
||||
// ) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// let srv = MockServer::start();
|
||||
|
||||
let mock = srv.mock(|when, then| {
|
||||
when.method(GET)
|
||||
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
|
||||
then.status(301)
|
||||
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
});
|
||||
// let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||
// let outfile = tmp_dir.path().join("outfile");
|
||||
|
||||
let mock2 = srv.mock(|when, then| {
|
||||
when.method(GET)
|
||||
.path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
|
||||
then.status(301)
|
||||
.header("Location", &srv.url("/some-redirect"))
|
||||
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
});
|
||||
// let mock = srv.mock(|when, then| {
|
||||
// when.method(GET)
|
||||
// .path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
|
||||
// then.status(301)
|
||||
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
// });
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--add-slash")
|
||||
.arg("--output")
|
||||
.arg(outfile.as_os_str())
|
||||
.unwrap();
|
||||
// let mock2 = srv.mock(|when, then| {
|
||||
// when.method(GET)
|
||||
// .path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
|
||||
// then.status(301)
|
||||
// .header("Location", &srv.url("/some-redirect"))
|
||||
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
// });
|
||||
|
||||
let contents = std::fs::read_to_string(outfile).unwrap();
|
||||
// let cmd = Command::cargo_bin("feroxbuster")
|
||||
// .unwrap()
|
||||
// .arg("--url")
|
||||
// .arg(srv.url("/"))
|
||||
// .arg("--wordlist")
|
||||
// .arg(file.as_os_str())
|
||||
// .arg("--add-slash")
|
||||
// .arg("--output")
|
||||
// .arg(outfile.as_os_str())
|
||||
// .arg("--threads")
|
||||
// .arg("1")
|
||||
// .unwrap();
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
// let contents = std::fs::read_to_string(outfile).unwrap();
|
||||
|
||||
assert!(contents.contains("WLD"));
|
||||
assert!(contents.contains("301"));
|
||||
assert!(contents.contains("/some-redirect"));
|
||||
assert!(contents.contains(" => "));
|
||||
assert!(contents.contains(&srv.url("/")));
|
||||
assert!(contents.contains("(url length: 32)"));
|
||||
// teardown_tmp_directory(tmp_dir);
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::contains(" => ")
|
||||
.and(predicate::str::contains("/some-redirect"))
|
||||
.and(predicate::str::contains("301"))
|
||||
.and(predicate::str::contains(srv.url("/")))
|
||||
.and(predicate::str::contains("(url length: 32)"))
|
||||
.and(predicate::str::contains("WLD")),
|
||||
);
|
||||
// assert!(contents.contains("WLD"));
|
||||
// assert!(contents.contains("301"));
|
||||
// assert!(contents.contains("/some-redirect"));
|
||||
// assert!(contents.contains(" => "));
|
||||
// assert!(contents.contains(&srv.url("/")));
|
||||
// assert!(contents.contains("(url length: 32)"));
|
||||
|
||||
assert_eq!(mock.hits(), 1);
|
||||
assert_eq!(mock2.hits(), 1);
|
||||
Ok(())
|
||||
}
|
||||
// cmd.assert().success().stdout(
|
||||
// predicate::str::contains(" => ")
|
||||
// .and(predicate::str::contains("/some-redirect"))
|
||||
// .and(predicate::str::contains("301"))
|
||||
// .and(predicate::str::contains(srv.url("/")))
|
||||
// .and(predicate::str::contains("(url length: 32)"))
|
||||
// .and(predicate::str::contains("WLD")),
|
||||
// );
|
||||
|
||||
// assert_eq!(mock.hits(), 1);
|
||||
// assert_eq!(mock2.hits(), 1);
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
// todo figure out why ci hates these tests
|
||||
|
||||
@@ -196,10 +196,10 @@ fn main_parallel_creates_output_directory() -> Result<(), Box<dyn std::error::Er
|
||||
let file_regex = Regex::new("ferox-[a-zA-Z_:0-9]+-[0-9]+.log").unwrap();
|
||||
let dir_regex = Regex::new("output-file-[0-9]+.logs").unwrap();
|
||||
|
||||
let sub_dir = output_dir.as_ref().join(&sub_dir);
|
||||
let sub_dir = output_dir.as_ref().join(sub_dir);
|
||||
|
||||
// created directory like output-file-1627845741.logs/
|
||||
assert!(dir_regex.is_match(&sub_dir.to_string_lossy().to_string()));
|
||||
assert!(dir_regex.is_match(&sub_dir.to_string_lossy()));
|
||||
|
||||
for entry in sub_dir.read_dir()? {
|
||||
let entry = entry?;
|
||||
|
||||
@@ -42,7 +42,13 @@ fn parser_incorrect_param_with_tack_h() {
|
||||
.arg("-h")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains(
|
||||
"[CAUTION] 4 -v's is probably too much",
|
||||
));
|
||||
.stdout(
|
||||
predicate::str::contains("[CAUTION]")
|
||||
.and(predicate::str::contains("4"))
|
||||
.and(predicate::str::contains("-v's"))
|
||||
.and(predicate::str::contains("is"))
|
||||
.and(predicate::str::contains("probably"))
|
||||
.and(predicate::str::contains("too"))
|
||||
.and(predicate::str::contains("much")),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,11 +20,13 @@ fn resume_scan_works() {
|
||||
// localhost:PORT/ <- complete
|
||||
// localhost:PORT/js <- will get scanned with /css and /stuff
|
||||
let complete_scan = format!(
|
||||
r#"{{"id":"057016a14769414aac9a7a62707598cb","url":"{}","scan_type":"Directory","status":"Complete"}}"#,
|
||||
srv.url("/")
|
||||
r#"{{"id":"057016a14769414aac9a7a62707598cb","url":"{}","normalized_url":"{}","scan_type":"Directory","status":"Complete"}}"#,
|
||||
srv.url("/"),
|
||||
srv.url("/"),
|
||||
);
|
||||
let incomplete_scan = format!(
|
||||
r#"{{"id":"400b2323a16f43468a04ffcbbeba34c6","url":"{}","scan_type":"Directory","status":"NotStarted"}}"#,
|
||||
r#"{{"id":"400b2323a16f43468a04ffcbbeba34c6","url":"{}","normalized_url":"{}/","scan_type":"Directory","status":"NotStarted"}}"#,
|
||||
srv.url("/js"),
|
||||
srv.url("/js")
|
||||
);
|
||||
let scans = format!(r#""scans":[{},{}]"#, complete_scan, incomplete_scan);
|
||||
|
||||
@@ -53,17 +53,17 @@ fn scanner_recursive_request_scan() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
let js_mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/js");
|
||||
then.status(301).header("Location", &srv.url("/js/"));
|
||||
then.status(301).header("Location", srv.url("/js/"));
|
||||
});
|
||||
|
||||
let js_prod_mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/js/prod");
|
||||
then.status(301).header("Location", &srv.url("/js/prod/"));
|
||||
then.status(301).header("Location", srv.url("/js/prod/"));
|
||||
});
|
||||
|
||||
let js_dev_mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/js/dev");
|
||||
then.status(301).header("Location", &srv.url("/js/dev/"));
|
||||
then.status(301).header("Location", srv.url("/js/dev/"));
|
||||
});
|
||||
|
||||
let js_dev_file_mock = srv.mock(|when, then| {
|
||||
@@ -116,17 +116,17 @@ fn scanner_recursive_request_scan_using_only_success_responses(
|
||||
|
||||
let js_mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/js/");
|
||||
then.status(200).header("Location", &srv.url("/js/"));
|
||||
then.status(200).header("Location", srv.url("/js/"));
|
||||
});
|
||||
|
||||
let js_prod_mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/js/prod/");
|
||||
then.status(200).header("Location", &srv.url("/js/prod/"));
|
||||
then.status(200).header("Location", srv.url("/js/prod/"));
|
||||
});
|
||||
|
||||
let js_dev_mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/js/dev/");
|
||||
then.status(200).header("Location", &srv.url("/js/dev/"));
|
||||
then.status(200).header("Location", srv.url("/js/dev/"));
|
||||
});
|
||||
|
||||
let js_dev_file_mock = srv.mock(|when, then| {
|
||||
@@ -852,3 +852,62 @@ fn collect_words_makes_appropriate_requests() {
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// send a request to an endpoint that has abnormal redirect logic, ala fast-api
|
||||
fn scanner_forced_recursion_ignores_normal_redirect_logic() -> Result<(), Box<dyn std::error::Error>>
|
||||
{
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||
|
||||
let mock1 = srv.mock(|when, then| {
|
||||
when.method(GET).path("/LICENSE");
|
||||
then.status(301)
|
||||
.body("this is a test")
|
||||
.header("Location", srv.url("/LICENSE"));
|
||||
});
|
||||
|
||||
let mock2 = srv.mock(|when, then| {
|
||||
when.method(GET).path("/LICENSE/LICENSE");
|
||||
then.status(404);
|
||||
});
|
||||
|
||||
let mock3 = srv.mock(|when, then| {
|
||||
when.method(GET).path("/LICENSE/LICENSE/LICENSE");
|
||||
then.status(404);
|
||||
});
|
||||
|
||||
let mock4 = srv.mock(|when, then| {
|
||||
when.method(GET).path("/LICENSE/LICENSE/LICENSE/LICENSE");
|
||||
then.status(404);
|
||||
});
|
||||
|
||||
let outfile = tmp_dir.path().join("output");
|
||||
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--force-recursion")
|
||||
.arg("-o")
|
||||
.arg(outfile.as_os_str())
|
||||
.unwrap();
|
||||
|
||||
let contents = std::fs::read_to_string(outfile)?;
|
||||
println!("{}", contents);
|
||||
|
||||
assert!(contents.contains("/LICENSE"));
|
||||
assert!(contents.contains("301"));
|
||||
assert!(contents.contains("14"));
|
||||
|
||||
assert_eq!(mock1.hits(), 2);
|
||||
assert_eq!(mock2.hits(), 1);
|
||||
assert_eq!(mock3.hits(), 0);
|
||||
assert_eq!(mock4.hits(), 0);
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ pub fn setup_tmp_directory(
|
||||
filename: &str,
|
||||
) -> Result<(TempDir, PathBuf), Box<dyn std::error::Error>> {
|
||||
let tmp_dir = TempDir::new()?;
|
||||
let file = tmp_dir.path().join(&filename);
|
||||
let file = tmp_dir.path().join(filename);
|
||||
write(&file, words.join("\n"))?;
|
||||
Ok((tmp_dir, file))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user