mirror of
https://github.com/BishopFox/sj.git
synced 2026-11-07 17:08:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83d1b57aef | ||
|
|
6f903a7f78 | ||
|
|
0d59e6c34f | ||
|
|
ceb99e4ddd | ||
|
|
8310c8995d | ||
|
|
017eb9af50 | ||
|
|
025f3f18cf | ||
|
|
585100037a | ||
|
|
f4b2634f42 | ||
|
|
2e9da2e810 | ||
|
|
0fd8b6e6ec | ||
|
|
8acc15aad1 | ||
|
|
25d12a52e3 | ||
|
|
a9f907e2f1 | ||
|
|
3b3fed2c99 | ||
|
|
1571b14873 | ||
|
|
94e65fd96f | ||
|
|
5274b1b0ab | ||
|
|
49d6867ebf | ||
|
|
65d424c6a7 | ||
|
|
d7b34598d2 | ||
|
|
4526ddbf23 | ||
|
|
31e286235b | ||
|
|
c84e679ac4 | ||
|
|
41ba000755 | ||
|
|
cc99aef9ae | ||
|
|
b98e8c7d25 | ||
|
|
2879da6f87 | ||
|
|
530143b4ee | ||
|
|
69de3abbf1 | ||
|
|
9fd1b78081 | ||
|
|
c5a46d35bd | ||
|
|
2877384d54 | ||
|
|
cb3e0bb16e | ||
|
|
291edfe60c | ||
|
|
69b316e7bc | ||
|
|
615f0c7a26 | ||
|
|
5bbf0453a5 | ||
|
|
a39028b463 | ||
|
|
e22c11c118 | ||
|
|
528a771c87 | ||
|
|
c85512dfe0 | ||
|
|
6a01afb367 | ||
|
|
7ac01dc2a1 | ||
|
|
9b48aab4fb |
No files matched your search
@@ -8,31 +8,35 @@ assignees: ''
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
A code block containing the command executed with output:
|
||||
|
||||
```bash
|
||||
$ sj automate -u https://petstore.swagger.io/v2/swagger.json
|
||||
[SOME OUTPUT AND/OR ERROR HERE]
|
||||
|
||||
#Note: To maintain privacy, please omit any IP address, URL, organizational information contained within the definition file, etc.
|
||||
```
|
||||
|
||||
**Expected behavior**
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
*Note: To maintain privacy, please omit any IP address, URL, organizational information contained within the definition file, etc.*
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
|
||||
Add any other context about the problem here.
|
||||
@@ -8,13 +8,17 @@ assignees: ''
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -11,6 +11,11 @@ builds:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w -X main.build={{.Version}}
|
||||
|
||||
|
||||
archives:
|
||||
- format: tar.gz
|
||||
|
||||
@@ -4,15 +4,16 @@
|
||||
|
||||
sj is a command line tool designed to assist with auditing of exposed Swagger/OpenAPI definition files by checking the associated API endpoints for weak authentication. It also provides command templates for manual vulnerability testing.
|
||||
|
||||
It does this by parsing the definition file for paths, parameters, and accepted methods before using the results with one of four commands:
|
||||
It does this by parsing the definition file for paths, parameters, and accepted methods before using the results with one of five sub-commands:
|
||||
- `automate` - Crafts a series of requests and analyzes the status code of the response.
|
||||
- `prepare` - Generates a list of commands to use for manual testing.
|
||||
- `endpoints` - Generates a list of raw API routes. *Path values will not be replaced with test data*.
|
||||
- `brute` - Sends a series of requests to a target to find operation definitions based on commonly used file paths.
|
||||
- `convert` - Converts a definition file from v2 to v3.
|
||||
|
||||
## Build
|
||||
|
||||
To compile from source, ensure you have Go version `>= 1.20` installed and run `go build` from within the repository:
|
||||
To compile from source, ensure you have Go version `>= 1.21` installed and run `go build` from within the repository:
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/BishopFox/sj.git
|
||||
@@ -38,32 +39,50 @@ $ export PATH=$PATH:~/go/bin
|
||||
```bash
|
||||
$ sj automate -u https://petstore.swagger.io/v2/swagger.json -q
|
||||
|
||||
INFO[0000] Gathering API details.
|
||||
INFO[0000] Gathering API details.
|
||||
INFO[0000] Sending requests at a rate of 15 requests per second.
|
||||
|
||||
Title: Swagger Petstore
|
||||
Description: This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.
|
||||
|
||||
INFO[0000] Available authentication mechanisms:
|
||||
- api_key
|
||||
- petstore_auth
|
||||
WARN[0000] Results for https://petstore.swagger.io/v2:
|
||||
|
||||
INFO[0000] Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/pet/findByTags?tags=1"
|
||||
INFO[0000] Endpoint accessible! Method=POST Status=200 Target="https://petstore.swagger.io/v2/store/order"
|
||||
WARN[0000] Manual testing may be required. Method=POST Status=500 Target="https://petstore.swagger.io/v2/user/createWithList"
|
||||
INFO[0000] Endpoint accessible! Method=POST Status=200 Target="https://petstore.swagger.io/v2/user"
|
||||
ERRO[0000] User not found Method=GET Status=404 Target="https://petstore.swagger.io/v2/user/test"
|
||||
WARN[0000] Manual testing may be required. Method=PUT Status=415 Target="https://petstore.swagger.io/v2/user/test"
|
||||
INFO[0000] Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/user/login?password=test&username=test"
|
||||
WARN[0000] Manual testing may be required. Method=POST Status=500 Target="https://petstore.swagger.io/v2/user/createWithArray"
|
||||
INFO[0000] Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/user/logout"
|
||||
WARN[0000] Manual testing may be required. Method=POST Status=415 Target="https://petstore.swagger.io/v2/pet/test"
|
||||
ERRO[0000] Pet not found Method=GET Status=404 Target="https://petstore.swagger.io/v2/pet/test"
|
||||
WARN[0000] Manual testing may be required. Method=POST Status=415 Target="https://petstore.swagger.io/v2/pet/test/uploadImage"
|
||||
ERRO[0000] Order not found Method=GET Status=404 Target="https://petstore.swagger.io/v2/store/order/test"
|
||||
INFO[0000] Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/store/inventory"
|
||||
INFO[0000] Endpoint accessible! Method=POST Status=200 Target="https://petstore.swagger.io/v2/pet"
|
||||
WARN[0000] Manual testing may be required. Method=PUT Status=415 Target="https://petstore.swagger.io/v2/pet"
|
||||
INFO[0001] Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/pet/findByStatus?status=1"
|
||||
WARN[0007] Results for http://petstore.swagger.io/v2:
|
||||
|
||||
WARN Manual testing may be required. Method=POST Status=415 Target=http://petstore.swagger.io/v2/pet
|
||||
WARN Manual testing may be required. Method=PUT Status=415 Target=http://petstore.swagger.io/v2/pet
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=http://petstore.swagger.io/v2/pet/1
|
||||
WARN Manual testing may be required. Method=POST Status=415 Target=http://petstore.swagger.io/v2/pet/1
|
||||
WARN Manual testing may be required. Method=POST Status=400 Target=http://petstore.swagger.io/v2/pet/1/uploadImage
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target="http://petstore.swagger.io/v2/pet/findByStatus?status=1"
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target="http://petstore.swagger.io/v2/pet/findByTags?tags=1"
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=http://petstore.swagger.io/v2/store/inventory
|
||||
WARN Manual testing may be required. Method=POST Status=415 Target=http://petstore.swagger.io/v2/store/order
|
||||
ERRO Order not found Method=GET Status=404 Target=http://petstore.swagger.io/v2/store/order/1
|
||||
WARN Manual testing may be required. Method=POST Status=415 Target=http://petstore.swagger.io/v2/user
|
||||
WARN Manual testing may be required. Method=POST Status=415 Target=http://petstore.swagger.io/v2/user/createWithArray
|
||||
WARN Manual testing may be required. Method=POST Status=415 Target=http://petstore.swagger.io/v2/user/createWithList
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=http://petstore.swagger.io/v2/user/login?password=test&username=test
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=http://petstore.swagger.io/v2/user/logout
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=http://petstore.swagger.io/v2/user/test
|
||||
WARN Manual testing may be required. Method=PUT Status=415 Target=http://petstore.swagger.io/v2/user/test
|
||||
WARN Manual testing may be required. Method=POST Status=415 Target=https://petstore.swagger.io/v2/pet
|
||||
WARN Manual testing may be required. Method=PUT Status=415 Target=https://petstore.swagger.io/v2/pet
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=https://petstore.swagger.io/v2/pet/1
|
||||
WARN Manual testing may be required. Method=POST Status=415 Target=https://petstore.swagger.io/v2/pet/1
|
||||
WARN Manual testing may be required. Method=POST Status=400 Target=https://petstore.swagger.io/v2/pet/1/uploadImage
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/pet/findByStatus?status=1"
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/pet/findByTags?tags=1"
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=https://petstore.swagger.io/v2/store/inventory
|
||||
WARN Manual testing may be required. Method=POST Status=415 Target=https://petstore.swagger.io/v2/store/order
|
||||
ERRO Order not found Method=GET Status=404 Target=https://petstore.swagger.io/v2/store/order/1
|
||||
WARN Manual testing may be required. Method=POST Status=415 Target=https://petstore.swagger.io/v2/user
|
||||
WARN Manual testing may be required. Method=POST Status=415 Target=https://petstore.swagger.io/v2/user/createWithArray
|
||||
WARN Manual testing may be required. Method=POST Status=415 Target=https://petstore.swagger.io/v2/user/createWithList
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=https://petstore.swagger.io/v2/user/login?password=test&username=test
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=https://petstore.swagger.io/v2/user/logout
|
||||
INFO Endpoint accessible! Method=GET Status=200 Target=https://petstore.swagger.io/v2/user/test
|
||||
WARN Manual testing may be required. Method=PUT Status=415 Target=https://petstore.swagger.io/v2/user/test
|
||||
```
|
||||
|
||||
> Use the `prepare` command to prepare a list of commands for manual testing. Currently supports both `curl` and `sqlmap`. You will likely have to modify these slightly.
|
||||
@@ -72,31 +91,46 @@ INFO[0001] Endpoint accessible! Method=GET Status=200 T
|
||||
$ sj prepare -u https://petstore.swagger.io/v2/swagger.json -q
|
||||
|
||||
INFO[0000] Gathering API details.
|
||||
|
||||
|
||||
Title: Swagger Petstore
|
||||
Description: This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.
|
||||
|
||||
INFO[0000] Available authentication mechanisms:
|
||||
- api_key
|
||||
- petstore_auth
|
||||
|
||||
curl -sk -X PUT 'https://petstore.swagger.io/v2/user/test' -d '{"test":"test"}'
|
||||
curl -sk -X GET 'https://petstore.swagger.io/v2/user/test'
|
||||
curl -sk -X GET 'https://petstore.swagger.io/v2/pet/test'
|
||||
curl -sk -X POST 'https://petstore.swagger.io/v2/pet/test' -d '{"test":"test"}'
|
||||
curl -sk -X POST 'https://petstore.swagger.io/v2/store/order' -d '{"test":"test"}'
|
||||
curl -sk -X POST 'https://petstore.swagger.io/v2/pet' -d '{"test":"test"}'
|
||||
curl -sk -X PUT 'https://petstore.swagger.io/v2/pet' -d '{"test":"test"}'
|
||||
curl -sk -X GET 'https://petstore.swagger.io/v2/store/order/test'
|
||||
curl -sk -X GET 'https://petstore.swagger.io/v2/pet/findByTags?tags=1'
|
||||
curl -sk -X POST 'https://petstore.swagger.io/v2/user/createWithList' -d '{"test":"test"}'
|
||||
curl -sk -X POST 'https://petstore.swagger.io/v2/user' -d '{"test":"test"}'
|
||||
curl -sk -X GET 'https://petstore.swagger.io/v2/user/logout'
|
||||
curl -sk -X GET 'https://petstore.swagger.io/v2/user/login?password=test&username=test'
|
||||
curl -sk -X POST 'https://petstore.swagger.io/v2/pet/test/uploadImage' -d '{"test":"test"}'
|
||||
curl -sk -X GET 'https://petstore.swagger.io/v2/pet/findByStatus?status=1'
|
||||
curl -sk -X GET 'https://petstore.swagger.io/v2/store/inventory'
|
||||
curl -sk -X POST 'https://petstore.swagger.io/v2/user/createWithArray' -d '{"test":"test"}'
|
||||
curl -sk -X POST 'https://petstore.swagger.io/v2/pet' -d '<root><name>test</name><id></id><photoUrls>unknown_type_populate_manually</photoUrls><tags>unknown_type_populate_manually</tags><status>test</status></root>' -H 'Content-Type: application/xml'
|
||||
curl -sk -X PUT 'https://petstore.swagger.io/v2/pet' -d '<root><photoUrls>unknown_type_populate_manually</photoUrls><tags>unknown_type_populate_manually</tags><status>test</status><id></id><name>test</name></root>' -H 'Content-Type: application/xml'
|
||||
curl -sk -X GET 'https://petstore.swagger.io/v2/user/logout' -H 'Content-Type: application/xml'
|
||||
curl -sk -X GET 'https://petstore.swagger.io/v2/user/login?password=test&username=test' -H 'Content-Type: application/xml'
|
||||
curl -sk -X GET 'https://petstore.swagger.io/v2/pet/findByTags?tags=1' -H 'Content-Type: application/xml'
|
||||
curl -sk -X POST 'https://petstore.swagger.io/v2/user/createWithList' -H 'Content-Type: application/json'
|
||||
curl -sk -X GET 'https://petstore.swagger.io/v2/pet/findByStatus?status=1' -H 'Content-Type: application/json'
|
||||
curl -sk -X POST 'https://petstore.swagger.io/v2/user' -d '{"email":"test","firstName":"test","id":1,"lastName":"test","password":"test","phone":"test","userStatus":1,"username":"test"}' -H 'Content-Type: application/json'
|
||||
curl -sk -X POST 'https://petstore.swagger.io/v2/store/order' -d '{"complete":false,"id":1,"petId":1,"quantity":1,"shipDate":"test","status":"test"}' -H 'Content-Type: application/json'
|
||||
curl -sk -X GET 'https://petstore.swagger.io/v2/store/order/1' -H 'Content-Type: application/json'
|
||||
curl -sk -X PUT 'https://petstore.swagger.io/v2/user/test' -d '{"email":"test","firstName":"test","id":1,"lastName":"test","password":"test","phone":"test","userStatus":1,"username":"test"}' -H 'Content-Type: application/json'
|
||||
curl -sk -X GET 'https://petstore.swagger.io/v2/user/test' -H 'Content-Type: application/json'
|
||||
curl -sk -X POST 'https://petstore.swagger.io/v2/pet/1' -d 'name=test&status=test' -H 'Content-Type: application/x-www-form-urlencoded'
|
||||
curl -sk -X GET 'https://petstore.swagger.io/v2/pet/1' -H 'Content-Type: application/x-www-form-urlencoded'
|
||||
curl -sk -X POST 'https://petstore.swagger.io/v2/user/createWithArray' -H 'Content-Type: application/json'
|
||||
curl -sk -X POST 'https://petstore.swagger.io/v2/pet/1/uploadImage' -d 'test=test' -H 'Content-Type: multipart/form-data'
|
||||
|
||||
curl -sk -X POST 'http://petstore.swagger.io/v2/store/order' -d '{"complete":false,"id":1,"petId":1,"quantity":1,"shipDate":"test","status":"test"}' -H 'Content-Type: application/json'
|
||||
curl -sk -X GET 'http://petstore.swagger.io/v2/store/order/1' -H 'Content-Type: application/json'
|
||||
curl -sk -X GET 'http://petstore.swagger.io/v2/user/test' -H 'Content-Type: application/json'
|
||||
curl -sk -X PUT 'http://petstore.swagger.io/v2/user/test' -d '{"email":"test","firstName":"test","id":1,"lastName":"test","password":"test","phone":"test","userStatus":1,"username":"test"}' -H 'Content-Type: application/json'
|
||||
curl -sk -X POST 'http://petstore.swagger.io/v2/pet/1' -d 'name=test&status=test' -H 'Content-Type: application/x-www-form-urlencoded'
|
||||
curl -sk -X GET 'http://petstore.swagger.io/v2/pet/1' -H 'Content-Type: application/x-www-form-urlencoded'
|
||||
curl -sk -X POST 'http://petstore.swagger.io/v2/user/createWithArray' -H 'Content-Type: application/json'
|
||||
curl -sk -X POST 'http://petstore.swagger.io/v2/pet/1/uploadImage' -d 'test=test' -H 'Content-Type: multipart/form-data'
|
||||
curl -sk -X GET 'http://petstore.swagger.io/v2/pet/findByStatus?status=1' -H 'Content-Type: multipart/form-data'
|
||||
curl -sk -X POST 'http://petstore.swagger.io/v2/user' -d '{"email":"test","firstName":"test","id":1,"lastName":"test","password":"test","phone":"test","userStatus":1,"username":"test"}' -H 'Content-Type: application/json'
|
||||
curl -sk -X PUT 'http://petstore.swagger.io/v2/pet' -d '<root><id>1</id><name>test</name><photoUrls>unknown_type_populate_manually</photoUrls><tags>unknown_type_populate_manually</tags><status>test</status></root>' -H 'Content-Type: application/xml'
|
||||
curl -sk -X POST 'http://petstore.swagger.io/v2/pet' -d '<root><status>test</status><id></id><name>test</name><photoUrls>unknown_type_populate_manually</photoUrls><tags>unknown_type_populate_manually</tags></root>' -H 'Content-Type: application/xml'
|
||||
curl -sk -X GET 'http://petstore.swagger.io/v2/store/inventory' -H 'Content-Type: application/xml'
|
||||
curl -sk -X GET 'http://petstore.swagger.io/v2/user/login?password=test&username=test' -H 'Content-Type: application/xml'
|
||||
curl -sk -X GET 'http://petstore.swagger.io/v2/user/logout' -H 'Content-Type: application/xml'
|
||||
curl -sk -X POST 'http://petstore.swagger.io/v2/user/createWithList' -H 'Content-Type: application/json'
|
||||
curl -sk -X GET 'http://petstore.swagger.io/v2/pet/findByTags?tags=1' -H 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
> Use the `endpoints` command to generate a list of raw endpoints from the provided definition file.
|
||||
@@ -125,13 +159,23 @@ INFO[0000] Gathering endpoints.
|
||||
> Use the `brute` command to send a series of requests in an attempt to find a definition file on the target.
|
||||
|
||||
```bash
|
||||
$ sj brute -u https://petstore.swagger.io/v2/swagger.json
|
||||
INFO[0000] Sending 2045 requests. This could take a while...
|
||||
Request: 343
|
||||
INFO[0015] Definition file found: https://petstore.swagger.io/v2/swagger
|
||||
$ sj brute -u https://petstore.swagger.io
|
||||
INFO[0000] Sending 2173 requests at a rate of 15 requests per second. This could take a while...
|
||||
Request: 1294
|
||||
INFO[0090] Definition file found: https://petstore.swagger.io/v2/swagger.json
|
||||
{"...SNIP..."}
|
||||
```
|
||||
|
||||
> Use the `convert` command to convert a definition file from version 2 to version 3.
|
||||
|
||||
```bash
|
||||
$ sj convert -u https://petstore.swagger.io/v2/swagger.json -o openapi.json
|
||||
|
||||
INFO[0000] Gathering API details.
|
||||
|
||||
INFO[0000] Wrote file to /current/directory/openapi.json
|
||||
```
|
||||
|
||||
## Help
|
||||
|
||||
A full list of commands can be found by using the `--help` flag:
|
||||
@@ -157,6 +201,9 @@ $ sj endpoints -u https://petstore.swagger.io/v2/swagger.json
|
||||
Perform a brute-force attack against the target to identify hidden definition files:
|
||||
$ sj brute -u https://petstore.swagger.io
|
||||
|
||||
Convert a Swagger (v2) definition file to an OpenAPI (v3) definition file:
|
||||
$ sj convert -u https://petstore.swagger.io/v2/swagger.json -o openapi.json
|
||||
|
||||
Usage:
|
||||
sj [flags]
|
||||
sj [command]
|
||||
@@ -164,12 +211,13 @@ Usage:
|
||||
Available Commands:
|
||||
automate Sends a series of automated requests to the discovered endpoints.
|
||||
brute Sends a series of automated requests to discover hidden API operation definitions.
|
||||
convert Converts a Swagger definition file to an OpenAPI v3 definition file.
|
||||
endpoints Prints a list of endpoints from the target.
|
||||
help Help about any command
|
||||
prepare Prepares a set of commands for manual testing of each endpoint.
|
||||
|
||||
Flags:
|
||||
-a, --agent string Set the User-Agent string. (default "Swagger Jacker (github.com/BishopFox/sj)")
|
||||
-A, --agent string Set the User-Agent string. (default "Swagger Jacker (github.com/BishopFox/sj)")
|
||||
-b, --base-path string Set the API base path if not defined in the definition file (i.e. /V2/).
|
||||
-f, --format string Declare the format of the definition file (json/yaml/yml/js). (default "json")
|
||||
-H, --headers stringArray Add custom headers, separated by a colon ("Name: Value"). Multiple flags are accepted.
|
||||
@@ -179,7 +227,8 @@ Flags:
|
||||
-o, --outfile string Output the results to a file. Only supported for the 'automate' and 'brute' commands at this time.
|
||||
-p, --proxy string Proxy host and port. Example: http://127.0.0.1:8080 (default "NOPROXY")
|
||||
-q, --quiet Do not prompt for user input - uses default values for all requests.
|
||||
-r, --randomize-user-agent Randomizes the user agent string. Default is 'false'.
|
||||
--randomize-user-agent Randomizes the user agent string. Default is 'false'.
|
||||
-r, --rate int Limit the number of requests per second. (default 15)
|
||||
-s, --safe-word stringArray Avoids 'dangerous word' check for the specified word(s). Multiple flags are accepted.
|
||||
-T, --target string Manually set a target for the requests to be made if separate from the host the documentation resides on.
|
||||
-t, --timeout int Set the request timeout period. (default 30)
|
||||
|
||||
@@ -23,6 +23,13 @@ var (
|
||||
|
||||
func CheckSecDefs(doc3 openapi3.T) (apiInQuery bool, apiKey string, apiKeyName string) {
|
||||
|
||||
if doc3.Components == nil || doc3.Components.SecuritySchemes == nil || len(doc3.Components.SecuritySchemes) == 0 {
|
||||
if !quiet {
|
||||
log.Warn("No security schemes defined in the OpenAPI specification.")
|
||||
}
|
||||
return false, "", ""
|
||||
}
|
||||
|
||||
if outputFormat == "json" && !quiet {
|
||||
if len(doc3.Components.SecuritySchemes) != 0 {
|
||||
log.Warnf("The following authentication mechanisms are supported. If necessary, supply these manually when using the JSON output format:\n")
|
||||
|
||||
+22
-2
@@ -13,6 +13,9 @@ import (
|
||||
|
||||
var getAccessibleEndpoints bool
|
||||
var outputFormat string
|
||||
var responsePreviewLength int
|
||||
var verbose bool
|
||||
|
||||
var automateCmd = &cobra.Command{
|
||||
Use: "automate",
|
||||
Short: "Sends a series of automated requests to the discovered endpoints.",
|
||||
@@ -20,10 +23,21 @@ var automateCmd = &cobra.Command{
|
||||
This enables the user to get a quick look at which endpoints require authentication and which ones do not. If a request
|
||||
responds in an abnormal way, manual testing should be conducted (prepare manual tests using the "prepare" command).`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
if outfile != "" && strings.ToLower(outputFormat) != "" {
|
||||
if !strings.HasSuffix(strings.ToLower(outfile), "json") && strings.ToLower(outputFormat) != "json" {
|
||||
log.Fatal("Only the JSON output format is supported at the moment.")
|
||||
} else if strings.HasSuffix(strings.ToLower(outfile), "json") && strings.ToLower(outputFormat) == "console" {
|
||||
outputFormat = "json"
|
||||
}
|
||||
}
|
||||
|
||||
if rateLimit <= 0 {
|
||||
log.Fatal("Invalid rate supplied. Must be a positive number")
|
||||
}
|
||||
|
||||
if randomUserAgent {
|
||||
if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
|
||||
log.Warnf("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +47,7 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
|
||||
|
||||
if strings.ToLower(outputFormat) != "json" {
|
||||
fmt.Printf("\n")
|
||||
log.Infof("Gathering API details.\n\n")
|
||||
log.Infof("Gathering API details.\n")
|
||||
}
|
||||
|
||||
if swaggerURL != "" {
|
||||
@@ -46,6 +60,9 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
|
||||
|
||||
bodyBytes, _ = io.ReadAll(specFile)
|
||||
}
|
||||
if rateLimit > 0 && strings.ToLower(outputFormat) != "json" {
|
||||
log.Info("Sending requests at a rate of ", rateLimit, " requests per second.")
|
||||
}
|
||||
GenerateRequests(bodyBytes, client)
|
||||
},
|
||||
}
|
||||
@@ -53,4 +70,7 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
|
||||
func init() {
|
||||
automateCmd.PersistentFlags().StringVarP(&outputFormat, "output-format", "F", "console", "The output format. Only 'console' (default) and 'json' are supported at the moment.")
|
||||
automateCmd.PersistentFlags().BoolVar(&getAccessibleEndpoints, "get-accessible-endpoints", false, "Only output the accessible endpoints (those that return a 200 status code).")
|
||||
automateCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "enable verbose mode, which shows a preview of each response.")
|
||||
automateCmd.PersistentFlags().IntVar(&responsePreviewLength, "response-preview-length", 50, "sets the response preview length when using verbose output.")
|
||||
|
||||
}
|
||||
+53
-29
@@ -2,6 +2,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -10,9 +11,11 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -29,6 +32,16 @@ var bruteCmd = &cobra.Command{
|
||||
Long: `The brute command sends requests to the target to find operation definitions based on commonly used file locations.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
if rateLimit <= 0 {
|
||||
log.Fatal("Invalid rate supplied. Must be a positive number")
|
||||
}
|
||||
|
||||
if randomUserAgent {
|
||||
if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
|
||||
log.Warnf("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
|
||||
}
|
||||
}
|
||||
|
||||
client := CheckAndConfigureProxy()
|
||||
|
||||
var allURLs []string
|
||||
@@ -60,7 +73,11 @@ var bruteCmd = &cobra.Command{
|
||||
log.Fatalf("failed to read words from file: %s", err)
|
||||
}
|
||||
}
|
||||
log.Infof("Sending %d requests. This could take a while...\n", len(allURLs))
|
||||
if rateLimit > 0 && strings.ToLower(outputFormat) != "json" {
|
||||
log.Infof("Sending %d requests at a rate of %d requests per second. This could take a while...\n", len(allURLs), rateLimit)
|
||||
} else {
|
||||
log.Infof("Sending %d requests. This could take a while...\n", len(allURLs))
|
||||
}
|
||||
|
||||
specFound, definitionFile := findDefinitionFile(allURLs, client)
|
||||
if specFound {
|
||||
@@ -111,39 +128,46 @@ func makeURLs(target string, endpoints []string, fileExtension string) []string
|
||||
}
|
||||
|
||||
func findDefinitionFile(urls []string, client http.Client) (bool, *openapi3.T) {
|
||||
var rateLimiter = rate.NewLimiter(rate.Every(time.Second/(time.Duration(rateLimit))), 1)
|
||||
|
||||
for i, url := range urls {
|
||||
ct := CheckContentType(client, url)
|
||||
if strings.Contains(ct, "application/json") {
|
||||
bodyBytes, _, _ := MakeRequest(client, "GET", url, timeout, nil)
|
||||
if bodyBytes != nil {
|
||||
checkSpec := UnmarshalSpec(bodyBytes)
|
||||
if (strings.HasPrefix(checkSpec.OpenAPI, "2") || strings.HasPrefix(checkSpec.OpenAPI, "3")) && checkSpec.Paths != nil {
|
||||
fmt.Println("")
|
||||
log.Infof("Definition file found: %s\n", url)
|
||||
return true, checkSpec
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(ct, "application/javascript") {
|
||||
bodyBytes, bodyString, _ := MakeRequest(client, "GET", url, timeout, nil)
|
||||
if bodyBytes != nil {
|
||||
regexPattern := regexp.MustCompile(`(?s)let\s+(\w+)\s*=\s*({.*?});`)
|
||||
matches := regexPattern.FindAllStringSubmatch(bodyString, -1)
|
||||
for _, match := range matches {
|
||||
jsonContent := match[2]
|
||||
checkSpec := UnmarshalSpec([]byte(jsonContent))
|
||||
if strings.HasPrefix(checkSpec.OpenAPI, "2") || strings.HasPrefix(checkSpec.OpenAPI, "3") {
|
||||
log.Infof("\nFound operation definitions embedded in JavaScript file at %s\n", url)
|
||||
return true, checkSpec
|
||||
if rateLimit > 0 {
|
||||
if err := rateLimiter.Wait(context.Background()); err != nil {
|
||||
fmt.Println("Rate limit error...")
|
||||
} else {
|
||||
ct := CheckContentType(client, url)
|
||||
if strings.Contains(ct, "application/json") {
|
||||
bodyBytes, _, _ := MakeRequest(client, "GET", url, timeout, nil)
|
||||
if bodyBytes != nil {
|
||||
checkSpec := UnmarshalSpec(bodyBytes)
|
||||
if (strings.HasPrefix(checkSpec.OpenAPI, "2") || strings.HasPrefix(checkSpec.OpenAPI, "3")) && checkSpec.Paths != nil {
|
||||
fmt.Println("")
|
||||
log.Infof("Definition file found: %s\n", url)
|
||||
return true, checkSpec
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(ct, "application/javascript") {
|
||||
bodyBytes, bodyString, _ := MakeRequest(client, "GET", url, timeout, nil)
|
||||
if bodyBytes != nil {
|
||||
regexPattern := regexp.MustCompile(`(?s)let\s+(\w+)\s*=\s*({.*?});`)
|
||||
matches := regexPattern.FindAllStringSubmatch(bodyString, -1)
|
||||
for _, match := range matches {
|
||||
jsonContent := match[2]
|
||||
checkSpec := UnmarshalSpec([]byte(jsonContent))
|
||||
if strings.HasPrefix(checkSpec.OpenAPI, "2") || strings.HasPrefix(checkSpec.OpenAPI, "3") {
|
||||
log.Infof("\nFound operation definitions embedded in JavaScript file at %s\n", url)
|
||||
return true, checkSpec
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if i == len(urls) {
|
||||
fmt.Printf("\033[2K\r%s%d\n", "Request: ", i+1)
|
||||
} else {
|
||||
fmt.Printf("\033[2K\r%s%d", "Request: ", i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
if i == len(urls) {
|
||||
fmt.Printf("\033[2K\r%s%d\n", "Request: ", i+1)
|
||||
} else {
|
||||
fmt.Printf("\033[2K\r%s%d", "Request: ", i+1)
|
||||
}
|
||||
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi2"
|
||||
"github.com/getkin/kin-openapi/openapi2conv"
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var convertCmd = &cobra.Command{
|
||||
Use: "convert",
|
||||
Short: "Converts a Swagger definition file to an OpenAPI v3 definition file.",
|
||||
Long: `The convert command converts a provided definition file from the Swagger specification (v2) to the OpenAPI specification (v3) and stores it into an output file.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
var bodyBytes []byte
|
||||
|
||||
if randomUserAgent {
|
||||
if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
|
||||
log.Warnf("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
|
||||
}
|
||||
}
|
||||
|
||||
client := CheckAndConfigureProxy()
|
||||
|
||||
if strings.ToLower(outputFormat) != "json" {
|
||||
fmt.Printf("\n")
|
||||
log.Infof("Gathering API details.\n\n")
|
||||
}
|
||||
|
||||
if swaggerURL != "" {
|
||||
bodyBytes, _, _ = MakeRequest(client, "GET", swaggerURL, timeout, nil)
|
||||
} else {
|
||||
specFile, err := os.Open(localFile)
|
||||
if err != nil {
|
||||
log.Fatal("Error opening definition file:", err)
|
||||
}
|
||||
|
||||
bodyBytes, _ = io.ReadAll(specFile)
|
||||
}
|
||||
|
||||
var doc openapi2.T
|
||||
var doc3 openapi3.T
|
||||
|
||||
format = strings.ToLower(format)
|
||||
if format == "yaml" || format == "yml" || strings.HasSuffix(swaggerURL, ".yaml") || strings.HasSuffix(swaggerURL, ".yml") {
|
||||
_ = yaml.Unmarshal(bodyBytes, &doc)
|
||||
_ = yaml.Unmarshal(bodyBytes, &doc3)
|
||||
} else {
|
||||
_ = json.Unmarshal(bodyBytes, &doc)
|
||||
_ = json.Unmarshal(bodyBytes, &doc3)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(doc3.OpenAPI, "3") {
|
||||
log.Warnln("Definition file is already version 3.")
|
||||
WriteConvertedDefinitionFile(bodyBytes)
|
||||
} else if strings.HasPrefix(doc.Swagger, "2") {
|
||||
newDoc, err := openapi2conv.ToV3(&doc)
|
||||
if err != nil {
|
||||
fmt.Printf("Error converting v2 document to v3: %s\n", err)
|
||||
}
|
||||
|
||||
if format == "json" {
|
||||
if strings.HasSuffix(outfile, "yaml") || strings.HasSuffix(outfile, "yml") {
|
||||
log.Warn("It looks like you're trying to save the file in YAML format. Supply the '-f yaml' option to do so (default: json).")
|
||||
}
|
||||
converted, err := json.Marshal(newDoc)
|
||||
if err != nil {
|
||||
log.Fatal("Error converting definition file to v3:", err)
|
||||
}
|
||||
if !strings.HasSuffix(string(converted), "}") {
|
||||
if outfile == "" {
|
||||
fmt.Println(string(converted))
|
||||
} else {
|
||||
WriteConvertedDefinitionFile(converted)
|
||||
}
|
||||
} else {
|
||||
endOfJSON := strings.LastIndex(string(converted), "}") + 1
|
||||
if outfile == "" {
|
||||
fmt.Println(string(converted)[:endOfJSON])
|
||||
} else {
|
||||
WriteConvertedDefinitionFile(converted)
|
||||
}
|
||||
}
|
||||
} else if format == "yaml" || format == "yml" {
|
||||
converted, err := yaml.Marshal(newDoc)
|
||||
if err != nil {
|
||||
log.Fatal("Error converting definition file to v3:", err)
|
||||
}
|
||||
if outfile == "" {
|
||||
fmt.Println(string(converted))
|
||||
} else {
|
||||
WriteConvertedDefinitionFile(converted)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
log.Fatal("Error parsing definition file.")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func WriteConvertedDefinitionFile(data []byte) {
|
||||
file, err := os.OpenFile(outfile, os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Errorf("Error opening file: %s\n", err)
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.Write(data)
|
||||
if err != nil {
|
||||
log.Errorf("Error writing file: %s\n", err)
|
||||
} else {
|
||||
f, _ := filepath.Abs(outfile)
|
||||
log.Infof("Wrote file to %s\n", f)
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,12 @@ var endpointsCmd = &cobra.Command{
|
||||
Long: `The endpoints command allows you to pull a list of endpoints out of a Swagger definition file.
|
||||
This list contains the raw endpoints (parameter values will not be appended or modified).`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if randomUserAgent {
|
||||
if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
|
||||
log.Warnf("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
|
||||
}
|
||||
}
|
||||
|
||||
client := CheckAndConfigureProxy()
|
||||
|
||||
var bodyBytes []byte
|
||||
|
||||
+178
-33
@@ -1,90 +1,235 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func writeLog(sc int, target, method string, errorMsg string) {
|
||||
if sc != 200 {
|
||||
if sc == 401 || sc == 403 {
|
||||
logUnauth(sc, target, method, errorMsg)
|
||||
} else if sc == 301 || sc == 302 {
|
||||
logRedirect(sc, target, method)
|
||||
} else if sc == 0 {
|
||||
logBad(sc, target, method)
|
||||
} else if sc == 404 {
|
||||
logNotFound(sc, target, method, errorMsg)
|
||||
} else if sc == 1 {
|
||||
logDangerous(target, method)
|
||||
} else {
|
||||
logManual(sc, target, method, errorMsg)
|
||||
}
|
||||
} else {
|
||||
logAccessible(sc, target, method)
|
||||
}
|
||||
type Result struct {
|
||||
Method string `json:"method"`
|
||||
Status int `json:"status"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
func logAccessible(status int, target, method string) {
|
||||
log.WithFields(log.Fields{
|
||||
type VerboseResult struct {
|
||||
Method string `json:"method"`
|
||||
Preview string `json:"preview"`
|
||||
Status int `json:"status"`
|
||||
Target string `json:"target"`
|
||||
Curl string `json:"curl"`
|
||||
}
|
||||
|
||||
var tempLogger *log.Logger
|
||||
|
||||
func writeLog(sc int, target, method, errorMsg, response string) {
|
||||
var file *os.File
|
||||
tempLogger = log.New()
|
||||
tempResponsePreviewLength := responsePreviewLength
|
||||
|
||||
if len(response) < responsePreviewLength {
|
||||
responsePreviewLength = len(response)
|
||||
}
|
||||
|
||||
if outfile != "" {
|
||||
file, err := os.OpenFile(outfile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
fmt.Println("Output file does not exist or cannot be created")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
tempLogger.SetOutput(file)
|
||||
}
|
||||
|
||||
if outputFormat == "console" {
|
||||
if strings.Contains(response, "\"") {
|
||||
tempLogger.SetFormatter(&log.TextFormatter{DisableQuote: true, DisableTimestamp: true})
|
||||
} else {
|
||||
tempLogger.SetFormatter(&log.TextFormatter{DisableTimestamp: true})
|
||||
}
|
||||
} else {
|
||||
tempLogger.SetFormatter(&log.JSONFormatter{DisableHTMLEscape: true, DisableTimestamp: true, PrettyPrint: true})
|
||||
}
|
||||
|
||||
if verbose {
|
||||
if sc != 200 {
|
||||
if sc == 401 || sc == 403 {
|
||||
logVerboseUnauth(sc, target, method, errorMsg, response, tempLogger)
|
||||
} else if sc == 301 || sc == 302 {
|
||||
logVerboseRedirect(sc, target, method, response, tempLogger)
|
||||
} else if sc == 0 {
|
||||
logVerboseBad(sc, target, method, response, tempLogger)
|
||||
} else if sc == 404 {
|
||||
logNotFound(sc, target, method, errorMsg, tempLogger)
|
||||
} else if sc == 1 {
|
||||
logDangerous(target, method, tempLogger)
|
||||
} else if sc == 8899 {
|
||||
logVerboseJSON(target, method)
|
||||
} else {
|
||||
logVerboseManual(sc, target, method, errorMsg, response, tempLogger)
|
||||
}
|
||||
} else {
|
||||
logVerboseAccessible(sc, target, method, response, tempLogger)
|
||||
}
|
||||
} else {
|
||||
if sc != 200 {
|
||||
if sc == 401 || sc == 403 {
|
||||
logUnauth(sc, target, method, errorMsg, tempLogger)
|
||||
} else if sc == 301 || sc == 302 {
|
||||
logRedirect(sc, target, method, tempLogger)
|
||||
} else if sc == 0 {
|
||||
logBad(sc, target, method, tempLogger)
|
||||
} else if sc == 404 {
|
||||
logNotFound(sc, target, method, errorMsg, tempLogger)
|
||||
} else if sc == 1 {
|
||||
logDangerous(target, method, tempLogger)
|
||||
} else if sc == 8899 {
|
||||
logJSON(target, method)
|
||||
} else {
|
||||
logManual(sc, target, method, errorMsg, tempLogger)
|
||||
}
|
||||
} else {
|
||||
if sc == 8899 {
|
||||
logJSON(target, method)
|
||||
} else {
|
||||
logAccessible(sc, target, method, tempLogger)
|
||||
}
|
||||
}
|
||||
}
|
||||
responsePreviewLength = tempResponsePreviewLength
|
||||
file.Close()
|
||||
}
|
||||
|
||||
func logAccessible(status int, target, method string, logger *log.Logger) {
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
}).Print("Endpoint accessible!")
|
||||
}
|
||||
|
||||
func logDangerous(target, method string) {
|
||||
log.WithFields(log.Fields{
|
||||
func logVerboseAccessible(status int, target, method, response string, logger *log.Logger) {
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
"Preview": response[:responsePreviewLength],
|
||||
}).Print("Endpoint accessible!")
|
||||
}
|
||||
|
||||
func logDangerous(target, method string, logger *log.Logger) {
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": "skipped",
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
}).Warn("Endpoint skipped due to dangerous keyword.")
|
||||
}).Warn("Endpoint skipped due to dangerous keyword (or request cancelled due to timeout).")
|
||||
}
|
||||
|
||||
func logManual(status int, target, method, errorMsg string) {
|
||||
func logManual(status int, target, method, errorMsg string, logger *log.Logger) {
|
||||
if errorMsg == "" {
|
||||
errorMsg = "Manual testing may be required."
|
||||
}
|
||||
log.WithFields(log.Fields{
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
}).Warn(errorMsg)
|
||||
}
|
||||
|
||||
func logNotFound(status int, target, method, errorMsg string) {
|
||||
func logVerboseManual(status int, target, method, errorMsg, response string, logger *log.Logger) {
|
||||
if errorMsg == "" {
|
||||
errorMsg = "Manual testing may be required."
|
||||
}
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
"Preview": response[:responsePreviewLength],
|
||||
}).Warn(errorMsg)
|
||||
}
|
||||
|
||||
func logNotFound(status int, target, method, errorMsg string, logger *log.Logger) {
|
||||
if errorMsg == "" {
|
||||
errorMsg = "Endpoint not found."
|
||||
}
|
||||
log.WithFields(log.Fields{
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
}).Error(errorMsg)
|
||||
}
|
||||
|
||||
func logRedirect(status int, target, method string) {
|
||||
log.WithFields(log.Fields{
|
||||
func logRedirect(status int, target, method string, logger *log.Logger) {
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
}).Error("Redirect detected. This likely requires authentication.")
|
||||
}
|
||||
|
||||
func logBad(status int, target, method string) {
|
||||
log.WithFields(log.Fields{
|
||||
func logVerboseRedirect(status int, target, method, response string, logger *log.Logger) {
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
"Preview": response[0:responsePreviewLength],
|
||||
}).Error("Redirect detected. This likely requires authentication.")
|
||||
}
|
||||
|
||||
func logBad(status int, target, method string, logger *log.Logger) {
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": "N/A",
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
}).Warn("Bad request (could not reach the target).")
|
||||
}
|
||||
|
||||
func logUnauth(status int, target, method, errorMsg string) {
|
||||
func logVerboseBad(status int, target, method, response string, logger *log.Logger) {
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": "N/A",
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
"Preview": response[:responsePreviewLength],
|
||||
}).Warn("Bad request (could not reach the target).")
|
||||
}
|
||||
|
||||
func logUnauth(status int, target, method, errorMsg string, logger *log.Logger) {
|
||||
if errorMsg == "" {
|
||||
errorMsg = "Unauthorized."
|
||||
}
|
||||
log.WithFields(log.Fields{
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
}).Error(errorMsg)
|
||||
}
|
||||
|
||||
func logVerboseUnauth(status int, target, method, errorMsg, response string, logger *log.Logger) {
|
||||
if errorMsg == "" {
|
||||
errorMsg = "Unauthorized."
|
||||
}
|
||||
logger.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
"Preview": response[:responsePreviewLength],
|
||||
}).Error(errorMsg)
|
||||
}
|
||||
|
||||
func logJSON(title, description string) {
|
||||
tempLogger.WithFields(log.Fields{
|
||||
"apiTitle": title,
|
||||
"description": description,
|
||||
"results": jsonResultArray,
|
||||
}).Println("Done")
|
||||
}
|
||||
|
||||
func logVerboseJSON(title, description string) {
|
||||
tempLogger.WithFields(log.Fields{
|
||||
"apiTitle": title,
|
||||
"description": description,
|
||||
"results": jsonVerboseResultArray,
|
||||
}).Println("Done")
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -15,6 +16,13 @@ var prepareCmd = &cobra.Command{
|
||||
Long: `The prepare command prepares a set of commands for manual testing of each endpoint.
|
||||
This enables you to test specific API functions for common vulnerabilities or misconfigurations.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
if randomUserAgent {
|
||||
if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
|
||||
log.Warnf("A supplied User Agent was detected (%s) while supplying the 'random-user-agent' flag.", UserAgent)
|
||||
}
|
||||
}
|
||||
|
||||
client := CheckAndConfigureProxy()
|
||||
|
||||
fmt.Printf("\n")
|
||||
@@ -35,6 +43,33 @@ This enables you to test specific API functions for common vulnerabilities or mi
|
||||
}
|
||||
var prepareFor string
|
||||
|
||||
func (s SwaggerRequest) PrintPreparedCommands(method string) {
|
||||
var preparedCommand string
|
||||
if strings.ToLower(prepareFor) == "curl" {
|
||||
preparedCommand = fmt.Sprintf("curl -sk -X %s '%s'", method, s.URL.String())
|
||||
} else if strings.ToLower(prepareFor) == "sqlmap" {
|
||||
preparedCommand = fmt.Sprintf("sqlmap --method=%s -u %s", method, s.URL.String())
|
||||
} else if strings.ToLower(prepareFor) == "ffuf" {
|
||||
log.Fatal("ffuf is not supported yet :(")
|
||||
// TODO
|
||||
} else {
|
||||
log.Fatal("External tool not supported. Only 'curl' and 'sqlmap' are supported options for the '-e' flag at this time.")
|
||||
}
|
||||
|
||||
if s.BodyData != nil {
|
||||
if strings.ToLower(prepareFor) == "sqlmap" {
|
||||
preparedCommand = preparedCommand + fmt.Sprintf(" --data='%s'", s.BodyData)
|
||||
} else {
|
||||
preparedCommand = preparedCommand + fmt.Sprintf(" -d '%s'", s.BodyData)
|
||||
}
|
||||
}
|
||||
if len(Headers) != 0 {
|
||||
preparedCommand = preparedCommand + fmt.Sprintf(" -H '%s'", strings.Join(Headers, "' -H '"))
|
||||
}
|
||||
|
||||
fmt.Println(preparedCommand)
|
||||
}
|
||||
|
||||
func init() {
|
||||
prepareCmd.PersistentFlags().StringVarP(&prepareFor, "external-tool", "e", "curl", "The external tool to prepare commands for. Generates syntax for 'curl' by default.")
|
||||
}
|
||||
+37
-8
@@ -2,6 +2,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
@@ -104,19 +105,15 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
|
||||
}
|
||||
req.Header.Set(h[0], h[1])
|
||||
} else {
|
||||
log.Fatal("Custom header provided cannot be used.")
|
||||
log.Warnf("Header provided (%s) cannot be used. Headers must be in 'Key: Value' format (this may be caused by a header declared within the definition file).\n", Headers[i])
|
||||
}
|
||||
}
|
||||
|
||||
// User-Agent handling
|
||||
if randomUserAgent {
|
||||
if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
|
||||
log.Fatalf("Cannot set a User Agent while supplying the 'random-user-agent' flag.")
|
||||
} else {
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
UserAgent = userAgents[rand.Intn(len(userAgents))]
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
}
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
UserAgent = userAgents[rand.Intn(len(userAgents))]
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
} else if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
}
|
||||
@@ -177,6 +174,15 @@ func CheckContentType(client http.Client, target string) string {
|
||||
log.Fatal("Error: could not create HTTP request - ", err)
|
||||
}
|
||||
|
||||
// User-Agent handling
|
||||
if randomUserAgent {
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
UserAgent = userAgents[rand.Intn(len(userAgents))]
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
} else if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req.WithContext(ctx))
|
||||
if err == context.DeadlineExceeded {
|
||||
log.Printf("Error: %s - skipping request.", err)
|
||||
@@ -193,3 +199,26 @@ func CheckContentType(client http.Client, target string) string {
|
||||
}
|
||||
return resp.Header.Get("Content-Type")
|
||||
}
|
||||
|
||||
func CheckAndConfigureProxy() (client http.Client) {
|
||||
var proxyUrl *url.URL
|
||||
|
||||
transport := &http.Transport{}
|
||||
|
||||
if insecure {
|
||||
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
|
||||
if proxy != "NOPROXY" {
|
||||
proxyUrl, _ = url.Parse(proxy)
|
||||
transport.Proxy = http.ProxyURL(proxyUrl)
|
||||
}
|
||||
|
||||
client = http.Client{
|
||||
Transport: transport,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
return client
|
||||
}
|
||||
+10
-5
@@ -5,7 +5,6 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var accessibleEndpointFound bool
|
||||
var apiTarget string
|
||||
var basePath string
|
||||
var format string
|
||||
@@ -15,6 +14,7 @@ var outfile string
|
||||
var proxy string
|
||||
var quiet bool
|
||||
var randomUserAgent bool
|
||||
var rateLimit int
|
||||
var safeWords []string
|
||||
var swaggerURL string
|
||||
var timeout int64
|
||||
@@ -39,14 +39,17 @@ Generate a list of raw API routes for use with custom scripts:
|
||||
$ sj endpoints -u https://petstore.swagger.io/v2/swagger.json
|
||||
|
||||
Perform a brute-force attack against the target to identify hidden definition files:
|
||||
$ sj brute -u https://petstore.swagger.io`,
|
||||
$ sj brute -u https://petstore.swagger.io
|
||||
|
||||
Convert a Swagger (v2) definition file to an OpenAPI (v3) definition file:
|
||||
$ sj convert -u https://petstore.swagger.io/v2/swagger.json -o openapi.json`,
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) < 1 {
|
||||
log.Error("Command not specified. See the --help flag for usage.")
|
||||
}
|
||||
},
|
||||
Version: "1.6.2",
|
||||
Version: "1.10.2",
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
@@ -58,7 +61,8 @@ func init() {
|
||||
rootCmd.AddCommand(endpointsCmd)
|
||||
rootCmd.AddCommand(prepareCmd)
|
||||
rootCmd.AddCommand(bruteCmd)
|
||||
rootCmd.PersistentFlags().StringVarP(&UserAgent, "agent", "a", "Swagger Jacker (github.com/BishopFox/sj)", "Set the User-Agent string.")
|
||||
rootCmd.AddCommand(convertCmd)
|
||||
rootCmd.PersistentFlags().StringVarP(&UserAgent, "agent", "A", "Swagger Jacker (github.com/BishopFox/sj)", "Set the User-Agent string.")
|
||||
rootCmd.PersistentFlags().StringVarP(&basePath, "base-path", "b", "", "Set the API base path if not defined in the definition file (i.e. /V2/).")
|
||||
rootCmd.PersistentFlags().StringVarP(&format, "format", "f", "json", "Declare the format of the definition file (json/yaml/yml/js).")
|
||||
rootCmd.PersistentFlags().StringArrayVarP(&Headers, "headers", "H", nil, "Add custom headers, separated by a colon (\"Name: Value\"). Multiple flags are accepted.")
|
||||
@@ -67,7 +71,8 @@ func init() {
|
||||
rootCmd.PersistentFlags().StringVarP(&outfile, "outfile", "o", "", "Output the results to a file. Only supported for the 'automate' and 'brute' commands at this time.")
|
||||
rootCmd.PersistentFlags().StringVarP(&proxy, "proxy", "p", "NOPROXY", "Proxy host and port. Example: http://127.0.0.1:8080")
|
||||
rootCmd.PersistentFlags().BoolVarP(&quiet, "quiet", "q", false, "Do not prompt for user input - uses default values for all requests.")
|
||||
rootCmd.PersistentFlags().BoolVarP(&randomUserAgent, "randomize-user-agent", "r", false, "Randomizes the user agent string. Default is 'false'.")
|
||||
rootCmd.PersistentFlags().BoolVar(&randomUserAgent, "randomize-user-agent", false, "Randomizes the user agent string. Default is 'false'.")
|
||||
rootCmd.PersistentFlags().IntVarP(&rateLimit, "rate", "r", 15, "Limit the number of requests per second.")
|
||||
rootCmd.PersistentFlags().StringArrayVarP(&safeWords, "safe-word", "s", nil, "Avoids 'dangerous word' check for the specified word(s). Multiple flags are accepted.")
|
||||
rootCmd.PersistentFlags().StringVarP(&apiTarget, "target", "T", "", "Manually set a target for the requests to be made if separate from the host the documentation resides on.")
|
||||
rootCmd.PersistentFlags().Int64VarP(&timeout, "timeout", "t", 30, "Set the request timeout period.")
|
||||
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type SchemaReference struct {
|
||||
Required []string
|
||||
Type string
|
||||
Properties struct {
|
||||
Property struct {
|
||||
Type string
|
||||
Format string
|
||||
Example string
|
||||
Reference string
|
||||
XML struct {
|
||||
Name string
|
||||
Namespace string
|
||||
Prefix string
|
||||
Attribute bool
|
||||
Wrapped bool
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func HandleSchemaReference(schema *openapi3.SchemaRef) {
|
||||
|
||||
}
|
||||
|
||||
func (s SwaggerRequest) SetParametersFromSchema(param *openapi3.ParameterRef, location, schemaRef string, req *openapi3.RequestBodyRef, counter int) SwaggerRequest {
|
||||
if param != nil {
|
||||
name := strings.TrimPrefix(schemaRef, "#/components/schemas/")
|
||||
if s.Def.Components.Schemas[name] != nil {
|
||||
schema := s.Def.Components.Schemas[name]
|
||||
if schema.Value.Properties != nil {
|
||||
for property := range schema.Value.Properties {
|
||||
if schema.Value.Properties[property].Ref != "" {
|
||||
if counter < 3 {
|
||||
s = s.SetParametersFromSchema(param, location, schema.Value.Properties[property].Ref, req, counter+1)
|
||||
} else {
|
||||
log.Warnf("Nested reference encountered for %s (Property: %s). Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path, property)
|
||||
break
|
||||
}
|
||||
} else if location == "path" {
|
||||
if schema.Value.Properties[property].Value.Example != "" && schema.Value.Properties[property].Value.Example != nil {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", schema.Value.Properties[property].Value.Example.(string))
|
||||
} else if schema.Value.Properties[property].Value.Type == "string" {
|
||||
if strings.Contains(s.URL.Path, param.Value.Name) {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "test")
|
||||
} else {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "test")
|
||||
}
|
||||
} else {
|
||||
if strings.Contains(s.URL.Path, param.Value.Name) {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "1")
|
||||
} else {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "1")
|
||||
}
|
||||
}
|
||||
} else if location == "query" {
|
||||
if schema.Value.Properties[property].Value.Example != "" && schema.Value.Properties[property].Value.Example != nil {
|
||||
s.Query.Add(param.Value.Name, (schema.Value.Properties[property].Value.Example).(string))
|
||||
} else if schema.Value.Properties[property].Value.Type == "string" {
|
||||
s.Query.Add(param.Value.Name, "test")
|
||||
} else {
|
||||
s.Query.Add(param.Value.Name, "1")
|
||||
}
|
||||
} else if location == "body" {
|
||||
if schema.Value.Properties[property].Value.Example != "" && schema.Value.Properties[property].Value.Example != nil {
|
||||
s.Body[param.Value.Name] = schema.Value.Properties[property].Value.Example
|
||||
} else if schema.Value.Properties[property].Value.Type == "string" {
|
||||
s.Body[param.Value.Name] = "test"
|
||||
} else {
|
||||
s.Body[param.Value.Name] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if schema.Value.Enum != nil {
|
||||
if location == "path" {
|
||||
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", fmt.Sprint(value))
|
||||
} else if location == "query" {
|
||||
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
|
||||
s.Query.Add(param.Value.Name, fmt.Sprint(value))
|
||||
} else if location == "body" {
|
||||
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
|
||||
s.Body[param.Value.Name] = value
|
||||
}
|
||||
} else if schema.Value.AllOf != nil {
|
||||
for i := range schema.Value.AllOf {
|
||||
if schema.Value.AllOf[i].Ref != "" {
|
||||
if counter < 3 {
|
||||
s = s.SetParametersFromSchema(param, location, schema.Value.AllOf[i].Ref, req, counter+1)
|
||||
} else {
|
||||
log.Warnf("Nested reference encountered for %s. Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if schema.Value.OneOf != nil && schema.Value.OneOf[0].Ref != "" {
|
||||
if counter < 3 {
|
||||
s = s.SetParametersFromSchema(param, location, schema.Value.OneOf[0].Ref, req, counter+1)
|
||||
} else {
|
||||
log.Warnf("Nested reference encountered for %s. Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path)
|
||||
}
|
||||
} else if schema.Value.AnyOf != nil && schema.Value.AnyOf[0].Ref != "" {
|
||||
if counter < 3 {
|
||||
s = s.SetParametersFromSchema(param, location, schema.Value.AnyOf[0].Ref, req, counter+1)
|
||||
} else {
|
||||
log.Warnf("Nested reference encountered for %s. Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
name := strings.TrimPrefix(schemaRef, "#/components/schemas/")
|
||||
if s.Def.Components.Schemas[name] != nil {
|
||||
schema := s.Def.Components.Schemas[name]
|
||||
if schema.Value.Properties != nil {
|
||||
for property := range schema.Value.Properties {
|
||||
if schema.Value.Properties[property].Ref != "" {
|
||||
if counter < 3 {
|
||||
s = s.SetParametersFromSchema(param, location, schema.Value.Properties[property].Ref, req, counter+1)
|
||||
} else {
|
||||
log.Warnf("Nested reference encountered for %s (Property: %s). Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path, property)
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if schema.Value.Properties[property].Value.Example != "" {
|
||||
s.Body[property] = schema.Value.Properties[property].Value.Example
|
||||
} else if schema.Value.Properties[property].Value.Type == "string" {
|
||||
s.Body[property] = "test"
|
||||
} else {
|
||||
s.Body[property] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if schema.Value.Enum != nil {
|
||||
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
|
||||
s.Body[name] = value
|
||||
} else {
|
||||
if schema.Value.AllOf != nil {
|
||||
for i := range schema.Value.AllOf {
|
||||
if schema.Value.AllOf[i].Ref != "" {
|
||||
if counter < 3 {
|
||||
s = s.SetParametersFromSchema(param, location, schema.Value.AllOf[i].Ref, req, counter+1)
|
||||
} else {
|
||||
log.Warnf("Nested reference encountered for %s. Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if schema.Value.OneOf != nil && schema.Value.OneOf[0].Ref != "" {
|
||||
if counter < 3 {
|
||||
s = s.SetParametersFromSchema(param, location, schema.Value.OneOf[0].Ref, req, counter+1)
|
||||
} else {
|
||||
log.Warnf("Nested reference encountered for %s. Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path)
|
||||
}
|
||||
} else if schema.Value.AnyOf != nil && schema.Value.AnyOf[0].Ref != "" {
|
||||
if counter < 3 {
|
||||
s = s.SetParametersFromSchema(param, location, schema.Value.AnyOf[0].Ref, req, counter+1)
|
||||
} else {
|
||||
log.Warnf("Nested reference encountered for %s. Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
+283
-305
@@ -1,15 +1,17 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -17,6 +19,7 @@ import (
|
||||
"github.com/getkin/kin-openapi/openapi2conv"
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/time/rate"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -37,7 +40,10 @@ type SwaggerRequest struct {
|
||||
}
|
||||
|
||||
var accessibleEndpoints []string
|
||||
var specJSON string
|
||||
var consoleResults map[int][]any
|
||||
var jsonResultsStringArray []string
|
||||
var jsonResultArray []Result
|
||||
var jsonVerboseResultArray []VerboseResult
|
||||
|
||||
func GenerateRequests(bodyBytes []byte, client http.Client) []string {
|
||||
|
||||
@@ -52,10 +58,6 @@ func GenerateRequests(bodyBytes []byte, client http.Client) []string {
|
||||
PrintSpecInfo(*s.Def.Info)
|
||||
}
|
||||
|
||||
if os.Args[1] == "automate" && outfile != "" {
|
||||
WriteJSONFile(`"results":[`, false)
|
||||
}
|
||||
|
||||
if s.Def.Paths == nil {
|
||||
log.Fatalf("Could not find any defined operations. Review the file manually.")
|
||||
}
|
||||
@@ -70,7 +72,7 @@ func GenerateRequests(bodyBytes []byte, client http.Client) []string {
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
if len(s.Def.Servers) > 1 && apiTarget == "" {
|
||||
if apiTarget == "" {
|
||||
for _, server := range s.Def.Servers {
|
||||
if outputFormat == "console" && os.Args[1] == "automate" {
|
||||
log.Warnf("Results for %s:\n", server.URL)
|
||||
@@ -79,6 +81,11 @@ func GenerateRequests(bodyBytes []byte, client http.Client) []string {
|
||||
if server.URL == "/" {
|
||||
s.Path = ""
|
||||
}
|
||||
|
||||
if strings.Contains(server.URL, "localhost") || strings.Contains(server.URL, "127.0.0.1") || strings.Contains(server.URL, "::1") {
|
||||
log.Warn("The server(s) documented in the definition file contain(s) a local host value and may result in errors. Supply a target manually using the '-T' flag.")
|
||||
}
|
||||
|
||||
u, _ := url.Parse(server.URL)
|
||||
s.URL = *u
|
||||
s = s.IterateOverPaths(client)
|
||||
@@ -94,11 +101,38 @@ func GenerateRequests(bodyBytes []byte, client http.Client) []string {
|
||||
} else {
|
||||
if apiTarget != "" {
|
||||
u, _ = url.Parse(apiTarget)
|
||||
} else {
|
||||
if swaggerURL == "" {
|
||||
u, _ = url.Parse(s.Def.Servers[0].URL)
|
||||
if strings.Contains(s.Def.Servers[0].URL, "localhost") || strings.Contains(s.Def.Servers[0].URL, "127.0.0.1") || strings.Contains(s.Def.Servers[0].URL, "::1") {
|
||||
log.Warn("The server documented in the definition file contains a local host value and may result in errors. Supply a target manually using the '-T' flag.")
|
||||
}
|
||||
}
|
||||
}
|
||||
s.URL = *u
|
||||
s = s.IterateOverPaths(client)
|
||||
}
|
||||
|
||||
slices.Sort(jsonResultsStringArray)
|
||||
for r := range jsonResultsStringArray {
|
||||
|
||||
var result Result
|
||||
var verboseResult VerboseResult
|
||||
if verbose {
|
||||
err := json.Unmarshal([]byte(strings.TrimPrefix(jsonResultsStringArray[r], ",")), &verboseResult)
|
||||
if err != nil {
|
||||
log.Fatal("Error marshalling JSON:", err)
|
||||
}
|
||||
jsonVerboseResultArray = append(jsonVerboseResultArray, verboseResult)
|
||||
} else {
|
||||
err := json.Unmarshal([]byte(strings.TrimPrefix(jsonResultsStringArray[r], ",")), &result)
|
||||
if err != nil {
|
||||
log.Fatal("Error marshalling JSON:", err)
|
||||
}
|
||||
jsonResultArray = append(jsonResultArray, result)
|
||||
}
|
||||
}
|
||||
|
||||
if os.Args[1] == "automate" {
|
||||
if outfile == "" && getAccessibleEndpoints && strings.ToLower(outputFormat) == "console" {
|
||||
var isDuplicateEndpoint bool
|
||||
@@ -118,16 +152,38 @@ func GenerateRequests(bodyBytes []byte, client http.Client) []string {
|
||||
isDuplicateEndpoint = false
|
||||
}
|
||||
}
|
||||
} else if outfile == "" && strings.ToLower(outputFormat) == "json" {
|
||||
results := strings.Join(s.ResultsJSON, ",")
|
||||
fmt.Printf(`%s"results":[%s]}%s`, specJSON, results, "\n")
|
||||
} else if outfile != "" {
|
||||
if accessibleEndpoints == nil {
|
||||
WriteJSONFile("]}\n", false)
|
||||
} else {
|
||||
WriteJSONFile(",", true)
|
||||
}
|
||||
if strings.ToLower(outputFormat) == "json" {
|
||||
writeLog(8899, s.Def.Info.Title, s.Def.Info.Description, "", "") // This is a temporary workaround to get the logging to work as intended.. needs to be fixed eventually.
|
||||
} else if strings.ToLower(outputFormat) == "console" && !getAccessibleEndpoints {
|
||||
var r []string
|
||||
for i := range consoleResults {
|
||||
tempString := fmt.Sprintf("%s,%s,%d,%s,%s", consoleResults[i][1], consoleResults[i][2], consoleResults[i][0], consoleResults[i][3], consoleResults[i][4])
|
||||
r = append(r, tempString)
|
||||
}
|
||||
sort.Strings(r)
|
||||
for i := range r {
|
||||
result := r[i]
|
||||
i1 := strings.Index(r[i], ",")
|
||||
endpoint := result[:i1]
|
||||
result = result[i1+1:]
|
||||
|
||||
i2 := strings.Index(result, ",")
|
||||
method := result[:i2]
|
||||
result = result[i2+1:]
|
||||
|
||||
i3 := strings.Index(result, ",")
|
||||
statusCode, _ := strconv.Atoi(result[:i3])
|
||||
result = result[i3+1:]
|
||||
|
||||
i4 := strings.Index(result, ",")
|
||||
errorMessage := result[:i4]
|
||||
result = result[i4+1:]
|
||||
|
||||
responsePreview := result
|
||||
|
||||
writeLog(statusCode, endpoint, method, errorMessage, responsePreview)
|
||||
}
|
||||
log.Infof("Results written to %s", outfile)
|
||||
}
|
||||
}
|
||||
return s.Paths
|
||||
@@ -146,12 +202,20 @@ func (s SwaggerRequest) IterateOverPaths(client http.Client) SwaggerRequest {
|
||||
"TRACE": pathItem.Trace,
|
||||
}
|
||||
|
||||
var rateLimiter = rate.NewLimiter(rate.Every(time.Second/(time.Duration(rateLimit))), 1)
|
||||
|
||||
for method, op := range operations {
|
||||
// Do all the things here :D
|
||||
s.BodyData = nil
|
||||
if op != nil {
|
||||
s.URL.Path = s.Path + path
|
||||
s = s.BuildDefinedRequests(client, method, pathItem, op)
|
||||
if rateLimit > 0 {
|
||||
if err := rateLimiter.Wait(context.Background()); err != nil {
|
||||
fmt.Println("Rate limit error...")
|
||||
} else {
|
||||
if op != nil {
|
||||
s.URL.Path = s.Path + path
|
||||
s = s.BuildDefinedRequests(client, method, pathItem, op)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,74 +250,61 @@ func (s SwaggerRequest) BuildDefinedRequests(client http.Client, method string,
|
||||
|
||||
s.URL.RawQuery = s.Query.Encode()
|
||||
if os.Args[1] == "automate" {
|
||||
_, _, sc := MakeRequest(client, method, s.URL.String(), timeout, bytes.NewReader(s.BodyData))
|
||||
tempResponsePreviewLength := responsePreviewLength
|
||||
_, resp, sc := MakeRequest(client, method, s.URL.String(), timeout, bytes.NewReader(s.BodyData))
|
||||
if len(resp) < responsePreviewLength {
|
||||
tempResponsePreviewLength = len(resp)
|
||||
}
|
||||
if sc == 200 {
|
||||
accessibleEndpointFound = true
|
||||
accessibleEndpoints = append(accessibleEndpoints, s.URL.String())
|
||||
}
|
||||
if outfile != "" {
|
||||
if strings.ToLower(outputFormat) != "console" {
|
||||
curlCommand := fmt.Sprintf("curl -X %s -sk %s", method, s.URL.String())
|
||||
if s.BodyData != nil {
|
||||
curlCommand = fmt.Sprintf("%s -d '%s'", curlCommand, string(s.BodyData))
|
||||
}
|
||||
if len(Headers) != 0 {
|
||||
curlCommand = fmt.Sprintf("%s -H '%s'", curlCommand, strings.Join(Headers, "' -H '"))
|
||||
}
|
||||
var result []byte
|
||||
if getAccessibleEndpoints {
|
||||
if sc == 200 {
|
||||
result := fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"%s"},`, sc, s.URL.String(), method, errorDescriptions[fmt.Sprint(sc)])
|
||||
WriteJSONFile(result, false)
|
||||
if verbose {
|
||||
result, _ = json.Marshal(VerboseResult{Method: method, Preview: resp[:tempResponsePreviewLength], Status: sc, Target: s.URL.String(), Curl: curlCommand})
|
||||
} else {
|
||||
result, _ = json.Marshal(Result{Method: method, Status: sc, Target: s.URL.String()})
|
||||
}
|
||||
if jsonResultsStringArray == nil {
|
||||
jsonResultsStringArray = append(jsonResultsStringArray, string(result))
|
||||
} else {
|
||||
jsonResultsStringArray = append(jsonResultsStringArray, ","+string(result))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result := fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"%s"},`, sc, s.URL.String(), method, errorDescriptions[fmt.Sprint(sc)])
|
||||
WriteJSONFile(result, false)
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
} else if outfile == "" {
|
||||
if strings.ToLower(outputFormat) != "console" {
|
||||
if getAccessibleEndpoints {
|
||||
if sc == 200 {
|
||||
s.ResultsJSON = append(s.ResultsJSON, fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"%s"}`, sc, s.URL.String(), method, errorDescriptions[fmt.Sprint(sc)]))
|
||||
}
|
||||
if verbose {
|
||||
result, _ = json.Marshal(VerboseResult{Method: method, Preview: resp[:tempResponsePreviewLength], Status: sc, Target: s.URL.String(), Curl: curlCommand})
|
||||
} else {
|
||||
if sc == 1 {
|
||||
s.ResultsJSON = append(s.ResultsJSON, fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"Skipped due to dangerous keyword in request"}`, sc, s.URL.String(), method))
|
||||
} else {
|
||||
s.ResultsJSON = append(s.ResultsJSON, fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"%s"}`, sc, s.URL.String(), method, errorDescriptions[fmt.Sprint(sc)]))
|
||||
}
|
||||
result, _ = json.Marshal(Result{Method: method, Status: sc, Target: s.URL.String()})
|
||||
}
|
||||
} else if !getAccessibleEndpoints && strings.ToLower(outputFormat) == "console" {
|
||||
writeLog(sc, s.URL.String(), method, errorDescriptions[fmt.Sprint(sc)])
|
||||
jsonResultsStringArray = append(jsonResultsStringArray, string(result))
|
||||
}
|
||||
} else {
|
||||
if !getAccessibleEndpoints {
|
||||
if consoleResults == nil {
|
||||
consoleResults = make(map[int][]any)
|
||||
}
|
||||
n := len(consoleResults)
|
||||
if n != 0 {
|
||||
n = n + 1
|
||||
}
|
||||
var r []any
|
||||
r = append(r, sc, s.URL.String(), method, errorDescriptions[fmt.Sprint(sc)], resp)
|
||||
|
||||
consoleResults[n] = r
|
||||
}
|
||||
}
|
||||
} else if os.Args[1] == "prepare" {
|
||||
if strings.ToLower(prepareFor) == "curl" {
|
||||
if s.BodyData == nil {
|
||||
if len(Headers) == 0 {
|
||||
fmt.Printf("curl -sk -X %s '%s'\n", method, s.URL.String())
|
||||
} else {
|
||||
fmt.Printf("curl -sk -X %s '%s' -H '%s'\n", method, s.URL.String(), strings.Join(Headers, "' -H '"))
|
||||
}
|
||||
} else {
|
||||
if len(Headers) == 0 {
|
||||
fmt.Printf("curl -sk -X %s '%s' -d '%s'\n", method, s.URL.String(), s.BodyData)
|
||||
} else {
|
||||
fmt.Printf("curl -sk -X %s '%s' -d '%s' -H '%s'\n", method, s.URL.String(), s.BodyData, strings.Join(Headers, "' -H '"))
|
||||
}
|
||||
}
|
||||
} else if strings.ToLower(prepareFor) == "sqlmap" {
|
||||
if s.BodyData == nil {
|
||||
if len(Headers) == 0 {
|
||||
fmt.Printf("sqlmap -u %s\n", s.URL.String())
|
||||
} else {
|
||||
fmt.Printf("sqlmap -u %s -H '%s'\n", s.URL.String(), strings.Join(Headers, "' -H '"))
|
||||
}
|
||||
} else {
|
||||
if len(Headers) == 0 {
|
||||
fmt.Printf("sqlmap -u %s --data='%s'\n", s.URL.String(), s.BodyData)
|
||||
} else {
|
||||
fmt.Printf("sqlmap -u %s --data='%s' -H '%s'\n", s.URL.String(), s.BodyData, strings.Join(Headers, "' -H '"))
|
||||
}
|
||||
}
|
||||
} else if strings.ToLower(prepareFor) == "ffuf" {
|
||||
// TODO
|
||||
} else {
|
||||
log.Fatal("External tool not supported. Only 'curl' and 'sqlmap' are supported options for the '-e' flag at this time.")
|
||||
}
|
||||
|
||||
s.PrintPreparedCommands(method)
|
||||
} else if os.Args[1] == "endpoints" {
|
||||
|
||||
for k := range s.Def.Paths {
|
||||
@@ -269,39 +320,52 @@ func (s SwaggerRequest) BuildDefinedRequests(client http.Client, method string,
|
||||
// This whole function needs to be refactored/cleaned up a bit
|
||||
func (s SwaggerRequest) AddParametersToRequest(op *openapi3.Operation) SwaggerRequest {
|
||||
for _, param := range op.Parameters {
|
||||
if param.Value == nil && param.Value.Schema.Ref == "" {
|
||||
if param.Value == nil {
|
||||
continue
|
||||
} else if param.Value.In == "path" {
|
||||
if param.Value.Schema.Ref != "" {
|
||||
s.SetParametersFromSchema(param, "path", param.Value.Schema.Ref, nil)
|
||||
} else if param.Value.Schema.Value.Type != "" && param.Value.Schema.Value.Type == "string" {
|
||||
if param.Value.Schema != nil {
|
||||
if param.Value.Schema.Ref != "" {
|
||||
s = s.SetParametersFromSchema(param, "path", param.Value.Schema.Ref, nil, 0)
|
||||
} else if param.Value.Schema.Value.Type != "" && param.Value.Schema.Value.Type == "string" {
|
||||
if strings.Contains(s.URL.Path, param.Value.Name) {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "test")
|
||||
} else {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "test")
|
||||
}
|
||||
} else {
|
||||
if strings.Contains(s.URL.Path, param.Value.Name) {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "1")
|
||||
} else {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "1")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if strings.Contains(s.URL.Path, param.Value.Name) {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "test")
|
||||
} else {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "test")
|
||||
}
|
||||
} else {
|
||||
if strings.Contains(s.URL.Path, param.Value.Name) {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "1")
|
||||
} else {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "1")
|
||||
}
|
||||
}
|
||||
} else if param.Value.In == "query" {
|
||||
if param.Value.Schema.Ref != "" {
|
||||
s.SetParametersFromSchema(param, "query", param.Value.Schema.Ref, nil)
|
||||
} else {
|
||||
if param.Value.Schema.Value.Type != "" && param.Value.Schema.Value.Type == "string" {
|
||||
s.Query.Add(param.Value.Name, "test")
|
||||
if param.Value.Schema != nil {
|
||||
if param.Value.Schema.Ref != "" {
|
||||
s = s.SetParametersFromSchema(param, "query", param.Value.Schema.Ref, nil, 0)
|
||||
} else {
|
||||
s.Query.Add(param.Value.Name, "1")
|
||||
if param.Value.Schema.Value.Type != "" && param.Value.Schema.Value.Type == "string" {
|
||||
s.Query.Add(param.Value.Name, "test")
|
||||
} else {
|
||||
s.Query.Add(param.Value.Name, "1")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s.Query.Add(param.Value.Name, "test")
|
||||
}
|
||||
} else if param.Value.In == "header" && param.Value.Required {
|
||||
|
||||
} else if param.Value.In == "header" && param.Value.Required && strings.ToLower(param.Value.Name) != "content-type" {
|
||||
Headers = append(Headers, fmt.Sprintf("%s: %s", param.Value.Name, "1"))
|
||||
} else if param.Value.In == "body" {
|
||||
} else if param.Value.In == "body" || param.Value.In == "formData" {
|
||||
if param.Value.Schema.Ref != "" {
|
||||
s.SetParametersFromSchema(param, "body", param.Value.Schema.Ref, nil)
|
||||
s = s.SetParametersFromSchema(param, "body", param.Value.Schema.Ref, nil, 0)
|
||||
}
|
||||
if param.Value.Schema.Value.Type == "string" {
|
||||
s.Body[param.Value.Name] = "test"
|
||||
@@ -319,82 +383,120 @@ func (s SwaggerRequest) AddParametersToRequest(op *openapi3.Operation) SwaggerRe
|
||||
}
|
||||
|
||||
if op.RequestBody != nil {
|
||||
if op.RequestBody.Value.Content != nil {
|
||||
for i := range op.RequestBody.Value.Content {
|
||||
if contentType == "" && Headers != nil && strings.Contains(strings.ToLower(strings.Join(Headers, ",")), "content-type") {
|
||||
headerString := strings.Join(Headers, ",")
|
||||
Headers = nil
|
||||
ctIndex := strings.Index(strings.ToLower(headerString), "content-type:") + 14
|
||||
headerString = headerString[ctIndex:]
|
||||
if strings.Contains(headerString, ",") {
|
||||
headerString = strings.TrimPrefix(headerString, ",")
|
||||
ctEndIndex := strings.Index(headerString[ctIndex:], ",") + 1
|
||||
headerString = headerString[:ctEndIndex]
|
||||
} else if !strings.Contains(strings.ToLower(headerString), "content-type") && (strings.Contains(strings.ToLower(headerString), "application/x-www-form-urlencoded") || strings.Contains(strings.ToLower(headerString), "application/json")) {
|
||||
headerString = ""
|
||||
}
|
||||
if headerString != "" {
|
||||
Headers = append(Headers, strings.Split(headerString, ",")...)
|
||||
if op.RequestBody.Value != nil {
|
||||
if op.RequestBody.Value.Content != nil {
|
||||
for i := range op.RequestBody.Value.Content {
|
||||
if contentType == "" {
|
||||
EnforceSingleContentType(i)
|
||||
} else {
|
||||
EnforceSingleContentType(contentType)
|
||||
}
|
||||
if op.RequestBody.Value.Content.Get(i).Schema != nil {
|
||||
if op.RequestBody.Value.Content.Get(i).Schema.Value == nil {
|
||||
s = s.SetParametersFromSchema(nil, "body", op.RequestBody.Value.Content.Get(i).Schema.Ref, op.RequestBody, 0)
|
||||
if strings.Contains(i, "json") {
|
||||
s.BodyData, _ = json.Marshal(s.Body)
|
||||
} else if strings.Contains(i, "x-www-form-urlencoded") {
|
||||
var formData []string
|
||||
for j := range s.Body {
|
||||
formData = append(formData, fmt.Sprintf("%s=%s", j, fmt.Sprint(s.Body[j])))
|
||||
}
|
||||
s.BodyData = []byte(strings.Join(formData, "&"))
|
||||
} else if strings.Contains(i, "xml") {
|
||||
type Element struct {
|
||||
XMLName xml.Name
|
||||
Content any `xml:",chardata"`
|
||||
}
|
||||
|
||||
}
|
||||
if op.RequestBody.Value.Content.Get(i).Schema.Value == nil {
|
||||
s.SetParametersFromSchema(nil, "body", op.RequestBody.Value.Content.Get(i).Schema.Ref, op.RequestBody)
|
||||
if strings.Contains(i, "json") {
|
||||
s.BodyData, _ = json.Marshal(s.Body)
|
||||
}
|
||||
} else if i == "application/x-www-form-urlencoded" {
|
||||
if contentType == "" && !strings.Contains(strings.ToLower(strings.Join(Headers, "")), "content-type") {
|
||||
Headers = append(Headers, "Content-Type: application/x-www-form-urlencoded")
|
||||
}
|
||||
var formData []string
|
||||
for j := range op.RequestBody.Value.Content.Get(i).Schema.Value.Properties {
|
||||
if op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value != nil {
|
||||
var valueType string = op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value.Type
|
||||
if valueType == "string" {
|
||||
s.Body[j] = "test"
|
||||
} else if valueType == "boolean" {
|
||||
s.Body[j] = false
|
||||
} else if valueType == "integer" || valueType == "number" {
|
||||
s.Body[j] = 1
|
||||
type Root struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
Elements []Element `xml:",any"`
|
||||
}
|
||||
|
||||
var elements []Element
|
||||
for key, value := range s.Body {
|
||||
elements = append(elements, Element{
|
||||
XMLName: xml.Name{Local: key},
|
||||
Content: value,
|
||||
})
|
||||
}
|
||||
|
||||
root := Root{
|
||||
Elements: elements,
|
||||
}
|
||||
|
||||
xmlData, err := xml.Marshal(root)
|
||||
if err != nil {
|
||||
log.Warn("Error marshalling XML data.")
|
||||
}
|
||||
s.BodyData = xmlData
|
||||
} else {
|
||||
s.Body[j] = "unknown_type_populate_manually"
|
||||
log.Warnf("Content type not supported. Test this path manually: %s (Content type: %s)\n", s.URL.Path, i)
|
||||
}
|
||||
formData = append(formData, fmt.Sprintf("%s=%s", j, fmt.Sprint(s.Body[j])))
|
||||
}
|
||||
}
|
||||
s.BodyData = []byte(strings.Join(formData, "&"))
|
||||
} else if strings.Contains(i, "json") {
|
||||
if contentType == "" && !strings.Contains(strings.ToLower(strings.Join(Headers, "")), "content-type") {
|
||||
Headers = append(Headers, "Content-Type: application/json")
|
||||
}
|
||||
for j := range op.RequestBody.Value.Content.Get(i).Schema.Value.Properties {
|
||||
if op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value != nil {
|
||||
var valueType string = op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value.Type
|
||||
if valueType == "string" {
|
||||
s.Body[j] = "test"
|
||||
} else if valueType == "boolean" {
|
||||
s.Body[j] = false
|
||||
} else if valueType == "integer" || valueType == "number" {
|
||||
s.Body[j] = 1
|
||||
} else {
|
||||
s.Body[j] = "unknown_type_populate_manually"
|
||||
} else {
|
||||
var formData []string
|
||||
|
||||
for j := range op.RequestBody.Value.Content.Get(i).Schema.Value.Properties {
|
||||
if op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Ref != "" {
|
||||
s = s.SetParametersFromSchema(nil, "body", op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Ref, op.RequestBody, 0)
|
||||
} else {
|
||||
var valueType string = op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value.Type
|
||||
if op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value != nil {
|
||||
if valueType == "string" {
|
||||
s.Body[j] = "test"
|
||||
} else if valueType == "boolean" {
|
||||
s.Body[j] = false
|
||||
} else if valueType == "integer" || valueType == "number" {
|
||||
s.Body[j] = 1
|
||||
} else {
|
||||
s.Body[j] = "unknown_type_populate_manually"
|
||||
}
|
||||
if i == "application/x-www-form-urlencoded" {
|
||||
formData = append(formData, fmt.Sprintf("%s=%s", j, fmt.Sprint(s.Body[j])))
|
||||
}
|
||||
}
|
||||
|
||||
if i == "application/x-www-form-urlencoded" {
|
||||
s.BodyData = []byte(strings.Join(formData, "&"))
|
||||
} else if strings.Contains(i, "json") || i == "*/*" {
|
||||
s.BodyData, _ = json.Marshal(s.Body)
|
||||
} else if strings.Contains(i, "xml") {
|
||||
//
|
||||
type Element struct {
|
||||
XMLName xml.Name
|
||||
Content any `xml:",chardata"`
|
||||
}
|
||||
|
||||
type Root struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
Elements []Element `xml:",any"`
|
||||
}
|
||||
|
||||
var elements []Element
|
||||
for key, value := range s.Body {
|
||||
elements = append(elements, Element{
|
||||
XMLName: xml.Name{Local: key},
|
||||
Content: value,
|
||||
})
|
||||
}
|
||||
|
||||
root := Root{
|
||||
Elements: elements,
|
||||
}
|
||||
|
||||
xmlData, err := xml.Marshal(root)
|
||||
if err != nil {
|
||||
log.Warn("Error marshalling XML data.")
|
||||
}
|
||||
s.BodyData = xmlData
|
||||
} else {
|
||||
s.Body["test"] = "test"
|
||||
s.BodyData = []byte("test=test")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
s.BodyData, _ = json.Marshal(s.Body)
|
||||
} else if strings.Contains(i, "multipart/form-data") {
|
||||
//TODO
|
||||
s.Body["MULTIPART_FORM_DATA"] = "TEST_MANUALLY"
|
||||
s.BodyData = []byte("MULTIPART_FORM_DATA=TEST_MANUALLY")
|
||||
} else if strings.Contains(i, "xml") {
|
||||
//TODO
|
||||
s.Body["XML"] = "TEST_MANUALLY"
|
||||
s.BodyData = []byte("XML=TEST_MANUALLY")
|
||||
} else {
|
||||
fmt.Println(i)
|
||||
s.Body["test"] = "test"
|
||||
s.BodyData = []byte("test=test")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -406,78 +508,6 @@ func (s SwaggerRequest) AddParametersToRequest(op *openapi3.Operation) SwaggerRe
|
||||
return s
|
||||
}
|
||||
|
||||
func (s SwaggerRequest) SetParametersFromSchema(param *openapi3.ParameterRef, location, schemaRef string, req *openapi3.RequestBodyRef) {
|
||||
if param != nil {
|
||||
name := strings.TrimPrefix(schemaRef, "#/components/schemas/")
|
||||
if s.Def.Components.Schemas[name] != nil {
|
||||
schema := s.Def.Components.Schemas[name]
|
||||
if schema.Value.Properties != nil {
|
||||
for property := range schema.Value.Properties {
|
||||
if schema.Value.Properties[property].Value.Type == "string" {
|
||||
if location == "path" {
|
||||
if strings.Contains(s.URL.Path, param.Value.Name) {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "test")
|
||||
} else {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "test")
|
||||
}
|
||||
} else {
|
||||
if strings.Contains(s.URL.Path, param.Value.Name) {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "1")
|
||||
} else {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "1")
|
||||
}
|
||||
}
|
||||
} else if location == "query" {
|
||||
if schema.Value.Properties[property].Value.Type == "string" {
|
||||
s.Query.Add(param.Value.Name, "test")
|
||||
} else {
|
||||
s.Query.Add(param.Value.Name, "1")
|
||||
}
|
||||
} else if location == "body" {
|
||||
if schema.Value.Properties[property].Value.Type == "string" {
|
||||
s.Body[param.Value.Name] = "test"
|
||||
} else {
|
||||
s.Body[param.Value.Name] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if schema.Value.Enum != nil {
|
||||
if location == "path" {
|
||||
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", fmt.Sprint(value))
|
||||
} else if location == "query" {
|
||||
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
|
||||
s.Query.Add(param.Value.Name, fmt.Sprint(value))
|
||||
} else if location == "body" {
|
||||
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
|
||||
s.Body[param.Value.Name] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
name := strings.TrimPrefix(schemaRef, "#/components/schemas/")
|
||||
if s.Def.Components.Schemas[name] != nil {
|
||||
schema := s.Def.Components.Schemas[name]
|
||||
if schema.Value.Properties != nil {
|
||||
for property := range schema.Value.Properties {
|
||||
if schema.Value.Properties[property].Ref != "" {
|
||||
s.SetParametersFromSchema(nil, "body", schema.Value.Properties[property].Ref, req)
|
||||
} else {
|
||||
if schema.Value.Properties[property].Value.Type == "string" {
|
||||
s.Body[property] = "test"
|
||||
} else {
|
||||
s.Body[property] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if schema.Value.Enum != nil {
|
||||
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
|
||||
s.Body[schema.Value.Title] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s SwaggerRequest) GetBasePath() string {
|
||||
if basePath == "" {
|
||||
if s.Def.Servers != nil {
|
||||
@@ -512,17 +542,17 @@ func (s SwaggerRequest) GetBasePath() string {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bp, _ := s.Def.Servers.BasePath()
|
||||
if bp != "" {
|
||||
basePath = bp
|
||||
}
|
||||
}
|
||||
basePath = strings.TrimSuffix(basePath, "/")
|
||||
return basePath
|
||||
}
|
||||
|
||||
func PrintSpecInfo(i openapi3.Info) {
|
||||
specJSON = fmt.Sprintf(`{"title":"%s","description":"%s",`, i.Title, i.Description)
|
||||
if outfile != "" {
|
||||
WriteJSONFile(specJSON, false)
|
||||
} else if strings.ToLower(outputFormat) == "console" {
|
||||
if strings.ToLower(outputFormat) == "console" {
|
||||
if i.Title != "" {
|
||||
fmt.Println("Title:", i.Title)
|
||||
}
|
||||
@@ -599,51 +629,6 @@ func UnmarshalSpec(bodyBytes []byte) (newDoc *openapi3.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func WriteJSONFile(result string, end bool) {
|
||||
if end {
|
||||
file, err := os.OpenFile(outfile, os.O_RDWR, 0644)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("File does not exist or cannot be created")
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
w := bufio.NewWriter(file)
|
||||
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
log.Error("Failed to get file info:", err)
|
||||
}
|
||||
fileSize := fileInfo.Size()
|
||||
if fileSize != 0 {
|
||||
lastChar := fileSize - 1
|
||||
|
||||
_, err = file.Seek(lastChar, 0)
|
||||
if err != nil {
|
||||
log.Error("Error seeking to last character", err)
|
||||
}
|
||||
fmt.Fprintf(w, "]}\n")
|
||||
|
||||
}
|
||||
w.Flush()
|
||||
} else {
|
||||
file, err := os.OpenFile(outfile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("File does not exist or cannot be created")
|
||||
os.Exit(1)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
w := bufio.NewWriter(file)
|
||||
|
||||
fmt.Fprintf(w, "%s", result)
|
||||
w.Flush()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func ExtractSpecFromJS(bodyBytes []byte) []byte {
|
||||
var openApiIndex int
|
||||
var specClose int
|
||||
@@ -692,25 +677,18 @@ func ExtractSpecFromJS(bodyBytes []byte) []byte {
|
||||
return bodyBytes
|
||||
}
|
||||
|
||||
func CheckAndConfigureProxy() (client http.Client) {
|
||||
var proxyUrl *url.URL
|
||||
func EnforceSingleContentType(newContentType string) {
|
||||
newContentType = strings.TrimSpace(newContentType)
|
||||
|
||||
transport := &http.Transport{}
|
||||
// Remove old 'Content-Type' header
|
||||
slices.DeleteFunc(Headers, func(h string) bool {
|
||||
return strings.HasPrefix(strings.ToLower(h), "content-type:")
|
||||
})
|
||||
|
||||
if insecure {
|
||||
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
Headers = append(Headers, "Content-Type: "+newContentType)
|
||||
|
||||
if proxy != "NOPROXY" {
|
||||
proxyUrl, _ = url.Parse(proxy)
|
||||
transport.Proxy = http.ProxyURL(proxyUrl)
|
||||
}
|
||||
|
||||
client = http.Client{
|
||||
Transport: transport,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
return client
|
||||
// Remove empty elements to avoid repetitions of "-H ''"
|
||||
Headers = slices.DeleteFunc(Headers, func(h string) bool {
|
||||
return strings.TrimSpace(h) == ""
|
||||
})
|
||||
}
|
||||
@@ -7,6 +7,7 @@ require (
|
||||
github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
golang.org/x/time v0.8.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
|
||||
@@ -56,6 +56,8 @@ github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
Reference in new issue
Block a user