mirror of
https://github.com/epi052/feroxbuster.git
synced 2026-05-23 04:51:13 -03:00
Compare commits
242 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0252bc375 | ||
|
|
1eca023d6e | ||
|
|
3ae3adf11b | ||
|
|
b94edc4e57 | ||
|
|
8116018b8b | ||
|
|
d1d37c135e | ||
|
|
1cd1d990de | ||
|
|
9c50038a25 | ||
|
|
1e3cd3a209 | ||
|
|
3536587260 | ||
|
|
19cd5c910a | ||
|
|
b8bfbb09f3 | ||
|
|
0a3130934c | ||
|
|
7b3201f2f8 | ||
|
|
521d341e36 | ||
|
|
3f3b24b26f | ||
|
|
2a4a150598 | ||
|
|
39b2da9735 | ||
|
|
87aaa84f1e | ||
|
|
2b7d134ede | ||
|
|
4eebacb077 | ||
|
|
1a6ad39b46 | ||
|
|
10b473d920 | ||
|
|
1d2dc8bd37 | ||
|
|
2132ceadd5 | ||
|
|
15bd50dc24 | ||
|
|
48ca8d510a | ||
|
|
9ca48fe877 | ||
|
|
2f09df921d | ||
|
|
93686acb48 | ||
|
|
294159088c | ||
|
|
da9bec5a67 | ||
|
|
69cf08bf1f | ||
|
|
035a8c75c0 | ||
|
|
ac56225405 | ||
|
|
82a7aa458c | ||
|
|
c29abe4ec4 | ||
|
|
bb6146c18f | ||
|
|
882aded16c | ||
|
|
86dc8edd3d | ||
|
|
0281057944 | ||
|
|
96fa07a5e5 | ||
|
|
3ee6641a7d | ||
|
|
90dd18af2e | ||
|
|
b98ab6d691 | ||
|
|
1723847672 | ||
|
|
125a55f72b | ||
|
|
ba58bd942e | ||
|
|
72fc0b026d | ||
|
|
5350724e5f | ||
|
|
0b208cd011 | ||
|
|
82f8f687fd | ||
|
|
b36c3e0318 | ||
|
|
85473916db | ||
|
|
7afb261206 | ||
|
|
0b2d77605e | ||
|
|
073291360a | ||
|
|
98d6fdf536 | ||
|
|
2b6de8e7dc | ||
|
|
d0cdf5766b | ||
|
|
46366291f1 | ||
|
|
b1d33f4f7d | ||
|
|
1e4d3802f8 | ||
|
|
a2bc9ecb49 | ||
|
|
4b0b26da02 | ||
|
|
fe5612ce71 | ||
|
|
ea51805552 | ||
|
|
2ff4dcde8a | ||
|
|
9f93c2381a | ||
|
|
ece220263b | ||
|
|
06312f1f09 | ||
|
|
14023f7e05 | ||
|
|
cc18dfc7d4 | ||
|
|
99bb0200e5 | ||
|
|
542db19180 | ||
|
|
d9718d0d6a | ||
|
|
491821f0b2 | ||
|
|
6d5235ab0a | ||
|
|
74b23141e0 | ||
|
|
8f4ffc8e22 | ||
|
|
7d314c7bac | ||
|
|
b6c41ae2d3 | ||
|
|
b80c58a073 | ||
|
|
a035f0eeaf | ||
|
|
672d17ec27 | ||
|
|
f7d4a3e7b4 | ||
|
|
00b0c3c62d | ||
|
|
f260a981ca | ||
|
|
f254fe172c | ||
|
|
cc1dc94459 | ||
|
|
e39f6cf16d | ||
|
|
2a406960c4 | ||
|
|
c65e2f02b3 | ||
|
|
831ae011e2 | ||
|
|
32a4db4b46 | ||
|
|
a439be0305 | ||
|
|
36994d208d | ||
|
|
b72c42e1d1 | ||
|
|
b508dcce8d | ||
|
|
c33d397360 | ||
|
|
449f6bda32 | ||
|
|
092515cf3a | ||
|
|
e5fe9bb360 | ||
|
|
2e42e3efac | ||
|
|
0d1cb25b69 | ||
|
|
653117bda6 | ||
|
|
5c32fab4cb | ||
|
|
904c70281a | ||
|
|
2d5825556f | ||
|
|
ef7fc7a8a3 | ||
|
|
e48a462471 | ||
|
|
f6047e9819 | ||
|
|
534cbe8fe1 | ||
|
|
adb5cd75cc | ||
|
|
3469e2c306 | ||
|
|
6de087ae79 | ||
|
|
07a9fdee41 | ||
|
|
7b9767107f | ||
|
|
5388d40c03 | ||
|
|
28769b5028 | ||
|
|
28fa90b093 | ||
|
|
0b16f368a4 | ||
|
|
c5e59b70f7 | ||
|
|
6756a1da74 | ||
|
|
d14de76f9a | ||
|
|
ef3cc05ee3 | ||
|
|
efd706cb9b | ||
|
|
63baa3ec57 | ||
|
|
51defffd3b | ||
|
|
439afd2e2a | ||
|
|
10ae4ee524 | ||
|
|
4af448d7b1 | ||
|
|
adc536bf4b | ||
|
|
fde52e95e1 | ||
|
|
e4ae5759ff | ||
|
|
89eda0e62b | ||
|
|
00330b053f | ||
|
|
0df1d34ee1 | ||
|
|
eddab0de13 | ||
|
|
f9335a7867 | ||
|
|
bc9779be2a | ||
|
|
c4b6fed6ef | ||
|
|
3818276c7e | ||
|
|
6596759132 | ||
|
|
5235208aa8 | ||
|
|
bce55e77f3 | ||
|
|
8d11bb1800 | ||
|
|
40fccb9761 | ||
|
|
28f63aae94 | ||
|
|
1eaf6fc232 | ||
|
|
dd1c824d98 | ||
|
|
42c06c87cc | ||
|
|
d36379ba1b | ||
|
|
83ba49a486 | ||
|
|
0b75e1a548 | ||
|
|
867e297284 | ||
|
|
63bd89ddc3 | ||
|
|
9f39ee3491 | ||
|
|
eabf97b776 | ||
|
|
3c3b976a71 | ||
|
|
81709b5009 | ||
|
|
ffa0c6b390 | ||
|
|
141fe74129 | ||
|
|
d4a69fa2ec | ||
|
|
9a3754a31d | ||
|
|
c89453c5c3 | ||
|
|
be57e620f0 | ||
|
|
45efaa7388 | ||
|
|
15cb5e1619 | ||
|
|
fc500a5cd5 | ||
|
|
c84612751c | ||
|
|
c8ecbd4ed6 | ||
|
|
fbf79ab7c1 | ||
|
|
a446192b9a | ||
|
|
26565be18d | ||
|
|
7c4bc213a3 | ||
|
|
a227dcf726 | ||
|
|
b70c92b1e6 | ||
|
|
033a57a9e9 | ||
|
|
dac74ae040 | ||
|
|
d4abb84214 | ||
|
|
5201c300e9 | ||
|
|
ec0e5299ed | ||
|
|
242c35c89f | ||
|
|
f717ee534e | ||
|
|
6b66f39122 | ||
|
|
4b3e9badbb | ||
|
|
c680be558a | ||
|
|
8cee7ce247 | ||
|
|
580aa19681 | ||
|
|
cd220fe471 | ||
|
|
15b4fd04e5 | ||
|
|
fceba0b68b | ||
|
|
eef4c9b5ed | ||
|
|
24da4e017c | ||
|
|
f3cedf01a5 | ||
|
|
08ee32595f | ||
|
|
4c4d1a2a61 | ||
|
|
64b54a6308 | ||
|
|
e27b3ee8da | ||
|
|
129725cedd | ||
|
|
17886da3df | ||
|
|
c8a46b7e5a | ||
|
|
f97d103fc6 | ||
|
|
aa2fecc5c1 | ||
|
|
6f2244e1ff | ||
|
|
a1dc90ba06 | ||
|
|
32f55ddfb7 | ||
|
|
9a65c7f1f5 | ||
|
|
0f6bc1c160 | ||
|
|
abef7a236b | ||
|
|
0cff62dbe2 | ||
|
|
a590188e44 | ||
|
|
dc3aa11966 | ||
|
|
57714d243a | ||
|
|
1d34a5e99f | ||
|
|
9ab3e5515e | ||
|
|
3abef25c8f | ||
|
|
454f3a4302 | ||
|
|
acb9c19f4d | ||
|
|
98f06951bd | ||
|
|
c9e1a7adbe | ||
|
|
c57cf82fce | ||
|
|
a3bcfaf95c | ||
|
|
c99afec740 | ||
|
|
fa9fd65c2f | ||
|
|
2af87971d5 | ||
|
|
e6753d9474 | ||
|
|
d23717dc6c | ||
|
|
4debe68ed6 | ||
|
|
e6b78e3986 | ||
|
|
7b268cf197 | ||
|
|
34ff884d52 | ||
|
|
7fef23f888 | ||
|
|
7a8d6d0d52 | ||
|
|
6d4f2a7ed9 | ||
|
|
329d04252f | ||
|
|
9b4092ea8c | ||
|
|
d942a7705a | ||
|
|
e3365b42a2 | ||
|
|
41689bd742 | ||
|
|
bc487475f0 |
283
.all-contributorsrc
Normal file
283
.all-contributorsrc
Normal file
@@ -0,0 +1,283 @@
|
||||
{
|
||||
"files": [
|
||||
"README.md"
|
||||
],
|
||||
"imageSize": 100,
|
||||
"commit": false,
|
||||
"contributors": [
|
||||
{
|
||||
"login": "joohoi",
|
||||
"name": "Joona Hoikkala",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5235109?v=4",
|
||||
"profile": "https://io.fi",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jsav0",
|
||||
"name": "J Savage",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/20546041?v=4",
|
||||
"profile": "https://github.com/jsav0",
|
||||
"contributions": [
|
||||
"infra",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "TGotwig",
|
||||
"name": "Thomas Gotwig",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/30773779?v=4",
|
||||
"profile": "http://www.tgotwig.dev",
|
||||
"contributions": [
|
||||
"infra",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "spikecodes",
|
||||
"name": "Spike",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/19519553?v=4",
|
||||
"profile": "https://github.com/spikecodes",
|
||||
"contributions": [
|
||||
"infra",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "evanrichter",
|
||||
"name": "Evan Richter",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/330292?v=4",
|
||||
"profile": "https://github.com/evanrichter",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mzpqnxow",
|
||||
"name": "AG",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8016228?v=4",
|
||||
"profile": "https://github.com/mzpqnxow",
|
||||
"contributions": [
|
||||
"ideas",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "n-thumann",
|
||||
"name": "Nicolas Thumann",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/46975855?v=4",
|
||||
"profile": "https://n-thumann.de/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "tomtastic",
|
||||
"name": "Tom Matthews",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/302127?v=4",
|
||||
"profile": "https://github.com/tomtastic",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "bsysop",
|
||||
"name": "bsysop",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/9998303?v=4",
|
||||
"profile": "https://github.com/bsysop",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "bpsizemore",
|
||||
"name": "Brian Sizemore",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/11645898?v=4",
|
||||
"profile": "http://bpsizemore.me",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "noraj",
|
||||
"name": "Alexandre ZANNI",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/16578570?v=4",
|
||||
"profile": "https://pwn.by/noraj",
|
||||
"contributions": [
|
||||
"infra",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "craig",
|
||||
"name": "Craig",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/99729?v=4",
|
||||
"profile": "https://github.com/craig",
|
||||
"contributions": [
|
||||
"infra"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "EONRaider",
|
||||
"name": "EONRaider",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/15611424?v=4",
|
||||
"profile": "https://www.reddit.com/u/EONRaider",
|
||||
"contributions": [
|
||||
"infra"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "wtwver",
|
||||
"name": "wtwver",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/53866088?v=4",
|
||||
"profile": "https://github.com/wtwver",
|
||||
"contributions": [
|
||||
"infra"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Tib3rius",
|
||||
"name": "Tib3rius",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/48113936?v=4",
|
||||
"profile": "https://tib3rius.com",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "0xdf",
|
||||
"name": "0xdf",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1489045?v=4",
|
||||
"profile": "https://github.com/0xdf",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "secure-77",
|
||||
"name": "secure-77",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/31564517?v=4",
|
||||
"profile": "http://secure77.de",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sbrun",
|
||||
"name": "Sophie Brun",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7712154?v=4",
|
||||
"profile": "https://github.com/sbrun",
|
||||
"contributions": [
|
||||
"infra"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "black-A",
|
||||
"name": "black-A",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/30686803?v=4",
|
||||
"profile": "https://github.com/black-A",
|
||||
"contributions": [
|
||||
"ideas"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dinosn",
|
||||
"name": "Nicolas Krassas",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3851678?v=4",
|
||||
"profile": "https://github.com/dinosn",
|
||||
"contributions": [
|
||||
"ideas"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "N0ur5",
|
||||
"name": "N0ur5",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/24260009?v=4",
|
||||
"profile": "https://github.com/N0ur5",
|
||||
"contributions": [
|
||||
"ideas"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "moscowchill",
|
||||
"name": "mchill",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/72578879?v=4",
|
||||
"profile": "https://github.com/moscowchill",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "BitThr3at",
|
||||
"name": "Naman",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/45028933?v=4",
|
||||
"profile": "http://BitThr3at.github.io",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sicks3c",
|
||||
"name": "Ayoub Elaich",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/32225186?v=4",
|
||||
"profile": "https://github.com/Sicks3c",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "HenryHoggard",
|
||||
"name": "Henry",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1208121?v=4",
|
||||
"profile": "https://github.com/HenryHoggard",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "SleepiPanda",
|
||||
"name": "SleepiPanda",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6428561?v=4",
|
||||
"profile": "https://github.com/SleepiPanda",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "uBadRequest",
|
||||
"name": "Bad Requests",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/47282747?v=4",
|
||||
"profile": "https://github.com/uBadRequest",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dnaka91",
|
||||
"name": "Dominik Nakamura",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/36804488?v=4",
|
||||
"profile": "https://home.dnaka91.rocks",
|
||||
"contributions": [
|
||||
"infra"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hunter0x8",
|
||||
"name": "Muhammad Ahsan",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/46222314?v=4",
|
||||
"profile": "https://github.com/hunter0x8",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
"projectName": "feroxbuster",
|
||||
"projectOwner": "epi052",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"skipCi": true
|
||||
}
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -11,7 +11,7 @@ Long form explanations of most of the items below can be found in the [CONTRIBUT
|
||||
|
||||
## Static analysis checks
|
||||
- [ ] All rust files are formatted using `cargo fmt`
|
||||
- [ ] All `clippy` checks pass when running `cargo clippy --all-targets --all-features -- -D warnings -A clippy::deref_addrof -A clippy::mutex-atomic`
|
||||
- [ ] All `clippy` checks pass when running `cargo clippy --all-targets --all-features -- -D warnings -A clippy::mutex-atomic`
|
||||
- [ ] All existing tests pass
|
||||
|
||||
## Documentation
|
||||
|
||||
1
.github/stale.yml
vendored
1
.github/stale.yml
vendored
@@ -6,6 +6,7 @@ daysUntilClose: 7
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- confirmed
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
|
||||
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -4,6 +4,8 @@ on: [push]
|
||||
|
||||
jobs:
|
||||
build-nix:
|
||||
env:
|
||||
IN_PIPELINE: true
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: github.ref == 'refs/heads/main'
|
||||
strategy:
|
||||
@@ -38,6 +40,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install System Dependencies
|
||||
run: |
|
||||
env
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends libssl-dev pkg-config gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu
|
||||
- uses: actions-rs/toolchain@v1
|
||||
@@ -84,6 +87,8 @@ jobs:
|
||||
path: ./target/x86_64-unknown-linux-musl/debian/*
|
||||
|
||||
build-macos:
|
||||
env:
|
||||
IN_PIPELINE: true
|
||||
runs-on: macos-latest
|
||||
if: github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
@@ -114,6 +119,8 @@ jobs:
|
||||
path: x86_64-macos-feroxbuster.tar.gz
|
||||
|
||||
build-windows:
|
||||
env:
|
||||
IN_PIPELINE: true
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: github.ref == 'refs/heads/main'
|
||||
strategy:
|
||||
@@ -146,4 +153,3 @@ jobs:
|
||||
with:
|
||||
name: ${{ matrix.name }}
|
||||
path: ${{ matrix.path }}
|
||||
|
||||
|
||||
34
.github/workflows/cicd-to-dockerhub.yml
vendored
Normal file
34
.github/workflows/cicd-to-dockerhub.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: ci-to-dockerhub
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/feroxbuster:latest
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -23,7 +23,7 @@ lcov_cobertura.py
|
||||
.dockerignore
|
||||
|
||||
# state file created during tests
|
||||
ferox-http*
|
||||
ferox-*.state
|
||||
|
||||
# python stuff cuz reasons
|
||||
Pipfile*
|
||||
|
||||
756
Cargo.lock
generated
756
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
31
Cargo.toml
31
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "feroxbuster"
|
||||
version = "2.2.4"
|
||||
version = "2.4.0"
|
||||
authors = ["Ben 'epi' Risher <epibar052@gmail.com>"]
|
||||
license = "MIT"
|
||||
edition = "2018"
|
||||
@@ -19,37 +19,40 @@ maintenance = { status = "actively-developed" }
|
||||
clap = "2.33"
|
||||
regex = "1"
|
||||
lazy_static = "1.4"
|
||||
dirs = "4.0"
|
||||
|
||||
[dependencies]
|
||||
futures = { version = "0.3.14"}
|
||||
tokio = { version = "1.5.0", features = ["full"] }
|
||||
tokio-util = {version = "0.6.6", features = ["codec"]}
|
||||
futures = { version = "0.3"}
|
||||
tokio = { version = "1.11", features = ["full"] }
|
||||
tokio-util = {version = "0.6", features = ["codec"]}
|
||||
log = "0.4"
|
||||
env_logger = "0.8.3"
|
||||
env_logger = "0.9"
|
||||
reqwest = { version = "0.11", features = ["socks"] }
|
||||
url = { version = "2.2", features = ["serde"]} # uses feature unification to add 'serde' to reqwest::Url
|
||||
serde_regex = "1.1"
|
||||
clap = "2.33"
|
||||
lazy_static = "1.4"
|
||||
toml = "0.5"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.64"
|
||||
serde_json = "1.0"
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
indicatif = "0.15"
|
||||
console = "0.14"
|
||||
console = "0.15"
|
||||
openssl = { version = "0.10", features = ["vendored"] }
|
||||
dirs = "3.0"
|
||||
dirs = "4.0"
|
||||
regex = "1"
|
||||
crossterm = "0.19"
|
||||
rlimit = "0.5.4"
|
||||
ctrlc = "3.1.9"
|
||||
crossterm = "0.20"
|
||||
rlimit = "0.6"
|
||||
ctrlc = "3.2"
|
||||
fuzzyhash = "0.2.1"
|
||||
anyhow = "1.0"
|
||||
leaky-bucket = "0.10.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.1"
|
||||
httpmock = "0.5.8"
|
||||
assert_cmd = "1.0.3"
|
||||
predicates = "1.0.8"
|
||||
httpmock = "0.6.2"
|
||||
assert_cmd = "2.0"
|
||||
predicates = "2.0"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
30
Dockerfile
30
Dockerfile
@@ -1,14 +1,28 @@
|
||||
FROM alpine:latest
|
||||
# Image: alpine:3.14.2
|
||||
FROM alpine@sha256:69704ef328d05a9f806b6b8502915e6a0a4faa4d72018dc42343f511490daf8a as build
|
||||
LABEL maintainer="wfnintr@null.net"
|
||||
|
||||
RUN sed -i -e 's/v[[:digit:]]\..*\//edge\//g' /etc/apk/repositories && apk upgrade --update-cache --available
|
||||
RUN sed -i -e 's/v[[:digit:]]\..*\//edge\//g' /etc/apk/repositories \
|
||||
&& apk upgrade --update-cache --available && apk add --update openssl
|
||||
|
||||
# download default wordlists
|
||||
RUN apk add --no-cache --virtual .depends subversion font-noto-emoji && \
|
||||
svn export https://github.com/danielmiessler/SecLists/trunk/Discovery/Web-Content /usr/share/seclists/Discovery/Web-Content && \
|
||||
apk del .depends
|
||||
|
||||
# install latest release
|
||||
RUN wget https://github.com/epi052/feroxbuster/releases/latest/download/x86_64-linux-feroxbuster.zip -qO feroxbuster.zip && unzip -d /usr/local/bin/ feroxbuster.zip feroxbuster && rm feroxbuster.zip && chmod +x /usr/local/bin/feroxbuster
|
||||
# Download latest release
|
||||
RUN wget https://github.com/epi052/feroxbuster/releases/latest/download/x86_64-linux-feroxbuster.zip -qO feroxbuster.zip \
|
||||
&& unzip -d /tmp/ feroxbuster.zip feroxbuster \
|
||||
&& chmod +x /tmp/feroxbuster \
|
||||
&& wget https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/Web-Content/raft-medium-directories.txt -O /tmp/raft-medium-directories.txt
|
||||
|
||||
# Image: alpine:3.14.2
|
||||
FROM alpine@sha256:69704ef328d05a9f806b6b8502915e6a0a4faa4d72018dc42343f511490daf8a as release
|
||||
|
||||
COPY --from=build /tmp/raft-medium-directories.txt /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
|
||||
COPY --from=build /tmp/feroxbuster /usr/local/bin/feroxbuster
|
||||
|
||||
RUN adduser \
|
||||
--gecos "" \
|
||||
--disabled-password \
|
||||
feroxbuster
|
||||
|
||||
USER feroxbuster
|
||||
|
||||
ENTRYPOINT ["feroxbuster"]
|
||||
|
||||
7
Makefile
7
Makefile
@@ -27,12 +27,17 @@ endif
|
||||
|
||||
TARGET = target/$(RELEASE)
|
||||
|
||||
.PHONY: all clean install uninstall
|
||||
.PHONY: all clean install uninstall test update
|
||||
|
||||
all: cli
|
||||
cli: $(TARGET)/$(BIN) $(TARGET)/$(BIN).1.gz $(SHR_SOURCES)
|
||||
install: all install-cli
|
||||
|
||||
verify:
|
||||
cargo fmt
|
||||
cargo clippy --all-targets --all-features -- -D warnings -A clippy::mutex-atomic
|
||||
cargo test
|
||||
|
||||
clean:
|
||||
cargo clean
|
||||
|
||||
|
||||
63
build.rs
63
build.rs
@@ -1,4 +1,7 @@
|
||||
use std::fs::{copy, create_dir_all, OpenOptions};
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
extern crate clap;
|
||||
extern crate dirs;
|
||||
|
||||
use clap::Shell;
|
||||
|
||||
@@ -20,4 +23,64 @@ fn main() {
|
||||
for shell in &shells {
|
||||
app.gen_completions("feroxbuster", *shell, outdir);
|
||||
}
|
||||
|
||||
// 0xdf pointed out an oddity when tab-completing options that expect file paths, the fix we
|
||||
// landed on was to add -o plusdirs to the bash completion script. The following code aims to
|
||||
// automate that fix and have it present in all future builds
|
||||
let mut contents = String::new();
|
||||
|
||||
let mut bash_file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(format!("{}/feroxbuster.bash", outdir))
|
||||
.expect("Couldn't open bash completion script");
|
||||
|
||||
bash_file
|
||||
.read_to_string(&mut contents)
|
||||
.expect("Couldn't read bash completion script");
|
||||
|
||||
contents = contents.replace("default feroxbuster", "default -o plusdirs feroxbuster");
|
||||
|
||||
bash_file
|
||||
.seek(SeekFrom::Start(0))
|
||||
.expect("Couldn't seek to position 0 in bash completion script");
|
||||
|
||||
bash_file
|
||||
.write_all(contents.as_bytes())
|
||||
.expect("Couldn't write updated bash completion script to disk");
|
||||
|
||||
// hunter0x8 let me know that when installing via cargo, it would be nice if we dropped a
|
||||
// config file during the build process. The following code will place an example config in
|
||||
// the user's configuration directory
|
||||
// - linux: $XDG_CONFIG_HOME or $HOME/.config
|
||||
// - macOS: $HOME/Library/Application Support
|
||||
// - windows: {FOLDERID_RoamingAppData}
|
||||
let mut config_dir = dirs::config_dir().expect("Couldn't resolve user's config directory");
|
||||
config_dir = config_dir.join("feroxbuster"); // $HOME/.config/feroxbuster
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hard-coding config name here to not rely on the crate we're building, if DEFAULT_CONFIG_NAME
|
||||
// ever changes, this will need to be updated
|
||||
let config_file = config_dir.join("ferox-config.toml");
|
||||
|
||||
if !config_file.exists() {
|
||||
// config file doesn't exist, add it to the config directory
|
||||
if copy("ferox-config.toml.example", config_file).is_err() {
|
||||
eprintln!("Couldn't copy example config into config directory");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
0
docs/.nojekyll
Normal file
0
docs/.nojekyll
Normal file
11
docs/index.html
Normal file
11
docs/index.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="refresh"
|
||||
content="0; url=https://epi052.github.io/feroxbuster-docs/">
|
||||
</head>
|
||||
<body>
|
||||
<p>The page has moved to:
|
||||
<a href="https://epi052.github.io/feroxbuster-docs/">feroxbuster-docs</a></p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -27,9 +27,12 @@
|
||||
# output = "/targets/ellingson_mineral_company/gibson.txt"
|
||||
# debug_log = "/var/log/find-the-derp.log"
|
||||
# user_agent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0"
|
||||
# random_agent = false
|
||||
# redirects = true
|
||||
# insecure = true
|
||||
# extensions = ["php", "html"]
|
||||
# url_denylist = ["http://dont-scan.me", "https://also-not.me"]
|
||||
# regex_denylist = ["/deny.*"]
|
||||
# no_recursion = true
|
||||
# add_slash = true
|
||||
# stdin = true
|
||||
|
||||
@@ -41,6 +41,7 @@ _feroxbuster() {
|
||||
'--user-agent=[Sets the User-Agent (default: feroxbuster/VERSION)]' \
|
||||
'*-x+[File extension(s) to search for (ex: -x php -x pdf js)]' \
|
||||
'*--extensions=[File extension(s) to search for (ex: -x php -x pdf js)]' \
|
||||
'*--dont-scan=[URL(s) or Regex Pattern(s) to exclude from recursion/scans]' \
|
||||
'*-H+[Specify HTTP headers (ex: -H Header:val '\''stuff: things'\'')]' \
|
||||
'*--headers=[Specify HTTP headers (ex: -H Header:val '\''stuff: things'\'')]' \
|
||||
'*-Q+[Specify URL query parameters (ex: -Q token=stuff -Q secret=key)]' \
|
||||
@@ -71,14 +72,16 @@ _feroxbuster() {
|
||||
'--json[Emit JSON logs to --output and --debug-log instead of normal text]' \
|
||||
'-D[Don'\''t auto-filter wildcard responses]' \
|
||||
'--dont-filter[Don'\''t auto-filter wildcard responses]' \
|
||||
'-A[Use a random User-Agent]' \
|
||||
'--random-agent[Use a random User-Agent]' \
|
||||
'-r[Follow redirects]' \
|
||||
'--redirects[Follow redirects]' \
|
||||
'-k[Disables TLS certificate validation]' \
|
||||
'--insecure[Disables TLS certificate validation]' \
|
||||
'-n[Do not scan recursively]' \
|
||||
'--no-recursion[Do not scan recursively]' \
|
||||
'(-x --extensions)-f[Append / to each request]' \
|
||||
'(-x --extensions)--add-slash[Append / to each request]' \
|
||||
'-f[Append / to each request]' \
|
||||
'--add-slash[Append / to each request]' \
|
||||
'(-u --url)--stdin[Read url(s) from STDIN]' \
|
||||
'-e[Extract links from response body (html, javascript, etc...); make new requests based on findings (default: false)]' \
|
||||
'--extract-links[Extract links from response body (html, javascript, etc...); make new requests based on findings (default: false)]' \
|
||||
|
||||
@@ -46,6 +46,7 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
|
||||
[CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/VERSION)')
|
||||
[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('--dont-scan', 'dont-scan', [CompletionResultType]::ParameterName, 'URL(s) or Regex Pattern(s) to exclude from recursion/scans')
|
||||
[CompletionResult]::new('-H', 'H', [CompletionResultType]::ParameterName, 'Specify HTTP headers (ex: -H Header:val ''stuff: things'')')
|
||||
[CompletionResult]::new('--headers', 'headers', [CompletionResultType]::ParameterName, 'Specify HTTP headers (ex: -H Header:val ''stuff: things'')')
|
||||
[CompletionResult]::new('-Q', 'Q', [CompletionResultType]::ParameterName, 'Specify URL query parameters (ex: -Q token=stuff -Q secret=key)')
|
||||
@@ -76,6 +77,8 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
|
||||
[CompletionResult]::new('--json', 'json', [CompletionResultType]::ParameterName, 'Emit JSON logs to --output and --debug-log instead of normal text')
|
||||
[CompletionResult]::new('-D', 'D', [CompletionResultType]::ParameterName, 'Don''t auto-filter wildcard responses')
|
||||
[CompletionResult]::new('--dont-filter', 'dont-filter', [CompletionResultType]::ParameterName, 'Don''t auto-filter wildcard responses')
|
||||
[CompletionResult]::new('-A', 'A', [CompletionResultType]::ParameterName, 'Use a random User-Agent')
|
||||
[CompletionResult]::new('--random-agent', 'random-agent', [CompletionResultType]::ParameterName, 'Use a random User-Agent')
|
||||
[CompletionResult]::new('-r', 'r', [CompletionResultType]::ParameterName, 'Follow redirects')
|
||||
[CompletionResult]::new('--redirects', 'redirects', [CompletionResultType]::ParameterName, 'Follow redirects')
|
||||
[CompletionResult]::new('-k', 'k', [CompletionResultType]::ParameterName, 'Disables TLS certificate validation')
|
||||
|
||||
@@ -20,7 +20,7 @@ _feroxbuster() {
|
||||
|
||||
case "${cmd}" in
|
||||
feroxbuster)
|
||||
opts=" -v -q -D -r -k -n -f -e -h -V -w -u -t -d -T -p -P -R -s -o -a -x -H -Q -S -X -W -N -C -L --verbosity --silent --quiet --auto-tune --auto-bail --json --dont-filter --redirects --insecure --no-recursion --add-slash --stdin --extract-links --help --version --wordlist --url --threads --depth --timeout --proxy --replay-proxy --replay-codes --status-codes --output --resume-from --debug-log --user-agent --extensions --headers --query --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --scan-limit --parallel --rate-limit --time-limit "
|
||||
opts=" -v -q -D -A -r -k -n -f -e -h -V -w -u -t -d -T -p -P -R -s -o -a -x -H -Q -S -X -W -N -C -L --verbosity --silent --quiet --auto-tune --auto-bail --json --dont-filter --random-agent --redirects --insecure --no-recursion --add-slash --stdin --extract-links --help --version --wordlist --url --threads --depth --timeout --proxy --replay-proxy --replay-codes --status-codes --output --resume-from --debug-log --user-agent --extensions --dont-scan --headers --query --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --scan-limit --parallel --rate-limit --time-limit "
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
@@ -131,6 +131,10 @@ _feroxbuster() {
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--dont-scan)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--headers)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
@@ -222,4 +226,4 @@ _feroxbuster() {
|
||||
esac
|
||||
}
|
||||
|
||||
complete -F _feroxbuster -o bashdefault -o default feroxbuster
|
||||
complete -F _feroxbuster -o bashdefault -o default -o plusdirs feroxbuster
|
||||
|
||||
@@ -12,6 +12,7 @@ complete -c feroxbuster -n "__fish_use_subcommand" -l resume-from -d 'State file
|
||||
complete -c feroxbuster -n "__fish_use_subcommand" -l debug-log -d 'Output file to write log entries (use w/ --json for JSON entries)'
|
||||
complete -c feroxbuster -n "__fish_use_subcommand" -s a -l user-agent -d 'Sets the User-Agent (default: feroxbuster/VERSION)'
|
||||
complete -c feroxbuster -n "__fish_use_subcommand" -s x -l extensions -d 'File extension(s) to search for (ex: -x php -x pdf js)'
|
||||
complete -c feroxbuster -n "__fish_use_subcommand" -l dont-scan -d 'URL(s) or Regex Pattern(s) to exclude from recursion/scans'
|
||||
complete -c feroxbuster -n "__fish_use_subcommand" -s H -l headers -d 'Specify HTTP headers (ex: -H Header:val \'stuff: things\')'
|
||||
complete -c feroxbuster -n "__fish_use_subcommand" -s Q -l query -d 'Specify URL query parameters (ex: -Q token=stuff -Q secret=key)'
|
||||
complete -c feroxbuster -n "__fish_use_subcommand" -s S -l filter-size -d 'Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)'
|
||||
@@ -31,6 +32,7 @@ complete -c feroxbuster -n "__fish_use_subcommand" -l auto-tune -d 'Automaticall
|
||||
complete -c feroxbuster -n "__fish_use_subcommand" -l auto-bail -d 'Automatically stop scanning when an excessive amount of errors are encountered'
|
||||
complete -c feroxbuster -n "__fish_use_subcommand" -l json -d 'Emit JSON logs to --output and --debug-log instead of normal text'
|
||||
complete -c feroxbuster -n "__fish_use_subcommand" -s D -l dont-filter -d 'Don\'t auto-filter wildcard responses'
|
||||
complete -c feroxbuster -n "__fish_use_subcommand" -s A -l random-agent -d 'Use a random User-Agent'
|
||||
complete -c feroxbuster -n "__fish_use_subcommand" -s r -l redirects -d 'Follow redirects'
|
||||
complete -c feroxbuster -n "__fish_use_subcommand" -s k -l insecure -d 'Disables TLS certificate validation'
|
||||
complete -c feroxbuster -n "__fish_use_subcommand" -s n -l no-recursion -d 'Do not scan recursively'
|
||||
|
||||
@@ -50,6 +50,9 @@ pub struct Banner {
|
||||
/// represents Configuration.user_agent
|
||||
user_agent: BannerEntry,
|
||||
|
||||
/// represents Configuration.random_agent
|
||||
random_agent: BannerEntry,
|
||||
|
||||
/// represents Configuration.config
|
||||
config: BannerEntry,
|
||||
|
||||
@@ -134,6 +137,9 @@ pub struct Banner {
|
||||
/// represents Configuration.auto_bail
|
||||
auto_bail: BannerEntry,
|
||||
|
||||
/// represents Configuration.url_denylist
|
||||
url_denylist: Vec<BannerEntry>,
|
||||
|
||||
/// current version of feroxbuster
|
||||
pub(super) version: String,
|
||||
|
||||
@@ -146,6 +152,7 @@ impl Banner {
|
||||
/// Create a new Banner from a Configuration and live targets
|
||||
pub fn new(tgts: &[String], config: &Configuration) -> Self {
|
||||
let mut targets = Vec::new();
|
||||
let mut url_denylist = Vec::new();
|
||||
let mut code_filters = Vec::new();
|
||||
let mut replay_codes = Vec::new();
|
||||
let mut headers = Vec::new();
|
||||
@@ -160,6 +167,22 @@ impl Banner {
|
||||
targets.push(BannerEntry::new("🎯", "Target Url", target));
|
||||
}
|
||||
|
||||
for denied_url in &config.url_denylist {
|
||||
url_denylist.push(BannerEntry::new(
|
||||
"🚫",
|
||||
"Don't Scan Url",
|
||||
denied_url.as_str(),
|
||||
));
|
||||
}
|
||||
|
||||
for denied_regex in &config.regex_denylist {
|
||||
url_denylist.push(BannerEntry::new(
|
||||
"🚫",
|
||||
"Don't Scan Regex",
|
||||
denied_regex.as_str(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut codes = vec![];
|
||||
for code in &config.status_codes {
|
||||
codes.push(status_colorizer(&code.to_string()))
|
||||
@@ -268,6 +291,7 @@ impl Banner {
|
||||
let wordlist = BannerEntry::new("📖", "Wordlist", &config.wordlist);
|
||||
let timeout = BannerEntry::new("💥", "Timeout (secs)", &config.timeout.to_string());
|
||||
let user_agent = BannerEntry::new("🦡", "User-Agent", &config.user_agent);
|
||||
let random_agent = BannerEntry::new("🦡", "User-Agent", "Random");
|
||||
let extract_links =
|
||||
BannerEntry::new("🔎", "Extract Links", &config.extract_links.to_string());
|
||||
let json = BannerEntry::new("🧔", "JSON Output", &config.json.to_string());
|
||||
@@ -296,6 +320,7 @@ impl Banner {
|
||||
filter_status,
|
||||
timeout,
|
||||
user_agent,
|
||||
random_agent,
|
||||
auto_bail,
|
||||
auto_tune,
|
||||
proxy,
|
||||
@@ -323,6 +348,7 @@ impl Banner {
|
||||
rate_limit,
|
||||
scan_limit,
|
||||
time_limit,
|
||||
url_denylist,
|
||||
config: cfg,
|
||||
version: VERSION.to_string(),
|
||||
update_status: UpdateStatus::Unknown,
|
||||
@@ -413,6 +439,10 @@ by Ben "epi" Risher {} ver: {}"#,
|
||||
writeln!(&mut writer, "{}", target)?;
|
||||
}
|
||||
|
||||
for denied_url in &self.url_denylist {
|
||||
writeln!(&mut writer, "{}", denied_url)?;
|
||||
}
|
||||
|
||||
writeln!(&mut writer, "{}", self.threads)?;
|
||||
writeln!(&mut writer, "{}", self.wordlist)?;
|
||||
writeln!(&mut writer, "{}", self.status_codes)?;
|
||||
@@ -424,7 +454,12 @@ by Ben "epi" Risher {} ver: {}"#,
|
||||
}
|
||||
|
||||
writeln!(&mut writer, "{}", self.timeout)?;
|
||||
writeln!(&mut writer, "{}", self.user_agent)?;
|
||||
|
||||
if config.random_agent {
|
||||
writeln!(&mut writer, "{}", self.random_agent)?;
|
||||
} else {
|
||||
writeln!(&mut writer, "{}", self.user_agent)?;
|
||||
}
|
||||
|
||||
// followed by the maybe printed or variably displayed values
|
||||
if !config.config.is_empty() {
|
||||
|
||||
@@ -10,7 +10,8 @@ use crate::{
|
||||
};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use clap::{value_t, ArgMatches};
|
||||
use reqwest::{Client, StatusCode};
|
||||
use regex::Regex;
|
||||
use reqwest::{Client, StatusCode, Url};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
@@ -154,6 +155,10 @@ pub struct Configuration {
|
||||
#[serde(default = "user_agent")]
|
||||
pub user_agent: String,
|
||||
|
||||
/// Use random User-Agent
|
||||
#[serde(default)]
|
||||
pub random_agent: bool,
|
||||
|
||||
/// Follow redirects
|
||||
#[serde(default)]
|
||||
pub redirects: bool,
|
||||
@@ -248,6 +253,13 @@ pub struct Configuration {
|
||||
/// Filter out response bodies that meet a certain threshold of similarity
|
||||
#[serde(default)]
|
||||
pub filter_similar: Vec<String>,
|
||||
|
||||
/// URLs that should never be scanned/recursed into
|
||||
#[serde(default)]
|
||||
pub url_denylist: Vec<Url>,
|
||||
|
||||
#[serde(with = "serde_regex", default)]
|
||||
pub regex_denylist: Vec<Regex>,
|
||||
}
|
||||
|
||||
impl Default for Configuration {
|
||||
@@ -291,6 +303,7 @@ impl Default for Configuration {
|
||||
redirects: false,
|
||||
no_recursion: false,
|
||||
extract_links: false,
|
||||
random_agent: false,
|
||||
save_state: true,
|
||||
proxy: String::new(),
|
||||
config: String::new(),
|
||||
@@ -304,6 +317,8 @@ impl Default for Configuration {
|
||||
extensions: Vec::new(),
|
||||
filter_size: Vec::new(),
|
||||
filter_regex: Vec::new(),
|
||||
url_denylist: Vec::new(),
|
||||
regex_denylist: Vec::new(),
|
||||
filter_line_count: Vec::new(),
|
||||
filter_word_count: Vec::new(),
|
||||
filter_status: Vec::new(),
|
||||
@@ -339,8 +354,11 @@ impl Configuration {
|
||||
/// - **auto_bail**: `false`
|
||||
/// - **save_state**: `true`
|
||||
/// - **user_agent**: `feroxbuster/VERSION`
|
||||
/// - **random_agent**: `false`
|
||||
/// - **insecure**: `false` (don't be insecure, i.e. don't allow invalid certs)
|
||||
/// - **extensions**: `None`
|
||||
/// - **url_denylist**: `None`
|
||||
/// - **regex_denylist**: `None`
|
||||
/// - **filter_size**: `None`
|
||||
/// - **filter_similar**: `None`
|
||||
/// - **filter_regex**: `None`
|
||||
@@ -538,6 +556,56 @@ impl Configuration {
|
||||
config.extensions = arg.map(|val| val.to_string()).collect();
|
||||
}
|
||||
|
||||
if args.is_present("stdin") {
|
||||
config.stdin = true;
|
||||
} else if let Some(url) = args.value_of("url") {
|
||||
config.target_url = String::from(url);
|
||||
}
|
||||
|
||||
if let Some(arg) = args.values_of("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
|
||||
// url to be scanned. With the addition of regex support, I want to move parsing
|
||||
// out of should_deny_url and into here, so it's performed once instead of thousands
|
||||
// of times
|
||||
for denier in arg.into_iter() {
|
||||
// could be an absolute url or a regex, need to determine which and populate the
|
||||
// appropriate vector
|
||||
match Url::parse(denier.trim_end_matches('/')) {
|
||||
Ok(absolute) => {
|
||||
// denier is an absolute url and can be parsed as such
|
||||
config.url_denylist.push(absolute);
|
||||
}
|
||||
Err(err) => {
|
||||
// there are some expected errors that happen when we try to parse a url
|
||||
// ex: Url::parse("/login") -> Err("relative URL without a base")
|
||||
// ex: Url::parse("http:") -> Err("empty host")
|
||||
//
|
||||
// these are known errors and are used to determine a valid value to
|
||||
// --dont-scan, when it's not an absolute url
|
||||
//
|
||||
// when expected errors are encountered, we're going to assume
|
||||
// that the input is a regular expression to be parsed. The possibility
|
||||
// exists that the user rolled their face across the keyboard and we're
|
||||
// dealing with the results, in which case we'll report it as an error and
|
||||
// give up
|
||||
if err.to_string().contains("relative URL without a base")
|
||||
|| err.to_string().contains("empty host")
|
||||
{
|
||||
let regex = Regex::new(denier)
|
||||
.unwrap_or_else(|e| report_and_exit(&e.to_string()));
|
||||
|
||||
config.regex_denylist.push(regex);
|
||||
} else {
|
||||
// unexpected error has occurred; bail
|
||||
report_and_exit(&err.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(arg) = args.values_of("filter_regex") {
|
||||
config.filter_regex = arg.map(|val| val.to_string()).collect();
|
||||
}
|
||||
@@ -623,12 +691,6 @@ impl Configuration {
|
||||
config.json = true;
|
||||
}
|
||||
|
||||
if args.is_present("stdin") {
|
||||
config.stdin = true;
|
||||
} else if let Some(url) = args.value_of("url") {
|
||||
config.target_url = String::from(url);
|
||||
}
|
||||
|
||||
////
|
||||
// organizational breakpoint; all options below alter the Client configuration
|
||||
////
|
||||
@@ -637,6 +699,10 @@ impl Configuration {
|
||||
update_config_if_present!(&mut config.user_agent, args, "user_agent", String);
|
||||
update_config_if_present!(&mut config.timeout, args, "timeout", u64);
|
||||
|
||||
if args.is_present("random_agent") {
|
||||
config.random_agent = true;
|
||||
}
|
||||
|
||||
if args.is_present("redirects") {
|
||||
config.redirects = true;
|
||||
}
|
||||
@@ -767,6 +833,15 @@ impl Configuration {
|
||||
update_if_not_default!(&mut conf.insecure, new.insecure, false);
|
||||
update_if_not_default!(&mut conf.extract_links, new.extract_links, false);
|
||||
update_if_not_default!(&mut conf.extensions, new.extensions, Vec::<String>::new());
|
||||
update_if_not_default!(&mut conf.url_denylist, new.url_denylist, Vec::<Url>::new());
|
||||
if !new.regex_denylist.is_empty() {
|
||||
// cant use the update_if_not_default macro due to the following error
|
||||
//
|
||||
// binary operation `!=` cannot be applied to type `Vec<regex::Regex>`
|
||||
//
|
||||
// if we get a non-empty list of regex in the new config, override the old
|
||||
conf.regex_denylist = new.regex_denylist;
|
||||
}
|
||||
update_if_not_default!(&mut conf.headers, new.headers, HashMap::new());
|
||||
update_if_not_default!(&mut conf.queries, new.queries, Vec::new());
|
||||
update_if_not_default!(&mut conf.no_recursion, new.no_recursion, false);
|
||||
@@ -809,6 +884,7 @@ impl Configuration {
|
||||
|
||||
update_if_not_default!(&mut conf.timeout, new.timeout, timeout());
|
||||
update_if_not_default!(&mut conf.user_agent, new.user_agent, user_agent());
|
||||
update_if_not_default!(&mut conf.random_agent, new.random_agent, false);
|
||||
update_if_not_default!(&mut conf.threads, new.threads, threads());
|
||||
update_if_not_default!(&mut conf.depth, new.depth, depth());
|
||||
update_if_not_default!(&mut conf.wordlist, new.wordlist, wordlist());
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use super::utils::*;
|
||||
use super::*;
|
||||
use crate::{traits::FeroxSerialize, DEFAULT_CONFIG_NAME};
|
||||
use regex::Regex;
|
||||
use reqwest::Url;
|
||||
use std::{collections::HashMap, fs::write};
|
||||
use tempfile::TempDir;
|
||||
|
||||
@@ -29,6 +31,8 @@ fn setup_config_test() -> Configuration {
|
||||
redirects = true
|
||||
insecure = true
|
||||
extensions = ["html", "php", "js"]
|
||||
url_denylist = ["http://dont-scan.me", "https://also-not.me"]
|
||||
regex_denylist = ["/deny.*"]
|
||||
headers = {stuff = "things", mostuff = "mothings"}
|
||||
queries = [["name","value"], ["rick", "astley"]]
|
||||
no_recursion = true
|
||||
@@ -72,24 +76,27 @@ fn default_configuration() {
|
||||
assert_eq!(config.timeout, timeout());
|
||||
assert_eq!(config.verbosity, 0);
|
||||
assert_eq!(config.scan_limit, 0);
|
||||
assert_eq!(config.silent, false);
|
||||
assert_eq!(config.quiet, false);
|
||||
assert!(!config.silent);
|
||||
assert!(!config.quiet);
|
||||
assert_eq!(config.output_level, OutputLevel::Default);
|
||||
assert_eq!(config.dont_filter, false);
|
||||
assert_eq!(config.auto_tune, false);
|
||||
assert_eq!(config.auto_bail, false);
|
||||
assert!(!config.dont_filter);
|
||||
assert!(!config.auto_tune);
|
||||
assert!(!config.auto_bail);
|
||||
assert_eq!(config.requester_policy, RequesterPolicy::Default);
|
||||
assert_eq!(config.no_recursion, false);
|
||||
assert_eq!(config.json, false);
|
||||
assert_eq!(config.save_state, true);
|
||||
assert_eq!(config.stdin, false);
|
||||
assert_eq!(config.add_slash, false);
|
||||
assert_eq!(config.redirects, false);
|
||||
assert_eq!(config.extract_links, false);
|
||||
assert_eq!(config.insecure, false);
|
||||
assert!(!config.no_recursion);
|
||||
assert!(!config.random_agent);
|
||||
assert!(!config.json);
|
||||
assert!(config.save_state);
|
||||
assert!(!config.stdin);
|
||||
assert!(!config.add_slash);
|
||||
assert!(!config.redirects);
|
||||
assert!(!config.extract_links);
|
||||
assert!(!config.insecure);
|
||||
assert!(config.regex_denylist.is_empty());
|
||||
assert_eq!(config.queries, Vec::new());
|
||||
assert_eq!(config.extensions, Vec::<String>::new());
|
||||
assert_eq!(config.filter_size, Vec::<u64>::new());
|
||||
assert_eq!(config.extensions, Vec::<String>::new());
|
||||
assert_eq!(config.url_denylist, Vec::<Url>::new());
|
||||
assert_eq!(config.filter_regex, Vec::<String>::new());
|
||||
assert_eq!(config.filter_similar, Vec::<String>::new());
|
||||
assert_eq!(config.filter_word_count, Vec::<usize>::new());
|
||||
@@ -186,35 +193,35 @@ fn config_reads_replay_proxy() {
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_silent() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(config.silent, true);
|
||||
assert!(config.silent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_quiet() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(config.quiet, true);
|
||||
assert!(config.quiet);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_json() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(config.json, true);
|
||||
assert!(config.json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_auto_bail() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(config.auto_bail, true);
|
||||
assert!(config.auto_bail);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_auto_tune() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(config.auto_tune, true);
|
||||
assert!(config.auto_tune);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -235,49 +242,49 @@ fn config_reads_output() {
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_redirects() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(config.redirects, true);
|
||||
assert!(config.redirects);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_insecure() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(config.insecure, true);
|
||||
assert!(config.insecure);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_no_recursion() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(config.no_recursion, true);
|
||||
assert!(config.no_recursion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_stdin() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(config.stdin, true);
|
||||
assert!(config.stdin);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_dont_filter() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(config.dont_filter, true);
|
||||
assert!(config.dont_filter);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_add_slash() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(config.add_slash, true);
|
||||
assert!(config.add_slash);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_extract_links() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(config.extract_links, true);
|
||||
assert!(config.extract_links);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -287,6 +294,29 @@ fn config_reads_extensions() {
|
||||
assert_eq!(config.extensions, vec!["html", "php", "js"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_regex_denylist() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(
|
||||
config.regex_denylist[0].as_str(),
|
||||
Regex::new("/deny.*").unwrap().as_str()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_url_denylist() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(
|
||||
config.url_denylist,
|
||||
vec![
|
||||
Url::parse("http://dont-scan.me").unwrap(),
|
||||
Url::parse("https://also-not.me").unwrap(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_filter_regex() {
|
||||
@@ -333,7 +363,7 @@ fn config_reads_filter_status() {
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_save_state() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(config.save_state, false);
|
||||
assert!(!config.save_state);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -371,6 +401,12 @@ fn config_reads_queries() {
|
||||
assert_eq!(config.queries, queries);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_default_not_random_agent() {
|
||||
let config = setup_config_test();
|
||||
assert!(!config.random_agent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
/// test that an error message is printed and panic is called when report_and_exit is called
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::{
|
||||
scan_manager::{FeroxState, PAUSE_SCAN},
|
||||
scanner::RESPONSES,
|
||||
statistics::StatError,
|
||||
utils::slugify_filename,
|
||||
utils::{open_file, write_to},
|
||||
SLEEP_DURATION,
|
||||
};
|
||||
@@ -17,7 +18,6 @@ use std::{
|
||||
},
|
||||
thread::sleep,
|
||||
time::Duration,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
/// Atomic boolean flag, used to determine whether or not the terminal input handler should exit
|
||||
@@ -77,22 +77,14 @@ impl TermInputHandler {
|
||||
pub fn sigint_handler(handles: Arc<Handles>) -> Result<()> {
|
||||
log::trace!("enter: sigint_handler({:?})", handles);
|
||||
|
||||
let ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
|
||||
|
||||
let slug = if !handles.config.target_url.is_empty() {
|
||||
let filename = if !handles.config.target_url.is_empty() {
|
||||
// target url populated
|
||||
handles
|
||||
.config
|
||||
.target_url
|
||||
.replace("://", "_")
|
||||
.replace("/", "_")
|
||||
.replace(".", "_")
|
||||
slugify_filename(&handles.config.target_url, "ferox", "state")
|
||||
} else {
|
||||
// stdin used
|
||||
"stdin".to_string()
|
||||
slugify_filename("stdin", "ferox", "state")
|
||||
};
|
||||
|
||||
let filename = format!("ferox-{}-{}.state", slug, ts);
|
||||
let warning = format!(
|
||||
"🚨 Caught {} 🚨 saving scan state to {} ...",
|
||||
style("ctrl+c").yellow(),
|
||||
|
||||
@@ -213,8 +213,9 @@ impl TermOutHandler {
|
||||
// should be replayed; not using logged_request due to replay proxy client
|
||||
make_request(
|
||||
self.config.replay_client.as_ref().unwrap(),
|
||||
&resp.url(),
|
||||
resp.url(),
|
||||
self.config.output_level,
|
||||
&self.config,
|
||||
tx_stats.clone(),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -3,17 +3,19 @@ use std::sync::Arc;
|
||||
use anyhow::{bail, Result};
|
||||
use tokio::sync::{mpsc, Semaphore};
|
||||
|
||||
use crate::response::FeroxResponse;
|
||||
use crate::url::FeroxUrl;
|
||||
use crate::{
|
||||
response::FeroxResponse,
|
||||
scan_manager::{FeroxScan, FeroxScans, ScanOrder},
|
||||
scanner::FeroxScanner,
|
||||
statistics::StatField::TotalScans,
|
||||
url::FeroxUrl,
|
||||
utils::should_deny_url,
|
||||
CommandReceiver, CommandSender, FeroxChannel, Joiner, SLEEP_DURATION,
|
||||
};
|
||||
|
||||
use super::command::Command::AddToUsizeField;
|
||||
use super::*;
|
||||
use reqwest::Url;
|
||||
use tokio::time::Duration;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -187,6 +189,8 @@ impl ScanHandler {
|
||||
/// wrapper around scanning a url to stay DRY
|
||||
async fn ordered_scan_url(&mut self, targets: Vec<String>, order: ScanOrder) -> Result<()> {
|
||||
log::trace!("enter: ordered_scan_url({:?}, {:?})", targets, order);
|
||||
let should_test_deny = !self.handles.config.url_denylist.is_empty()
|
||||
|| !self.handles.config.regex_denylist.is_empty();
|
||||
|
||||
for target in targets {
|
||||
if self.data.contains(&target) && matches!(order, ScanOrder::Latest) {
|
||||
@@ -203,6 +207,13 @@ impl ScanHandler {
|
||||
self.data.add_directory_scan(&target, order).1 // add the new target; return FeroxScan
|
||||
};
|
||||
|
||||
if should_test_deny && should_deny_url(&Url::parse(&target)?, self.handles.clone())? {
|
||||
// response was caught by a user-provided deny list
|
||||
// checking this last, since it's most susceptible to longer runtimes due to what
|
||||
// input is received
|
||||
continue;
|
||||
}
|
||||
|
||||
let list = self.get_wordlist()?;
|
||||
|
||||
log::info!("scan handler received {} - beginning scan", target);
|
||||
@@ -243,6 +254,11 @@ 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
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut base_depth = 1_usize;
|
||||
|
||||
for (base_url, base_url_depth) in &self.depths {
|
||||
@@ -256,11 +272,6 @@ impl ScanHandler {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !response.is_directory() {
|
||||
// not a directory
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let targets = vec![response.url().to_string()];
|
||||
self.ordered_scan_url(targets, ScanOrder::Latest).await?;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::utils::should_deny_url;
|
||||
use crate::{
|
||||
client,
|
||||
event_handlers::{
|
||||
@@ -53,13 +54,19 @@ pub struct Extractor<'a> {
|
||||
|
||||
/// Extractor implementation
|
||||
impl<'a> Extractor<'a> {
|
||||
/// business logic that handles getting links from a normal http body response
|
||||
pub async fn extract(&self) -> Result<()> {
|
||||
let links = match self.target {
|
||||
ExtractionTarget::ResponseBody => self.extract_from_body().await?,
|
||||
ExtractionTarget::RobotsTxt => self.extract_from_robots().await?,
|
||||
};
|
||||
/// perform extraction from the given target and return any links found
|
||||
pub async fn extract(&self) -> Result<HashSet<String>> {
|
||||
log::trace!("enter: extract (this fn has associated trace exit msg)");
|
||||
match self.target {
|
||||
ExtractionTarget::ResponseBody => Ok(self.extract_from_body().await?),
|
||||
ExtractionTarget::RobotsTxt => Ok(self.extract_from_robots().await?),
|
||||
}
|
||||
}
|
||||
|
||||
/// given a set of links from a normal http body response, task the request handler to make
|
||||
/// the requests
|
||||
pub async fn request_links(&self, links: HashSet<String>) -> Result<()> {
|
||||
log::trace!("enter: request_links({:?})", links);
|
||||
let recursive = if self.handles.config.no_recursion {
|
||||
RecursionStatus::NotRecursive
|
||||
} else {
|
||||
@@ -121,6 +128,7 @@ impl<'a> Extractor<'a> {
|
||||
rx.await?;
|
||||
}
|
||||
}
|
||||
log::trace!("exit: request_links");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -141,7 +149,7 @@ impl<'a> Extractor<'a> {
|
||||
|
||||
let body = self.response.unwrap().text();
|
||||
|
||||
for capture in self.links_regex.captures_iter(&body) {
|
||||
for capture in self.links_regex.captures_iter(body) {
|
||||
// remove single & double quotes from both ends of the capture
|
||||
// capture[0] is the entire match, additional capture groups start at [1]
|
||||
let link = capture[0].trim_matches(|c| c == '\'' || c == '"');
|
||||
@@ -267,7 +275,7 @@ impl<'a> Extractor<'a> {
|
||||
};
|
||||
|
||||
let new_url = old_url
|
||||
.join(&link)
|
||||
.join(link)
|
||||
.with_context(|| format!("Could not join {} with {}", old_url, link))?;
|
||||
|
||||
links.insert(new_url.to_string());
|
||||
@@ -289,10 +297,10 @@ impl<'a> Extractor<'a> {
|
||||
pub(super) async fn request_link(&self, url: &str) -> Result<FeroxResponse> {
|
||||
log::trace!("enter: request_link({})", url);
|
||||
|
||||
let ferox_url = FeroxUrl::from_string(&url, self.handles.clone());
|
||||
let ferox_url = FeroxUrl::from_string(url, self.handles.clone());
|
||||
|
||||
// create a url based on the given command line options
|
||||
let new_url = ferox_url.format(&"", None)?;
|
||||
let new_url = ferox_url.format("", None)?;
|
||||
|
||||
let scanned_urls = self.handles.ferox_scans()?;
|
||||
|
||||
@@ -302,6 +310,19 @@ impl<'a> Extractor<'a> {
|
||||
bail!("previously seen url");
|
||||
}
|
||||
|
||||
if (!self.handles.config.url_denylist.is_empty()
|
||||
|| !self.handles.config.regex_denylist.is_empty())
|
||||
&& should_deny_url(&new_url, self.handles.clone())?
|
||||
{
|
||||
// can't allow a denied url to be requested
|
||||
bail!(
|
||||
"prevented request to {} due to {:?} || {:?}",
|
||||
url,
|
||||
self.handles.config.url_denylist,
|
||||
self.handles.config.regex_denylist,
|
||||
);
|
||||
}
|
||||
|
||||
// make the request and store the response
|
||||
let new_response = logged_request(&new_url, self.handles.clone()).await?;
|
||||
|
||||
@@ -332,7 +353,7 @@ impl<'a> Extractor<'a> {
|
||||
if let Some(new_path) = capture.name("url_path") {
|
||||
let mut new_url = Url::parse(&self.url)?;
|
||||
new_url.set_path(new_path.as_str());
|
||||
if self.add_all_sub_paths(&new_url.path(), &mut links).is_err() {
|
||||
if self.add_all_sub_paths(new_url.path(), &mut links).is_err() {
|
||||
log::warn!("could not add sub-paths from {} to {:?}", new_url, links);
|
||||
}
|
||||
}
|
||||
@@ -383,6 +404,7 @@ impl<'a> Extractor<'a> {
|
||||
&client,
|
||||
&url,
|
||||
self.handles.config.output_level,
|
||||
&self.handles.config,
|
||||
self.handles.stats.tx.clone(),
|
||||
)
|
||||
.await?;
|
||||
@@ -391,7 +413,7 @@ impl<'a> Extractor<'a> {
|
||||
FeroxResponse::from(response, true, self.handles.config.output_level).await;
|
||||
|
||||
log::trace!("exit: get_robots_file -> {}", ferox_response);
|
||||
return Ok(ferox_response);
|
||||
Ok(ferox_response)
|
||||
}
|
||||
|
||||
/// update total number of links extracted and expected responses
|
||||
|
||||
@@ -54,8 +54,8 @@ fn setup_extractor(target: ExtractionTarget, scanned_urls: Arc<FeroxScans>) -> E
|
||||
/// in the expected array
|
||||
fn extractor_get_sub_paths_from_path_with_multiple_paths() {
|
||||
let path = "homepage/assets/img/icons/handshake.svg";
|
||||
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(&path);
|
||||
let b_paths = BODY_EXT.get_sub_paths_from_path(&path);
|
||||
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(path);
|
||||
let b_paths = BODY_EXT.get_sub_paths_from_path(path);
|
||||
let expected = vec![
|
||||
"homepage/",
|
||||
"homepage/assets/",
|
||||
@@ -67,8 +67,8 @@ fn extractor_get_sub_paths_from_path_with_multiple_paths() {
|
||||
assert_eq!(r_paths.len(), expected.len());
|
||||
assert_eq!(b_paths.len(), expected.len());
|
||||
for expected_path in expected {
|
||||
assert_eq!(r_paths.contains(&expected_path.to_string()), true);
|
||||
assert_eq!(b_paths.contains(&expected_path.to_string()), true);
|
||||
assert!(r_paths.contains(&expected_path.to_string()));
|
||||
assert!(b_paths.contains(&expected_path.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,15 +78,15 @@ fn extractor_get_sub_paths_from_path_with_multiple_paths() {
|
||||
/// returned
|
||||
fn extractor_get_sub_paths_from_path_with_enclosing_slashes() {
|
||||
let path = "/homepage/assets/";
|
||||
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(&path);
|
||||
let b_paths = BODY_EXT.get_sub_paths_from_path(&path);
|
||||
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(path);
|
||||
let b_paths = BODY_EXT.get_sub_paths_from_path(path);
|
||||
let expected = vec!["homepage/", "homepage/assets"];
|
||||
|
||||
assert_eq!(r_paths.len(), expected.len());
|
||||
assert_eq!(b_paths.len(), expected.len());
|
||||
for expected_path in expected {
|
||||
assert_eq!(r_paths.contains(&expected_path.to_string()), true);
|
||||
assert_eq!(b_paths.contains(&expected_path.to_string()), true);
|
||||
assert!(r_paths.contains(&expected_path.to_string()));
|
||||
assert!(b_paths.contains(&expected_path.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,15 +95,15 @@ fn extractor_get_sub_paths_from_path_with_enclosing_slashes() {
|
||||
/// included
|
||||
fn extractor_get_sub_paths_from_path_with_only_a_word() {
|
||||
let path = "homepage";
|
||||
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(&path);
|
||||
let b_paths = BODY_EXT.get_sub_paths_from_path(&path);
|
||||
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(path);
|
||||
let b_paths = BODY_EXT.get_sub_paths_from_path(path);
|
||||
let expected = vec!["homepage"];
|
||||
|
||||
assert_eq!(r_paths.len(), expected.len());
|
||||
assert_eq!(b_paths.len(), expected.len());
|
||||
for expected_path in expected {
|
||||
assert_eq!(r_paths.contains(&expected_path.to_string()), true);
|
||||
assert_eq!(b_paths.contains(&expected_path.to_string()), true);
|
||||
assert!(r_paths.contains(&expected_path.to_string()));
|
||||
assert!(b_paths.contains(&expected_path.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,15 +111,15 @@ fn extractor_get_sub_paths_from_path_with_only_a_word() {
|
||||
/// extract sub paths from the given url fragment; expect 1 sub path, forward slash removed
|
||||
fn extractor_get_sub_paths_from_path_with_an_absolute_word() {
|
||||
let path = "/homepage";
|
||||
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(&path);
|
||||
let b_paths = BODY_EXT.get_sub_paths_from_path(&path);
|
||||
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(path);
|
||||
let b_paths = BODY_EXT.get_sub_paths_from_path(path);
|
||||
let expected = vec!["homepage"];
|
||||
|
||||
assert_eq!(r_paths.len(), expected.len());
|
||||
assert_eq!(b_paths.len(), expected.len());
|
||||
for expected_path in expected {
|
||||
assert_eq!(r_paths.contains(&expected_path.to_string()), true);
|
||||
assert_eq!(b_paths.contains(&expected_path.to_string()), true);
|
||||
assert!(r_paths.contains(&expected_path.to_string()));
|
||||
assert!(b_paths.contains(&expected_path.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,16 +211,23 @@ async fn extractor_get_links_with_absolute_url_that_differs_from_target_domain()
|
||||
let mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/some-path");
|
||||
then.status(200).body(
|
||||
"\"http://defintely.not.a.thing.probably.com/homepage/assets/img/icons/handshake.svg\"",
|
||||
"\"http://definitely.not.a.thing.probably.com/homepage/assets/img/icons/handshake.svg\"",
|
||||
);
|
||||
});
|
||||
|
||||
let client = Client::new();
|
||||
let url = Url::parse(&srv.url("/some-path")).unwrap();
|
||||
let config = Configuration::new().unwrap();
|
||||
|
||||
let response = make_request(&client, &url, OutputLevel::Default, tx_stats.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
let response = make_request(
|
||||
&client,
|
||||
&url,
|
||||
OutputLevel::Default,
|
||||
&config,
|
||||
tx_stats.clone(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let (handles, _rx) = Handles::for_testing(None, None);
|
||||
|
||||
let handles = Arc::new(handles);
|
||||
|
||||
@@ -41,7 +41,7 @@ impl FeroxFilters {
|
||||
if let Ok(filters) = self.filters.lock() {
|
||||
for filter in filters.iter() {
|
||||
// wildcard.should_filter goes here
|
||||
if filter.should_filter_response(&response) {
|
||||
if filter.should_filter_response(response) {
|
||||
if filter.as_any().downcast_ref::<WildcardFilter>().is_some() {
|
||||
tx_stats
|
||||
.send(AddToUsizeField(WildcardsFiltered, 1))
|
||||
|
||||
@@ -56,7 +56,7 @@ pub async fn initialize(handles: Arc<Handles>) -> Result<()> {
|
||||
// add any regex filters to filters handler's FeroxFilters (-X|--filter-regex)
|
||||
for regex_filter in &handles.config.filter_regex {
|
||||
let raw = regex_filter;
|
||||
let compiled = skip_fail!(Regex::new(&raw));
|
||||
let compiled = skip_fail!(Regex::new(raw));
|
||||
|
||||
let filter = RegexFilter {
|
||||
raw_string: raw.to_owned(),
|
||||
@@ -69,7 +69,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));
|
||||
let url = skip_fail!(Url::parse(similarity_filter));
|
||||
|
||||
// attempt to request the given url
|
||||
let resp = skip_fail!(logged_request(&url, handles.clone()).await);
|
||||
|
||||
@@ -127,6 +127,19 @@ fn wildcard_should_filter_when_static_wildcard_found() {
|
||||
assert!(filter.should_filter_response(&resp));
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test should_filter on WilcardFilter where static logic matches but response length is 0
|
||||
fn wildcard_should_filter_when_static_wildcard_len_is_zero() {
|
||||
let mut resp = FeroxResponse::default();
|
||||
resp.set_wildcard(true);
|
||||
resp.set_url("http://localhost");
|
||||
|
||||
// default WildcardFilter is used in the code that executes when response.content_length() == 0
|
||||
let filter = WildcardFilter::new(false);
|
||||
|
||||
assert!(filter.should_filter_response(&resp));
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test should_filter on WilcardFilter where dynamic logic matches
|
||||
fn wildcard_should_filter_when_dynamic_wildcard_found() {
|
||||
|
||||
@@ -68,6 +68,14 @@ impl FeroxFilter for WildcardFilter {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.size == u64::MAX && response.content_length() == 0 {
|
||||
// static wildcard size found during testing
|
||||
// but response length was zero; pointed out by @Tib3rius
|
||||
log::debug!("static wildcard: filtered out {}", response.url());
|
||||
log::trace!("exit: should_filter_response -> true");
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.dynamic != u64::MAX {
|
||||
// dynamic wildcard offset found during testing
|
||||
|
||||
@@ -76,7 +84,7 @@ impl FeroxFilter for WildcardFilter {
|
||||
// except that I don't want an empty string taking up the last index in the
|
||||
// event that the url ends with a forward slash. It's ugly enough to be split
|
||||
// into its own function for readability.
|
||||
let url_len = FeroxUrl::path_length_of_url(&response.url());
|
||||
let url_len = FeroxUrl::path_length_of_url(response.url());
|
||||
|
||||
if url_len + self.dynamic == response.content_length() {
|
||||
log::debug!("dynamic wildcard: filtered out {}", response.url());
|
||||
|
||||
@@ -156,7 +156,15 @@ impl HeuristicTests {
|
||||
log::trace!("enter: make_wildcard_request({}, {})", target, length);
|
||||
|
||||
let unique_str = self.unique_string(length);
|
||||
let nonexistent_url = target.format(&unique_str, None)?;
|
||||
|
||||
// To take care of slash when needed
|
||||
let slash = if self.handles.config.add_slash {
|
||||
Some("/")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let nonexistent_url = target.format(&unique_str, slash)?;
|
||||
|
||||
let response = logged_request(&nonexistent_url.to_owned(), self.handles.clone()).await?;
|
||||
|
||||
@@ -207,7 +215,7 @@ impl HeuristicTests {
|
||||
let mut good_urls = vec![];
|
||||
|
||||
for target_url in target_urls {
|
||||
let url = FeroxUrl::from_string(&target_url, self.handles.clone());
|
||||
let url = FeroxUrl::from_string(target_url, self.handles.clone());
|
||||
let request = skip_fail!(url.format("", None));
|
||||
|
||||
let result = logged_request(&request, self.handles.clone()).await;
|
||||
|
||||
21
src/lib.rs
21
src/lib.rs
@@ -43,7 +43,7 @@ pub(crate) type FeroxChannel<T> = (UnboundedSender<T>, UnboundedReceiver<T>);
|
||||
pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// Maximum number of file descriptors that can be opened during a scan
|
||||
pub const DEFAULT_OPEN_FILE_LIMIT: usize = 8192;
|
||||
pub const DEFAULT_OPEN_FILE_LIMIT: u64 = 8192;
|
||||
|
||||
/// Default value used to determine near-duplicate web pages (equivalent to 95%)
|
||||
pub const SIMILARITY_THRESHOLD: u32 = 95;
|
||||
@@ -73,7 +73,8 @@ pub const HIGH_ERROR_RATIO: f64 = 0.90;
|
||||
/// * 401 Unauthorized
|
||||
/// * 403 Forbidden
|
||||
/// * 405 Method Not Allowed
|
||||
pub const DEFAULT_STATUS_CODES: [StatusCode; 9] = [
|
||||
/// * 500 Internal Server Error
|
||||
pub const DEFAULT_STATUS_CODES: [StatusCode; 10] = [
|
||||
StatusCode::OK,
|
||||
StatusCode::NO_CONTENT,
|
||||
StatusCode::MOVED_PERMANENTLY,
|
||||
@@ -83,12 +84,28 @@ pub const DEFAULT_STATUS_CODES: [StatusCode; 9] = [
|
||||
StatusCode::UNAUTHORIZED,
|
||||
StatusCode::FORBIDDEN,
|
||||
StatusCode::METHOD_NOT_ALLOWED,
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
];
|
||||
|
||||
/// Default filename for config file settings
|
||||
///
|
||||
/// Expected location is in the same directory as the feroxbuster binary.
|
||||
pub const DEFAULT_CONFIG_NAME: &str = "ferox-config.toml";
|
||||
/// User agents to select from when random agent is being used
|
||||
pub const USER_AGENTS: [&str; 12] = [
|
||||
"Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (Windows Phone 10.0; Android 6.0.1; Microsoft; RM-1152) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Mobile Safari/537.36 Edge/15.15254",
|
||||
"Mozilla/5.0 (Linux; Android 7.0; Pixel C Build/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/52.0.2743.98 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246",
|
||||
"Mozilla/5.0 (X11; CrOS x86_64 8172.45.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.64 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9",
|
||||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36",
|
||||
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1",
|
||||
"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
|
||||
"Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)",
|
||||
"Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)",
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
89
src/main.rs
89
src/main.rs
@@ -1,8 +1,9 @@
|
||||
use std::{
|
||||
env::args,
|
||||
fs::File,
|
||||
fs::{create_dir, remove_file, File},
|
||||
io::{stderr, BufRead, BufReader},
|
||||
ops::Index,
|
||||
path::Path,
|
||||
process::Command,
|
||||
sync::{atomic::Ordering, Arc},
|
||||
};
|
||||
@@ -27,7 +28,7 @@ use feroxbuster::{
|
||||
progress::{PROGRESS_BAR, PROGRESS_PRINTER},
|
||||
scan_manager::{self},
|
||||
scanner,
|
||||
utils::fmt_err,
|
||||
utils::{fmt_err, slugify_filename},
|
||||
};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use feroxbuster::{utils::set_open_file_limit, DEFAULT_OPEN_FILE_LIMIT};
|
||||
@@ -153,6 +154,28 @@ async fn get_targets(handles: Arc<Handles>) -> Result<Vec<String>> {
|
||||
targets.push(handles.config.target_url.clone());
|
||||
}
|
||||
|
||||
// remove footgun that arises if a --dont-scan value matches on a base url
|
||||
for target in &targets {
|
||||
for denier in &handles.config.regex_denylist {
|
||||
if denier.is_match(target) {
|
||||
bail!(
|
||||
"The regex '{}' matches {}; the scan will never start",
|
||||
denier,
|
||||
target
|
||||
);
|
||||
}
|
||||
}
|
||||
for denier in &handles.config.url_denylist {
|
||||
if denier.as_str().trim_end_matches('/') == target.trim_end_matches('/') {
|
||||
bail!(
|
||||
"The url '{}' matches {}; the scan will never start",
|
||||
denier,
|
||||
target
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::trace!("exit: get_targets -> {:?}", targets);
|
||||
|
||||
Ok(targets)
|
||||
@@ -230,7 +253,7 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
|
||||
Err(e) => {
|
||||
// should only happen in the event that there was an error reading from stdin
|
||||
clean_up(handles, tasks).await?;
|
||||
bail!("Could not get determine initial targets: {}", e);
|
||||
bail!("Could not determine initial targets: {}", e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -267,10 +290,56 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
|
||||
// from removing --parallel)
|
||||
original.remove(parallel_index);
|
||||
|
||||
// to log unique files to a shared folder, we need to first check for the presence
|
||||
// of -o|--output.
|
||||
let out_dir = if !config.output.is_empty() {
|
||||
// -o|--output was used, so we'll attempt to create a directory to store the files
|
||||
let output_path = Path::new(&handles.config.output);
|
||||
|
||||
// this only returns None if the path terminates in `..`. Since I don't want to
|
||||
// hand-hold to that degree, we'll unwrap and fail if the output path ends in `..`
|
||||
let base_name = output_path.file_name().unwrap();
|
||||
|
||||
let new_folder = slugify_filename(&base_name.to_string_lossy(), "", "logs");
|
||||
|
||||
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
|
||||
create_dir(&final_path).unwrap_or(());
|
||||
|
||||
final_path.to_string_lossy().to_string()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// unvalidated targets fresh from stdin, just spawn children and let them do all checks
|
||||
for target in targets {
|
||||
// add the current target to the provided command
|
||||
let mut cloned = original.clone();
|
||||
|
||||
if !out_dir.is_empty() {
|
||||
// output directory value is not empty, need to join output directory with
|
||||
// unique scan filename
|
||||
|
||||
// unwrap is ok, we already know -o was used
|
||||
let out_idx = original
|
||||
.iter()
|
||||
.position(|s| *s == "--output" || *s == "-o")
|
||||
.unwrap();
|
||||
|
||||
let filename = slugify_filename(&target, "ferox", "log");
|
||||
|
||||
let full_path = Path::new(&out_dir)
|
||||
.join(filename)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
// a +1 to the index is fine here, as clap has already validated that
|
||||
// -o|--output has a value associated with it
|
||||
cloned[out_idx + 1] = full_path;
|
||||
}
|
||||
|
||||
cloned.push("-u".to_string());
|
||||
cloned.push(target);
|
||||
|
||||
@@ -294,8 +363,22 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
|
||||
});
|
||||
}
|
||||
|
||||
// the output handler creates an empty file to which it will try to write, because
|
||||
// this happens before we enter the --parallel branch, we need to remove that file
|
||||
// if it's empty
|
||||
let output = handles.config.output.to_owned();
|
||||
|
||||
clean_up(handles, tasks).await?;
|
||||
|
||||
let file = Path::new(&output);
|
||||
if file.exists() {
|
||||
// expectation is that this is always true for the first ferox process
|
||||
if file.metadata()?.len() == 0 {
|
||||
// empty file, attempt to remove it
|
||||
remove_file(file)?;
|
||||
}
|
||||
}
|
||||
|
||||
log::trace!("exit: parallel branch && wrapped main");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use clap::{App, Arg, ArgGroup};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use std::env;
|
||||
use std::process;
|
||||
|
||||
lazy_static! {
|
||||
/// Regex used to validate values passed to --time-limit
|
||||
@@ -16,7 +18,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() -> App<'static, 'static> {
|
||||
App::new("feroxbuster")
|
||||
let mut app = App::new("feroxbuster")
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.author("Ben 'epi' Risher (@epi052)")
|
||||
.about("A fast, simple, recursive content discovery tool written in Rust")
|
||||
@@ -190,6 +192,15 @@ pub fn initialize() -> App<'static, 'static> {
|
||||
"Sets the User-Agent (default: feroxbuster/VERSION)"
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("random_agent")
|
||||
.short("A")
|
||||
.long("random-agent")
|
||||
.takes_value(false)
|
||||
.help(
|
||||
"Use a random User-Agent"
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("redirects")
|
||||
.short("r")
|
||||
@@ -216,6 +227,17 @@ pub fn initialize() -> App<'static, 'static> {
|
||||
"File extension(s) to search for (ex: -x php -x pdf js)",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("url_denylist")
|
||||
.long("dont-scan")
|
||||
.value_name("URL")
|
||||
.takes_value(true)
|
||||
.multiple(true)
|
||||
.use_delimiter(true)
|
||||
.help(
|
||||
"URL(s) or Regex Pattern(s) to exclude from recursion/scans",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("headers")
|
||||
.short("H")
|
||||
@@ -252,7 +274,6 @@ pub fn initialize() -> App<'static, 'static> {
|
||||
.short("f")
|
||||
.long("add-slash")
|
||||
.takes_value(false)
|
||||
.conflicts_with("extensions")
|
||||
.help("Append / to each request")
|
||||
)
|
||||
.arg(
|
||||
@@ -410,7 +431,20 @@ EXAMPLES:
|
||||
|
||||
Ludicrous speed... go!
|
||||
./feroxbuster -u http://127.1 -t 200
|
||||
"#)
|
||||
"#);
|
||||
|
||||
for arg in env::args() {
|
||||
// secure-77 noticed that when an incorrect flag/option is used, the short help message is printed
|
||||
// which is fine, but if you add -h|--help, it still errors out on the bad flag/option,
|
||||
// never showing the full help message. This code addresses that behavior
|
||||
if arg == "--help" || arg == "-h" {
|
||||
app.print_long_help().unwrap();
|
||||
println!(); // just a newline to mirror original --help output
|
||||
process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
/// Validate that a string is formatted as a number followed by s, m, h, or d (10d, 30s, etc...)
|
||||
@@ -432,7 +466,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
/// initalize parser, expect a clap::App returned
|
||||
/// initialize parser, expect a clap::App returned
|
||||
fn parser_initialize_gives_defaults() {
|
||||
let app = initialize();
|
||||
assert_eq!(app.get_name(), "feroxbuster");
|
||||
|
||||
@@ -51,7 +51,7 @@ pub fn add_bar(prefix: &str, length: u64, bar_type: BarType) -> ProgressBar {
|
||||
|
||||
progress_bar.set_style(style);
|
||||
|
||||
progress_bar.set_prefix(&prefix);
|
||||
progress_bar.set_prefix(prefix);
|
||||
|
||||
progress_bar
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ impl FeroxResponse {
|
||||
|
||||
/// Set `FeroxResponse`'s `url` attribute, has no affect if an error occurs
|
||||
pub fn set_url(&mut self, url: &str) {
|
||||
match Url::parse(&url) {
|
||||
match Url::parse(url) {
|
||||
Ok(url) => {
|
||||
self.url = url;
|
||||
}
|
||||
@@ -339,7 +339,7 @@ impl FeroxSerialize for FeroxResponse {
|
||||
lines,
|
||||
words,
|
||||
chars,
|
||||
status_colorizer(&status),
|
||||
status_colorizer(status),
|
||||
self.url(),
|
||||
FeroxUrl::path_length_of_url(&self.url)
|
||||
);
|
||||
|
||||
@@ -36,7 +36,7 @@ pub struct FeroxScan {
|
||||
pub(super) scan_type: ScanType,
|
||||
|
||||
/// The order in which the scan was received
|
||||
pub(super) scan_order: ScanOrder,
|
||||
pub(crate) scan_order: ScanOrder,
|
||||
|
||||
/// Number of requests to populate the progress bar with
|
||||
pub(super) num_requests: u64,
|
||||
|
||||
@@ -431,7 +431,7 @@ impl FeroxScans {
|
||||
OutputLevel::Silent => BarType::Hidden,
|
||||
};
|
||||
|
||||
let progress_bar = add_bar(&url, bar_length, bar_type);
|
||||
let progress_bar = add_bar(url, bar_length, bar_type);
|
||||
|
||||
progress_bar.reset_elapsed();
|
||||
|
||||
@@ -441,7 +441,7 @@ impl FeroxScans {
|
||||
};
|
||||
|
||||
let ferox_scan = FeroxScan::new(
|
||||
&url,
|
||||
url,
|
||||
scan_type,
|
||||
scan_order,
|
||||
bar_length,
|
||||
@@ -462,7 +462,7 @@ 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)
|
||||
self.add_scan(url, ScanType::Directory, scan_order)
|
||||
}
|
||||
|
||||
/// Given a url, create a new `FeroxScan` and add it to `FeroxScans` as a File Scan
|
||||
@@ -471,7 +471,7 @@ impl FeroxScans {
|
||||
///
|
||||
/// Also return a reference to the new `FeroxScan`
|
||||
pub fn add_file_scan(&self, url: &str, scan_order: ScanOrder) -> (bool, Arc<FeroxScan>) {
|
||||
self.add_scan(&url, ScanType::File, scan_order)
|
||||
self.add_scan(url, ScanType::File, scan_order)
|
||||
}
|
||||
|
||||
/// small helper to determine whether any scans are active or not
|
||||
|
||||
@@ -52,7 +52,7 @@ fn add_url_to_list_of_scanned_urls_with_unknown_url() {
|
||||
let urls = FeroxScans::default();
|
||||
let url = "http://unknown_url";
|
||||
let (result, _scan) = urls.add_scan(url, ScanType::Directory, ScanOrder::Latest);
|
||||
assert_eq!(result, true);
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -71,11 +71,11 @@ fn add_url_to_list_of_scanned_urls_with_known_url() {
|
||||
Some(pb),
|
||||
);
|
||||
|
||||
assert_eq!(urls.insert(scan), true);
|
||||
assert!(urls.insert(scan));
|
||||
|
||||
let (result, _scan) = urls.add_scan(url, ScanType::Directory, ScanOrder::Latest);
|
||||
|
||||
assert_eq!(result, false);
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -93,27 +93,23 @@ fn stop_progress_bar_stops_bar() {
|
||||
Some(pb),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
scan.progress_bar
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.is_finished(),
|
||||
false
|
||||
);
|
||||
assert!(!scan
|
||||
.progress_bar
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.is_finished());
|
||||
|
||||
scan.stop_progress_bar();
|
||||
|
||||
assert_eq!(
|
||||
scan.progress_bar
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.is_finished(),
|
||||
true
|
||||
);
|
||||
assert!(scan
|
||||
.progress_bar
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.is_finished());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -131,11 +127,11 @@ fn add_url_to_list_of_scanned_urls_with_known_url_without_slash() {
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(urls.insert(scan), true);
|
||||
assert!(urls.insert(scan));
|
||||
|
||||
let (result, _scan) = urls.add_scan(url, ScanType::File, ScanOrder::Latest);
|
||||
|
||||
assert_eq!(result, false);
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
@@ -171,8 +167,8 @@ async fn call_display_scans() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(urls.insert(scan), true);
|
||||
assert_eq!(urls.insert(scan_two), true);
|
||||
assert!(urls.insert(scan));
|
||||
assert!(urls.insert(scan_two));
|
||||
|
||||
urls.display_scans().await;
|
||||
}
|
||||
@@ -330,7 +326,7 @@ fn ferox_response_serialize_and_deserialize() {
|
||||
|
||||
assert_eq!(response.url().as_str(), "https://nerdcore.com/css");
|
||||
assert_eq!(response.url().path(), "/css");
|
||||
assert_eq!(response.wildcard(), true);
|
||||
assert!(response.wildcard());
|
||||
assert_eq!(response.status().as_u16(), 301);
|
||||
assert_eq!(response.content_length(), 173);
|
||||
assert_eq!(response.line_count(), 10);
|
||||
@@ -382,12 +378,79 @@ fn feroxstates_feroxserialize_implementation() {
|
||||
assert!(expected_strs.eval(&ferox_state.as_str()));
|
||||
|
||||
let json_state = ferox_state.as_json().unwrap();
|
||||
let expected = format!(
|
||||
r#"{{"scans":[{{"id":"{}","url":"https://spiritanimal.com","scan_type":"Directory","status":"NotStarted","num_requests":0}}],"config":{{"type":"configuration","wordlist":"/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt","config":"","proxy":"","replay_proxy":"","target_url":"","status_codes":[200,204,301,302,307,308,401,403,405],"replay_codes":[200,204,301,302,307,308,401,403,405],"filter_status":[],"threads":50,"timeout":7,"verbosity":0,"silent":false,"quiet":false,"auto_bail":false,"auto_tune":false,"json":false,"output":"","debug_log":"","user_agent":"feroxbuster/{}","redirects":false,"insecure":false,"extensions":[],"headers":{{}},"queries":[],"no_recursion":false,"extract_links":false,"add_slash":false,"stdin":false,"depth":4,"scan_limit":0,"parallel":0,"rate_limit":0,"filter_size":[],"filter_line_count":[],"filter_word_count":[],"filter_regex":[],"dont_filter":false,"resumed":false,"resume_from":"","save_state":false,"time_limit":"","filter_similar":[]}},"responses":[{{"type":"response","url":"https://nerdcore.com/css","path":"/css","wildcard":true,"status":301,"content_length":173,"line_count":10,"word_count":16,"headers":{{"server":"nginx/1.16.1"}}}}]"#,
|
||||
saved_id, VERSION
|
||||
);
|
||||
println!("{}\n{}", expected, json_state);
|
||||
assert!(predicates::str::contains(expected).eval(&json_state));
|
||||
for expected in [
|
||||
r#""scans""#,
|
||||
&format!(r#""id":"{}""#, saved_id),
|
||||
r#""url":"https://spiritanimal.com""#,
|
||||
r#""scan_type":"Directory""#,
|
||||
r#""status":"NotStarted""#,
|
||||
r#""num_requests":0"#,
|
||||
r#""config""#,
|
||||
r#""type":"configuration""#,
|
||||
r#""wordlist":"/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt""#,
|
||||
r#""config""#,
|
||||
r#""proxy":"""#,
|
||||
r#""replay_proxy":"""#,
|
||||
r#""target_url":"""#,
|
||||
r#""status_codes":[200,204,301,302,307,308,401,403,405,500]"#,
|
||||
r#""replay_codes":[200,204,301,302,307,308,401,403,405,500]"#,
|
||||
r#""filter_status":[]"#,
|
||||
r#""threads":50"#,
|
||||
r#""timeout":7"#,
|
||||
r#""verbosity":0"#,
|
||||
r#""silent":false"#,
|
||||
r#""quiet":false"#,
|
||||
r#""auto_bail":false"#,
|
||||
r#""auto_tune":false"#,
|
||||
r#""json":false"#,
|
||||
r#""output":"""#,
|
||||
r#""debug_log":"""#,
|
||||
&format!(r#""user_agent":"feroxbuster/{}""#, VERSION),
|
||||
r#""random_agent":false"#,
|
||||
r#""redirects":false"#,
|
||||
r#""insecure":false"#,
|
||||
r#""extensions":[]"#,
|
||||
r#""headers""#,
|
||||
r#""queries":[]"#,
|
||||
r#""no_recursion":false"#,
|
||||
r#""extract_links":false"#,
|
||||
r#""add_slash":false"#,
|
||||
r#""stdin":false"#,
|
||||
r#""depth":4"#,
|
||||
r#""scan_limit":0"#,
|
||||
r#""parallel":0"#,
|
||||
r#""rate_limit":0"#,
|
||||
r#""filter_size":[]"#,
|
||||
r#""filter_line_count":[]"#,
|
||||
r#""filter_word_count":[]"#,
|
||||
r#""filter_regex":[]"#,
|
||||
r#""dont_filter":false"#,
|
||||
r#""resumed":false"#,
|
||||
r#""resume_from":"""#,
|
||||
r#""save_state":false"#,
|
||||
r#""time_limit":"""#,
|
||||
r#""filter_similar":[]"#,
|
||||
r#""url_denylist":[]"#,
|
||||
r#""responses""#,
|
||||
r#""type":"response""#,
|
||||
r#""url":"https://nerdcore.com/css""#,
|
||||
r#""path":"/css""#,
|
||||
r#""wildcard":true"#,
|
||||
r#""status":301"#,
|
||||
r#""content_length":173"#,
|
||||
r#""line_count":10"#,
|
||||
r#""word_count":16"#,
|
||||
r#""headers""#,
|
||||
r#""server":"nginx/1.16.1"#,
|
||||
]
|
||||
.iter()
|
||||
{
|
||||
assert!(
|
||||
predicates::str::contains(*expected).eval(&json_state),
|
||||
"{}",
|
||||
expected
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[should_panic]
|
||||
|
||||
@@ -83,7 +83,8 @@ impl FeroxScanner {
|
||||
.target(RobotsTxt)
|
||||
.build()?;
|
||||
|
||||
let _ = extractor.extract().await;
|
||||
let links = extractor.extract().await?;
|
||||
extractor.request_links(links).await?;
|
||||
}
|
||||
|
||||
let scanned_urls = self.handles.ferox_scans()?;
|
||||
|
||||
@@ -90,11 +90,9 @@ impl PolicyData {
|
||||
atomic_store!(self.remove_limit, true);
|
||||
}
|
||||
}
|
||||
self.set_limit(heap.value() as usize);
|
||||
} else if heap.has_children() {
|
||||
// streak not at 3, just check that we can move down, and do so
|
||||
heap.move_left();
|
||||
self.set_limit(heap.value() as usize);
|
||||
} else {
|
||||
// tree bottomed out, need to move back up the tree a bit
|
||||
let current = heap.value();
|
||||
@@ -104,9 +102,8 @@ impl PolicyData {
|
||||
if current > heap.value() {
|
||||
heap.move_up();
|
||||
}
|
||||
|
||||
self.set_limit(heap.value() as usize);
|
||||
}
|
||||
self.set_limit(heap.value() as usize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +197,7 @@ mod tests {
|
||||
pd.adjust_up(&3);
|
||||
assert_eq!(pd.heap.read().unwrap().value(), 300);
|
||||
assert_eq!(pd.limit.load(Ordering::Relaxed), 300);
|
||||
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), false);
|
||||
assert!(!pd.remove_limit.load(Ordering::Relaxed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -217,7 +214,7 @@ mod tests {
|
||||
pd.adjust_up(&3);
|
||||
assert_eq!(pd.heap.read().unwrap().value(), 200);
|
||||
assert_eq!(pd.limit.load(Ordering::Relaxed), 200);
|
||||
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), true);
|
||||
assert!(pd.remove_limit.load(Ordering::Relaxed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -234,7 +231,7 @@ mod tests {
|
||||
pd.adjust_up(&3);
|
||||
assert_eq!(pd.heap.read().unwrap().value(), 350);
|
||||
assert_eq!(pd.limit.load(Ordering::Relaxed), 350);
|
||||
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), false);
|
||||
assert!(!pd.remove_limit.load(Ordering::Relaxed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -251,7 +248,7 @@ mod tests {
|
||||
pd.adjust_up(&3);
|
||||
assert_eq!(pd.heap.read().unwrap().value(), 300);
|
||||
assert_eq!(pd.limit.load(Ordering::Relaxed), 300);
|
||||
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), false);
|
||||
assert!(!pd.remove_limit.load(Ordering::Relaxed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -269,7 +266,7 @@ mod tests {
|
||||
pd.adjust_up(&0);
|
||||
assert_eq!(pd.heap.read().unwrap().value(), 43);
|
||||
assert_eq!(pd.limit.load(Ordering::Relaxed), 43);
|
||||
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), false);
|
||||
assert!(!pd.remove_limit.load(Ordering::Relaxed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -287,7 +284,7 @@ mod tests {
|
||||
pd.adjust_up(&0);
|
||||
assert_eq!(pd.heap.read().unwrap().value(), 37);
|
||||
assert_eq!(pd.limit.load(Ordering::Relaxed), 37);
|
||||
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), false);
|
||||
assert!(!pd.remove_limit.load(Ordering::Relaxed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -27,6 +27,8 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{policy_data::PolicyData, FeroxScanner, PolicyTrigger};
|
||||
use crate::utils::should_deny_url;
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Makes multiple requests based on the presence of extensions
|
||||
pub(super) struct Requester {
|
||||
@@ -45,11 +47,16 @@ pub(super) struct Requester {
|
||||
/// FeroxScan associated with the creation of this Requester
|
||||
ferox_scan: Arc<FeroxScan>,
|
||||
|
||||
/// cache of previously seen links gotten via link extraction. since the requester is passed
|
||||
/// around as an arc, and seen_links needs to be mutable, putting it behind a lock for
|
||||
/// interior mutability, similar to the tuning_lock below
|
||||
seen_links: RwLock<HashSet<String>>,
|
||||
|
||||
/// simple lock to control access to tuning to a single thread (per-scan)
|
||||
///
|
||||
/// need a usize to determine the number of consecutive non-error calls that a requester has
|
||||
/// seen; this will satisfy the non-mut self constraint (due to us being behind an Arc, and
|
||||
/// the need for a counter
|
||||
/// the need for a counter)
|
||||
tuning_lock: Mutex<usize>,
|
||||
}
|
||||
|
||||
@@ -73,6 +80,7 @@ impl Requester {
|
||||
Ok(Self {
|
||||
ferox_scan,
|
||||
policy_data,
|
||||
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||
rate_limiter: RwLock::new(rate_limiter),
|
||||
handles: scanner.handles.clone(),
|
||||
target_url: scanner.target_url.to_owned(),
|
||||
@@ -298,6 +306,9 @@ impl Requester {
|
||||
let urls =
|
||||
FeroxUrl::from_string(&self.target_url, self.handles.clone()).formatted_urls(word)?;
|
||||
|
||||
let should_test_deny = !self.handles.config.url_denylist.is_empty()
|
||||
|| !self.handles.config.regex_denylist.is_empty();
|
||||
|
||||
for url in urls {
|
||||
// auto_tune is true, or rate_limit was set (mutually exclusive to user)
|
||||
// and a rate_limiter has been created
|
||||
@@ -313,6 +324,11 @@ impl Requester {
|
||||
}
|
||||
}
|
||||
|
||||
if should_test_deny && should_deny_url(&url, self.handles.clone())? {
|
||||
// can't allow a denied url to be requested
|
||||
continue;
|
||||
}
|
||||
|
||||
let response = logged_request(&url, self.handles.clone()).await?;
|
||||
|
||||
if (should_tune || self.handles.config.auto_bail)
|
||||
@@ -367,7 +383,26 @@ impl Requester {
|
||||
.handles(self.handles.clone())
|
||||
.build()?;
|
||||
|
||||
extractor.extract().await?;
|
||||
let new_links: HashSet<_>;
|
||||
let extracted = extractor.extract().await?;
|
||||
|
||||
{
|
||||
// gain and quickly drop the read lock on seen_links, using it while unlocked
|
||||
// to determine if there are any new links to process
|
||||
let read_links = self.seen_links.read().await;
|
||||
new_links = extracted.difference(&read_links).cloned().collect();
|
||||
}
|
||||
|
||||
if !new_links.is_empty() {
|
||||
// using is_empty instead of direct iteration to acquire the write lock behind
|
||||
// some kind of less expensive gate (and not in a loop, obv)
|
||||
let mut write_links = self.seen_links.write().await;
|
||||
for new_link in &new_links {
|
||||
write_links.insert(new_link.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
extractor.request_links(new_links).await?;
|
||||
}
|
||||
|
||||
// everything else should be reported
|
||||
@@ -536,6 +571,7 @@ mod tests {
|
||||
|
||||
let requester = Requester {
|
||||
handles,
|
||||
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||
tuning_lock: Mutex::new(0),
|
||||
ferox_scan: Arc::new(FeroxScan::default()),
|
||||
target_url: "http://localhost".to_string(),
|
||||
@@ -563,6 +599,7 @@ mod tests {
|
||||
|
||||
let requester = Requester {
|
||||
handles,
|
||||
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||
tuning_lock: Mutex::new(0),
|
||||
ferox_scan: ferox_scan.clone(),
|
||||
target_url: "http://localhost".to_string(),
|
||||
@@ -587,6 +624,7 @@ mod tests {
|
||||
|
||||
let requester = Requester {
|
||||
handles,
|
||||
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||
tuning_lock: Mutex::new(0),
|
||||
ferox_scan: ferox_scan.clone(),
|
||||
target_url: "http://localhost".to_string(),
|
||||
@@ -626,6 +664,7 @@ mod tests {
|
||||
|
||||
let requester = Requester {
|
||||
handles,
|
||||
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||
tuning_lock: Mutex::new(0),
|
||||
ferox_scan: ferox_scan.clone(),
|
||||
target_url: "http://localhost".to_string(),
|
||||
@@ -680,6 +719,7 @@ mod tests {
|
||||
let req_clone = scan_two.clone();
|
||||
let requester = Requester {
|
||||
handles,
|
||||
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||
tuning_lock: Mutex::new(0),
|
||||
ferox_scan: req_clone,
|
||||
target_url: "http://one/one/stuff.php".to_string(),
|
||||
@@ -713,6 +753,7 @@ mod tests {
|
||||
|
||||
let requester = Requester {
|
||||
handles,
|
||||
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||
tuning_lock: Mutex::new(0),
|
||||
ferox_scan: Arc::new(FeroxScan::default()),
|
||||
target_url: "http://one/one/stuff.php".to_string(),
|
||||
@@ -734,6 +775,7 @@ mod tests {
|
||||
|
||||
let requester = Requester {
|
||||
handles,
|
||||
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||
tuning_lock: Mutex::new(0),
|
||||
ferox_scan: Arc::new(FeroxScan::default()),
|
||||
target_url: "http://localhost".to_string(),
|
||||
@@ -756,6 +798,7 @@ mod tests {
|
||||
|
||||
let requester = Arc::new(Requester {
|
||||
handles,
|
||||
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||
tuning_lock: Mutex::new(0),
|
||||
ferox_scan: Arc::new(FeroxScan::default()),
|
||||
target_url: "http://localhost".to_string(),
|
||||
@@ -772,7 +815,7 @@ mod tests {
|
||||
|
||||
requester.cool_down().await;
|
||||
|
||||
assert_eq!(resp.await.unwrap(), true);
|
||||
assert!(resp.await.unwrap());
|
||||
println!("{}", start.elapsed().as_millis());
|
||||
assert!(start.elapsed().as_millis() >= 3500);
|
||||
}
|
||||
@@ -785,6 +828,7 @@ mod tests {
|
||||
|
||||
let requester = Requester {
|
||||
handles,
|
||||
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||
tuning_lock: Mutex::new(0),
|
||||
ferox_scan: Arc::new(FeroxScan::default()),
|
||||
target_url: "http://localhost".to_string(),
|
||||
@@ -822,6 +866,7 @@ mod tests {
|
||||
|
||||
let requester = Requester {
|
||||
handles,
|
||||
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||
tuning_lock: Mutex::new(0),
|
||||
ferox_scan: Arc::new(scan),
|
||||
target_url: "http://localhost".to_string(),
|
||||
@@ -857,6 +902,7 @@ mod tests {
|
||||
|
||||
let requester = Requester {
|
||||
handles,
|
||||
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||
tuning_lock: Mutex::new(0),
|
||||
ferox_scan: Arc::new(scan),
|
||||
target_url: "http://localhost".to_string(),
|
||||
@@ -884,6 +930,7 @@ mod tests {
|
||||
|
||||
let mut requester = Requester {
|
||||
handles,
|
||||
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||
tuning_lock: Mutex::new(0),
|
||||
ferox_scan: Arc::new(FeroxScan::default()),
|
||||
target_url: "http://localhost".to_string(),
|
||||
@@ -891,28 +938,16 @@ mod tests {
|
||||
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
requester.too_many_status_errors(PolicyTrigger::Errors),
|
||||
false
|
||||
);
|
||||
assert!(!requester.too_many_status_errors(PolicyTrigger::Errors));
|
||||
|
||||
assert_eq!(
|
||||
requester.too_many_status_errors(PolicyTrigger::Status429),
|
||||
false
|
||||
);
|
||||
assert!(!requester.too_many_status_errors(PolicyTrigger::Status429));
|
||||
requester.ferox_scan.progress_bar().set_position(10);
|
||||
requester.ferox_scan.add_429();
|
||||
requester.ferox_scan.add_429();
|
||||
requester.ferox_scan.add_429();
|
||||
assert_eq!(
|
||||
requester.too_many_status_errors(PolicyTrigger::Status429),
|
||||
true
|
||||
);
|
||||
assert!(requester.too_many_status_errors(PolicyTrigger::Status429));
|
||||
|
||||
assert_eq!(
|
||||
requester.too_many_status_errors(PolicyTrigger::Status403),
|
||||
false
|
||||
);
|
||||
assert!(!requester.too_many_status_errors(PolicyTrigger::Status403));
|
||||
requester.ferox_scan = Arc::new(FeroxScan::default());
|
||||
requester.ferox_scan.progress_bar().set_position(10);
|
||||
requester.ferox_scan.add_403();
|
||||
@@ -924,10 +959,7 @@ mod tests {
|
||||
requester.ferox_scan.add_403();
|
||||
requester.ferox_scan.add_403();
|
||||
requester.ferox_scan.add_403();
|
||||
assert_eq!(
|
||||
requester.too_many_status_errors(PolicyTrigger::Status403),
|
||||
true
|
||||
);
|
||||
assert!(requester.too_many_status_errors(PolicyTrigger::Status403));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
@@ -941,6 +973,7 @@ mod tests {
|
||||
|
||||
let requester = Requester {
|
||||
handles,
|
||||
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||
tuning_lock: Mutex::new(0),
|
||||
ferox_scan: Arc::new(FeroxScan::default()),
|
||||
target_url: "http://localhost".to_string(),
|
||||
@@ -983,6 +1016,7 @@ mod tests {
|
||||
|
||||
let requester = Requester {
|
||||
handles,
|
||||
seen_links: RwLock::new(HashSet::<String>::new()),
|
||||
tuning_lock: Mutex::new(0),
|
||||
ferox_scan: scan.clone(),
|
||||
target_url: "http://localhost".to_string(),
|
||||
|
||||
@@ -620,12 +620,10 @@ impl Stats {
|
||||
atomic_increment!(self.expected_per_scan, value);
|
||||
}
|
||||
StatField::TotalScans => {
|
||||
let multiplier = self.num_extensions.max(1);
|
||||
|
||||
atomic_increment!(self.total_scans, value);
|
||||
atomic_increment!(
|
||||
self.total_expected,
|
||||
value * self.expected_per_scan.load(Ordering::Relaxed) * multiplier
|
||||
value * self.expected_per_scan.load(Ordering::Relaxed)
|
||||
);
|
||||
}
|
||||
StatField::TotalExpected => {
|
||||
|
||||
@@ -55,10 +55,7 @@ fn save_writes_stats_object_to_disk() {
|
||||
stats.add_status_code(StatusCode::OK);
|
||||
stats.add_status_code(StatusCode::OK);
|
||||
let outfile = NamedTempFile::new().unwrap();
|
||||
if stats
|
||||
.save(174.33, &outfile.path().to_str().unwrap())
|
||||
.is_ok()
|
||||
{}
|
||||
if stats.save(174.33, outfile.path().to_str().unwrap()).is_ok() {}
|
||||
|
||||
assert!(stats.as_json().unwrap().contains("statistics"));
|
||||
assert!(stats.as_json().unwrap().contains("11")); // requests made
|
||||
|
||||
84
src/url.rs
84
src/url.rs
@@ -42,7 +42,13 @@ impl FeroxUrl {
|
||||
|
||||
let mut urls = vec![];
|
||||
|
||||
match self.format(word, None) {
|
||||
let slash = if self.handles.config.add_slash {
|
||||
Some("/")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
match self.format(word, slash) {
|
||||
// default request, i.e. no extension
|
||||
Ok(url) => urls.push(url),
|
||||
Err(_) => self.handles.stats.send(AddError(UrlFormat))?,
|
||||
@@ -55,7 +61,6 @@ impl FeroxUrl {
|
||||
Err(_) => self.handles.stats.send(AddError(UrlFormat))?,
|
||||
}
|
||||
}
|
||||
|
||||
log::trace!("exit: formatted_urls -> {:?}", urls);
|
||||
Ok(urls)
|
||||
}
|
||||
@@ -66,7 +71,7 @@ impl FeroxUrl {
|
||||
pub fn format(&self, word: &str, extension: Option<&str>) -> Result<Url> {
|
||||
log::trace!("enter: format({}, {:?})", word, extension);
|
||||
|
||||
if Url::parse(&word).is_ok() {
|
||||
if Url::parse(word).is_ok() {
|
||||
// when a full url is passed in as a word to be joined to a base url using
|
||||
// reqwest::Url::join, the result is that the word (url) completely overwrites the base
|
||||
// url, potentially resulting in requests to places that aren't actually the target
|
||||
@@ -97,13 +102,25 @@ impl FeroxUrl {
|
||||
self.target.to_string()
|
||||
};
|
||||
|
||||
// extensions and slashes are mutually exclusive cases
|
||||
let word = if extension.is_some() {
|
||||
format!("{}.{}", word, extension.unwrap())
|
||||
} else if self.handles.config.add_slash && !word.ends_with('/') {
|
||||
// -f used, and word doesn't already end with a /
|
||||
format!("{}/", word)
|
||||
} else if word.starts_with("//") {
|
||||
// As of version 2.3.4, extensions and trailing slashes are no longer mutually exclusive.
|
||||
// Trailing slashes are now treated as just another extension, which is pretty clever.
|
||||
//
|
||||
// In addition to the change above, @cortantief ID'd a bug here that incorrectly handled
|
||||
// 2 leading forward slashes when extensions were used. This block addresses the bugfix.
|
||||
let mut word = if let Some(ext) = extension {
|
||||
// We handle the special case of forward slash
|
||||
// That allow us to treat it as an extension with a particular format
|
||||
if ext == "/" {
|
||||
format!("{}/", word)
|
||||
} else {
|
||||
format!("{}.{}", word, ext)
|
||||
}
|
||||
} else {
|
||||
String::from(word)
|
||||
};
|
||||
|
||||
// We check separately if the current word begins with 2 forward slashes
|
||||
if word.starts_with("//") {
|
||||
// bug ID'd by @Sicks3c, when a wordlist contains words that begin with 2 forward slashes
|
||||
// i.e. //1_40_0/static/js, it gets joined onto the base url in a surprising way
|
||||
// ex: https://localhost/ + //1_40_0/static/js -> https://1_40_0/static/js
|
||||
@@ -111,9 +128,7 @@ impl FeroxUrl {
|
||||
// and simply removes prefixed forward slashes if there are two of them. Additionally,
|
||||
// trim_start_matches will trim the pattern until it's gone, so even if there are more than
|
||||
// 2 /'s, they'll still be trimmed
|
||||
word.trim_start_matches('/').to_string()
|
||||
} else {
|
||||
String::from(word)
|
||||
word = word.trim_start_matches('/').to_string();
|
||||
};
|
||||
|
||||
let base_url = Url::parse(&url)?;
|
||||
@@ -451,6 +466,20 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// word with two prepended slashes and extensions doesn't discard the entire domain
|
||||
fn format_url_word_with_two_prepended_slashes_and_extensions() {
|
||||
let handles = Arc::new(Handles::for_testing(None, None).0);
|
||||
let url = FeroxUrl::from_string("http://localhost", handles);
|
||||
for ext in ["rocks", "fun"] {
|
||||
let to_check = format!("http://localhost/upload/ferox.{}", ext);
|
||||
assert_eq!(
|
||||
url.format("//upload/ferox", Some(ext)).unwrap(),
|
||||
reqwest::Url::parse(&to_check[..]).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// word that is a fully formed url, should return an error
|
||||
fn format_url_word_that_is_a_url() {
|
||||
@@ -460,4 +489,33 @@ mod tests {
|
||||
|
||||
assert!(formatted.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// sending url + word with both an extension and add-slash should get back
|
||||
/// two urls, one with '/' appended to the word, and the other with the extension
|
||||
/// appended
|
||||
fn formatted_urls_with_postslash_and_extensions() {
|
||||
let config = Configuration {
|
||||
add_slash: true,
|
||||
extensions: vec!["rocks".to_string(), "fun".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
let handles = Arc::new(Handles::for_testing(None, Some(Arc::new(config))).0);
|
||||
let url = FeroxUrl::from_string("http://localhost", handles);
|
||||
match url.formatted_urls("ferox") {
|
||||
Ok(urls) => {
|
||||
// 3 = One for the main word + slash and for the two extensions
|
||||
assert_eq!(urls.len(), 3);
|
||||
assert_eq!(
|
||||
urls,
|
||||
[
|
||||
Url::parse("http://localhost/ferox/").unwrap(),
|
||||
Url::parse("http://localhost/ferox.rocks").unwrap(),
|
||||
Url::parse("http://localhost/ferox.fun").unwrap(),
|
||||
]
|
||||
)
|
||||
}
|
||||
Err(err) => panic!("{}", err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
429
src/utils.rs
429
src/utils.rs
@@ -1,16 +1,20 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use console::{strip_ansi_codes, style, user_attended};
|
||||
use indicatif::ProgressBar;
|
||||
use regex::Regex;
|
||||
use reqwest::{Client, Response, StatusCode, Url};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use rlimit::{getrlimit, setrlimit, Resource, Rlim};
|
||||
use rlimit::{getrlimit, setrlimit, Resource};
|
||||
use std::{
|
||||
fs,
|
||||
io::{self, BufWriter, Write},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use crate::config::Configuration;
|
||||
use crate::{
|
||||
config::OutputLevel,
|
||||
event_handlers::{
|
||||
@@ -21,8 +25,12 @@ use crate::{
|
||||
send_command,
|
||||
statistics::StatError::{Connection, Other, Redirection, Request, Timeout},
|
||||
traits::FeroxSerialize,
|
||||
USER_AGENTS,
|
||||
};
|
||||
|
||||
/// simple counter for grabbing 'random' user agents
|
||||
static mut USER_AGENT_CTR: usize = 0;
|
||||
|
||||
/// Given the path to a file, open the file in append mode (create it if it doesn't exist) and
|
||||
/// return a reference to the buffered file
|
||||
pub fn open_file(filename: &str) -> Result<BufWriter<fs::File>> {
|
||||
@@ -92,7 +100,7 @@ pub async fn logged_request(url: &Url, handles: Arc<Handles>) -> Result<Response
|
||||
let level = handles.config.output_level;
|
||||
let tx_stats = handles.stats.tx.clone();
|
||||
|
||||
let response = make_request(client, url, level, tx_stats).await;
|
||||
let response = make_request(client, url, level, &handles.config, tx_stats).await;
|
||||
|
||||
let scans = handles.ferox_scans()?;
|
||||
|
||||
@@ -119,6 +127,7 @@ pub async fn make_request(
|
||||
client: &Client,
|
||||
url: &Url,
|
||||
output_level: OutputLevel,
|
||||
config: &Configuration,
|
||||
tx_stats: UnboundedSender<Command>,
|
||||
) -> Result<Response> {
|
||||
log::trace!(
|
||||
@@ -128,7 +137,20 @@ pub async fn make_request(
|
||||
tx_stats
|
||||
);
|
||||
|
||||
match client.get(url.to_owned()).send().await {
|
||||
let mut request = client.get(url.to_owned());
|
||||
|
||||
if config.random_agent {
|
||||
let index = unsafe {
|
||||
USER_AGENT_CTR += 1;
|
||||
USER_AGENT_CTR % USER_AGENTS.len()
|
||||
};
|
||||
|
||||
let user_agent = USER_AGENTS[index];
|
||||
|
||||
request = request.header("User-Agent", user_agent);
|
||||
}
|
||||
|
||||
match request.send().await {
|
||||
Err(e) => {
|
||||
log::trace!("exit: make_request -> {}", e);
|
||||
|
||||
@@ -217,16 +239,15 @@ pub fn create_report_string(
|
||||
/// as the adjustment made here is only valid for the scan itself (and any child processes, of which
|
||||
/// there are none).
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn set_open_file_limit(limit: usize) -> bool {
|
||||
pub fn set_open_file_limit(limit: u64) -> bool {
|
||||
log::trace!("enter: set_open_file_limit");
|
||||
|
||||
if let Ok((soft, hard)) = getrlimit(Resource::NOFILE) {
|
||||
if hard.as_usize() > limit {
|
||||
if hard > limit {
|
||||
// our default open file limit is less than the current hard limit, this means we can
|
||||
// set the soft limit to our default
|
||||
let new_soft_limit = Rlim::from_usize(limit);
|
||||
|
||||
if setrlimit(Resource::NOFILE, new_soft_limit, hard).is_ok() {
|
||||
if setrlimit(Resource::NOFILE, limit, hard).is_ok() {
|
||||
log::debug!("set open file descriptor limit to {}", limit);
|
||||
|
||||
log::trace!("exit: set_open_file_limit -> {}", true);
|
||||
@@ -287,15 +308,183 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// determine if a url should be denied based on the given absolute url
|
||||
fn should_deny_absolute(url_to_test: &Url, denier: &Url, handles: Arc<Handles>) -> Result<bool> {
|
||||
log::trace!(
|
||||
"enter: should_deny_absolute({}, {:?})",
|
||||
url_to_test.as_str(),
|
||||
denier.as_str(),
|
||||
);
|
||||
|
||||
// simplest case is an exact match, check for it first
|
||||
if url_to_test == denier {
|
||||
log::trace!("exit: should_deny_absolute -> true");
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
match (url_to_test.host(), denier.host()) {
|
||||
// .host() will return an enum with ipv4|6 or domain and is comparable
|
||||
// whereas .domain() returns None for ip addresses
|
||||
(Some(normed_host), Some(denier_host)) => {
|
||||
if normed_host != denier_host {
|
||||
// domains don't even match
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// one or the other couldn't determine the host value, which probably means
|
||||
// it's not suitable for further comparison
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
let tested_host = url_to_test.host().unwrap(); // match above will catch errors
|
||||
|
||||
// at this point, we have a matching set of ips or domain names. now we can process the
|
||||
// url path. The goal is to determine whether the given url's path is a subpath of any
|
||||
// url in the deny list, for example
|
||||
// GIVEN URL URL DENY LIST USER-SPECIFIED URLS TO SCAN
|
||||
// http://some.domain/stuff/things, [http://some.domain/stuff], [http://some.domain] => true
|
||||
// http://some.domain/stuff/things, [http://some.domain/stuff/things], [http://some.domain] => true
|
||||
// http://some.domain/stuff/things, [http://some.domain/api], [http://some.domain] => false
|
||||
// the examples above are all pretty obvious, the kicker comes when the blocking url's
|
||||
// path is a parent to a scanned url
|
||||
// http://some.domain/stuff/things, [http://some.domain/], [http://some.domain/stuff] => false
|
||||
// http://some.domain/api, [http://some.domain/], [http://some.domain/stuff] => true
|
||||
// we want to deny all children of the parent, unless that child is a child of a scan
|
||||
// we specified through -u(s) or --stdin
|
||||
|
||||
let deny_path = denier.path();
|
||||
let tested_path = url_to_test.path();
|
||||
|
||||
if tested_path.starts_with(deny_path) {
|
||||
// at this point, we know that the given normalized path is a sub-path of the
|
||||
// current deny-url, now we just need to check to see if this deny-url is a parent
|
||||
// to a scanned url that is also a parent of the given url
|
||||
for ferox_scan in handles.ferox_scans()?.get_active_scans() {
|
||||
let scanner = Url::parse(ferox_scan.url().trim_end_matches('/'))
|
||||
.with_context(|| format!("Could not parse {} as a url", ferox_scan))?;
|
||||
|
||||
if let Some(scan_host) = scanner.host() {
|
||||
// same domain/ip check we perform on the denier above
|
||||
if tested_host != scan_host {
|
||||
// domains don't even match, keep on keepin' on...
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// couldn't process .host from scanner
|
||||
continue;
|
||||
};
|
||||
|
||||
let scan_path = scanner.path();
|
||||
|
||||
if scan_path.starts_with(deny_path) && tested_path.starts_with(scan_path) {
|
||||
// user-specified scan url is a sub-path of the deny-urls's path AND the
|
||||
// url to check is a sub-path of the user-specified scan url
|
||||
//
|
||||
// the assumption is the user knew what they wanted and we're going to give
|
||||
// the scanned url precedence, even though it's a sub-path
|
||||
log::trace!("exit: should_deny_absolute -> false");
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
log::trace!("exit: should_deny_absolute -> true");
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
log::trace!("exit: should_deny_absolute -> false");
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// determine if a url should be denied based on the given regular expression
|
||||
///
|
||||
/// the regex ONLY matches against the PATH of the url (not the scheme, host, port, etc)
|
||||
fn should_deny_regex(url_to_test: &Url, denier: &Regex) -> bool {
|
||||
log::trace!(
|
||||
"enter: should_deny_regex({}, {})",
|
||||
url_to_test.as_str(),
|
||||
denier,
|
||||
);
|
||||
|
||||
let result = denier.is_match(url_to_test.as_str());
|
||||
|
||||
log::trace!("exit: should_deny_regex -> {}", result);
|
||||
result
|
||||
}
|
||||
|
||||
/// determines whether or not a given url should be denied based on the user-supplied --dont-scan
|
||||
/// flag
|
||||
pub fn should_deny_url(url: &Url, handles: Arc<Handles>) -> Result<bool> {
|
||||
log::trace!(
|
||||
"enter: should_deny_url({}, {:?}, {:?})",
|
||||
url.as_str(),
|
||||
handles.config.url_denylist,
|
||||
handles.ferox_scans()?
|
||||
);
|
||||
|
||||
// normalization for comparison is to remove the trailing / if one exists, this is done for
|
||||
// the given url and any url to which it's compared
|
||||
let normed_url = Url::parse(url.to_string().trim_end_matches('/'))?;
|
||||
|
||||
for denier in &handles.config.url_denylist {
|
||||
// note to self: it may seem as though we can use regex only for --dont-scan, however, in
|
||||
// doing so, we lose the ability to block a parent directory while scanning a child
|
||||
if let Ok(should_deny) = should_deny_absolute(&normed_url, denier, handles.clone()) {
|
||||
if should_deny {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for denier in &handles.config.regex_denylist {
|
||||
if should_deny_regex(&normed_url, denier) {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
// made it to the end of the deny lists unscathed, return false, indicating we should not deny
|
||||
// this particular url
|
||||
log::trace!("exit: should_deny_url -> false");
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// given a url and filename-suffix, return a unique filename comprised of the slugified url,
|
||||
/// current unix timestamp and suffix
|
||||
///
|
||||
/// ex: ferox-http_telsa_com-1606947491.state
|
||||
pub fn slugify_filename(url: &str, prefix: &str, suffix: &str) -> String {
|
||||
log::trace!("enter: slugify({:?}, {:?}, {:?})", url, prefix, suffix);
|
||||
|
||||
let ts = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_else(|_| Duration::from_secs(0))
|
||||
.as_secs();
|
||||
|
||||
let altered_prefix = if !prefix.is_empty() {
|
||||
format!("{}-", prefix)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let slug = url.replace("://", "_").replace("/", "_").replace(".", "_");
|
||||
|
||||
let filename = format!("{}{}-{}.{}", altered_prefix, slug, ts, suffix);
|
||||
|
||||
log::trace!("exit: slugify -> {}", filename);
|
||||
filename
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::Configuration;
|
||||
use crate::scan_manager::{FeroxScans, ScanOrder};
|
||||
|
||||
#[test]
|
||||
/// set_open_file_limit with a low requested limit succeeds
|
||||
fn utils_set_open_file_limit_with_low_requested_limit() {
|
||||
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
|
||||
let lower_limit = hard.as_usize() - 1;
|
||||
let lower_limit = hard - 1;
|
||||
assert!(set_open_file_limit(lower_limit));
|
||||
}
|
||||
|
||||
@@ -303,9 +492,9 @@ mod tests {
|
||||
/// set_open_file_limit with a high requested limit succeeds
|
||||
fn utils_set_open_file_limit_with_high_requested_limit() {
|
||||
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
|
||||
let higher_limit = hard.as_usize() + 1;
|
||||
let higher_limit = hard + 1;
|
||||
// calculate a new soft to ensure soft != hard and hit that logic branch
|
||||
let new_soft = Rlim::from_usize(hard.as_usize() - 1);
|
||||
let new_soft = hard - 1;
|
||||
setrlimit(Resource::NOFILE, new_soft, hard).unwrap();
|
||||
assert!(set_open_file_limit(higher_limit));
|
||||
}
|
||||
@@ -316,7 +505,7 @@ mod tests {
|
||||
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
|
||||
// calculate a new soft to ensure soft == hard and hit the failure logic branch
|
||||
setrlimit(Resource::NOFILE, hard, hard).unwrap();
|
||||
assert!(!set_open_file_limit(hard.as_usize())); // returns false
|
||||
assert!(!set_open_file_limit(hard)); // returns false
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -366,4 +555,222 @@ mod tests {
|
||||
fn status_colorizer_returns_as_is() {
|
||||
assert_eq!(status_colorizer("farfignewton"), "farfignewton".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// provide a url that should be blocked where the denier is an exact match for the tested url
|
||||
/// expect true
|
||||
fn should_deny_url_blocks_when_denier_is_exact_match() {
|
||||
let scan_url = "https://testdomain.com/";
|
||||
let deny_url = "https://testdomain.com/denied";
|
||||
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
|
||||
|
||||
let scans = Arc::new(FeroxScans::default());
|
||||
scans.add_directory_scan(scan_url, ScanOrder::Initial);
|
||||
|
||||
let mut config = Configuration::new().unwrap();
|
||||
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
|
||||
let config = Arc::new(config);
|
||||
|
||||
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
|
||||
|
||||
assert!(should_deny_url(&tested_url, handles).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// provide a url that has a different host than the denier but the same path, expect false
|
||||
fn should_deny_url_doesnt_compare_mismatched_domains() {
|
||||
let scan_url = "https://testdomain.com/";
|
||||
let deny_url = "https://dev.testdomain.com/denied";
|
||||
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
|
||||
|
||||
let scans = Arc::new(FeroxScans::default());
|
||||
scans.add_directory_scan(scan_url, ScanOrder::Initial);
|
||||
|
||||
let mut config = Configuration::new().unwrap();
|
||||
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
|
||||
let config = Arc::new(config);
|
||||
|
||||
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
|
||||
|
||||
assert!(!should_deny_url(&tested_url, handles).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// provide a denier from which we can't check a host, which results in no comparison, expect false
|
||||
fn should_deny_url_doesnt_compare_non_domains() {
|
||||
let scan_url = "https://testdomain.com/";
|
||||
let deny_url = "unix:/run/foo.socket";
|
||||
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
|
||||
|
||||
let scans = Arc::new(FeroxScans::default());
|
||||
scans.add_directory_scan(scan_url, ScanOrder::Initial);
|
||||
|
||||
let mut config = Configuration::new().unwrap();
|
||||
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
|
||||
let config = Arc::new(config);
|
||||
|
||||
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
|
||||
|
||||
assert!(!should_deny_url(&tested_url, handles).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// provide a url that has a different host than the denier but the same path, expect false
|
||||
/// because the denier is a parent to the tested, even tho the scanned doesn't compare, it
|
||||
/// still returns true
|
||||
fn should_deny_url_doesnt_compare_mismatched_domains_in_scanned() {
|
||||
let deny_url = "https://testdomain.com/";
|
||||
let scan_url = "https://dev.testdomain.com/denied";
|
||||
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
|
||||
|
||||
let scans = Arc::new(FeroxScans::default());
|
||||
scans.add_directory_scan(scan_url, ScanOrder::Initial);
|
||||
|
||||
let mut config = Configuration::new().unwrap();
|
||||
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
|
||||
let config = Arc::new(config);
|
||||
|
||||
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
|
||||
|
||||
assert!(should_deny_url(&tested_url, handles).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// provide a denier from which we can't check a host, which results in no comparison, expect false
|
||||
/// because the denier is a parent to the tested, even tho the scanned doesn't compare, it
|
||||
/// still returns true
|
||||
fn should_deny_url_doesnt_compare_non_domains_in_scanned() {
|
||||
let deny_url = "https://testdomain.com/";
|
||||
let scan_url = "unix:/run/foo.socket";
|
||||
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
|
||||
|
||||
let scans = Arc::new(FeroxScans::default());
|
||||
scans.add_directory_scan(scan_url, ScanOrder::Initial);
|
||||
|
||||
let mut config = Configuration::new().unwrap();
|
||||
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
|
||||
let config = Arc::new(config);
|
||||
|
||||
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
|
||||
|
||||
assert!(should_deny_url(&tested_url, handles).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// provide a denier where the tested url is a sub-path and the scanned url is not, expect true
|
||||
fn should_deny_url_blocks_child() {
|
||||
let scan_url = "https://testdomain.com/";
|
||||
let deny_url = "https://testdomain.com/api";
|
||||
let tested_url = Url::parse("https://testdomain.com/api/denied/").unwrap();
|
||||
|
||||
let scans = Arc::new(FeroxScans::default());
|
||||
scans.add_directory_scan(scan_url, ScanOrder::Initial);
|
||||
|
||||
let mut config = Configuration::new().unwrap();
|
||||
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
|
||||
let config = Arc::new(config);
|
||||
|
||||
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
|
||||
|
||||
assert!(should_deny_url(&tested_url, handles).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// provide a denier where the tested url is not a sub-path and the scanned url is not, expect false
|
||||
fn should_deny_url_doesnt_block_non_child() {
|
||||
let scan_url = "https://testdomain.com/";
|
||||
let deny_url = "https://testdomain.com/api";
|
||||
let tested_url = Url::parse("https://testdomain.com/not-denied/").unwrap();
|
||||
|
||||
let scans = Arc::new(FeroxScans::default());
|
||||
scans.add_directory_scan(scan_url, ScanOrder::Initial);
|
||||
|
||||
let mut config = Configuration::new().unwrap();
|
||||
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
|
||||
let config = Arc::new(config);
|
||||
|
||||
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
|
||||
|
||||
assert!(!should_deny_url(&tested_url, handles).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// provide a denier where the tested url is a sub-path and the scanned url is not, expect true
|
||||
fn should_deny_url_blocks_child_when_scan_url_isnt_parent() {
|
||||
let scan_url = "https://testdomain.com/api";
|
||||
let deny_url = "https://testdomain.com/";
|
||||
let tested_url = Url::parse("https://testdomain.com/stuff/").unwrap();
|
||||
|
||||
let scans = Arc::new(FeroxScans::default());
|
||||
scans.add_directory_scan(scan_url, ScanOrder::Initial);
|
||||
|
||||
let mut config = Configuration::new().unwrap();
|
||||
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
|
||||
let config = Arc::new(config);
|
||||
|
||||
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
|
||||
|
||||
assert!(should_deny_url(&tested_url, handles).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// provide a denier where the tested url is not a sub-path and the scanned url is not, expect false
|
||||
fn should_deny_url_doesnt_block_child_when_scan_url_is_parent() {
|
||||
let scan_url = "https://testdomain.com/api";
|
||||
let deny_url = "https://testdomain.com/";
|
||||
let tested_url = Url::parse("https://testdomain.com/api/not-denied/").unwrap();
|
||||
|
||||
let scans = Arc::new(FeroxScans::default());
|
||||
scans.add_directory_scan(scan_url, ScanOrder::Initial);
|
||||
|
||||
let mut config = Configuration::new().unwrap();
|
||||
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
|
||||
let config = Arc::new(config);
|
||||
|
||||
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
|
||||
|
||||
assert!(!should_deny_url(&tested_url, handles).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// provide a denier where the tested url is matched against a regular expression in the path
|
||||
/// of the url
|
||||
fn should_deny_url_blocks_urls_based_on_regex_in_path() {
|
||||
let scan_url = "https://testdomain.com/";
|
||||
let deny_pattern = "/deni.*";
|
||||
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
|
||||
|
||||
let scans = Arc::new(FeroxScans::default());
|
||||
scans.add_directory_scan(scan_url, ScanOrder::Initial);
|
||||
|
||||
let mut config = Configuration::new().unwrap();
|
||||
config.regex_denylist = vec![Regex::new(deny_pattern).unwrap()];
|
||||
let config = Arc::new(config);
|
||||
|
||||
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
|
||||
|
||||
assert!(should_deny_url(&tested_url, handles).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// provide a denier where the tested url is matched against a regular expression in the scheme
|
||||
/// of the url
|
||||
fn should_deny_url_blocks_urls_based_on_regex_in_scheme() {
|
||||
let scan_url = "https://testdomain.com/";
|
||||
let deny_pattern = "http:";
|
||||
let tested_http_url = Url::parse("http://testdomain.com/denied/").unwrap();
|
||||
let tested_https_url = Url::parse("https://testdomain.com/denied/").unwrap();
|
||||
|
||||
let scans = Arc::new(FeroxScans::default());
|
||||
scans.add_directory_scan(scan_url, ScanOrder::Initial);
|
||||
|
||||
let mut config = Configuration::new().unwrap();
|
||||
config.regex_denylist = vec![Regex::new(deny_pattern).unwrap()];
|
||||
let config = Arc::new(config);
|
||||
|
||||
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
|
||||
|
||||
assert!(!should_deny_url(&tested_https_url, handles.clone()).unwrap());
|
||||
assert!(should_deny_url(&tested_http_url, handles).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,65 @@ fn banner_prints_headers() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
||||
/// expect to see all mandatory prints + multiple dont scan url & regex entries
|
||||
fn banner_prints_denied_urls() {
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg("http://localhost")
|
||||
.arg("--dont-scan")
|
||||
.arg("http://dont-scan.me")
|
||||
.arg("https://also-not.me")
|
||||
.arg("https:")
|
||||
.arg("/deny.*")
|
||||
.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("Don't Scan Url"))
|
||||
.and(predicate::str::contains("Don't Scan Regex"))
|
||||
.and(predicate::str::contains("http://dont-scan.me"))
|
||||
.and(predicate::str::contains("https://also-not.me"))
|
||||
.and(predicate::str::contains("https:"))
|
||||
.and(predicate::str::contains("/deny.*"))
|
||||
.and(predicate::str::contains("─┴─")),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
||||
/// expect to see all mandatory prints + multiple headers
|
||||
fn banner_prints_random_agent() {
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg("http://localhost")
|
||||
.arg("--random-agent")
|
||||
.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("Random"))
|
||||
.and(predicate::str::contains("─┴─")),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
||||
/// expect to see all mandatory prints + multiple size filters
|
||||
|
||||
282
tests/test_deny_list.rs
Normal file
282
tests/test_deny_list.rs
Normal file
@@ -0,0 +1,282 @@
|
||||
mod utils;
|
||||
use assert_cmd::prelude::*;
|
||||
use assert_cmd::Command;
|
||||
use httpmock::Method::GET;
|
||||
use httpmock::MockServer;
|
||||
use predicates::prelude::*;
|
||||
use utils::{setup_tmp_directory, teardown_tmp_directory};
|
||||
|
||||
#[test]
|
||||
/// test that the deny list prevents a request if the requested url is a match
|
||||
fn deny_list_works_during_with_a_normal_scan() {
|
||||
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("/LICENSE");
|
||||
then.status(200).body("this is a test");
|
||||
});
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--dont-scan")
|
||||
.arg(srv.url("/LICENSE"))
|
||||
.unwrap();
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
|
||||
cmd.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains(srv.url("/LICENSE")).not());
|
||||
|
||||
assert_eq!(mock.hits(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test that the deny list prevents requests of urls found during extraction
|
||||
fn deny_list_works_during_extraction() {
|
||||
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("/LICENSE");
|
||||
then.status(200)
|
||||
.body(&srv.url("'/homepage/assets/img/icons/handshake.svg'"));
|
||||
});
|
||||
|
||||
let mock_two = srv.mock(|when, then| {
|
||||
when.method(GET)
|
||||
.path("/homepage/assets/img/icons/handshake.svg");
|
||||
then.status(200);
|
||||
});
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--extract-links")
|
||||
.arg("--dont-scan")
|
||||
.arg(srv.url("/homepage/"))
|
||||
.unwrap();
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::contains("/LICENSE")
|
||||
.and(predicate::str::contains("200"))
|
||||
.and(predicate::str::contains("/homepage/assets/img/icons/handshake.svg").not()),
|
||||
);
|
||||
|
||||
assert_eq!(mock.hits(), 1);
|
||||
assert_eq!(mock_two.hits(), 0);
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test that the deny list prevents requests of urls found during recursion
|
||||
fn deny_list_works_during_recursion() {
|
||||
let srv = MockServer::start();
|
||||
let urls = [
|
||||
"js".to_string(),
|
||||
"prod".to_string(),
|
||||
"dev".to_string(),
|
||||
"file.js".to_string(),
|
||||
];
|
||||
let (tmp_dir, file) = setup_tmp_directory(&urls, "wordlist").unwrap();
|
||||
|
||||
let js_mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/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/"));
|
||||
});
|
||||
|
||||
let js_dev_mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/js/dev");
|
||||
then.status(301).header("Location", &srv.url("/js/dev/"));
|
||||
});
|
||||
|
||||
let js_dev_file_mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/js/dev/file.js");
|
||||
then.status(200)
|
||||
.body("this is a test and is more bytes than other ones");
|
||||
});
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("-t")
|
||||
.arg("1")
|
||||
.arg("--dont-scan")
|
||||
.arg(srv.url("/js/dev"))
|
||||
.unwrap();
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::is_match("301.*js")
|
||||
.unwrap()
|
||||
.and(predicate::str::is_match("301.*js/prod").unwrap())
|
||||
.and(predicate::str::is_match("301.*js/dev").unwrap())
|
||||
.not()
|
||||
.and(predicate::str::is_match("200.*js/dev/file.js").unwrap())
|
||||
.not(),
|
||||
);
|
||||
|
||||
assert_eq!(js_mock.hits(), 1);
|
||||
assert_eq!(js_prod_mock.hits(), 1);
|
||||
assert_eq!(js_dev_mock.hits(), 0);
|
||||
assert_eq!(js_dev_file_mock.hits(), 0);
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test that the deny list prevents requests of urls found during recursion when the denier is a
|
||||
/// parent of a user-specified scan
|
||||
fn deny_list_works_during_recursion_with_inverted_parents() {
|
||||
let srv = MockServer::start();
|
||||
let urls = [
|
||||
"js".to_string(),
|
||||
"prod".to_string(),
|
||||
"dev".to_string(),
|
||||
"api".to_string(),
|
||||
"file.js".to_string(),
|
||||
];
|
||||
let (tmp_dir, file) = setup_tmp_directory(&urls, "wordlist").unwrap();
|
||||
|
||||
let js_mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/js");
|
||||
then.status(301).header("Location", &srv.url("/js/"));
|
||||
});
|
||||
|
||||
let api_mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/api");
|
||||
then.status(200);
|
||||
});
|
||||
|
||||
let js_prod_mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/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/"));
|
||||
});
|
||||
|
||||
let js_dev_file_mock = srv.mock(|when, then| {
|
||||
when.method(GET).path("/js/dev/file.js");
|
||||
then.status(200)
|
||||
.body("this is a test and is more bytes than other ones");
|
||||
});
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/js"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("-t")
|
||||
.arg("1")
|
||||
.arg("-vvvv")
|
||||
.arg("--dont-scan")
|
||||
.arg(srv.url("/"))
|
||||
.unwrap();
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::is_match("301.*js")
|
||||
.unwrap()
|
||||
.and(predicate::str::is_match("301.*js/prod").unwrap())
|
||||
.and(predicate::str::is_match("301.*js/dev").unwrap())
|
||||
.and(predicate::str::is_match("200.*js/dev/file.js").unwrap())
|
||||
.and(predicate::str::is_match("200.*api").unwrap())
|
||||
.not(),
|
||||
);
|
||||
|
||||
assert_eq!(js_mock.hits(), 1);
|
||||
assert_eq!(js_prod_mock.hits(), 1);
|
||||
assert_eq!(js_dev_mock.hits(), 1);
|
||||
assert_eq!(js_dev_file_mock.hits(), 1);
|
||||
assert_eq!(api_mock.hits(), 0);
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test that a regex that prevents the base url from being scanned results in an early exit
|
||||
fn deny_list_prevents_regex_that_denies_base_url() {
|
||||
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("/LICENSE");
|
||||
then.status(200).body("this is a test");
|
||||
});
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--dont-scan")
|
||||
.arg("/")
|
||||
.unwrap();
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
|
||||
let err_msg = format!(
|
||||
"Could not determine initial targets: The regex '/' matches {}/; the scan will never start",
|
||||
srv.base_url()
|
||||
);
|
||||
cmd.assert()
|
||||
.success()
|
||||
.stderr(predicate::str::contains(err_msg));
|
||||
|
||||
assert_eq!(mock.hits(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test that a url that prevents the base url from being scanned results in an early exit
|
||||
fn deny_list_prevents_url_that_denies_base_url() {
|
||||
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("/LICENSE");
|
||||
then.status(200).body("this is a test");
|
||||
});
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--dont-scan")
|
||||
.arg(srv.base_url())
|
||||
.unwrap();
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
|
||||
let err_msg = format!(
|
||||
"Could not determine initial targets: The url '{}/' matches {}/; the scan will never start",
|
||||
srv.base_url(),
|
||||
srv.base_url()
|
||||
);
|
||||
|
||||
cmd.assert()
|
||||
.success()
|
||||
.stderr(predicate::str::contains(err_msg));
|
||||
|
||||
assert_eq!(mock.hits(), 0);
|
||||
}
|
||||
@@ -224,11 +224,11 @@ fn test_dynamic_wildcard_request_found() {
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
|
||||
assert_eq!(contents.contains("WLD"), true);
|
||||
assert_eq!(contents.contains("Got"), true);
|
||||
assert_eq!(contents.contains("200"), true);
|
||||
assert_eq!(contents.contains("(url length: 32)"), true);
|
||||
assert_eq!(contents.contains("(url length: 96)"), true);
|
||||
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")
|
||||
@@ -391,11 +391,11 @@ fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file() {
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
|
||||
assert_eq!(contents.contains("WLD"), true);
|
||||
assert_eq!(contents.contains("Got"), true);
|
||||
assert_eq!(contents.contains("200"), true);
|
||||
assert_eq!(contents.contains("(url length: 32)"), true);
|
||||
assert_eq!(contents.contains("(url length: 96)"), true);
|
||||
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")
|
||||
@@ -451,12 +451,12 @@ fn heuristics_wildcard_test_with_redirect_as_response_code(
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
|
||||
assert_eq!(contents.contains("WLD"), true);
|
||||
assert_eq!(contents.contains("301"), true);
|
||||
assert_eq!(contents.contains("/some-redirect"), true);
|
||||
assert_eq!(contents.contains("redirects to => "), true);
|
||||
assert_eq!(contents.contains(&srv.url("/")), true);
|
||||
assert_eq!(contents.contains("(url length: 32)"), true);
|
||||
assert!(contents.contains("WLD"));
|
||||
assert!(contents.contains("301"));
|
||||
assert!(contents.contains("/some-redirect"));
|
||||
assert!(contents.contains("redirects to => "));
|
||||
assert!(contents.contains(&srv.url("/")));
|
||||
assert!(contents.contains("(url length: 32)"));
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::contains("redirects to => ")
|
||||
|
||||
@@ -3,7 +3,7 @@ use assert_cmd::Command;
|
||||
use httpmock::Method::GET;
|
||||
use httpmock::{MockServer, Regex};
|
||||
use predicates::prelude::*;
|
||||
use std::fs::read_to_string;
|
||||
use std::fs::{read_dir, read_to_string};
|
||||
use utils::{setup_tmp_directory, teardown_tmp_directory};
|
||||
|
||||
#[test]
|
||||
@@ -153,3 +153,73 @@ fn main_parallel_spawns_children() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// send three targets over stdin with --output enabled, expect parallel to create a new directory
|
||||
/// and the log files therein
|
||||
fn main_parallel_creates_output_directory() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let t1 = MockServer::start();
|
||||
let t2 = MockServer::start();
|
||||
let t3 = MockServer::start();
|
||||
|
||||
let words = [
|
||||
String::from("LICENSE"),
|
||||
String::from("stuff"),
|
||||
String::from("things"),
|
||||
String::from("mostuff"),
|
||||
String::from("mothings"),
|
||||
];
|
||||
let (word_tmp_dir, wordlist) = setup_tmp_directory(&words, "wordlist")?;
|
||||
let (output_dir, outfile) = setup_tmp_directory(&[], "output-file")?;
|
||||
let (tgt_tmp_dir, targets) =
|
||||
setup_tmp_directory(&[t1.url("/"), t2.url("/"), t3.url("/")], "targets")?;
|
||||
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--stdin")
|
||||
.arg("--parallel")
|
||||
.arg("2")
|
||||
.arg("--output")
|
||||
.arg(outfile.as_os_str())
|
||||
.arg("--wordlist")
|
||||
.arg(wordlist.as_os_str())
|
||||
.pipe_stdin(targets)
|
||||
.unwrap()
|
||||
.assert()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("Could not connect to any target provided")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
.not(), // no target url found
|
||||
);
|
||||
|
||||
// output_dir should return something similar to output-file-1627845244.logs with the
|
||||
// line below. if it ever fails, can use the regex below to filter out the right directory
|
||||
let sub_dir = read_dir(&output_dir)?.next().unwrap()?.file_name();
|
||||
|
||||
let mut num_logs = 0;
|
||||
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);
|
||||
|
||||
// created directory like output-file-1627845741.logs/
|
||||
assert!(dir_regex.is_match(&sub_dir.to_string_lossy().to_string()));
|
||||
|
||||
for entry in sub_dir.read_dir()? {
|
||||
let entry = entry?;
|
||||
// created each file like ferox-https_localhost-1627845741.log
|
||||
println!("name: {:?}", entry.file_name().to_string_lossy());
|
||||
assert!(file_regex.is_match(&entry.file_name().to_string_lossy()));
|
||||
num_logs += 1;
|
||||
}
|
||||
|
||||
// should be 3 log files total
|
||||
assert_eq!(num_logs, 3);
|
||||
|
||||
teardown_tmp_directory(word_tmp_dir);
|
||||
teardown_tmp_directory(tgt_tmp_dir);
|
||||
teardown_tmp_directory(output_dir);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
46
tests/test_parser.rs
Normal file
46
tests/test_parser.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use assert_cmd::Command;
|
||||
use predicates::prelude::*;
|
||||
|
||||
#[test]
|
||||
/// specify an incorrect param (-fc) with --help after it on the command line
|
||||
/// old behavior printed
|
||||
/// error: Found argument '-c' which wasn't expected, or isn't valid in this context
|
||||
///
|
||||
/// USAGE:
|
||||
/// feroxbuster --add-slash --url <URL>...
|
||||
///
|
||||
/// For more information try --help
|
||||
///
|
||||
/// the new behavior we expect to see is to print the long form help message, of which
|
||||
/// Ludicrous speed... go! is near the bottom of that output, so we can test for that
|
||||
fn parser_incorrect_param_with_tack_tack_help() {
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("-fc")
|
||||
.arg("--help")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("Ludicrous speed... go!"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// specify an incorrect param (-fc) with --help after it on the command line
|
||||
/// old behavior printed
|
||||
/// error: Found argument '-c' which wasn't expected, or isn't valid in this context
|
||||
///
|
||||
/// USAGE:
|
||||
/// feroxbuster --add-slash --url <URL>...
|
||||
///
|
||||
/// For more information try --help
|
||||
///
|
||||
/// the new behavior we expect to see is to print the long form help message, of which
|
||||
/// Ludicrous speed... go! is near the bottom of that output, so we can test for that
|
||||
fn parser_incorrect_param_with_tack_h() {
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("-fc")
|
||||
.arg("-h")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("Ludicrous speed... go!"));
|
||||
}
|
||||
@@ -77,7 +77,7 @@ fn auto_bail_cancels_scan_with_timeouts() {
|
||||
|
||||
// read debug log to get the number of errors enforced
|
||||
for line in debug_log.lines() {
|
||||
let log: serde_json::Value = serde_json::from_str(&line).unwrap_or_default();
|
||||
let log: serde_json::Value = serde_json::from_str(line).unwrap_or_default();
|
||||
if let Some(message) = log.get("message") {
|
||||
let str_msg = message.as_str().unwrap_or_default().to_string();
|
||||
|
||||
@@ -156,7 +156,7 @@ fn auto_bail_cancels_scan_with_403s() {
|
||||
|
||||
// read debug log to get the number of errors enforced
|
||||
for line in debug_log.lines() {
|
||||
let log: serde_json::Value = serde_json::from_str(&line).unwrap_or_default();
|
||||
let log: serde_json::Value = serde_json::from_str(line).unwrap_or_default();
|
||||
if let Some(message) = log.get("message") {
|
||||
let str_msg = message.as_str().unwrap_or_default().to_string();
|
||||
|
||||
@@ -238,7 +238,7 @@ fn auto_bail_cancels_scan_with_429s() {
|
||||
|
||||
// read debug log to get the number of errors enforced
|
||||
for line in debug_log.lines() {
|
||||
let log: serde_json::Value = serde_json::from_str(&line).unwrap_or_default();
|
||||
let log: serde_json::Value = serde_json::from_str(line).unwrap_or_default();
|
||||
if let Some(message) = log.get("message") {
|
||||
let str_msg = message.as_str().unwrap_or_default().to_string();
|
||||
|
||||
|
||||
@@ -93,9 +93,13 @@ fn resume_scan_works() {
|
||||
#[test]
|
||||
/// kick off scan with a time limit;
|
||||
fn time_limit_enforced_when_specified() {
|
||||
let srv = MockServer::start();
|
||||
let t1 = MockServer::start();
|
||||
let t2 = MockServer::start();
|
||||
|
||||
let (tmp_dir, file) =
|
||||
setup_tmp_directory(&["css".to_string(), "stuff".to_string()], "wordlist").unwrap();
|
||||
let (tgt_tmp_dir, targets) =
|
||||
setup_tmp_directory(&[t1.url("/"), t2.url("/")], "targets").unwrap();
|
||||
|
||||
// ensure the command will run long enough by adding crap to the wordlist
|
||||
let more_words = read_to_string(Path::new("tests/extra-words")).unwrap();
|
||||
@@ -109,12 +113,13 @@ fn time_limit_enforced_when_specified() {
|
||||
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--stdin")
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--time-limit")
|
||||
.arg("5s")
|
||||
.pipe_stdin(targets)
|
||||
.unwrap()
|
||||
.assert()
|
||||
.failure();
|
||||
|
||||
@@ -127,4 +132,5 @@ fn time_limit_enforced_when_specified() {
|
||||
assert!(now.elapsed() > lower_bound && now.elapsed() < upper_bound);
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
teardown_tmp_directory(tgt_tmp_dir);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user